From 8ce06a5517051b032ec1d381edcb00994f91a646 Mon Sep 17 00:00:00 2001 From: TwoSquirrels Date: Wed, 2 Oct 2024 03:53:18 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E3=83=95=E3=83=AD=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=A7=E3=81=AE=E5=9F=8B=E3=82=81=E8=BE=BC=E3=81=BF=E7=BD=AE?= =?UTF-8?q?=E6=8F=9B=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../composables/usePostMessage.ts | 18 +- src/lib/markdown/internalLinkEmbedder.ts | 168 ------------------ 2 files changed, 2 insertions(+), 184 deletions(-) delete mode 100644 src/lib/markdown/internalLinkEmbedder.ts diff --git a/src/components/Main/MainView/MessageInput/composables/usePostMessage.ts b/src/components/Main/MainView/MessageInput/composables/usePostMessage.ts index 131d01259..626815557 100644 --- a/src/components/Main/MainView/MessageInput/composables/usePostMessage.ts +++ b/src/components/Main/MainView/MessageInput/composables/usePostMessage.ts @@ -1,6 +1,5 @@ import type { ChannelId } from '/@/types/entity-ids' import apis, { buildFilePathForPost, formatResizeError } from '/@/lib/apis' -import { replace as embedInternalLink } from '/@/lib/markdown/internalLinkEmbedder' import useChannelPath from '/@/composables/useChannelPath' import { computed, ref, unref } from 'vue' import { nullUuid } from '/@/lib/basic/uuid' @@ -93,23 +92,10 @@ const usePostMessage = ( bothChannelsMapInitialFetchPromise.value ]) - const embededText = embedInternalLink(state.text, { - getUser: findUserByName, - getGroup: getUserGroupByName, - getChannel: path => { - try { - const id = channelPathToId(path.split('/'), channelTree.value) - return { id } - } catch { - return undefined - } - } - }) - const dummyFileUrls = state.attachments.map(() => buildFilePathForPost(nullUuid) ) - const dummyText = createContent(embededText, dummyFileUrls) + const dummyText = createContent(state.text, dummyFileUrls) if (countLength(dummyText) > MESSAGE_MAX_LENGTH) { addErrorToast('メッセージが長すぎます') return @@ -124,7 +110,7 @@ const usePostMessage = ( }) await apis.postMessage(cId, { - content: createContent(embededText, fileUrls) + content: createContent(state.text, fileUrls) }) clearState() diff --git a/src/lib/markdown/internalLinkEmbedder.ts b/src/lib/markdown/internalLinkEmbedder.ts deleted file mode 100644 index da2724194..000000000 --- a/src/lib/markdown/internalLinkEmbedder.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * https://github.com/traPtitech/traQ/blob/master/utils/message/replacer.goと同様 - */ - -const mentionRegex = /:?[@@]([^\s@@]{0,31}[^\s@@:])/g -const userStartsRegex = /^[@@]([a-zA-Z0-9_-]{1,32})/g -const channelRegex = /[##]([a-zA-Z0-9_/-]+)/g - -const backQuote = '`' -const dollar = '$' -const defaultCodeTokenLength = 3 - -export type ReplaceGetters = UserAndGroupGetters & ChannelGetter - -interface UserAndGroupGetters { - /** - * nameは大文字小文字を無視する - */ - getUser: (userName: string) => Readonly | undefined - /** - * nameは大文字小文字を無視する - */ - getGroup: (groupName: string) => Readonly | undefined -} -interface ChannelGetter { - /** - * nameは大文字小文字を無視する - */ - getChannel: (channelPath: string) => Readonly | undefined -} - -export interface Entity { - id: string -} - -/** - * コードブロックとLaTeXブロック内でない箇所の内部リンク埋め込みを行う - * replacer.goのReplaceと同様のコード - */ -export const replace = (m: string, getters: Readonly) => { - let inCodeBlock = false - let inLatexBlock = false - let codeTokenLength = defaultCodeTokenLength - - const lines = m.split('\n') - const newLines = lines.map(line => { - if (!inLatexBlock && line.startsWith('`'.repeat(codeTokenLength))) { - // `の数が一致するものと組み合うようにする - if (!inCodeBlock) { - codeTokenLength = countPrefix(line, backQuote) - } else { - codeTokenLength = defaultCodeTokenLength - } - - inCodeBlock = !inCodeBlock - } - if (!inCodeBlock && line.startsWith('$$')) { - inLatexBlock = !inLatexBlock - } - if (inCodeBlock || inLatexBlock) { - return line - } - // 「```」のブロックでも「$$」ブロック内でもないときに置換 - - let newLine = '' - // 「`」「$」で囲まれていないところの始めの文字のindex - let noExpressionStartIndex = 0 - const chs = [...line] - for (let i = 0; i < chs.length; i++) { - const ch = chs[i] - if (ch !== backQuote && ch !== dollar) { - continue - } - - // 囲まれていない場所が終了したのでその箇所は置換する - newLine += replaceAll( - chs.slice(noExpressionStartIndex, i).join(''), - getters - ) - - if (ch === dollar) { - // 「`」は「$」よりも優先されるので - // 「$ ` $」のように「`」がペアの「$」より前にあるときは - // 「$」のペアとして処理しない - const backQuoteI = chs.indexOf(backQuote, i + 1) - const dollarI = chs.indexOf(dollar, i + 1) - if (backQuoteI !== -1 && dollarI !== -1 && backQuoteI < dollarI) { - newLine += ch - noExpressionStartIndex = i + 1 - continue - } - } - const newI = chs.indexOf(ch, i + 1) - if (newI === -1) { - // 「$」/「`」のペアがないとき - newLine += ch - noExpressionStartIndex = i + 1 - continue - } - newLine += chs.slice(i, newI).join('') - i = newI - noExpressionStartIndex = newI - } - // 最後のペア以降の置換 - newLine += replaceAll(chs.slice(noExpressionStartIndex).join(''), getters) - return newLine - }) - return newLines.join('\n') -} - -const replaceAll = (m: string, getters: Readonly) => { - return replaceMention(replaceChannel(m, getters), getters) -} - -const replaceMention = (m: string, getters: Readonly) => { - return m.replace(mentionRegex, s => { - // 始まりが:なものを除外 - if (s.startsWith(':')) { - return s - } - - // .slice(1)は先頭の@を消すため - // 小文字化はgetter内で行う - const name = s.slice(1) - const uid = getters.getUser(name)?.id - if (uid) { - return `!{"type":"user","raw":"${s}","id":"${uid}"}` - } - const gid = getters.getGroup(name)?.id - if (gid) { - return `!{"type":"group","raw":"${s}","id":"${gid}"}` - } - - return s.replace(userStartsRegex, s => { - // .slice(1)は先頭の@を消すため - // 小文字化はgetter内で行う - const name = s.slice(1) - - const uid = getters.getUser(name)?.id - if (uid) { - return `!{"type":"user","raw":"${s}","id":"${uid}"}` - } - return s - }) - }) -} - -const replaceChannel = (m: string, getter: Readonly) => { - return m.replace(channelRegex, s => { - // .slice(1)は先頭の#を消すため - // 小文字化はgetter内で行う - const t = s.slice(1) - const cid = getter.getChannel(t)?.id - if (cid) { - return `!{"type":"channel","raw":"${s}","id":"${cid}"}` - } - return s - }) -} - -const countPrefix = (line: string, letter: string) => { - let count = 0 - for (const ch of line) { - if (ch !== letter) break - count++ - } - return count -} From 2a1e4377a0c9e6431719c332f90d20a1a90d9892 Mon Sep 17 00:00:00 2001 From: TwoSquirrels Date: Wed, 2 Oct 2024 04:14:51 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=E5=9F=8B=E3=82=81=E8=BE=BC=E3=81=BF?= =?UTF-8?q?=E7=BD=AE=E6=8F=9B=E3=81=AB=E5=AF=BE=E5=BF=9C=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/markdown/internalLinkEmbedder.spec.ts | 124 ------------------ 1 file changed, 124 deletions(-) delete mode 100644 tests/unit/lib/markdown/internalLinkEmbedder.spec.ts diff --git a/tests/unit/lib/markdown/internalLinkEmbedder.spec.ts b/tests/unit/lib/markdown/internalLinkEmbedder.spec.ts deleted file mode 100644 index 5eca18a4f..000000000 --- a/tests/unit/lib/markdown/internalLinkEmbedder.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { - ReplaceGetters, - Entity -} from '/@/lib/markdown/internalLinkEmbedder' -import { replace } from '/@/lib/markdown/internalLinkEmbedder' - -const users = { - 'dfdff0c9-5de0-46ee-9721-2525e8bb3d44': { - name: 'a', - id: 'dfdff0c9-5de0-46ee-9721-2525e8bb3d44' - }, - 'dfdff0c9-5de0-46ee-9721-2525e8bb3d45': { - name: 'takashi_trap', - id: 'dfdff0c9-5de0-46ee-9721-2525e8bb3d45' - }, - 'dfdff0c9-5de0-46ee-9721-2525e8bb3d46': { - name: 'takashi_trape', - id: 'dfdff0c9-5de0-46ee-9721-2525e8bb3d46' - }, - 'dfdff0c9-5de0-46ee-9721-2525e8bb3d47': { - name: 'very_long_long_long_long_lo_name', - id: 'dfdff0c9-5de0-46ee-9721-2525e8bb3d47' - } -} -const groups = { - 'dfabf0c9-5de0-46ee-9721-2525e8bb3d45': { - name: 'okあok', - id: 'dfabf0c9-5de0-46ee-9721-2525e8bb3d45' - }, - 'dfabf0c9-5de0-46ee-9721-2525e8bb3d46': { - name: 'takashi_trapo', - id: 'dfabf0c9-5de0-46ee-9721-2525e8bb3d46' - } -} -const channels = { - 'ea452867-553b-4808-a14f-a47ee0009ee6': { - name: 'a', - id: 'ea452867-553b-4808-a14f-a47ee0009ee6' - } -} - -interface EntityWithName extends Entity { - name: string -} - -const createFindFunc = - (store: Record) => - (name: string): EntityWithName | undefined => - Object.values(store).find(e => e.name.toLowerCase() === name.toLowerCase()) - -const testStore: ReplaceGetters = { - getUser: createFindFunc(users), - getGroup: createFindFunc(groups), - getChannel: createFindFunc(channels) -} - -/** - * 置換前, 置換後 - */ -type Spec = [string, string] - -const specs: Spec[] = [ - [ - 'aaaa#aeee `#a` @takashi_trapa @takashi_trap @#a\n```\n#a @takashi_trap\n```\n@okあok', - 'aaaa#aeee `#a` @takashi_trapa !{"type":"user","raw":"@takashi_trap","id":"dfdff0c9-5de0-46ee-9721-2525e8bb3d45"} @!{"type":"channel","raw":"#a","id":"ea452867-553b-4808-a14f-a47ee0009ee6"}\n```\n#a @takashi_trap\n```\n!{"type":"group","raw":"@okあok","id":"dfabf0c9-5de0-46ee-9721-2525e8bb3d45"}' - ], - ['$$\\text{@takashi_trap}$$', '$$\\text{@takashi_trap}$$'], - ['$$\n```\n@takashi_trap\n```\n$$', '$$\n```\n@takashi_trap\n```\n$$'], - [ - '`$@takashi_trap$` @takashi_trap @very_long_long_long_long_lo_name', - '`$@takashi_trap$` !{"type":"user","raw":"@takashi_trap","id":"dfdff0c9-5de0-46ee-9721-2525e8bb3d45"} !{"type":"user","raw":"@very_long_long_long_long_lo_name","id":"dfdff0c9-5de0-46ee-9721-2525e8bb3d47"}' - ], - [ - '`@takashi_trap` $@takashi_trap$ $$ $ `$@takashi_trap$$@takashi_trap$`$@takashi_trap$`$`', - '`@takashi_trap` $@takashi_trap$ $$ $ `$@takashi_trap$$@takashi_trap$`$@takashi_trap$`$`' - ], - [ - '`$@takashi_trap$` $@takashi_trap$ `@takashi_trap`', - '`$@takashi_trap$` $@takashi_trap$ `@takashi_trap`' - ], - ['`okあok`', '`okあok`'], - ['$okあok$', '$okあok$'], - ['`$okあok$`', '`$okあok$`'], - [ - '````\n```\n@takashi_trap\n```\n````\n\n```\n@takashi_trap\n```', - '````\n```\n@takashi_trap\n```\n````\n\n```\n@takashi_trap\n```' - ], - [ - '@takashi_trapああ a@takashi_trap', - '!{"type":"user","raw":"@takashi_trap","id":"dfdff0c9-5de0-46ee-9721-2525e8bb3d45"}ああ a!{"type":"user","raw":"@takashi_trap","id":"dfdff0c9-5de0-46ee-9721-2525e8bb3d45"}' - ], - [ - ':@takashi_trap:ああ a@takashi_trap', - ':@takashi_trap:ああ a!{"type":"user","raw":"@takashi_trap","id":"dfdff0c9-5de0-46ee-9721-2525e8bb3d45"}' - ], - [ - '@takashi_trapああ:', - '!{"type":"user","raw":"@takashi_trap","id":"dfdff0c9-5de0-46ee-9721-2525e8bb3d45"}ああ:' - ], - [ - '@takashi_trap:@takashi_trap: :@takashi_trap: :takashi_trap', - '!{"type":"user","raw":"@takashi_trap","id":"dfdff0c9-5de0-46ee-9721-2525e8bb3d45"}:@takashi_trap: :@takashi_trap: :takashi_trap' - ], - [ - '@takashi_trapo @takashi_trape', - '!{"type":"group","raw":"@takashi_trapo","id":"dfabf0c9-5de0-46ee-9721-2525e8bb3d46"} !{"type":"user","raw":"@takashi_trape","id":"dfdff0c9-5de0-46ee-9721-2525e8bb3d46"}' - ], - [ - '@)、(@takashi_trap @takashi_trap)', - '@)、(!{"type":"user","raw":"@takashi_trap","id":"dfdff0c9-5de0-46ee-9721-2525e8bb3d45"} !{"type":"user","raw":"@takashi_trap","id":"dfdff0c9-5de0-46ee-9721-2525e8bb3d45"})' - ], - [ - '@a', - '!{"type":"user","raw":"@a","id":"dfdff0c9-5de0-46ee-9721-2525e8bb3d44"}' - ] -] - -describe('internalLinkEmbedder', () => { - specs.forEach(([before, after], i) => { - it(`can embed internal links ${i}`, () => { - expect(replace(before, testStore)).toEqual(after) - }) - }) -})