mirror of
https://github.com/modrinth/code.git
synced 2026-09-04 05:48:57 +00:00
minor fixes to disclosures, basic markdown support (#7133)
This commit is contained in:
@@ -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>
|
||||||
@@ -7,6 +7,7 @@ export { default as AutoLink } from './AutoLink.vue'
|
|||||||
export { default as Avatar } from './Avatar.vue'
|
export { default as Avatar } from './Avatar.vue'
|
||||||
export { default as Badge } from './Badge.vue'
|
export { default as Badge } from './Badge.vue'
|
||||||
export { default as BaseTerminal } from './BaseTerminal.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 BigOptionButton } from './BigOptionButton.vue'
|
||||||
export { default as BulletDivider } from './BulletDivider.vue'
|
export { default as BulletDivider } from './BulletDivider.vue'
|
||||||
export { default as Button } from './buttons/Button.vue'
|
export { default as Button } from './buttons/Button.vue'
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ArchiveIcon } from '@modrinth/assets'
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import Admonition from '#ui/components/base/Admonition.vue'
|
import Admonition from '#ui/components/base/Admonition.vue'
|
||||||
|
import BasicMarkdownText from '#ui/components/base/BasicMarkdownText.vue'
|
||||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -32,6 +33,9 @@ const messages = defineMessages({
|
|||||||
<template #icon="{ iconClass }">
|
<template #icon="{ iconClass }">
|
||||||
<ArchiveIcon :class="iconClass" />
|
<ArchiveIcon :class="iconClass" />
|
||||||
</template>
|
</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>
|
</Admonition>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</NewModal>
|
</NewModal>
|
||||||
<h2 class="text-lg m-0">{{ formatMessage(commonMessages.detailsLabel) }}</h2>
|
<h2 class="text-lg m-0">{{ formatMessage(commonMessages.detailsLabel) }}</h2>
|
||||||
<div
|
<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">
|
<div v-if="photosensitivityDisclosure" class="text-orange">
|
||||||
<EyeIcon aria-hidden="true" />
|
<EyeIcon aria-hidden="true" />
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
{{ capitalizeString(formatMessage(messages.photosensitivityTitle)) }}
|
{{ capitalizeString(formatMessage(messages.photosensitivityTitle)) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="photosensitivityDisclosure.note" class="text-sm text-secondary">
|
<span v-if="photosensitivityDisclosure.note" class="text-sm text-secondary">
|
||||||
{{ photosensitivityDisclosure.note }}
|
<BasicMarkdownText :text="photosensitivityDisclosure.note" :target="linkTarget" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
{{ capitalizeString(aiGeneratedLabel) }}
|
{{ capitalizeString(aiGeneratedLabel) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="aiDisclosure.note" class="text-sm text-secondary">
|
<span v-if="aiDisclosure.note" class="text-sm text-secondary">
|
||||||
{{ aiDisclosure.note }}
|
<BasicMarkdownText :text="aiDisclosure.note" :target="linkTarget" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
{{ capitalizeString(formatMessage(messages.advertisingTitle)) }}
|
{{ capitalizeString(formatMessage(messages.advertisingTitle)) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="advertisingDisclosure.note" class="text-sm text-secondary">
|
<span v-if="advertisingDisclosure.note" class="text-sm text-secondary">
|
||||||
{{ advertisingDisclosure.note }}
|
<BasicMarkdownText :text="advertisingDisclosure.note" :target="linkTarget" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
:key="`${feature}-${index}`"
|
:key="`${feature}-${index}`"
|
||||||
class="text-sm text-secondary"
|
class="text-sm text-secondary"
|
||||||
>
|
>
|
||||||
{{ feature }}
|
<BasicMarkdownText :text="feature" :target="linkTarget" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
:key="`${entry}-${index}`"
|
:key="`${entry}-${index}`"
|
||||||
class="text-sm text-secondary"
|
class="text-sm text-secondary"
|
||||||
>
|
>
|
||||||
{{ entry }}
|
<BasicMarkdownText :text="entry" :target="linkTarget" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
{{ capitalizeString(formatMessage(messages.systemInteractionsTitle)) }}
|
{{ capitalizeString(formatMessage(messages.systemInteractionsTitle)) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="systemInteractionsDisclosure.note" class="text-sm text-secondary">
|
<span v-if="systemInteractionsDisclosure.note" class="text-sm text-secondary">
|
||||||
{{ systemInteractionsDisclosure.note }}
|
<BasicMarkdownText :text="systemInteractionsDisclosure.note" :target="linkTarget" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
{{ capitalizeString(formatMessage(messages.derivativeWork)) }}
|
{{ capitalizeString(formatMessage(messages.derivativeWork)) }}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
v-for="(source, index) in derivativeWorkDisclosure.sources"
|
v-for="(source, index) in visibleDerivativeSources"
|
||||||
:key="`${source.label}-${index}`"
|
:key="`${source.label}-${index}`"
|
||||||
class="flex flex-col gap-1"
|
class="flex flex-col gap-1"
|
||||||
>
|
>
|
||||||
@@ -139,14 +139,36 @@
|
|||||||
:href="source.link"
|
:href="source.link"
|
||||||
:target="linkTarget"
|
:target="linkTarget"
|
||||||
rel="noopener nofollow ugc"
|
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 }}
|
{{ source.label }}
|
||||||
<ExternalIcon />
|
<ExternalIcon />
|
||||||
</a>
|
</a>
|
||||||
<span v-else class="text-sm">{{ source.label }}</span>
|
<span v-else class="text-sm">
|
||||||
<span v-if="source.note" class="text-sm text-secondary">{{ source.note }}</span>
|
<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>
|
</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>
|
</div>
|
||||||
<div v-if="showFollowers">
|
<div v-if="showFollowers">
|
||||||
@@ -207,6 +229,7 @@ import {
|
|||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
CircleDollarSignIcon,
|
CircleDollarSignIcon,
|
||||||
CircuitBoardIcon,
|
CircuitBoardIcon,
|
||||||
|
DropdownIcon,
|
||||||
ExternalIcon,
|
ExternalIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
GitForkIcon,
|
GitForkIcon,
|
||||||
@@ -226,7 +249,7 @@ import { defineMessage, defineMessages, useVIntl } from '../../composables/i18n'
|
|||||||
import { injectModrinthClient } from '../../providers'
|
import { injectModrinthClient } from '../../providers'
|
||||||
import { commonMessages } from '../../utils/common-messages'
|
import { commonMessages } from '../../utils/common-messages'
|
||||||
import { getActiveDisclosures } from '../../utils/disclosures'
|
import { getActiveDisclosures } from '../../utils/disclosures'
|
||||||
import { Avatar, IntlFormatted } from '../base'
|
import { Avatar, BasicMarkdownText, IntlFormatted } from '../base'
|
||||||
import { NewModal } from '../modal'
|
import { NewModal } from '../modal'
|
||||||
|
|
||||||
const LICENSE_STALE_TIME = 1000 * 60 * 10
|
const LICENSE_STALE_TIME = 1000 * 60 * 10
|
||||||
@@ -285,6 +308,14 @@ const messages = defineMessages({
|
|||||||
id: 'project.disclosure.derivative-work.title',
|
id: 'project.disclosure.derivative-work.title',
|
||||||
defaultMessage: 'This is a derivative work of:',
|
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: {
|
telemetryTitle: {
|
||||||
id: 'project.disclosure.telemetry.title',
|
id: 'project.disclosure.telemetry.title',
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
@@ -322,6 +353,28 @@ const derivativeWorkDisclosure = computed(() => findDisclosure('derivative_work'
|
|||||||
const photosensitivityDisclosure = computed(() => findDisclosure('epilepsy_triggers'))
|
const photosensitivityDisclosure = computed(() => findDisclosure('epilepsy_triggers'))
|
||||||
const systemInteractionsDisclosure = computed(() => findDisclosure('system_interactions'))
|
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 = {
|
const aiUseLabels = {
|
||||||
code: defineMessage({
|
code: defineMessage({
|
||||||
id: 'project.disclosure.ai-generated-content.use.code',
|
id: 'project.disclosure.ai-generated-content.use.code',
|
||||||
|
|||||||
@@ -3386,6 +3386,12 @@
|
|||||||
"project.disclosure.ai-generated-content.use.text": {
|
"project.disclosure.ai-generated-content.use.text": {
|
||||||
"defaultMessage": "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": {
|
"project.disclosure.derivative-work.title": {
|
||||||
"defaultMessage": "This is a derivative work of:"
|
"defaultMessage": "This is a derivative work of:"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -229,3 +229,54 @@ export const md = (options = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const renderString = (string: string) => configuredXss.process(md().render(string))
|
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))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user