v0.2.87
This commit is contained in:
1 parent
acfe926066
commit
df403c0aa7
26 files changed
+1502
-144
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
globalThis.location = {origin:'https://example.org'};
|
||||
let source = readFileSync('static/js/markdown.js','utf8')
|
||||
.replace(/^import .*;$/gm, '');
|
||||
source = `const EMOJI_SHORTCODES = {}; const t = (key,vars,alternative) => alternative; const parseImageAlias = () => null;\n` + source;
|
||||
const {renderMarkdown, setMarkdownMentions} = await import('data:text/javascript;base64,'+Buffer.from(source).toString('base64'));
|
||||
function has(re, text, message) { assert.match(text,re,message); }
|
||||
|
||||
let html = renderMarkdown('# Instrukcja testowa\n[Przejdź](#instrukcja-testowa)\n# Instrukcja testowa\n[TOC]\n## Etap drugi');
|
||||
has(/<h1 id="instrukcja-testowa"/,html,'first anchor');
|
||||
has(/<h1 id="instrukcja-testowa-2"/,html,'duplicate heading anchor');
|
||||
has(/href="#instrukcja-testowa"/,html,'link to section');
|
||||
has(/heading-permalink/,html,'visible link icon');
|
||||
has(/href="#etap-drugi"/,html,'TOC anchor');
|
||||
|
||||
html = renderMarkdown(' ```js\n const value = 1;\n console.log(value);\n ```\nafter');
|
||||
has(/<pre[^>]*style="margin-left:2ch"[^>]*><code class="language-javascript">const value = 1;\n console\.log\(value\);<\/code><\/pre>/,html,'code container indentation + dedent');
|
||||
has(/<p[^>]*>after<\/p>/,html,'code closed');
|
||||
html = renderMarkdown(' ```python=42\n print(42)\n ```');
|
||||
has(/class="code-with-lines[^>]*style="margin-left:4ch"/,html,'numbered fenced code whole block shifted');
|
||||
has(/data-line="42">print\(42\)/,html,'numbered content dedented');
|
||||
html = renderMarkdown(' ```\n ~~~\n ```');
|
||||
has(/<code>~~~<\/code>/,html,'other marker must remain inside code');
|
||||
html = renderMarkdown('```\n## hidden in code\n```\n## Visible');
|
||||
assert.doesNotMatch(html, /id="hidden-in-code"/);
|
||||
has(/id="visible"/,html,'headings outside code');
|
||||
|
||||
setMarkdownMentions([{nickname:'Ala',color:'#aabbcc'},{nickname:'John+Dev',color:'red'}]);
|
||||
html = renderMarkdown('Cześć @Ala i @John+Dev! @Other @Alana a@Ala `@Ala` [@Ala](https://site.test)');
|
||||
assert.equal((html.match(/class="markdown-mention"/g)||[]).length,2,'valid mentions only');
|
||||
has(/style="--owner:#aabbcc"/,html,'real author color');
|
||||
has(/@Other @Alana a@Ala/,html,'nonmembers and suffixes not chips');
|
||||
has(/<code>@Ala<\/code>/,html,'inline code excluded');
|
||||
has(/<a href="https:\/\/site.test\/"[^>]*>@Ala<\/a>/,html,'links excluded');
|
||||
assert.doesNotMatch(renderMarkdown('```\n@Ala\n```'), /markdown-mention/);
|
||||
html = renderMarkdown('Sentence @Ala. Domain @Ala.example');
|
||||
assert.equal((html.match(/class="markdown-mention"/g) || []).length, 1, 'punctuation after a mention is allowed, domains are not');
|
||||
setMarkdownMentions([]);
|
||||
assert.doesNotMatch(renderMarkdown('@Ala'), /markdown-mention/);
|
||||
|
||||
console.log('PASS: anchors, TOC, duplicate headings, indented fences, numbered blocks, mentions, exclusions');
|
||||
html = renderMarkdown('[TOC]\n# Procedura\n<details>\n<summary>Szczegóły</summary>\n## Procedura\n</details>');
|
||||
has(/id="procedura"/,html);
|
||||
has(/id="procedura-2"/,html,'nested heading anchor unique');
|
||||
has(/href="#procedura-2"/,html,'TOC link for heading inside details');
|
||||
console.log('PASS: anchors in nested details match TOC');
|
||||
setMarkdownMentions([{nickname: 'a[1]\\x', color: '#123456'}, {nickname: '<f>&"', color: '#112233'}]);
|
||||
html = renderMarkdown('Hello @a[1]\\x and @<f>&" but not @a[1]\\xy');
|
||||
assert.equal((html.match(/class="markdown-mention"/g)||[]).length, 2, 'escaped nicknames');
|
||||
assert.doesNotMatch(html, /<f>/, 'no raw HTML from nickname');
|
||||
console.log('PASS: special-character nicknames remain escaped');
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Browser checks for @mention suggestions. Run from repository root."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
HTML = '''<!doctype html><html lang="pl"><head>
|
||||
<meta charset="utf-8">
|
||||
__STYLES__
|
||||
<style>
|
||||
body { padding: 18px; }
|
||||
.editor-shell { width: min(580px, 90vw); height: 340px; }
|
||||
#editor { height: 100%; }
|
||||
</style></head><body>
|
||||
<div class="editor-shell">
|
||||
<div class="line-gutter"></div>
|
||||
<textarea id="editor" wrap="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<script type="module">
|
||||
__SCRIPT__
|
||||
window.autocomplete = createMentionAutocomplete(document.querySelector('#editor'));
|
||||
window.mentionInputTypes = [];
|
||||
document.querySelector('#editor').addEventListener('input', e => window.mentionInputTypes.push(e.inputType));
|
||||
</script></body></html>'''
|
||||
|
||||
|
||||
def main():
|
||||
source = (ROOT / 'static/js/mention-autocomplete.js').read_text()
|
||||
source = source.replace('import { t } from "@rustpad/i18n";', 'const t = (key, vars, fallback) => fallback;')
|
||||
styles = (ROOT / 'static/css/styles.css').read_text()
|
||||
html = HTML.replace('__STYLES__', f'<style>{styles}</style>').replace('__SCRIPT__', source)
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(executable_path="/usr/bin/chromium", args=["--no-sandbox"])
|
||||
try:
|
||||
page = browser.new_page(viewport={"width": 870, "height": 550})
|
||||
errors = []
|
||||
page.on("pageerror", lambda error: errors.append(str(error)))
|
||||
page.set_content(html)
|
||||
page.wait_for_function("!!window.autocomplete")
|
||||
editor = page.locator('#editor')
|
||||
options = page.locator('.mention-suggestions__option')
|
||||
page.evaluate('''() => window.autocomplete.setUsers([
|
||||
{nickname:'Dominik',color:'#ff0000'},
|
||||
{nickname:'Dora',color:'#00ff00'},
|
||||
{nickname:'Jan Domański',color:'#ff8800'},
|
||||
{nickname:'Anna',color:'red'}
|
||||
])''')
|
||||
|
||||
editor.fill('Witaj @do')
|
||||
assert options.count() == 3, options.all_text_contents()
|
||||
assert options.all_text_contents() == ['@Dominik', '@Dora', '@Jan Domański']
|
||||
assert editor.get_attribute('aria-expanded') == 'true'
|
||||
assert options.nth(0).get_attribute('aria-selected') == 'true'
|
||||
editor.press('ArrowDown')
|
||||
assert options.nth(1).get_attribute('aria-selected') == 'true'
|
||||
editor.press('Enter')
|
||||
assert editor.input_value() == 'Witaj @Dora '
|
||||
assert page.evaluate('window.mentionInputTypes.at(-1)') == 'insertReplacementText'
|
||||
assert page.locator('.mention-suggestions').is_hidden()
|
||||
|
||||
editor.fill('Test @do')
|
||||
editor.press('ArrowDown')
|
||||
editor.press('ArrowDown')
|
||||
editor.press('Tab')
|
||||
assert editor.input_value() == 'Test @Jan Domański '
|
||||
|
||||
editor.fill('@d')
|
||||
assert options.count() == 3
|
||||
editor.press('Escape')
|
||||
assert page.locator('.mention-suggestions').is_hidden()
|
||||
editor.press('ArrowDown')
|
||||
assert page.locator('.mention-suggestions').is_hidden(), 'Escape must dismiss the query'
|
||||
editor.fill('@do')
|
||||
options.first.click()
|
||||
assert editor.input_value() == '@Dominik '
|
||||
|
||||
editor.fill('user@example.org')
|
||||
assert page.locator('.mention-suggestions').is_hidden(), 'email should not trigger suggestions'
|
||||
editor.fill('hello @unknown')
|
||||
assert page.locator('.mention-suggestions').is_hidden(), 'only authorized members should be shown'
|
||||
editor.fill('Hi @')
|
||||
assert options.count() == 4
|
||||
page.evaluate('window.autocomplete.setUsers([])')
|
||||
assert page.locator('.mention-suggestions').is_hidden(), 'clear users on logout'
|
||||
editor.fill('Hi @do')
|
||||
assert page.locator('.mention-suggestions').is_hidden(), 'no guest suggestions'
|
||||
|
||||
page.evaluate('window.autocomplete.setUsers([{nickname:"Dominik",color:"#ff0000"}])')
|
||||
editor.fill('Hi @do')
|
||||
assert options.count() == 1
|
||||
geometry = page.locator('.mention-suggestions').bounding_box()
|
||||
shell_geometry = page.locator('.editor-shell').bounding_box()
|
||||
assert geometry['x'] >= shell_geometry['x'] - 1
|
||||
assert geometry['y'] >= shell_geometry['y'] - 1
|
||||
assert geometry['x'] + geometry['width'] <= shell_geometry['x'] + shell_geometry['width'] + 1
|
||||
assert geometry['y'] + geometry['height'] <= shell_geometry['y'] + shell_geometry['height'] + 1
|
||||
|
||||
editor.fill('Edycja @do.')
|
||||
page.evaluate('''() => {let e = document.querySelector('#editor'); e.setSelectionRange(10,10); e.dispatchEvent(new Event('keyup'));}''')
|
||||
assert options.count() == 1
|
||||
editor.press('Enter')
|
||||
assert editor.input_value() == 'Edycja @Dominik.' # no unnecessary space before punctuation
|
||||
|
||||
assert not errors, errors
|
||||
print('PASS: Chromium editor completion, keyboard/mouse, privacy, positioning, punctuation')
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
let source = readFileSync('static/js/mention-autocomplete.js', 'utf8');
|
||||
source = source.replace(/^import \{ t \} from "@rustpad\/i18n";$/m, 'const t = (key, vars, fallback) => fallback;');
|
||||
const { mentionAtCaret, filterMentionUsers, normalizeMentionUsers } = await import(
|
||||
'data:text/javascript;base64,' + Buffer.from(source).toString('base64')
|
||||
);
|
||||
|
||||
assert.deepEqual(mentionAtCaret('Cześć @do', 9), { start: 6, end: 9, query: 'do' });
|
||||
assert.deepEqual(mentionAtCaret('@', 1), { start: 0, end: 1, query: '' });
|
||||
assert.deepEqual(mentionAtCaret('Pierwsza linia\nWitaj (@Żół', 26), { start: 22, end: 26, query: 'Żół' });
|
||||
assert.equal(mentionAtCaret('john@example.org', 12), null, 'do not suggest in addresses');
|
||||
assert.equal(mentionAtCaret('abc@do', 6), null, 'no mention inside a word');
|
||||
assert.equal(mentionAtCaret('Hello @do ', 10), null, 'space ends a query');
|
||||
assert.equal(mentionAtCaret('Hello @do-more', 14)?.query, 'do-more', 'support dashes');
|
||||
assert.equal(mentionAtCaret('Hello @@do', 10), null, 'double at is not a mention');
|
||||
assert.equal(mentionAtCaret('Hello @do', -1), null);
|
||||
|
||||
const members = normalizeMentionUsers([
|
||||
{ nickname: 'Dominik', color: '#ff0000', email: 'secret@example.org' },
|
||||
{ nickname: 'DOMINIK', color: '#aaaaaa' },
|
||||
{ nickname: 'Dora', color: 'red' },
|
||||
{ nickname: 'Jan Domański', color: '#00ff00' },
|
||||
{ nickname: '', color: '#00ff00' },
|
||||
{ nickname: 'Joanna', color: '#123abc' },
|
||||
]);
|
||||
assert.equal(members.length, 4, 'deduplicate case insensitive, skip empty');
|
||||
assert.deepEqual(filterMentionUsers(members, 'do').map(user => user.nickname), ['Dominik', 'Dora', 'Jan Domański']);
|
||||
assert.equal(filterMentionUsers(members, 'DO')[0].nickname, 'Dominik');
|
||||
assert.equal(filterMentionUsers(members, 'zz').length, 0);
|
||||
assert.equal(members[0].color, '#ff0000');
|
||||
assert.match(members[1].color, /^hsl\(/, 'invalid user color falls back safely');
|
||||
assert.equal(Object.hasOwn(members[0], 'email'), false, 'no email addresses in UI model');
|
||||
console.log('PASS: mention context, filtering, normalization, privacy');
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Browser integration test for the mentioned-users bar and cyclic jumps."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HTML = '''<!doctype html><html lang="pl"><head><meta charset="utf-8">
|
||||
<style>__STYLES__</style>
|
||||
<style>
|
||||
body { padding: 20px; }
|
||||
.workspace { width: min(970px, calc(100vw - 40px)); height: 400px; grid-template-columns: 1fr 1fr; }
|
||||
.editor-column { display: flex; flex-direction: column; height: 100%; border: 1px solid #aaa; }
|
||||
.preview-column { display: grid; min-width: 0; min-height: 0; }
|
||||
.editor-shell { flex: 1 1 auto; min-height: 0; }
|
||||
#editor { box-sizing: border-box; width: 100%; height: 100%; padding: 18px; line-height: 22px; }
|
||||
#preview { box-sizing: border-box; width: 100%; height: 100%; overflow: auto; border: 1px solid #aaa; padding: 18px; }
|
||||
</style>
|
||||
</head><body class="pad-page">
|
||||
<div id="editor-workspace" class="workspace view-split">
|
||||
<div class="editor-column">
|
||||
<div class="column-label">Editor</div>
|
||||
<div class="participant-badges"><span class="participant-badge">Autor</span></div>
|
||||
<div id="mentioned-users" class="mentioned-users" role="group" hidden>
|
||||
<span class="mentioned-users__label">Wspomniani</span>
|
||||
<div id="mentioned-users-list" class="mentioned-users__list"></div>
|
||||
</div>
|
||||
<div class="editor-shell"><textarea id="editor" wrap="off"></textarea></div>
|
||||
</div>
|
||||
<div class="preview-column">
|
||||
<div id="preview-label" class="column-label">Markdown preview</div>
|
||||
<div id="mentioned-users-preview" class="mentioned-users mentioned-users--preview" role="group" hidden>
|
||||
<span class="mentioned-users__label">Wspomniani</span>
|
||||
<div id="mentioned-users-preview-list" class="mentioned-users__list"></div>
|
||||
</div>
|
||||
<article class="markdown-body preview" id="preview"></article>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module">
|
||||
import {createMentionNavigator} from '__NAVIGATION_URL__';
|
||||
import {setMarkdownMentions,renderMarkdown} from '__MARKDOWN_URL__';
|
||||
const editor = document.querySelector('#editor');
|
||||
const preview = document.querySelector('#preview');
|
||||
let view = 'split', members = [];
|
||||
const navigator = createMentionNavigator({
|
||||
container: document.querySelector('#mentioned-users'),
|
||||
list: document.querySelector('#mentioned-users-list'),
|
||||
previewContainer: document.querySelector('#mentioned-users-preview'),
|
||||
previewList: document.querySelector('#mentioned-users-preview-list'),
|
||||
editor, preview, getView: () => view, onEditorScroll: () => {},
|
||||
});
|
||||
window.runCase = (source, users) => {
|
||||
editor.value = source; members = users;
|
||||
navigator.setUsers(members);
|
||||
setMarkdownMentions(members);
|
||||
preview.innerHTML = renderMarkdown(source);
|
||||
navigator.render({markdown:true});
|
||||
};
|
||||
window.setView = value => {
|
||||
view=value;
|
||||
document.querySelector('#editor-workspace').className = `workspace view-${value}`;
|
||||
};
|
||||
window.setUsers = value => {
|
||||
navigator.setUsers(value);
|
||||
setMarkdownMentions(value);
|
||||
preview.innerHTML = renderMarkdown(editor.value);
|
||||
navigator.render({markdown:true});
|
||||
};
|
||||
window.ready = true;
|
||||
</script></body></html>'''
|
||||
|
||||
|
||||
def as_data_url(source):
|
||||
return 'data:text/javascript;base64,' + base64.b64encode(source.encode('utf-8')).decode('ascii')
|
||||
|
||||
|
||||
def create_fixture():
|
||||
autocomplete = (ROOT / 'static/js/mention-autocomplete.js').read_text()
|
||||
autocomplete = autocomplete.replace('import { t } from "@rustpad/i18n";',
|
||||
'const t = (key, values, fallback) => fallback;')
|
||||
navigation = (ROOT / 'static/js/mention-navigation.js').read_text()
|
||||
navigation = navigation.replace('import { t } from "@rustpad/i18n";',
|
||||
'const t = (key, values, fallback) => fallback;')
|
||||
navigation = navigation.replace('"@rustpad/mention-autocomplete"',
|
||||
f'"{as_data_url(autocomplete)}"')
|
||||
markdown = (ROOT / 'static/js/markdown.js').read_text()
|
||||
markdown = '\n'.join(line for line in markdown.split('\n') if not line.startswith('import '))
|
||||
markdown = ('const EMOJI_SHORTCODES = {}; '
|
||||
'const t = (key, values, fallback) => fallback; '
|
||||
'const parseImageAlias = () => null;\n') + markdown
|
||||
styles = (ROOT / 'static/css/styles.css').read_text()
|
||||
return (HTML.replace('__STYLES__', styles)
|
||||
.replace('__NAVIGATION_URL__', as_data_url(navigation))
|
||||
.replace('__MARKDOWN_URL__', as_data_url(markdown)))
|
||||
|
||||
|
||||
def main():
|
||||
fixture = create_fixture()
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(executable_path='/usr/bin/chromium', args=['--no-sandbox'])
|
||||
try:
|
||||
page = browser.new_page(viewport={'width': 1020, 'height': 680})
|
||||
errors = []
|
||||
page.on('pageerror', lambda error: errors.append(str(error)))
|
||||
page.set_content(fixture)
|
||||
page.wait_for_function('window.ready === true')
|
||||
lines = [
|
||||
'Na początku @Dominik i @Dominik, do tego @Dora.',
|
||||
'`@Dominik` [@Dominik](https://example.com/@Dora)',
|
||||
'```sh',
|
||||
'@Dominik',
|
||||
'```',
|
||||
*[f'Wypełnienie linii {i}' for i in range(1, 18)],
|
||||
'<details>',
|
||||
'<summary>Szczegóły</summary>',
|
||||
'Wewnątrz @Dominik',
|
||||
'</details>',
|
||||
*[f'Treść linii {i}' for i in range(18, 38)],
|
||||
'Na końcu @Dominik.',
|
||||
]
|
||||
content = '\n'.join(lines)
|
||||
users = [{'nickname': 'Dominik', 'color': '#123456'}, {'nickname':'Dora', 'color':'#654321', 'email':'private@example.org'}]
|
||||
page.evaluate('([text,users]) => window.runCase(text,users)', [content, users])
|
||||
bar = page.locator('#mentioned-users')
|
||||
assert bar.is_visible()
|
||||
badges = page.locator('#mentioned-users-list .mentioned-users__badge')
|
||||
preview_badges = page.locator('#mentioned-users-preview-list .mentioned-users__badge')
|
||||
assert page.locator('#mentioned-users-preview').is_hidden()
|
||||
assert badges.count() == 2
|
||||
assert preview_badges.count() == 2
|
||||
assert badges.all_text_contents() == ['@Dominik4', '@Dora1']
|
||||
assert 'private' not in bar.inner_text()
|
||||
button = badges.first
|
||||
# The two code/link occurrences are intentionally skipped.
|
||||
source_starts = [content.index('@Dominik'),content.index('@Dominik',content.index('@Dominik')+1),content.index('@Dominik',content.index('Wewnątrz')), content.rindex('@Dominik')]
|
||||
for step, start in enumerate(source_starts, 1):
|
||||
button.click()
|
||||
assert page.evaluate('document.querySelector("#editor").selectionStart') == start, step
|
||||
assert page.evaluate('document.querySelector("#editor").selectionEnd') == start + len('@Dominik')
|
||||
assert page.locator('.markdown-mention.is-mention-target').count() == 1
|
||||
assert button.locator('.mentioned-users__count').inner_text() == f'{step}/4'
|
||||
assert page.locator('details.markdown-details').evaluate('element => element.open') is True
|
||||
button.click()
|
||||
assert page.evaluate('document.querySelector("#editor").selectionStart') == source_starts[0]
|
||||
assert button.locator('.mentioned-users__count').inner_text() == '1/4'
|
||||
dora = badges.nth(1)
|
||||
dora.click()
|
||||
assert page.evaluate('document.querySelector("#editor").selectionStart') == content.index('@Dora')
|
||||
assert dora.locator('.mentioned-users__count').inner_text() == '1/1'
|
||||
page.evaluate('window.setView("preview")')
|
||||
assert bar.is_hidden()
|
||||
assert page.locator('#mentioned-users-preview').is_visible()
|
||||
assert page.locator('#preview').is_visible()
|
||||
original_selection = page.evaluate('document.querySelector("#editor").selectionStart')
|
||||
preview_badges.first.click()
|
||||
assert page.evaluate('document.querySelector("#editor").selectionStart') == original_selection
|
||||
assert page.locator('.markdown-mention.is-mention-target').count() == 1
|
||||
page.evaluate('window.setView("edit")')
|
||||
assert bar.is_visible()
|
||||
assert page.locator('#mentioned-users-preview').is_hidden()
|
||||
button.click()
|
||||
assert page.evaluate('document.querySelector("#editor").selectionStart') == source_starts[2]
|
||||
page.evaluate('window.setUsers([])')
|
||||
assert bar.is_hidden()
|
||||
assert page.locator('#mentioned-users-preview').is_hidden()
|
||||
assert badges.count() == 0
|
||||
assert preview_badges.count() == 0
|
||||
assert not errors, errors
|
||||
print('PASS: Chromium mentions list, access, cyclic navigation, source selection, details, view switching')
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,37 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const autocomplete = readFileSync('static/js/mention-autocomplete.js', 'utf8')
|
||||
.replace(/^import \{ t \} from "@rustpad\/i18n";$/m, 'const t = (key, params, fallback) => fallback;');
|
||||
const autocompleteModule = await import('data:text/javascript;base64,' + Buffer.from(autocomplete).toString('base64'));
|
||||
const navigation = readFileSync('static/js/mention-navigation.js', 'utf8')
|
||||
.replace(/^import \{ t \} from "@rustpad\/i18n";$/m, 'const t = (key, params, fallback) => fallback;')
|
||||
.replace(/^import \{ normalizeMentionUsers \} from "@rustpad\/mention-autocomplete";$/m,
|
||||
`const normalizeMentionUsers = ${autocompleteModule.normalizeMentionUsers.toString()};`);
|
||||
const { collectMentionOccurrences } = await import('data:text/javascript;base64,' + Buffer.from(navigation).toString('base64'));
|
||||
|
||||
const users = [
|
||||
{nickname:'Dominik',color:'#1256aa',email:'private@example.org'},
|
||||
{nickname:'dOMINIK',color:'#abc123'},
|
||||
{nickname:'Dora',color:'#dd00dd'},
|
||||
{nickname:'Ala+Dev',color:'#123abc'},
|
||||
];
|
||||
function matches(source, options) { return collectMentionOccurrences(source, users, options); }
|
||||
function valid(source, options) { return matches(source, options).map(mention => source.slice(mention.start, mention.end)); }
|
||||
assert.deepEqual(valid('Tu @Dominik, @Dora oraz @dominik.'), ['@Dominik', '@Dora', '@dominik']);
|
||||
assert.deepEqual(valid('Przykład @DominiK @Ala+Dev, @Dominik'), ['@DominiK', '@Ala+Dev', '@Dominik']);
|
||||
assert.deepEqual(valid('john@Dominik.org a@Dora @@Dominik @Dominika @Other'), []);
|
||||
assert.deepEqual(valid('`@Dominik` i @Dora [@Dominik](https://example.org) oraz https://example.org/@Dora'), ['@Dora']);
|
||||
assert.deepEqual(valid('[file=secret@Dominik,key] i [image=image@Dora] oraz @Dominik'), ['@Dominik']);
|
||||
assert.deepEqual(valid('```sh\n@Dominik\n```\nPo @Dora\n~~~\n@Dora\n~~~\n@Dominik'), ['@Dora', '@Dominik']);
|
||||
assert.deepEqual(valid(' ```js\n@Dominik\n ```\n@Dominik'), ['@Dominik']);
|
||||
assert.deepEqual(valid('````rust\n@Dominik\n```\n@Dora\n````\n@Dora'), ['@Dora']);
|
||||
assert.deepEqual(valid('@Dora', { markdown:false }), ['@Dora']);
|
||||
assert.deepEqual(valid('```\n@Dominik\n```', { markdown:false }), ['@Dominik']);
|
||||
const source = '🙂 @Dominik\nDrugi @Dora i @Dominik';
|
||||
const items = matches(source);
|
||||
assert.deepEqual(items.map(item=>item.start), [source.indexOf('@Dominik'),source.indexOf('@Dora'),source.lastIndexOf('@Dominik')]);
|
||||
assert.deepEqual(items.map(item=>item.sourceLine), [1,2,2]);
|
||||
assert.ok(items.every(item => !Object.hasOwn(item, 'email')));
|
||||
assert.equal(collectMentionOccurrences('Hej @Dominik', []).length, 0);
|
||||
console.log('PASS: mention navigation positions, permissions, Markdown exclusions, UTF-16, code fences');
|
||||
Reference in new issue
Block a user