Files
cinny/src/app/components/editor/output.test.ts
T
Lotus CIandClaude Opus 5.5 75d55e861b
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
feat(composer): offer to format pasted code as a code block (#107)
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
2026-09-25 11:05:49 -04:00

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&lt;b"><code>a&lt;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>');
});