diff --git a/test/api/unit/libs/highlightMentions.test.js b/test/api/unit/libs/highlightMentions.test.js index bd3955162e..4495f8cbbc 100644 --- a/test/api/unit/libs/highlightMentions.test.js +++ b/test/api/unit/libs/highlightMentions.test.js @@ -1,4 +1,5 @@ import mongoose from 'mongoose'; + import highlightMentions from '../../../../website/server/libs/highlightMentions'; describe('highlightMentions', () => { @@ -101,12 +102,28 @@ describe('highlightMentions', () => { expect(result[0]).to.equal('http://www.medium.com/@user/blog [@user](/profile/111)'); }); + // https://spec.commonmark.org/0.29/#example-483 + it('doesn\'t highlight user in a link without url', async () => { + const text = '[@user2]()'; + const result = await highlightMentions(text); + expect(result[0]).to.equal(text); + }); + // https://github.com/HabitRPG/habitica/issues/12217 it('doesn\'t highlight user in link with url-escapable characters', async () => { const text = '[test](https://habitica.fandom.com/ru/@wiki/Снаряжение)'; const result = await highlightMentions(text); expect(result[0]).to.equal(text); }); + + // https://github.com/HabitRPG/habitica/issues/12223 + it('matches a link in between two the same links', async () => { + const text = '[here](http://habitica.wikia.com/wiki/The_Keep:Pirate_Cove/FAQ)\n@user\n[hier](http://habitica.wikia.com/wiki/The_Keep:Pirate_Cove/FAQ)'; + + const result = await highlightMentions(text); + + expect(result[0]).to.equal('[here](http://habitica.wikia.com/wiki/The_Keep:Pirate_Cove/FAQ)\n[@user](/profile/111)\n[hier](http://habitica.wikia.com/wiki/The_Keep:Pirate_Cove/FAQ)'); + }); }); describe('exceptions in code blocks', () => { @@ -149,14 +166,6 @@ describe('highlightMentions', () => { expect(result[0]).to.equal('[@user](/profile/111) `@user`'); }); - - it('matches a link in between two the same links', async () => { - const text = '[here](http://habitica.wikia.com/wiki/The_Keep:Pirate_Cove/FAQ)\n@user\n[hier](http://habitica.wikia.com/wiki/The_Keep:Pirate_Cove/FAQ)'; - - const result = await highlightMentions(text); - - expect(result[0]).to.equal('[here](http://habitica.wikia.com/wiki/The_Keep:Pirate_Cove/FAQ)\n[@user](/profile/111)\n[hier](http://habitica.wikia.com/wiki/The_Keep:Pirate_Cove/FAQ)'); - }); }); it('github issue 12118, method crashes when square brackets are used', async () => { diff --git a/website/server/libs/highlightMentions.js b/website/server/libs/highlightMentions.js index e6f2ac5ad8..7afe0cff6b 100644 --- a/website/server/libs/highlightMentions.js +++ b/website/server/libs/highlightMentions.js @@ -2,6 +2,7 @@ import escapeRegExp from 'lodash/escapeRegExp'; import habiticaMarkdown from 'habitica-markdown'; import { model as User } from '../models/user'; +import logger from './logger'; const mentionRegex = /\B@[-\w]+/g; const ignoreTokenTypes = ['code_block', 'code_inline', 'fence', 'link_open']; @@ -70,8 +71,17 @@ function withOptionalIndentation (content) { return content.split('\n').map(line => `\\s*${line}`).join('\n'); } -// This is essentially a workaround around the fact that markdown-it doesn't -// provide sourcemap functionality and is the most brittle part of this code. +/* This is essentially a workaround around the fact that markdown-it doesn't + * provide sourcemap functionality and is the most brittle part of this code. + * + * Known errors (Not supported markdown link variants): + * - [a]() https://spec.commonmark.org/0.29/#example-489 + * - [link](\(foo\)) https://spec.commonmark.org/0.29/#example-492 + * - [link](foo(and(bar))) https://spec.commonmark.org/0.29/#example-493 + * - [link](foo\(and\(bar\)) https://spec.commonmark.org/0.29/#example-494 + * - [link]() https://spec.commonmark.org/0.29/#example-495 + * - [link](foo\)\:) https://spec.commonmark.org/0.29/#example-496 + */ function toSourceMapRegex (token) { const { type, content, markup } = token; const contentRegex = escapeRegExp(content); @@ -86,7 +96,7 @@ function toSourceMapRegex (token) { } else if (type === 'link_open') { const texts = token.textContents.map(escapeRegExp); regexStr = markup === 'linkify' || markup === 'autolink' ? texts[0] - : `\\[[^\\]]*${texts.join('[^\\]]*')}[^\\]]*\\]\\([^)]+\\)`; + : `\\[[^\\]]*${texts.join('[^\\]]*')}[^\\]]*\\]\\([^)]*\\)`; } else { throw new Error(`No source mapping regex defined for ignore blocks of type ${type}`); } @@ -112,7 +122,10 @@ function findTextBlocks (text) { const match = targetText.match(regex); if (!match) { - // Should not happen, but insert to handle bugs gracefully + logger.error( + new Error('Failed to match source-mapping regex to find ignore block'), + { text, targetText, regex: String(regex) }, + ); return; }