一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
115 行
3.7 KiB
TypeScript
115 行
3.7 KiB
TypeScript
import { noteOutline, noteSections, renderOutline, resolveSection } from './noteOutline';
|
|
|
|
describe('noteOutline', () => {
|
|
|
|
test('should build positional paths reflecting nesting', () => {
|
|
const body = [
|
|
'Preamble text',
|
|
'',
|
|
'# One',
|
|
'',
|
|
'## One A',
|
|
'',
|
|
'## One B',
|
|
'',
|
|
'### One B i',
|
|
'',
|
|
'# Two',
|
|
'',
|
|
].join('\n');
|
|
|
|
expect(noteOutline(body).map(e => [e.path, e.heading])).toEqual([
|
|
['0', ''],
|
|
['1', 'One'],
|
|
['1.1', 'One A'],
|
|
['1.2', 'One B'],
|
|
['1.2.1', 'One B i'],
|
|
['2', 'Two'],
|
|
]);
|
|
});
|
|
|
|
test.each([
|
|
['### deep\n\n# top\n\n## mid\n', ['1', '2', '2.1']],
|
|
['# a\n\n### x\n\n## y\n\n# b\n', ['1', '1.1', '1.2', '2']],
|
|
['## a\n\n## b\n\n# c\n\n## d\n', ['1', '2', '3', '3.1']],
|
|
])('should keep paths unique when heading levels are irregular (%#)', (body, expected) => {
|
|
expect(noteOutline(body).map(e => e.path)).toEqual(expected);
|
|
});
|
|
|
|
test('should omit the preamble entry when there is no content before the first heading', () => {
|
|
const outline = noteOutline('# One\n\ntext\n');
|
|
expect(outline.map(e => e.path)).toEqual(['1']);
|
|
});
|
|
|
|
test('should return a single section covering the whole body when there are no headings', () => {
|
|
const body = 'Just some text\n\nand more.\n';
|
|
expect(noteOutline(body)).toEqual([{ path: '0', heading: '', chars: body.length }]);
|
|
});
|
|
|
|
test('should include subsections in a section size and extent', () => {
|
|
const body = '# One\n\nabc\n\n## One A\n\ndef\n\n# Two\n\nxyz\n';
|
|
const sections = noteSections(body);
|
|
const one = sections.find(s => s.path === '1');
|
|
expect(body.slice(one.start, one.end)).toBe('# One\n\nabc\n\n## One A\n\ndef\n\n');
|
|
expect(noteOutline(body).find(e => e.path === '1').chars).toBe(one.end - one.start);
|
|
});
|
|
|
|
test('should ignore heading-like lines inside code blocks', () => {
|
|
const body = '# Real\n\n```\n# Fake\n```\n';
|
|
expect(noteOutline(body).map(e => e.heading)).toEqual(['Real']);
|
|
});
|
|
|
|
test('should detect setext headings', () => {
|
|
const body = 'Title\n=====\n\ntext\n\nSub\n---\n\nmore\n';
|
|
expect(noteOutline(body).map(e => [e.path, e.heading])).toEqual([['1', 'Title'], ['1.1', 'Sub']]);
|
|
});
|
|
|
|
test.each([
|
|
['1.2', 'Target'],
|
|
['Target', 'Target'],
|
|
[' target ', 'Target'],
|
|
])('should resolve section address %s', (address, expectedHeading) => {
|
|
const body = '# One\n\n## Other\n\n## Target\n\ncontent\n';
|
|
const resolution = resolveSection(body, address);
|
|
expect(resolution.error).toBeNull();
|
|
expect(resolution.section.heading).toBe(expectedHeading);
|
|
});
|
|
|
|
test.each([
|
|
['# a\tb\n', 'a b'],
|
|
['a\nb\n===\n', 'a b'],
|
|
['# spaced out \n', 'spaced out'],
|
|
])('should collapse whitespace in headings (%#)', (body, expected) => {
|
|
expect(noteOutline(body)[0].heading).toBe(expected);
|
|
// The collapsed form must still resolve, since that is what the model sees.
|
|
expect(resolveSection(body, expected).section.path).toBe('1');
|
|
});
|
|
|
|
test('should render the outline as tab-separated lines', () => {
|
|
const body = '# One\n\nabc\n\n## One A\n\ndef\n';
|
|
expect(renderOutline(body)).toBe([
|
|
'path\tchars\theading',
|
|
'1\t26\tOne',
|
|
'1.1\t14\tOne A',
|
|
].join('\n'));
|
|
});
|
|
|
|
test('should error and list candidates when several headings match', () => {
|
|
const body = '# Desktop\n\n## Build\n\n# Mobile\n\n## Build\n';
|
|
const resolution = resolveSection(body, 'Build');
|
|
expect(resolution.section).toBeNull();
|
|
expect(resolution.error).toContain('1.1 (under "Desktop")');
|
|
expect(resolution.error).toContain('2.1 (under "Mobile")');
|
|
});
|
|
|
|
test.each([
|
|
['9.9'],
|
|
['No such heading'],
|
|
])('should error on unresolvable address %s', (address) => {
|
|
const resolution = resolveSection('# One\n\ntext\n', address);
|
|
expect(resolution.section).toBeNull();
|
|
expect(resolution.error).toBeTruthy();
|
|
});
|
|
|
|
});
|