CI / Build & Quality Checks (push) Successful in 1m52s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 10s
CI / Trigger Desktop Build (push) Successful in 4s
CI / Playwright smoke (e2e) (push) Successful in 10m45s
When a multi-line paste looks like code, a chip above the composer asks "That looks like code. Format it as a code block (js)?" with Format as code / No thanks. It never converts on its own; typing, No, or 8 s dismiss it, and Settings → Editor → "Offer to Format Pasted Code" turns it off. - utils/looksLikeCode.ts (pure, tested): ≥ 3 lines and ≥ 2 of indentation with depth changes, statement terminators, operators + brackets, keywords, monospace/<pre> clipboard HTML, or SQL clause lines. Prose guards: quoted replies, URL lists, long sentence-punctuated lines, plain-word lines, markdown lists. Language guess only when fairly sure (js/ts/python/sql/ rust/c/php). - Accept rebuilds the pasted paragraphs as one code block from their plain text (code lines hold text only) and leaves the caret after it. - Code blocks carry an optional lang → <code class="language-js"> (whitelisted identifier only). Verified in Chromium: pasting a JS function shows the chip; Format as code → sent formatted_body is <pre><code class="language-js"> with indentation intact; a three-line prose paste shows no chip; typing after a paste dismisses it. Unit tests: 14 detector fixtures + 2 output tests; chromium e2e 19 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
111 lines
4.0 KiB
TypeScript
111 lines
4.0 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { Descendant } from 'slate';
|
|
import { toMatrixCustomHTML } from './output';
|
|
import { BlockType } from './types';
|
|
|
|
// Loose Slate node builders for the test.
|
|
const txt = (text: string, marks: Record<string, unknown> = {}) =>
|
|
({ text, ...marks }) as unknown as Descendant;
|
|
const el = (type: BlockType, children: unknown[]) => ({ type, children }) as unknown as Descendant;
|
|
|
|
const OPTS = {
|
|
allowTextFormatting: true,
|
|
allowInlineMarkdown: false,
|
|
allowBlockMarkdown: false,
|
|
allowMath: true,
|
|
};
|
|
|
|
test('inline $…$ → data-mx-maths span, surrounding prose preserved', () => {
|
|
assert.equal(
|
|
toMatrixCustomHTML(txt('a $x^2$ b'), OPTS),
|
|
'a <span data-mx-maths="x^2"><code>x^2</code></span> b',
|
|
);
|
|
});
|
|
|
|
test('single-line block $$…$$ → data-mx-maths div', () => {
|
|
assert.equal(
|
|
toMatrixCustomHTML(txt('$$E=mc^2$$'), OPTS),
|
|
'<div data-mx-maths="E=mc^2"><code>E=mc^2</code></div>',
|
|
);
|
|
});
|
|
|
|
test('LaTeX special chars are escaped in attribute + fallback', () => {
|
|
assert.equal(
|
|
toMatrixCustomHTML(txt('$a<b$'), OPTS),
|
|
'<span data-mx-maths="a<b"><code>a<b</code></span>',
|
|
);
|
|
});
|
|
|
|
test('math bypasses markdown — underscores are not italicised', () => {
|
|
// Without pre-markdown extraction, `_` would become <em>.
|
|
assert.equal(
|
|
toMatrixCustomHTML(txt('$a_b$'), { ...OPTS, allowInlineMarkdown: true }),
|
|
'<span data-mx-maths="a_b"><code>a_b</code></span>',
|
|
);
|
|
});
|
|
|
|
test('currency ($5 and $10) stays literal', () => {
|
|
assert.equal(toMatrixCustomHTML(txt('$5 and $10'), OPTS), '$5 and $10');
|
|
});
|
|
|
|
test('no math conversion inside an inline code mark', () => {
|
|
const out = toMatrixCustomHTML(txt('$x$', { code: true }), OPTS);
|
|
assert.ok(!out.includes('data-mx-maths'));
|
|
assert.ok(out.includes('<code>$x$</code>'));
|
|
});
|
|
|
|
test('no math conversion inside a code block', () => {
|
|
const block = el(BlockType.CodeBlock, [el(BlockType.CodeLine, [txt('$x$')])]);
|
|
const out = toMatrixCustomHTML(block, OPTS);
|
|
assert.ok(!out.includes('data-mx-maths'));
|
|
assert.ok(out.includes('$x$'));
|
|
});
|
|
|
|
test('a message with no math is unchanged', () => {
|
|
assert.equal(toMatrixCustomHTML(txt('just hello'), OPTS), 'just hello');
|
|
});
|
|
|
|
// Gitea #184 O3 — markdown mode: fences and backtick spans typed as plain
|
|
// paragraphs are literal too (the block markdown parser only sees the fence
|
|
// after the lines are joined, so math must be skipped while serialising them).
|
|
const MD_OPTS = { ...OPTS, allowInlineMarkdown: true, allowBlockMarkdown: true };
|
|
|
|
test('markdown: no math conversion inside a typed ``` fence', () => {
|
|
const paragraphs = [
|
|
el(BlockType.Paragraph, [txt('```')]),
|
|
el(BlockType.Paragraph, [txt('$x$ literal')]),
|
|
el(BlockType.Paragraph, [txt('```')]),
|
|
el(BlockType.Paragraph, [txt('after $y$')]),
|
|
];
|
|
const out = toMatrixCustomHTML(paragraphs, MD_OPTS);
|
|
assert.ok(out.includes('<pre'), 'fence became a code block');
|
|
assert.ok(out.includes('$x$ literal'), 'code block content stays literal');
|
|
assert.ok(out.includes('data-mx-maths="y"'), 'math after the fence still converts');
|
|
});
|
|
|
|
test('markdown: no math conversion inside a backtick span, math outside still converts', () => {
|
|
const out = toMatrixCustomHTML(el(BlockType.Paragraph, [txt('use `$x$` and $y$')]), MD_OPTS);
|
|
assert.ok(/<code[^>]*>\$x\$<\/code>/.test(out), 'backtick span stays literal');
|
|
assert.ok(out.includes('data-mx-maths="y"'));
|
|
});
|
|
|
|
test('[#107] a code block language becomes class="language-…"', () => {
|
|
const block = {
|
|
...(el(BlockType.CodeBlock, [el(BlockType.CodeLine, [txt('let a = 1;')])]) as object),
|
|
lang: 'js',
|
|
} as unknown as Descendant;
|
|
assert.equal(
|
|
toMatrixCustomHTML(block, OPTS),
|
|
'<pre><code class="language-js">let a = 1;\n</code></pre>',
|
|
);
|
|
});
|
|
|
|
test('[#107] an unsafe language value is dropped', () => {
|
|
const block = {
|
|
...(el(BlockType.CodeBlock, [el(BlockType.CodeLine, [txt('x')])]) as object),
|
|
lang: 'js" onclick="x',
|
|
} as unknown as Descendant;
|
|
assert.equal(toMatrixCustomHTML(block, OPTS), '<pre><code>x\n</code></pre>');
|
|
});
|