@taiga-ai/mcp-server 0.25.0-rc.474 → 0.26.0-dev.479
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/dist/index.js +292 -115
- package/package.json +15 -2
- package/scripts/build.mjs +0 -37
- package/scripts/prepare-publish.cjs +0 -49
- package/src/client.test.ts +0 -424
- package/src/client.ts +0 -977
- package/src/config.test.ts +0 -24
- package/src/config.ts +0 -28
- package/src/index.ts +0 -45
- package/src/modern.integration.test.ts +0 -153
- package/src/server.integration.test.ts +0 -9214
- package/src/server.ts +0 -18
- package/src/tools/annotations.ts +0 -38
- package/src/tools/autonomy.ts +0 -86
- package/src/tools/backlogs.ts +0 -802
- package/src/tools/builds.ts +0 -668
- package/src/tools/catalogue.test.ts +0 -170
- package/src/tools/catalogue.ts +0 -223
- package/src/tools/context.ts +0 -513
- package/src/tools/documents.ts +0 -375
- package/src/tools/errors.ts +0 -172
- package/src/tools/hints.ts +0 -99
- package/src/tools/inbox.ts +0 -546
- package/src/tools/index.ts +0 -239
- package/src/tools/inputs.ts +0 -47
- package/src/tools/instructions.ts +0 -41
- package/src/tools/jobs.ts +0 -90
- package/src/tools/knowledge.ts +0 -249
- package/src/tools/maintenance.ts +0 -547
- package/src/tools/markdown.test.ts +0 -249
- package/src/tools/markdown.ts +0 -151
- package/src/tools/observe.ts +0 -311
- package/src/tools/product-documents.ts +0 -184
- package/src/tools/products.ts +0 -529
- package/src/tools/prompts.ts +0 -265
- package/src/tools/queue.ts +0 -250
- package/src/tools/readiness.ts +0 -177
- package/src/tools/remote.test.ts +0 -44
- package/src/tools/remote.ts +0 -107
- package/src/tools/repositories.ts +0 -360
- package/src/tools/request-prompt.test.ts +0 -32
- package/src/tools/request-prompt.ts +0 -28
- package/src/tools/resources.ts +0 -217
- package/src/tools/sections.test.ts +0 -143
- package/src/tools/sections.ts +0 -287
- package/src/tools/setup.ts +0 -455
- package/src/tools/summarize.test.ts +0 -20
- package/src/tools/summarize.ts +0 -96
- package/src/tools/wait.ts +0 -490
- package/src/version.test.ts +0 -10
- package/src/version.ts +0 -14
- package/taiga-ai-mcp-server-0.25.0-rc.474.tgz +0 -0
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -9
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { fence, nestHeadings, renderDocument } from './markdown.js';
|
|
3
|
-
|
|
4
|
-
describe('fence', () => {
|
|
5
|
-
it('is one backtick longer than the longest run inside the body, four at least', () => {
|
|
6
|
-
expect(fence('markdown', 'plain')).toBe('````markdown\nplain\n````');
|
|
7
|
-
expect(fence('markdown', 'a ````` five-run inside')).toBe('``````markdown\na ````` five-run inside\n``````');
|
|
8
|
-
// Two hundred thousand runs would overflow a spread into Math.max.
|
|
9
|
-
expect(fence('text', '` '.repeat(200_000)).startsWith('````text\n')).toBe(true);
|
|
10
|
-
});
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
describe('renderDocument', () => {
|
|
14
|
-
it('renders a section map as a titled Markdown document with one heading per section', () => {
|
|
15
|
-
const rendered = renderDocument({
|
|
16
|
-
name: 'Security Policy',
|
|
17
|
-
type: 'policy_security',
|
|
18
|
-
version: 3,
|
|
19
|
-
data: {
|
|
20
|
-
sections: {
|
|
21
|
-
summary: { source: 'template', priority: 'core', content: ' Controls for code. ' },
|
|
22
|
-
scope: { title: 'Scope of the policy', content: 'Everything.' },
|
|
23
|
-
legacy: 'bare text',
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
expect(rendered.mimeType).toBe('text/markdown');
|
|
29
|
-
expect(rendered.text).toBe(
|
|
30
|
-
[
|
|
31
|
-
'# Security Policy',
|
|
32
|
-
'',
|
|
33
|
-
'type: policy_security | version: 3 | core sections: summary',
|
|
34
|
-
'',
|
|
35
|
-
'## summary',
|
|
36
|
-
'',
|
|
37
|
-
'Controls for code.',
|
|
38
|
-
'',
|
|
39
|
-
'## Scope of the policy',
|
|
40
|
-
'',
|
|
41
|
-
'Everything.',
|
|
42
|
-
'',
|
|
43
|
-
'## legacy',
|
|
44
|
-
'',
|
|
45
|
-
'bare text',
|
|
46
|
-
'',
|
|
47
|
-
].join('\n')
|
|
48
|
-
);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('tracks fences as CommonMark does: indented, longer closers, info strings, and headings indented up to three spaces', () => {
|
|
52
|
-
const body = [
|
|
53
|
-
'````',
|
|
54
|
-
'```',
|
|
55
|
-
'# inside a four-tick fence: a three-tick line does not close it',
|
|
56
|
-
'`````',
|
|
57
|
-
'## after a five-tick closer',
|
|
58
|
-
' ```sh',
|
|
59
|
-
'# indented opener',
|
|
60
|
-
' ```',
|
|
61
|
-
' ## indented heading',
|
|
62
|
-
' # four spaces is code, not a heading',
|
|
63
|
-
'```',
|
|
64
|
-
'```sh',
|
|
65
|
-
'# an opener-with-info inside an open fence is not its closer',
|
|
66
|
-
'```',
|
|
67
|
-
'## out again',
|
|
68
|
-
'```json``` is not a fence opener',
|
|
69
|
-
'## still a heading',
|
|
70
|
-
'~~~',
|
|
71
|
-
'```',
|
|
72
|
-
'# backticks inside a tilde fence',
|
|
73
|
-
'~~~',
|
|
74
|
-
'## last',
|
|
75
|
-
].join('\n');
|
|
76
|
-
|
|
77
|
-
expect(nestHeadings(body)).toBe(
|
|
78
|
-
[
|
|
79
|
-
'````',
|
|
80
|
-
'```',
|
|
81
|
-
'# inside a four-tick fence: a three-tick line does not close it',
|
|
82
|
-
'`````',
|
|
83
|
-
'### after a five-tick closer',
|
|
84
|
-
' ```sh',
|
|
85
|
-
'# indented opener',
|
|
86
|
-
' ```',
|
|
87
|
-
' ### indented heading',
|
|
88
|
-
' # four spaces is code, not a heading',
|
|
89
|
-
'```',
|
|
90
|
-
'```sh',
|
|
91
|
-
'# an opener-with-info inside an open fence is not its closer',
|
|
92
|
-
'```',
|
|
93
|
-
'### out again',
|
|
94
|
-
'```json``` is not a fence opener',
|
|
95
|
-
'### still a heading',
|
|
96
|
-
'~~~',
|
|
97
|
-
'```',
|
|
98
|
-
'# backticks inside a tilde fence',
|
|
99
|
-
'~~~',
|
|
100
|
-
'### last',
|
|
101
|
-
].join('\n')
|
|
102
|
-
);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("edits by the parser's offsets: CRLF and lone CR line endings, a tab after a container marker, an empty heading, an HTML block, deep nesting", () => {
|
|
106
|
-
expect(nestHeadings('```\r\n# in\r\n```\r\n# out')).toBe('```\r\n# in\r\n```\r\n### out');
|
|
107
|
-
expect(nestHeadings('x\ry\n# h\n## i')).toBe('x\ry\n### h\n### i');
|
|
108
|
-
expect(nestHeadings('>\t# q\n-\t# x')).toBe('>\t### q\n-\t### x');
|
|
109
|
-
expect(nestHeadings('#\n##\n# x')).toBe('###\n###\n### x');
|
|
110
|
-
// A heading-shaped line inside an HTML block is HTML to a CommonMark reader, so it stays.
|
|
111
|
-
expect(nestHeadings('<details>\n<summary>s</summary>\n## inside\n</details>\n\n## after')).toBe(
|
|
112
|
-
'<details>\n<summary>s</summary>\n## inside\n</details>\n\n### after'
|
|
113
|
-
);
|
|
114
|
-
// Deeper than a recursive walk could take: nested quotes, which the
|
|
115
|
-
// parser handles in milliseconds (nested list markers it does not).
|
|
116
|
-
const deep = `${'> '.repeat(20_000)}# deep`;
|
|
117
|
-
expect(nestHeadings(deep)).toBe(`${'> '.repeat(20_000)}### deep`);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('turns a setext heading into the ATX heading one level down, inside a quote too, and leaves a thematic break alone', () => {
|
|
121
|
-
expect(nestHeadings('> Quoted\n> ---\ntext\n\n---\n\nAfter\n===')).toBe('> ### Quoted\ntext\n\n---\n\n### After');
|
|
122
|
-
// Serialized from the parsed content: a quote's prefix on a continuation
|
|
123
|
-
// line, a hard line break, a leading # that is not an ATX marker, a
|
|
124
|
-
// trailing # run that would read as a closing sequence, inline markup.
|
|
125
|
-
expect(nestHeadings('> A\n> B\n> ---')).toBe('> ### A B');
|
|
126
|
-
expect(nestHeadings('> > A\n> > B\n> > ===')).toBe('> > ### A B');
|
|
127
|
-
expect(nestHeadings('Title\\\ntwo\n---')).toBe('### Title two');
|
|
128
|
-
expect(nestHeadings('#1 priority\n===')).toBe('### #1 priority');
|
|
129
|
-
expect(nestHeadings('Title ##\n---')).toBe('### Title #\\#');
|
|
130
|
-
expect(nestHeadings('*em* and `code` [l](u)\n===')).toBe('### *em* and `code` [l](u)');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('reads containers as CommonMark does: a fence in a list item, headings in list items and quotes, a setext heading', () => {
|
|
134
|
-
const body = [
|
|
135
|
-
'- ```md',
|
|
136
|
-
' # literal inside a fence that a list item opened',
|
|
137
|
-
' ```',
|
|
138
|
-
'- ## in a list item',
|
|
139
|
-
'> ## quoted',
|
|
140
|
-
'> # indented code inside a quote',
|
|
141
|
-
'Setext',
|
|
142
|
-
'======',
|
|
143
|
-
'Two line',
|
|
144
|
-
'setext two',
|
|
145
|
-
'---',
|
|
146
|
-
'## atx after',
|
|
147
|
-
].join('\n');
|
|
148
|
-
|
|
149
|
-
expect(nestHeadings(body)).toBe(
|
|
150
|
-
[
|
|
151
|
-
'- ```md',
|
|
152
|
-
' # literal inside a fence that a list item opened',
|
|
153
|
-
' ```',
|
|
154
|
-
'- ### in a list item',
|
|
155
|
-
'> ### quoted',
|
|
156
|
-
'> # indented code inside a quote',
|
|
157
|
-
'### Setext',
|
|
158
|
-
'### Two line setext two',
|
|
159
|
-
'### atx after',
|
|
160
|
-
].join('\n')
|
|
161
|
-
);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("nests a section body's own headings under the section heading, leaving fenced code alone", () => {
|
|
165
|
-
const rendered = renderDocument({
|
|
166
|
-
name: 'Threat Model',
|
|
167
|
-
type: 'threat_model',
|
|
168
|
-
data: {
|
|
169
|
-
sections: {
|
|
170
|
-
overview: {
|
|
171
|
-
content: [
|
|
172
|
-
'# Overview',
|
|
173
|
-
'Text.',
|
|
174
|
-
'## Attack vectors',
|
|
175
|
-
'##### Deep',
|
|
176
|
-
'###### Deepest stays',
|
|
177
|
-
'```md',
|
|
178
|
-
'# not a heading',
|
|
179
|
-
'```',
|
|
180
|
-
'~~~',
|
|
181
|
-
'## nor this',
|
|
182
|
-
'~~~',
|
|
183
|
-
'## After the fences',
|
|
184
|
-
'#hashtag is not a heading',
|
|
185
|
-
].join('\n'),
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
expect(rendered.text).toBe(
|
|
192
|
-
[
|
|
193
|
-
'# Threat Model',
|
|
194
|
-
'',
|
|
195
|
-
'type: threat_model',
|
|
196
|
-
'',
|
|
197
|
-
'## overview',
|
|
198
|
-
'',
|
|
199
|
-
'### Overview',
|
|
200
|
-
'Text.',
|
|
201
|
-
'### Attack vectors',
|
|
202
|
-
'###### Deep',
|
|
203
|
-
'###### Deepest stays',
|
|
204
|
-
'```md',
|
|
205
|
-
'# not a heading',
|
|
206
|
-
'```',
|
|
207
|
-
'~~~',
|
|
208
|
-
'## nor this',
|
|
209
|
-
'~~~',
|
|
210
|
-
'### After the fences',
|
|
211
|
-
'#hashtag is not a heading',
|
|
212
|
-
'',
|
|
213
|
-
].join('\n')
|
|
214
|
-
);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('carries what sits beside the sections as JSON, so nothing is lost', () => {
|
|
218
|
-
const rendered = renderDocument({
|
|
219
|
-
name: 'Design Standards',
|
|
220
|
-
type: 'design_standards',
|
|
221
|
-
data: { sections: { summary: { content: 'Colors.' } }, tokens: { primary: '#000' } },
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
expect(rendered.text).toContain('## Other data\n\n````json\n{\n "tokens": {\n "primary": "#000"\n }\n}\n````');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('describes large data beside the sections instead of inlining it, and skips empty keys', () => {
|
|
228
|
-
const rendered = renderDocument({
|
|
229
|
-
name: 'Design Standards',
|
|
230
|
-
type: 'design_standards',
|
|
231
|
-
data: { sections: { summary: { content: 'Colors.' } }, tokens: null, components: { big: 'x'.repeat(25_000) } },
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
expect(rendered.text).toContain('## Other data');
|
|
235
|
-
expect(rendered.text).toContain(
|
|
236
|
-
'Beside its sections this document carries components (25010 characters); read them with the get tool as JSON.'
|
|
237
|
-
);
|
|
238
|
-
expect(rendered.text).not.toContain('tokens');
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('hands over a document that is not a section map as JSON', () => {
|
|
242
|
-
const doc = { type: 'ui_direction', version: 1, data: { picks: { palette: 'ocean' } } };
|
|
243
|
-
|
|
244
|
-
const rendered = renderDocument(doc);
|
|
245
|
-
|
|
246
|
-
expect(rendered.mimeType).toBe('application/json');
|
|
247
|
-
expect(JSON.parse(rendered.text)).toEqual(doc);
|
|
248
|
-
});
|
|
249
|
-
});
|
package/src/tools/markdown.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
2
|
-
import { toMarkdown } from 'mdast-util-to-markdown';
|
|
3
|
-
import type { Heading, Node, Parent, PhrasingContent } from 'mdast';
|
|
4
|
-
import { isRecord, isSectionObject, sectionContent, sectionsOf, type StoredDocument } from './sections.js';
|
|
5
|
-
|
|
6
|
-
/** A document as rendered: its data, and the facts its title and header line name. */
|
|
7
|
-
interface RenderedDocument extends StoredDocument {
|
|
8
|
-
name?: unknown;
|
|
9
|
-
type?: unknown;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* A document as text an IDE can attach: the name as the title, one line of
|
|
14
|
-
* facts, each section as a heading with its text. Policies, instructions and
|
|
15
|
-
* product documents are section maps; what a document carries beside its
|
|
16
|
-
* sections (design tokens, components) follows as JSON or, past a size, is
|
|
17
|
-
* described, and a document that is no section map at all is handed over as
|
|
18
|
-
* JSON.
|
|
19
|
-
*/
|
|
20
|
-
export function renderDocument(doc: RenderedDocument): { mimeType: string; text: string } {
|
|
21
|
-
const data = isRecord(doc.data) ? doc.data : null;
|
|
22
|
-
const sections = data ? sectionsOf(data) : null;
|
|
23
|
-
if (!data || !sections) return { mimeType: 'application/json', text: JSON.stringify(doc, null, 2) };
|
|
24
|
-
|
|
25
|
-
const title = typeof doc.name === 'string' ? doc.name : String(doc.type ?? 'Document');
|
|
26
|
-
const lines = [`# ${title}`];
|
|
27
|
-
const facts = (['type', 'origin', 'version', 'stage', 'derivationOutdated'] as const)
|
|
28
|
-
.filter((key) => doc[key] !== undefined && doc[key] !== null)
|
|
29
|
-
.map((key) => `${key}: ${String(doc[key])}`);
|
|
30
|
-
// Which sections are core is stated here, from metadata, not marked in
|
|
31
|
-
// the headings: a heading is text a section could forge, this line is not.
|
|
32
|
-
const core = Object.keys(sections).filter((id) => {
|
|
33
|
-
const section = sections[id];
|
|
34
|
-
return isSectionObject(section) && section.priority === 'core';
|
|
35
|
-
});
|
|
36
|
-
if (core.length > 0) facts.push(`core sections: ${core.join(', ')}`);
|
|
37
|
-
if (facts.length > 0) lines.push('', facts.join(' | '));
|
|
38
|
-
for (const id of Object.keys(sections)) {
|
|
39
|
-
const content = sectionContent(sections, id);
|
|
40
|
-
if (content === null) continue;
|
|
41
|
-
lines.push('', `## ${sectionTitle(sections[id], id)}`, '', nestHeadings(content.trim()));
|
|
42
|
-
}
|
|
43
|
-
const { sections: _sections, ...rest } = data;
|
|
44
|
-
const other = Object.fromEntries(Object.entries(rest).filter(([, value]) => !isEmpty(value)));
|
|
45
|
-
if (Object.keys(other).length > 0) {
|
|
46
|
-
const json = JSON.stringify(other, null, 2);
|
|
47
|
-
lines.push('', '## Other data', '');
|
|
48
|
-
if (json.length > OTHER_DATA_LIMIT) {
|
|
49
|
-
// Design tokens and component sources run to hundreds of kilobytes;
|
|
50
|
-
// an attachment is not the place for them.
|
|
51
|
-
const keys = Object.entries(other).map(([key, value]) => `${key} (${JSON.stringify(value).length} characters)`);
|
|
52
|
-
lines.push(`Beside its sections this document carries ${keys.join(', ')}; read them with the get tool as JSON.`);
|
|
53
|
-
} else {
|
|
54
|
-
lines.push(fence('json', json));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return { mimeType: 'text/markdown', text: `${lines.join('\n')}\n` };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Past this many characters, data beside the sections is described, not inlined. */
|
|
61
|
-
const OTHER_DATA_LIMIT = 20_000;
|
|
62
|
-
|
|
63
|
-
const isEmpty = (value: unknown) =>
|
|
64
|
-
value === null ||
|
|
65
|
-
value === undefined ||
|
|
66
|
-
(Array.isArray(value) && value.length === 0) ||
|
|
67
|
-
(isRecord(value) && Object.keys(value).length === 0);
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* A section body's own headings, one level down, so they sit under the
|
|
71
|
-
* section heading instead of beside it: a derived document writes each
|
|
72
|
-
* section with its own H2. A CommonMark parser says what is a heading, so
|
|
73
|
-
* fenced code, indented code, list items and block quotes are read as a
|
|
74
|
-
* reader's renderer reads them, and a heading-shaped line inside an HTML
|
|
75
|
-
* block is HTML, not a heading, and stays. Every heading lands at least one
|
|
76
|
-
* level under the section's H2, so an H1 and an H2 both become H3 and the
|
|
77
|
-
* rest move one down, H6 at most: an ATX heading (the # kind, an empty one
|
|
78
|
-
* included) takes the marks it needs; a setext heading (underlined)
|
|
79
|
-
* becomes the ATX heading of that depth, serialized from its parsed
|
|
80
|
-
* content (so a quote's prefix, a hard line break or a trailing # run are
|
|
81
|
-
* handled as Markdown, not as text). The edits go by the parser's
|
|
82
|
-
* character offsets, so no line counting of ours can disagree with its own.
|
|
83
|
-
*/
|
|
84
|
-
export function nestHeadings(body: string): string {
|
|
85
|
-
const edits: { start: number; end: number; text: string }[] = [];
|
|
86
|
-
// An explicit stack, and no spread into push: a body of thousands of
|
|
87
|
-
// nested quotes, or a list of very many items, must not overflow a walk.
|
|
88
|
-
const pending: Node[] = [fromMarkdown(body)];
|
|
89
|
-
while (pending.length > 0) {
|
|
90
|
-
const node = pending.pop()!;
|
|
91
|
-
if (
|
|
92
|
-
node.type === 'heading' &&
|
|
93
|
-
node.position?.start.offset !== undefined &&
|
|
94
|
-
node.position.end.offset !== undefined
|
|
95
|
-
) {
|
|
96
|
-
const start = node.position.start.offset;
|
|
97
|
-
const end = node.position.end.offset;
|
|
98
|
-
const heading = node as Heading;
|
|
99
|
-
const target = nestedDepth(heading.depth);
|
|
100
|
-
const atx = /^#{1,6}(\s|$)/.test(body.slice(start, start + 7));
|
|
101
|
-
if (atx && target > heading.depth) edits.push({ start, end: start, text: '#'.repeat(target - heading.depth) });
|
|
102
|
-
else if (!atx) edits.push({ start, end, text: atxOf(target, heading.children) });
|
|
103
|
-
}
|
|
104
|
-
if ('children' in node) for (const child of (node as Parent).children) pending.push(child);
|
|
105
|
-
}
|
|
106
|
-
let out = body;
|
|
107
|
-
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
108
|
-
out = `${out.slice(0, edit.start)}${edit.text}${out.slice(edit.end)}`;
|
|
109
|
-
}
|
|
110
|
-
return out;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Under the section's H2: at least H3, one level down otherwise, H6 at most. */
|
|
114
|
-
const nestedDepth = (depth: number): Heading['depth'] => Math.min(6, Math.max(3, depth + 1)) as Heading['depth'];
|
|
115
|
-
|
|
116
|
-
/** An ATX heading of this depth from parsed content, its line breaks turned into spaces: an ATX heading is one line. */
|
|
117
|
-
function atxOf(depth: Heading['depth'], children: PhrasingContent[]): string {
|
|
118
|
-
const oneLine = (nodes: PhrasingContent[]): PhrasingContent[] =>
|
|
119
|
-
nodes.map((child) => {
|
|
120
|
-
if (child.type === 'break') return { type: 'text', value: ' ' };
|
|
121
|
-
if (child.type === 'text') return { ...child, value: child.value.replace(/\r\n|\r|\n/g, ' ') };
|
|
122
|
-
return 'children' in child ? { ...child, children: oneLine(child.children as PhrasingContent[]) } : child;
|
|
123
|
-
});
|
|
124
|
-
return toMarkdown({ type: 'heading', depth, children: oneLine(children) }).trimEnd();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** The section's title when it carries one, else its id. */
|
|
128
|
-
function sectionTitle(section: unknown, id: string): string {
|
|
129
|
-
return isSectionObject(section) && typeof section.title === 'string' && section.title.trim()
|
|
130
|
-
? section.title.trim()
|
|
131
|
-
: id;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** The mime type a read will answer with, for a list entry that must not disagree with it. */
|
|
135
|
-
export function mimeTypeOf(doc: StoredDocument): string {
|
|
136
|
-
const data = isRecord(doc.data) ? doc.data : null;
|
|
137
|
-
return data && sectionsOf(data) ? 'text/markdown' : 'application/json';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* A fence the body cannot close: one backtick longer than the longest run
|
|
142
|
-
* inside it, four at least.
|
|
143
|
-
*/
|
|
144
|
-
export function fence(lang: string, body: string): string {
|
|
145
|
-
// A loop, not a spread into Math.max: a document with very many backtick
|
|
146
|
-
// runs would exceed the argument limit and throw.
|
|
147
|
-
let longest = 0;
|
|
148
|
-
for (const match of body.matchAll(/`+/g)) longest = Math.max(longest, match[0].length);
|
|
149
|
-
const marks = '`'.repeat(Math.max(4, longest + 1));
|
|
150
|
-
return `${marks}${lang}\n${body.trim()}\n${marks}`;
|
|
151
|
-
}
|