@ctcstack/ctcdocs-core 0.20.0 → 0.21.0
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/dist/published-markdown.js +67 -3
- package/package.json +1 -1
- package/src/published-markdown.ts +72 -4
|
@@ -4,9 +4,13 @@ import remarkParse from 'remark-parse';
|
|
|
4
4
|
import remarkStringify from 'remark-stringify';
|
|
5
5
|
import { unified } from 'unified';
|
|
6
6
|
import { markdownProjectionPath, resolvePermanentLink, } from './project-layout.js';
|
|
7
|
+
/*
|
|
8
|
+
* Tables are written without padding: one long cell would otherwise pad every
|
|
9
|
+
* row of its column, and an assistant would read the spaces (ADR-046).
|
|
10
|
+
*/
|
|
7
11
|
const processor = unified()
|
|
8
12
|
.use(remarkParse)
|
|
9
|
-
.use(remarkGfm)
|
|
13
|
+
.use(remarkGfm, { tablePipeAlign: false })
|
|
10
14
|
.use(remarkStringify, {
|
|
11
15
|
bullet: '-',
|
|
12
16
|
emphasis: '*',
|
|
@@ -43,8 +47,68 @@ function markdownUrl(value, stableSlugs, permanentLinks) {
|
|
|
43
47
|
}
|
|
44
48
|
return `${markdownProjectionPath(slug)}${url.search}${url.hash}`;
|
|
45
49
|
}
|
|
46
|
-
|
|
50
|
+
/** A table longer than this is split into parts (ADR-046). */
|
|
51
|
+
const SPLIT_TABLE_CHARACTERS = 3_000;
|
|
52
|
+
/** About as long as each part of a split table is, header included. */
|
|
53
|
+
const TABLE_PART_CHARACTERS = 2_000;
|
|
54
|
+
function textLength(node) {
|
|
55
|
+
if ('value' in node) {
|
|
56
|
+
return node.value.length;
|
|
57
|
+
}
|
|
58
|
+
if ('children' in node) {
|
|
59
|
+
return node.children.reduce((sum, child) => sum + textLength(child), 0);
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
/** About how many characters a table row takes, with its pipes. */
|
|
64
|
+
function rowLength(row) {
|
|
65
|
+
return row.children.reduce((sum, cell) => sum + textLength(cell) + 3, 2);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Splits each long table into consecutive tables that repeat its header row,
|
|
69
|
+
* so that a passage search returns from the middle of one still says what each
|
|
70
|
+
* column is (ADR-046). A table that fits stays whole.
|
|
71
|
+
*/
|
|
72
|
+
function splitLongTables(node) {
|
|
73
|
+
const children = [];
|
|
74
|
+
for (const child of node.children) {
|
|
75
|
+
if ('children' in child && child.type !== 'table') {
|
|
76
|
+
splitLongTables(child);
|
|
77
|
+
}
|
|
78
|
+
if (child.type !== 'table') {
|
|
79
|
+
children.push(child);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const [header, ...rows] = child.children;
|
|
83
|
+
const lengths = child.children.map(rowLength);
|
|
84
|
+
const total = lengths.reduce((sum, length) => sum + length, 0);
|
|
85
|
+
if (!header || rows.length < 2 || total <= SPLIT_TABLE_CHARACTERS) {
|
|
86
|
+
children.push(child);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const headerLength = lengths[0] ?? 0;
|
|
90
|
+
let part = [];
|
|
91
|
+
let partLength = headerLength;
|
|
92
|
+
const flush = () => {
|
|
93
|
+
children.push({ ...child, children: [header, ...part] });
|
|
94
|
+
part = [];
|
|
95
|
+
partLength = headerLength;
|
|
96
|
+
};
|
|
97
|
+
rows.forEach((row, index) => {
|
|
98
|
+
const length = lengths[index + 1] ?? 0;
|
|
99
|
+
if (part.length > 0 && partLength + length > TABLE_PART_CHARACTERS) {
|
|
100
|
+
flush();
|
|
101
|
+
}
|
|
102
|
+
part.push(row);
|
|
103
|
+
partLength += length;
|
|
104
|
+
});
|
|
105
|
+
flush();
|
|
106
|
+
}
|
|
107
|
+
node.children = children;
|
|
108
|
+
}
|
|
109
|
+
function projectBody(body, stableSlugs, permanentLinks) {
|
|
47
110
|
const tree = processor.parse(body);
|
|
111
|
+
splitLongTables(tree);
|
|
48
112
|
walk(tree, (node) => {
|
|
49
113
|
if (node.type === 'link') {
|
|
50
114
|
node.url = markdownUrl(node.url, stableSlugs, permanentLinks) ?? node.url;
|
|
@@ -104,7 +168,7 @@ export function inlineMarkdown(value) {
|
|
|
104
168
|
* `fetch` (ADR-042), so it is also what the sync measures (ADR-043).
|
|
105
169
|
*/
|
|
106
170
|
export function publishedMarkdownBody(input) {
|
|
107
|
-
const body =
|
|
171
|
+
const body = projectBody(cleanBody(input.body, input.ownershipHeader), input.stableSlugs, input.permanentLinks);
|
|
108
172
|
return [
|
|
109
173
|
`# ${inlineMarkdown(input.title)}`,
|
|
110
174
|
'',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctcstack/ctcdocs-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Configuration schema, project layout, generated-path allowlist and the published Markdown form shared by the CTCDocs platform packages.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ctcdocs",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Root, RootContent } from 'mdast';
|
|
1
|
+
import type { Parents, Root, RootContent, Table } from 'mdast';
|
|
2
2
|
import * as cheerio from 'cheerio';
|
|
3
3
|
import remarkGfm from 'remark-gfm';
|
|
4
4
|
import remarkParse from 'remark-parse';
|
|
@@ -10,9 +10,13 @@ import {
|
|
|
10
10
|
resolvePermanentLink,
|
|
11
11
|
} from './project-layout.js';
|
|
12
12
|
|
|
13
|
+
/*
|
|
14
|
+
* Tables are written without padding: one long cell would otherwise pad every
|
|
15
|
+
* row of its column, and an assistant would read the spaces (ADR-046).
|
|
16
|
+
*/
|
|
13
17
|
const processor = unified()
|
|
14
18
|
.use(remarkParse)
|
|
15
|
-
.use(remarkGfm)
|
|
19
|
+
.use(remarkGfm, { tablePipeAlign: false })
|
|
16
20
|
.use(remarkStringify, {
|
|
17
21
|
bullet: '-',
|
|
18
22
|
emphasis: '*',
|
|
@@ -83,12 +87,76 @@ function markdownUrl(
|
|
|
83
87
|
return `${markdownProjectionPath(slug)}${url.search}${url.hash}`;
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
|
|
90
|
+
/** A table longer than this is split into parts (ADR-046). */
|
|
91
|
+
const SPLIT_TABLE_CHARACTERS = 3_000;
|
|
92
|
+
/** About as long as each part of a split table is, header included. */
|
|
93
|
+
const TABLE_PART_CHARACTERS = 2_000;
|
|
94
|
+
|
|
95
|
+
function textLength(node: Root | RootContent): number {
|
|
96
|
+
if ('value' in node) {
|
|
97
|
+
return node.value.length;
|
|
98
|
+
}
|
|
99
|
+
if ('children' in node) {
|
|
100
|
+
return node.children.reduce((sum, child) => sum + textLength(child), 0);
|
|
101
|
+
}
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** About how many characters a table row takes, with its pipes. */
|
|
106
|
+
function rowLength(row: Table['children'][number]): number {
|
|
107
|
+
return row.children.reduce((sum, cell) => sum + textLength(cell) + 3, 2);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Splits each long table into consecutive tables that repeat its header row,
|
|
112
|
+
* so that a passage search returns from the middle of one still says what each
|
|
113
|
+
* column is (ADR-046). A table that fits stays whole.
|
|
114
|
+
*/
|
|
115
|
+
function splitLongTables(node: Parents): void {
|
|
116
|
+
const children: RootContent[] = [];
|
|
117
|
+
for (const child of node.children) {
|
|
118
|
+
if ('children' in child && child.type !== 'table') {
|
|
119
|
+
splitLongTables(child);
|
|
120
|
+
}
|
|
121
|
+
if (child.type !== 'table') {
|
|
122
|
+
children.push(child);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const [header, ...rows] = child.children;
|
|
126
|
+
const lengths = child.children.map(rowLength);
|
|
127
|
+
const total = lengths.reduce((sum, length) => sum + length, 0);
|
|
128
|
+
if (!header || rows.length < 2 || total <= SPLIT_TABLE_CHARACTERS) {
|
|
129
|
+
children.push(child);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const headerLength = lengths[0] ?? 0;
|
|
133
|
+
let part: Table['children'] = [];
|
|
134
|
+
let partLength = headerLength;
|
|
135
|
+
const flush = () => {
|
|
136
|
+
children.push({ ...child, children: [header, ...part] });
|
|
137
|
+
part = [];
|
|
138
|
+
partLength = headerLength;
|
|
139
|
+
};
|
|
140
|
+
rows.forEach((row, index) => {
|
|
141
|
+
const length = lengths[index + 1] ?? 0;
|
|
142
|
+
if (part.length > 0 && partLength + length > TABLE_PART_CHARACTERS) {
|
|
143
|
+
flush();
|
|
144
|
+
}
|
|
145
|
+
part.push(row);
|
|
146
|
+
partLength += length;
|
|
147
|
+
});
|
|
148
|
+
flush();
|
|
149
|
+
}
|
|
150
|
+
node.children = children as Parents['children'];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function projectBody(
|
|
87
154
|
body: string,
|
|
88
155
|
stableSlugs: ReadonlySet<string>,
|
|
89
156
|
permanentLinks: Readonly<Record<string, string>>,
|
|
90
157
|
): string {
|
|
91
158
|
const tree = processor.parse(body) as Root;
|
|
159
|
+
splitLongTables(tree);
|
|
92
160
|
walk(tree, (node) => {
|
|
93
161
|
if (node.type === 'link') {
|
|
94
162
|
node.url = markdownUrl(node.url, stableSlugs, permanentLinks) ?? node.url;
|
|
@@ -164,7 +232,7 @@ export function publishedMarkdownBody(
|
|
|
164
232
|
'title' | 'ownershipHeader' | 'body' | 'stableSlugs' | 'permanentLinks'
|
|
165
233
|
>,
|
|
166
234
|
): string {
|
|
167
|
-
const body =
|
|
235
|
+
const body = projectBody(
|
|
168
236
|
cleanBody(input.body, input.ownershipHeader),
|
|
169
237
|
input.stableSlugs,
|
|
170
238
|
input.permanentLinks,
|