minor fixes to disclosures, basic markdown support (#7133)

This commit is contained in:
Prospector
2026-08-13 10:58:47 -07:00
committed by GitHub
parent 97b5138672
commit ab7359a4c5
6 changed files with 175 additions and 13 deletions
@@ -0,0 +1,47 @@
<script setup lang="ts">
import { renderBasicInlineMarkdown } from '@modrinth/utils'
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
text: string
target?: string
}>(),
{
target: '_blank',
},
)
const html = computed(() =>
renderBasicInlineMarkdown(props.text, {
target: props.target,
}),
)
</script>
<template>
<span class="basic-markdown-text" v-html="html" />
</template>
<style scoped>
.basic-markdown-text {
display: block;
max-width: 100%;
overflow-wrap: break-word;
}
.basic-markdown-text :deep(a) {
color: var(--color-link);
}
.basic-markdown-text :deep(a:hover) {
text-decoration: underline;
}
.basic-markdown-text :deep(code) {
background-color: var(--surface-4);
font-size: 12px;
padding: 2px 4px;
border-radius: 4px;
}
</style>
+1
View File
@@ -7,6 +7,7 @@ export { default as AutoLink } from './AutoLink.vue'
export { default as Avatar } from './Avatar.vue'
export { default as Badge } from './Badge.vue'
export { default as BaseTerminal } from './BaseTerminal.vue'
export { default as BasicMarkdownText } from './BasicMarkdownText.vue'
export { default as BigOptionButton } from './BigOptionButton.vue'
export { default as BulletDivider } from './BulletDivider.vue'
export { default as Button } from './buttons/Button.vue'
@@ -3,6 +3,7 @@ import { ArchiveIcon } from '@modrinth/assets'
import { computed } from 'vue'
import Admonition from '#ui/components/base/Admonition.vue'
import BasicMarkdownText from '#ui/components/base/BasicMarkdownText.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
const props = defineProps<{
@@ -32,6 +33,9 @@ const messages = defineMessages({
<template #icon="{ iconClass }">
<ArchiveIcon :class="iconClass" />
</template>
<span>{{ trimmedReason ?? formatMessage(messages.body, { title }) }}</span>
<span v-if="trimmedReason">
<BasicMarkdownText :text="trimmedReason" />
</span>
<span v-else>{{ formatMessage(messages.body, { title }) }}</span>
</Admonition>
</template>
@@ -14,7 +14,7 @@
</NewModal>
<h2 class="text-lg m-0">{{ formatMessage(commonMessages.detailsLabel) }}</h2>
<div
class="flex flex-col gap-3 [&>div>svg]:shrink-0 [&>div>svg]:mt-[1px] [&>div]:flex [&>div]:gap-2 [&>div]:items-start"
class="flex flex-col gap-3 [&>div>svg]:shrink-0 [&>div>svg]:mt-[1px] [&>div]:flex [&>div]:gap-2 [&>div]:items-start [&>div>div]:min-w-0"
>
<div v-if="photosensitivityDisclosure" class="text-orange">
<EyeIcon aria-hidden="true" />
@@ -23,7 +23,7 @@
{{ capitalizeString(formatMessage(messages.photosensitivityTitle)) }}
</span>
<span v-if="photosensitivityDisclosure.note" class="text-sm text-secondary">
{{ photosensitivityDisclosure.note }}
<BasicMarkdownText :text="photosensitivityDisclosure.note" :target="linkTarget" />
</span>
</div>
</div>
@@ -34,7 +34,7 @@
{{ capitalizeString(aiGeneratedLabel) }}
</span>
<span v-if="aiDisclosure.note" class="text-sm text-secondary">
{{ aiDisclosure.note }}
<BasicMarkdownText :text="aiDisclosure.note" :target="linkTarget" />
</span>
</div>
</div>
@@ -45,7 +45,7 @@
{{ capitalizeString(formatMessage(messages.advertisingTitle)) }}
</span>
<span v-if="advertisingDisclosure.note" class="text-sm text-secondary">
{{ advertisingDisclosure.note }}
<BasicMarkdownText :text="advertisingDisclosure.note" :target="linkTarget" />
</span>
</div>
</div>
@@ -60,7 +60,7 @@
:key="`${feature}-${index}`"
class="text-sm text-secondary"
>
{{ feature }}
<BasicMarkdownText :text="feature" :target="linkTarget" />
</span>
</div>
</div>
@@ -81,7 +81,7 @@
:key="`${entry}-${index}`"
class="text-sm text-secondary"
>
{{ entry }}
<BasicMarkdownText :text="entry" :target="linkTarget" />
</span>
</div>
</div>
@@ -92,7 +92,7 @@
{{ capitalizeString(formatMessage(messages.systemInteractionsTitle)) }}
</span>
<span v-if="systemInteractionsDisclosure.note" class="text-sm text-secondary">
{{ systemInteractionsDisclosure.note }}
<BasicMarkdownText :text="systemInteractionsDisclosure.note" :target="linkTarget" />
</span>
</div>
</div>
@@ -130,7 +130,7 @@
{{ capitalizeString(formatMessage(messages.derivativeWork)) }}
</span>
<div
v-for="(source, index) in derivativeWorkDisclosure.sources"
v-for="(source, index) in visibleDerivativeSources"
:key="`${source.label}-${index}`"
class="flex flex-col gap-1"
>
@@ -139,14 +139,36 @@
:href="source.link"
:target="linkTarget"
rel="noopener nofollow ugc"
class="text-blue text-sm flex items-center gap-1 hover:underline"
class="text-blue text-sm min-w-0 break-words hover:underline"
>
{{ source.label }}
<ExternalIcon />
</a>
<span v-else class="text-sm">{{ source.label }}</span>
<span v-if="source.note" class="text-sm text-secondary">{{ source.note }}</span>
<span v-else class="text-sm">
<BasicMarkdownText :text="source.label" :target="linkTarget" />
</span>
<span v-if="source.note" class="text-sm text-secondary">
<BasicMarkdownText :text="source.note" :target="linkTarget" />
</span>
</div>
<button
v-if="hasMoreDerivativeSources"
type="button"
class="flex w-fit items-center gap-1 border-none bg-transparent p-0 text-sm font-semibold text-secondary cursor-pointer active:scale-95 transition-transform"
@click="showAllDerivativeSources = !showAllDerivativeSources"
>
<DropdownIcon
class="h-4 w-4 transition-transform"
:class="{ 'rotate-180': showAllDerivativeSources }"
/>
{{
showAllDerivativeSources
? formatMessage(messages.showLessDerivativeSources)
: formatMessage(messages.showMoreDerivativeSources, {
count: hiddenDerivativeSourcesCount,
})
}}
</button>
</div>
</div>
<div v-if="showFollowers">
@@ -207,6 +229,7 @@ import {
CalendarIcon,
CircleDollarSignIcon,
CircuitBoardIcon,
DropdownIcon,
ExternalIcon,
EyeIcon,
GitForkIcon,
@@ -226,7 +249,7 @@ import { defineMessage, defineMessages, useVIntl } from '../../composables/i18n'
import { injectModrinthClient } from '../../providers'
import { commonMessages } from '../../utils/common-messages'
import { getActiveDisclosures } from '../../utils/disclosures'
import { Avatar, IntlFormatted } from '../base'
import { Avatar, BasicMarkdownText, IntlFormatted } from '../base'
import { NewModal } from '../modal'
const LICENSE_STALE_TIME = 1000 * 60 * 10
@@ -285,6 +308,14 @@ const messages = defineMessages({
id: 'project.disclosure.derivative-work.title',
defaultMessage: 'This is a derivative work of:',
},
showMoreDerivativeSources: {
id: 'project.disclosure.derivative-work.show-more',
defaultMessage: 'Show {count} more',
},
showLessDerivativeSources: {
id: 'project.disclosure.derivative-work.show-fewer',
defaultMessage: 'Show fewer',
},
telemetryTitle: {
id: 'project.disclosure.telemetry.title',
defaultMessage:
@@ -322,6 +353,28 @@ const derivativeWorkDisclosure = computed(() => findDisclosure('derivative_work'
const photosensitivityDisclosure = computed(() => findDisclosure('epilepsy_triggers'))
const systemInteractionsDisclosure = computed(() => findDisclosure('system_interactions'))
const DERIVATIVE_SOURCES_PREVIEW_LIMIT = 3
const showAllDerivativeSources = ref(false)
const visibleDerivativeSources = computed(() => {
const sources = derivativeWorkDisclosure.value?.sources ?? []
if (showAllDerivativeSources.value || sources.length <= DERIVATIVE_SOURCES_PREVIEW_LIMIT) {
return sources
}
return sources.slice(0, DERIVATIVE_SOURCES_PREVIEW_LIMIT)
})
const hasMoreDerivativeSources = computed(
() => (derivativeWorkDisclosure.value?.sources.length ?? 0) > DERIVATIVE_SOURCES_PREVIEW_LIMIT,
)
const hiddenDerivativeSourcesCount = computed(() =>
Math.max(
0,
(derivativeWorkDisclosure.value?.sources.length ?? 0) - DERIVATIVE_SOURCES_PREVIEW_LIMIT,
),
)
const aiUseLabels = {
code: defineMessage({
id: 'project.disclosure.ai-generated-content.use.code',
+6
View File
@@ -3386,6 +3386,12 @@
"project.disclosure.ai-generated-content.use.text": {
"defaultMessage": "text"
},
"project.disclosure.derivative-work.show-fewer": {
"defaultMessage": "Show fewer"
},
"project.disclosure.derivative-work.show-more": {
"defaultMessage": "Show {count} more"
},
"project.disclosure.derivative-work.title": {
"defaultMessage": "This is a derivative work of:"
},
+51
View File
@@ -229,3 +229,54 @@ export const md = (options = {}) => {
}
export const renderString = (string: string) => configuredXss.process(md().render(string))
const basicMarkdownXss = new FilterXSS({
whiteList: {
a: ['href', 'target', 'rel'],
strong: [],
em: [],
code: [],
br: [],
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style'],
})
export const renderBasicInlineMarkdown = (
string: string,
options: {
target?: string
} = {},
) => {
const instance = new MarkdownIt({
html: false,
linkify: true,
breaks: true,
})
instance.disable(['image', 'strikethrough'])
instance.linkify.set({
fuzzyLink: false,
fuzzyIP: false,
})
const defaultLinkOpenRenderer =
instance.renderer.rules.link_open ||
function (tokens, idx, options, _env, self) {
return self.renderToken(tokens, idx, options)
}
instance.renderer.rules.link_open = function (tokens, idx, renderOptions, env, self) {
const token = tokens[idx]
token.attrSet('rel', 'noopener nofollow ugc')
if (options.target) {
token.attrSet('target', options.target)
}
return defaultLinkOpenRenderer(tokens, idx, renderOptions, env, self)
}
return basicMarkdownXss.process(instance.renderInline(string))
}