@diplodoc/folding-headings-extension 0.2.0 → 0.2.2
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 +4 -0
- package/build/common/const.js +9 -0
- package/build/plugin/const.js +13 -0
- package/build/plugin/headingBlockRule.js +59 -0
- package/build/plugin/index.js +2 -308
- package/build/plugin/plugin.js +19 -0
- package/build/plugin/sectionsCoreRule.js +104 -0
- package/build/plugin/transform.js +53 -0
- package/build/plugin/utils.js +44 -0
- package/build/runtime/const.js +10 -0
- package/build/runtime/controller.js +55 -0
- package/build/runtime/index.js +6 -2
- package/build/runtime/utils.js +8 -0
- package/package.json +9 -9
- package/build/test/data.d.ts +0 -147
- package/build/test/plugin.spec.d.ts +0 -1
- /package/build/{src/common → common}/const.d.ts +0 -0
- /package/build/{src/plugin → plugin}/const.d.ts +0 -0
- /package/build/{src/plugin → plugin}/headingBlockRule.d.ts +0 -0
- /package/build/{src/plugin → plugin}/index.d.ts +0 -0
- /package/build/{src/plugin → plugin}/plugin.d.ts +0 -0
- /package/build/{src/plugin → plugin}/sectionsCoreRule.d.ts +0 -0
- /package/build/{src/plugin → plugin}/transform.d.ts +0 -0
- /package/build/{src/plugin → plugin}/utils.d.ts +0 -0
- /package/build/{src/runtime → runtime}/const.d.ts +0 -0
- /package/build/{src/runtime → runtime}/controller.d.ts +0 -0
- /package/build/{src/runtime → runtime}/index.d.ts +0 -0
- /package/build/{src/runtime → runtime}/utils.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
**english** | [русский](https://github.com/diplodoc-platform/folding-headings-extension/blob/master/README.ru.md)
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
[](https://www.npmjs.org/package/@diplodoc/folding-headings-extension)
|
|
2
6
|
[](https://sonarcloud.io/summary/overall?id=diplodoc-platform_folding-headings-extension)
|
|
3
7
|
[](https://sonarcloud.io/summary/overall?id=diplodoc-platform_folding-headings-extension)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { SectionCN, DATA_KEY, SectionAttr } from '../common/const';
|
|
2
|
+
export const ENV_FLAG_NAME = 'has-folding-headings';
|
|
3
|
+
export const TokenType = {
|
|
4
|
+
Heading: 'heading',
|
|
5
|
+
HeadingOpen: 'heading_open',
|
|
6
|
+
HeadingClose: 'heading_close',
|
|
7
|
+
Section: 'heading_section',
|
|
8
|
+
SectionOpen: 'heading_section_open',
|
|
9
|
+
SectionClose: 'heading_section_close',
|
|
10
|
+
Content: 'heading_section_content',
|
|
11
|
+
ContentOpen: 'heading_section_content_open',
|
|
12
|
+
ContentClose: 'heading_section_content_close',
|
|
13
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { TokenType } from './const';
|
|
2
|
+
// copied from https://github.com/markdown-it/markdown-it/blob/14.1.0/lib/rules_block/heading.mjs
|
|
3
|
+
// modified: support syntax ###+ for folding headings
|
|
4
|
+
export const headingBlockRule = (state, startLine, _endLine, silent) => {
|
|
5
|
+
const { isSpace } = state.md.utils;
|
|
6
|
+
let pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
7
|
+
let max = state.eMarks[startLine];
|
|
8
|
+
// if it's indented more than 3 spaces, it should be a code block
|
|
9
|
+
if (state.sCount[startLine] - state.blkIndent >= 4) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
let ch = state.src.charCodeAt(pos);
|
|
13
|
+
if (ch !== 0x23 /* # */ || pos >= max) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
// count heading level
|
|
17
|
+
let level = 1;
|
|
18
|
+
ch = state.src.charCodeAt(++pos);
|
|
19
|
+
while (ch === 0x23 /* # */ && pos < max && level <= 6) {
|
|
20
|
+
level++;
|
|
21
|
+
ch = state.src.charCodeAt(++pos);
|
|
22
|
+
}
|
|
23
|
+
let folding = false;
|
|
24
|
+
if (ch === 0x2b /* + */) {
|
|
25
|
+
folding = true;
|
|
26
|
+
ch = state.src.charCodeAt(++pos);
|
|
27
|
+
}
|
|
28
|
+
if (level > 6 || (pos < max && !isSpace(ch))) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (silent) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
// Let's cut tails like ' ### ' from the end of string
|
|
35
|
+
max = state.skipSpacesBack(max, pos);
|
|
36
|
+
const tmp = state.skipCharsBack(max, 0x23, pos); // #
|
|
37
|
+
if (tmp > pos && isSpace(state.src.charCodeAt(tmp - 1))) {
|
|
38
|
+
max = tmp;
|
|
39
|
+
}
|
|
40
|
+
state.line = startLine + 1;
|
|
41
|
+
let token = state.push(TokenType.HeadingOpen, 'h' + String(level), 1);
|
|
42
|
+
token.markup = '########'.slice(0, level) + (folding ? '+' : '');
|
|
43
|
+
token.map = [startLine, state.line];
|
|
44
|
+
if (folding) {
|
|
45
|
+
token.meta ||= {};
|
|
46
|
+
token.meta.folding = true;
|
|
47
|
+
}
|
|
48
|
+
token = state.push('inline', '', 0);
|
|
49
|
+
token.content = state.src.slice(pos, max).trim();
|
|
50
|
+
token.map = [startLine, state.line];
|
|
51
|
+
token.children = [];
|
|
52
|
+
token = state.push(TokenType.HeadingClose, 'h' + String(level), -1);
|
|
53
|
+
token.markup = '########'.slice(0, level) + (folding ? '+' : '');
|
|
54
|
+
if (folding) {
|
|
55
|
+
token.meta ||= {};
|
|
56
|
+
token.meta.folding = true;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
};
|
package/build/plugin/index.js
CHANGED
|
@@ -1,308 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/plugin/index.ts
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
TokenType: () => TokenType,
|
|
24
|
-
transform: () => transform
|
|
25
|
-
});
|
|
26
|
-
module.exports = __toCommonJS(index_exports);
|
|
27
|
-
|
|
28
|
-
// src/common/const.ts
|
|
29
|
-
var DATA_KEY = "heading-section";
|
|
30
|
-
var SectionAttr = {
|
|
31
|
-
DataId: "data-diplodoc-id",
|
|
32
|
-
DataKey: "data-diplodoc-key"
|
|
33
|
-
};
|
|
34
|
-
var SectionCN = {
|
|
35
|
-
Section: "heading-section",
|
|
36
|
-
Content: "heading-section-content"
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// src/plugin/const.ts
|
|
40
|
-
var ENV_FLAG_NAME = "has-folding-headings";
|
|
41
|
-
var TokenType = {
|
|
42
|
-
Heading: "heading",
|
|
43
|
-
HeadingOpen: "heading_open",
|
|
44
|
-
HeadingClose: "heading_close",
|
|
45
|
-
Section: "heading_section",
|
|
46
|
-
SectionOpen: "heading_section_open",
|
|
47
|
-
SectionClose: "heading_section_close",
|
|
48
|
-
Content: "heading_section_content",
|
|
49
|
-
ContentOpen: "heading_section_content_open",
|
|
50
|
-
ContentClose: "heading_section_content_close"
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// src/plugin/headingBlockRule.ts
|
|
54
|
-
var headingBlockRule = (state, startLine, _endLine, silent) => {
|
|
55
|
-
const { isSpace } = state.md.utils;
|
|
56
|
-
let pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
57
|
-
let max = state.eMarks[startLine];
|
|
58
|
-
if (state.sCount[startLine] - state.blkIndent >= 4) {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
let ch = state.src.charCodeAt(pos);
|
|
62
|
-
if (ch !== 35 || pos >= max) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
let level = 1;
|
|
66
|
-
ch = state.src.charCodeAt(++pos);
|
|
67
|
-
while (ch === 35 && pos < max && level <= 6) {
|
|
68
|
-
level++;
|
|
69
|
-
ch = state.src.charCodeAt(++pos);
|
|
70
|
-
}
|
|
71
|
-
let folding = false;
|
|
72
|
-
if (ch === 43) {
|
|
73
|
-
folding = true;
|
|
74
|
-
ch = state.src.charCodeAt(++pos);
|
|
75
|
-
}
|
|
76
|
-
if (level > 6 || pos < max && !isSpace(ch)) {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
if (silent) {
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
max = state.skipSpacesBack(max, pos);
|
|
83
|
-
const tmp = state.skipCharsBack(max, 35, pos);
|
|
84
|
-
if (tmp > pos && isSpace(state.src.charCodeAt(tmp - 1))) {
|
|
85
|
-
max = tmp;
|
|
86
|
-
}
|
|
87
|
-
state.line = startLine + 1;
|
|
88
|
-
let token = state.push(TokenType.HeadingOpen, "h" + String(level), 1);
|
|
89
|
-
token.markup = "########".slice(0, level) + (folding ? "+" : "");
|
|
90
|
-
token.map = [startLine, state.line];
|
|
91
|
-
if (folding) {
|
|
92
|
-
token.meta ||= {};
|
|
93
|
-
token.meta.folding = true;
|
|
94
|
-
}
|
|
95
|
-
token = state.push("inline", "", 0);
|
|
96
|
-
token.content = state.src.slice(pos, max).trim();
|
|
97
|
-
token.map = [startLine, state.line];
|
|
98
|
-
token.children = [];
|
|
99
|
-
token = state.push(TokenType.HeadingClose, "h" + String(level), -1);
|
|
100
|
-
token.markup = "########".slice(0, level) + (folding ? "+" : "");
|
|
101
|
-
if (folding) {
|
|
102
|
-
token.meta ||= {};
|
|
103
|
-
token.meta.folding = true;
|
|
104
|
-
}
|
|
105
|
-
return true;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// src/plugin/sectionsCoreRule.ts
|
|
109
|
-
var import_utils = require("@diplodoc/utils");
|
|
110
|
-
|
|
111
|
-
// src/plugin/utils.ts
|
|
112
|
-
function headingLevel(header) {
|
|
113
|
-
return parseInt(header.charAt(1), 10);
|
|
114
|
-
}
|
|
115
|
-
function last(arr) {
|
|
116
|
-
return arr[arr.length - 1];
|
|
117
|
-
}
|
|
118
|
-
function hidden(box, field, value) {
|
|
119
|
-
if (!(field in box)) {
|
|
120
|
-
Object.defineProperty(box, field, {
|
|
121
|
-
enumerable: false,
|
|
122
|
-
value
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
return box;
|
|
126
|
-
}
|
|
127
|
-
function copyRuntime({ runtime, output }, cache) {
|
|
128
|
-
const PATH_TO_RUNTIME = "../runtime";
|
|
129
|
-
const { join, resolve } = dynrequire("node:path");
|
|
130
|
-
const runtimeFiles = {
|
|
131
|
-
"index.js": runtime.script,
|
|
132
|
-
"index.css": runtime.style
|
|
133
|
-
};
|
|
134
|
-
for (const [originFile, outputFile] of Object.entries(runtimeFiles)) {
|
|
135
|
-
const file = join(PATH_TO_RUNTIME, originFile);
|
|
136
|
-
if (!cache.has(file)) {
|
|
137
|
-
cache.add(file);
|
|
138
|
-
copy(resolve(__dirname, file), join(output, outputFile));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
function copy(from, to) {
|
|
143
|
-
const { mkdirSync, copyFileSync } = dynrequire("node:fs");
|
|
144
|
-
const { dirname } = dynrequire("node:path");
|
|
145
|
-
mkdirSync(dirname(to), { recursive: true });
|
|
146
|
-
copyFileSync(from, to);
|
|
147
|
-
}
|
|
148
|
-
function dynrequire(module) {
|
|
149
|
-
return eval(`require('${module}')`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// src/plugin/sectionsCoreRule.ts
|
|
153
|
-
function createSectionsCoreRule(externalGenerateID) {
|
|
154
|
-
const generateID = externalGenerateID ?? (0, import_utils.createIDGeneratorByStrategy)("random");
|
|
155
|
-
return (state) => {
|
|
156
|
-
const Token = state.Token;
|
|
157
|
-
const tokens = [];
|
|
158
|
-
const sections = [];
|
|
159
|
-
let nestedLevel = 0;
|
|
160
|
-
for (const token of state.tokens) {
|
|
161
|
-
if (token.type.search(TokenType.Heading) !== 0) {
|
|
162
|
-
nestedLevel += token.nesting;
|
|
163
|
-
}
|
|
164
|
-
if (last(sections) && nestedLevel < last(sections).nesting) {
|
|
165
|
-
closeSectionsToCurrentNesting(nestedLevel);
|
|
166
|
-
}
|
|
167
|
-
const hasFolding = token.markup.endsWith("#+");
|
|
168
|
-
if (token.type === TokenType.HeadingOpen) {
|
|
169
|
-
const section = {
|
|
170
|
-
header: headingLevel(token.tag),
|
|
171
|
-
nesting: nestedLevel
|
|
172
|
-
};
|
|
173
|
-
closeSections(section);
|
|
174
|
-
if (hasFolding) {
|
|
175
|
-
tokens.push(openSection());
|
|
176
|
-
sections.push(section);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
tokens.push(token);
|
|
180
|
-
if (token.type === TokenType.HeadingClose && hasFolding) {
|
|
181
|
-
tokens.push(openContent());
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
closeAllSections();
|
|
185
|
-
if (state.tokens.length !== tokens.length) {
|
|
186
|
-
state.env ??= {};
|
|
187
|
-
state.env[ENV_FLAG_NAME] = true;
|
|
188
|
-
}
|
|
189
|
-
state.tokens = tokens;
|
|
190
|
-
function openSection() {
|
|
191
|
-
const t = new Token(TokenType.SectionOpen, "section", 1);
|
|
192
|
-
t.attrPush(["class", SectionCN.Section]);
|
|
193
|
-
t.attrPush([SectionAttr.DataKey, DATA_KEY]);
|
|
194
|
-
t.attrPush([SectionAttr.DataId, generateID(DATA_KEY)]);
|
|
195
|
-
t.block = true;
|
|
196
|
-
return t;
|
|
197
|
-
}
|
|
198
|
-
function closeSection() {
|
|
199
|
-
const t = new Token(TokenType.SectionClose, "section", -1);
|
|
200
|
-
t.block = true;
|
|
201
|
-
return t;
|
|
202
|
-
}
|
|
203
|
-
function openContent() {
|
|
204
|
-
const t = new Token(TokenType.ContentOpen, "div", 1);
|
|
205
|
-
t.attrPush(["class", SectionCN.Content]);
|
|
206
|
-
t.block = true;
|
|
207
|
-
return t;
|
|
208
|
-
}
|
|
209
|
-
function closeContent() {
|
|
210
|
-
const t = new Token(TokenType.ContentClose, "div", -1);
|
|
211
|
-
t.block = true;
|
|
212
|
-
return t;
|
|
213
|
-
}
|
|
214
|
-
function closeSections(section) {
|
|
215
|
-
while (last(sections) && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
216
|
-
section.header <= last(sections).header && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
217
|
-
section.nesting <= last(sections).nesting) {
|
|
218
|
-
sections.pop();
|
|
219
|
-
tokens.push(closeContent());
|
|
220
|
-
tokens.push(closeSection());
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
function closeSectionsToCurrentNesting(nesting) {
|
|
224
|
-
while (last(sections) && nesting < last(sections).nesting) {
|
|
225
|
-
sections.pop();
|
|
226
|
-
tokens.push(closeContent());
|
|
227
|
-
tokens.push(closeSection());
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
function closeAllSections() {
|
|
231
|
-
while (sections.pop()) {
|
|
232
|
-
tokens.push(closeContent());
|
|
233
|
-
tokens.push(closeSection());
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// src/plugin/plugin.ts
|
|
240
|
-
function createFoldingHeadingPlugin(generateID) {
|
|
241
|
-
return (md) => {
|
|
242
|
-
md.block.ruler.at("heading", headingBlockRule, {
|
|
243
|
-
alt: ["paragraph", "reference", "blockquote"]
|
|
244
|
-
});
|
|
245
|
-
md.core.ruler.push("heading_sections", createSectionsCoreRule(generateID));
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// src/plugin/transform.ts
|
|
250
|
-
var registerTransform = (md, {
|
|
251
|
-
runtime,
|
|
252
|
-
bundle,
|
|
253
|
-
output,
|
|
254
|
-
generateID
|
|
255
|
-
}) => {
|
|
256
|
-
md.use(createFoldingHeadingPlugin(generateID));
|
|
257
|
-
md.core.ruler.push("heading_sections_after", ({ env }) => {
|
|
258
|
-
hidden(env, "bundled", /* @__PURE__ */ new Set());
|
|
259
|
-
if (env?.[ENV_FLAG_NAME]) {
|
|
260
|
-
env.meta = env.meta || {};
|
|
261
|
-
env.meta.script = env.meta.script || [];
|
|
262
|
-
env.meta.script.push(runtime.script);
|
|
263
|
-
env.meta.style = env.meta.style || [];
|
|
264
|
-
env.meta.style.push(runtime.style);
|
|
265
|
-
if (bundle) {
|
|
266
|
-
copyRuntime({ runtime, output }, env.bundled);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
};
|
|
271
|
-
function transform(options = {}) {
|
|
272
|
-
const { bundle = true } = options;
|
|
273
|
-
if (bundle && typeof options.runtime === "string") {
|
|
274
|
-
throw new TypeError("Option `runtime` should be record when `bundle` is enabled.");
|
|
275
|
-
}
|
|
276
|
-
const runtime = typeof options.runtime === "string" ? { script: options.runtime, style: options.runtime } : options.runtime || {
|
|
277
|
-
script: "_assets/folding-headings-extension.js",
|
|
278
|
-
style: "_assets/folding-headings-extension.css"
|
|
279
|
-
};
|
|
280
|
-
const plugin = function(md, { output = ".", generateID } = {}) {
|
|
281
|
-
registerTransform(md, {
|
|
282
|
-
runtime,
|
|
283
|
-
bundle,
|
|
284
|
-
output,
|
|
285
|
-
generateID
|
|
286
|
-
});
|
|
287
|
-
};
|
|
288
|
-
Object.assign(plugin, {
|
|
289
|
-
collect(input, { destRoot = "." }) {
|
|
290
|
-
const MdIt = dynrequire("markdown-it");
|
|
291
|
-
const md = new MdIt().use((md2) => {
|
|
292
|
-
registerTransform(md2, {
|
|
293
|
-
runtime,
|
|
294
|
-
bundle,
|
|
295
|
-
output: destRoot
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
md.parse(input, {});
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
return plugin;
|
|
302
|
-
}
|
|
303
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
304
|
-
0 && (module.exports = {
|
|
305
|
-
TokenType,
|
|
306
|
-
transform
|
|
307
|
-
});
|
|
308
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
export { transform } from './transform';
|
|
2
|
+
export { TokenType } from './const';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { headingBlockRule } from './headingBlockRule';
|
|
2
|
+
import { createSectionsCoreRule } from './sectionsCoreRule';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a folding-heading plugin with an optional external ID generator.
|
|
5
|
+
* When `generateID` is provided, all generated section IDs share the same counter
|
|
6
|
+
* as other plugins, ensuring deterministic output regardless of --merge-includes.
|
|
7
|
+
* When omitted, the plugin uses its internal fallback generator.
|
|
8
|
+
*
|
|
9
|
+
* @param generateID - Optional external ID generator shared with the rest of the transform pipeline.
|
|
10
|
+
* @returns Markdown-it plugin that registers folding heading parsing and section wrapping rules.
|
|
11
|
+
*/
|
|
12
|
+
export function createFoldingHeadingPlugin(generateID) {
|
|
13
|
+
return (md) => {
|
|
14
|
+
md.block.ruler.at('heading', headingBlockRule, {
|
|
15
|
+
alt: ['paragraph', 'reference', 'blockquote'],
|
|
16
|
+
});
|
|
17
|
+
md.core.ruler.push('heading_sections', createSectionsCoreRule(generateID));
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createIDGeneratorByStrategy } from '@diplodoc/utils';
|
|
2
|
+
import { DATA_KEY, ENV_FLAG_NAME, SectionAttr, SectionCN, TokenType } from './const';
|
|
3
|
+
import { headingLevel, last } from './utils';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a core rule that wraps folding headings in `<section>` elements with unique IDs.
|
|
6
|
+
* @param externalGenerateID - Optional external ID generator (e.g. from MarkdownItPluginOpts).
|
|
7
|
+
* When provided, ensures all plugins share the same per-file generator.
|
|
8
|
+
* When omitted, uses random fallback behavior.
|
|
9
|
+
* @returns Markdown-it core rule that injects section wrapper tokens around folding headings.
|
|
10
|
+
*/
|
|
11
|
+
export function createSectionsCoreRule(externalGenerateID) {
|
|
12
|
+
// Use the external generator if provided, otherwise use random fallback.
|
|
13
|
+
// Random IDs are the default behavior for production builds.
|
|
14
|
+
const generateID = externalGenerateID ?? createIDGeneratorByStrategy('random');
|
|
15
|
+
return (state) => {
|
|
16
|
+
const Token = state.Token;
|
|
17
|
+
const tokens = []; // output
|
|
18
|
+
const sections = [];
|
|
19
|
+
let nestedLevel = 0;
|
|
20
|
+
for (const token of state.tokens) {
|
|
21
|
+
// record level of nesting
|
|
22
|
+
if (token.type.search(TokenType.Heading) !== 0) {
|
|
23
|
+
nestedLevel += token.nesting;
|
|
24
|
+
}
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
26
|
+
if (last(sections) && nestedLevel < last(sections).nesting) {
|
|
27
|
+
closeSectionsToCurrentNesting(nestedLevel);
|
|
28
|
+
}
|
|
29
|
+
const hasFolding = token.markup.endsWith('#+');
|
|
30
|
+
// add sections before headers
|
|
31
|
+
if (token.type === TokenType.HeadingOpen) {
|
|
32
|
+
const section = {
|
|
33
|
+
header: headingLevel(token.tag),
|
|
34
|
+
nesting: nestedLevel,
|
|
35
|
+
};
|
|
36
|
+
closeSections(section);
|
|
37
|
+
if (hasFolding) {
|
|
38
|
+
tokens.push(openSection());
|
|
39
|
+
sections.push(section);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
tokens.push(token);
|
|
43
|
+
if (token.type === TokenType.HeadingClose && hasFolding) {
|
|
44
|
+
tokens.push(openContent());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// end for every token
|
|
48
|
+
closeAllSections();
|
|
49
|
+
if (state.tokens.length !== tokens.length) {
|
|
50
|
+
state.env ??= {};
|
|
51
|
+
state.env[ENV_FLAG_NAME] = true;
|
|
52
|
+
}
|
|
53
|
+
state.tokens = tokens;
|
|
54
|
+
function openSection() {
|
|
55
|
+
const t = new Token(TokenType.SectionOpen, 'section', 1);
|
|
56
|
+
t.attrPush(['class', SectionCN.Section]);
|
|
57
|
+
t.attrPush([SectionAttr.DataKey, DATA_KEY]);
|
|
58
|
+
t.attrPush([SectionAttr.DataId, generateID(DATA_KEY)]);
|
|
59
|
+
t.block = true;
|
|
60
|
+
return t;
|
|
61
|
+
}
|
|
62
|
+
function closeSection() {
|
|
63
|
+
const t = new Token(TokenType.SectionClose, 'section', -1);
|
|
64
|
+
t.block = true;
|
|
65
|
+
return t;
|
|
66
|
+
}
|
|
67
|
+
function openContent() {
|
|
68
|
+
const t = new Token(TokenType.ContentOpen, 'div', 1);
|
|
69
|
+
t.attrPush(['class', SectionCN.Content]);
|
|
70
|
+
t.block = true;
|
|
71
|
+
return t;
|
|
72
|
+
}
|
|
73
|
+
function closeContent() {
|
|
74
|
+
const t = new Token(TokenType.ContentClose, 'div', -1);
|
|
75
|
+
t.block = true;
|
|
76
|
+
return t;
|
|
77
|
+
}
|
|
78
|
+
function closeSections(section) {
|
|
79
|
+
while (last(sections) &&
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
81
|
+
section.header <= last(sections).header &&
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
83
|
+
section.nesting <= last(sections).nesting) {
|
|
84
|
+
sections.pop();
|
|
85
|
+
tokens.push(closeContent());
|
|
86
|
+
tokens.push(closeSection());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function closeSectionsToCurrentNesting(nesting) {
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
91
|
+
while (last(sections) && nesting < last(sections).nesting) {
|
|
92
|
+
sections.pop();
|
|
93
|
+
tokens.push(closeContent());
|
|
94
|
+
tokens.push(closeSection());
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function closeAllSections() {
|
|
98
|
+
while (sections.pop()) {
|
|
99
|
+
tokens.push(closeContent());
|
|
100
|
+
tokens.push(closeSection());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ENV_FLAG_NAME } from './const';
|
|
2
|
+
import { createFoldingHeadingPlugin } from './plugin';
|
|
3
|
+
import { copyRuntime, dynrequire, hidden } from './utils';
|
|
4
|
+
const registerTransform = (md, { runtime, bundle, output, generateID, }) => {
|
|
5
|
+
md.use(createFoldingHeadingPlugin(generateID));
|
|
6
|
+
md.core.ruler.push('heading_sections_after', ({ env }) => {
|
|
7
|
+
hidden(env, 'bundled', new Set());
|
|
8
|
+
if (env?.[ENV_FLAG_NAME]) {
|
|
9
|
+
env.meta = env.meta || {};
|
|
10
|
+
env.meta.script = env.meta.script || [];
|
|
11
|
+
env.meta.script.push(runtime.script);
|
|
12
|
+
env.meta.style = env.meta.style || [];
|
|
13
|
+
env.meta.style.push(runtime.style);
|
|
14
|
+
if (bundle) {
|
|
15
|
+
copyRuntime({ runtime, output }, env.bundled);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
export function transform(options = {}) {
|
|
21
|
+
const { bundle = true } = options;
|
|
22
|
+
if (bundle && typeof options.runtime === 'string') {
|
|
23
|
+
throw new TypeError('Option `runtime` should be record when `bundle` is enabled.');
|
|
24
|
+
}
|
|
25
|
+
const runtime = typeof options.runtime === 'string'
|
|
26
|
+
? { script: options.runtime, style: options.runtime }
|
|
27
|
+
: options.runtime || {
|
|
28
|
+
script: '_assets/folding-headings-extension.js',
|
|
29
|
+
style: '_assets/folding-headings-extension.css',
|
|
30
|
+
};
|
|
31
|
+
const plugin = function (md, { output = '.', generateID } = {}) {
|
|
32
|
+
registerTransform(md, {
|
|
33
|
+
runtime,
|
|
34
|
+
bundle,
|
|
35
|
+
output,
|
|
36
|
+
generateID,
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
Object.assign(plugin, {
|
|
40
|
+
collect(input, { destRoot = '.' }) {
|
|
41
|
+
const MdIt = dynrequire('markdown-it');
|
|
42
|
+
const md = new MdIt().use((md) => {
|
|
43
|
+
registerTransform(md, {
|
|
44
|
+
runtime,
|
|
45
|
+
bundle,
|
|
46
|
+
output: destRoot,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
md.parse(input, {});
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
return plugin;
|
|
53
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function headingLevel(header) {
|
|
2
|
+
return parseInt(header.charAt(1), 10);
|
|
3
|
+
}
|
|
4
|
+
export function last(arr) {
|
|
5
|
+
return arr[arr.length - 1];
|
|
6
|
+
}
|
|
7
|
+
export function hidden(box, field, value) {
|
|
8
|
+
if (!(field in box)) {
|
|
9
|
+
Object.defineProperty(box, field, {
|
|
10
|
+
enumerable: false,
|
|
11
|
+
value: value,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return box;
|
|
15
|
+
}
|
|
16
|
+
export function copyRuntime({ runtime, output }, cache) {
|
|
17
|
+
const PATH_TO_RUNTIME = '../runtime';
|
|
18
|
+
const { join, resolve } = dynrequire('node:path');
|
|
19
|
+
const runtimeFiles = {
|
|
20
|
+
'index.js': runtime.script,
|
|
21
|
+
'index.css': runtime.style,
|
|
22
|
+
};
|
|
23
|
+
for (const [originFile, outputFile] of Object.entries(runtimeFiles)) {
|
|
24
|
+
const file = join(PATH_TO_RUNTIME, originFile);
|
|
25
|
+
if (!cache.has(file)) {
|
|
26
|
+
cache.add(file);
|
|
27
|
+
copy(resolve(__dirname, file), join(output, outputFile));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function copy(from, to) {
|
|
32
|
+
const { mkdirSync, copyFileSync } = dynrequire('node:fs');
|
|
33
|
+
const { dirname } = dynrequire('node:path');
|
|
34
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
35
|
+
copyFileSync(from, to);
|
|
36
|
+
}
|
|
37
|
+
/*
|
|
38
|
+
* Runtime require hidden for builders.
|
|
39
|
+
* Used for nodejs api
|
|
40
|
+
*/
|
|
41
|
+
export function dynrequire(module) {
|
|
42
|
+
// eslint-disable-next-line no-eval
|
|
43
|
+
return eval(`require('${module}')`);
|
|
44
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { SectionAttr, DATA_KEY } from '../common/const';
|
|
2
|
+
export const GLOBAL_KEY = 'heading_sections';
|
|
3
|
+
export const Selector = {
|
|
4
|
+
Heading: '.yfm .heading-section > h1,h2,h3,h4,h5,h6',
|
|
5
|
+
Section: '.yfm .heading-section',
|
|
6
|
+
Content: '.yfm .heading-section-content',
|
|
7
|
+
};
|
|
8
|
+
export const ClassName = {
|
|
9
|
+
Open: 'open',
|
|
10
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ClassName, DATA_KEY, SectionAttr, Selector } from './const';
|
|
2
|
+
import { getEventTarget, isCustom } from './utils';
|
|
3
|
+
export class HeadingSectionContoller {
|
|
4
|
+
__doc;
|
|
5
|
+
constructor(doc) {
|
|
6
|
+
this.__doc = doc;
|
|
7
|
+
this.__doc.addEventListener('click', this._onDocClick);
|
|
8
|
+
}
|
|
9
|
+
open(id) {
|
|
10
|
+
for (const elem of this._findSections(id)) {
|
|
11
|
+
elem.classList.add(ClassName.Open);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
toggle(id) {
|
|
15
|
+
for (const elem of this._findSections(id)) {
|
|
16
|
+
elem.classList.toggle(ClassName.Open);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
close(id) {
|
|
20
|
+
for (const elem of this._findSections(id)) {
|
|
21
|
+
elem.classList.remove(ClassName.Open);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
destroy() {
|
|
25
|
+
this.__doc.removeEventListener('click', this._onDocClick);
|
|
26
|
+
}
|
|
27
|
+
_findSections(id) {
|
|
28
|
+
return Array.from(this.__doc.querySelectorAll(`section[${SectionAttr.DataKey}=${DATA_KEY}][${SectionAttr.DataId}=${id}]`));
|
|
29
|
+
}
|
|
30
|
+
_onDocClick = (event) => {
|
|
31
|
+
if (isCustom(event))
|
|
32
|
+
return;
|
|
33
|
+
const heading = this._findHeading(event);
|
|
34
|
+
if (heading)
|
|
35
|
+
this._toogleSection(heading);
|
|
36
|
+
};
|
|
37
|
+
_findHeading(event) {
|
|
38
|
+
const target = getEventTarget(event);
|
|
39
|
+
if (this._matchHeading(target))
|
|
40
|
+
return target;
|
|
41
|
+
const path = event.composedPath?.();
|
|
42
|
+
return path?.find(this._matchHeading);
|
|
43
|
+
}
|
|
44
|
+
_matchHeading = (target) => {
|
|
45
|
+
if (!(target instanceof HTMLElement))
|
|
46
|
+
return false;
|
|
47
|
+
return (target?.matches?.(Selector.Heading) && target.parentElement?.matches(Selector.Section));
|
|
48
|
+
};
|
|
49
|
+
_toogleSection(heading) {
|
|
50
|
+
const section = heading.parentElement;
|
|
51
|
+
if (section instanceof HTMLElement) {
|
|
52
|
+
section.classList.toggle(ClassName.Open);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
package/build/runtime/index.js
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { GLOBAL_KEY } from './const';
|
|
2
|
+
import { HeadingSectionContoller } from './controller';
|
|
3
|
+
import './styles/index.scss';
|
|
4
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined' && !window[GLOBAL_KEY]) {
|
|
5
|
+
window[GLOBAL_KEY] = new HeadingSectionContoller(document);
|
|
6
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const getEventTarget = (event) => {
|
|
2
|
+
const path = event.composedPath();
|
|
3
|
+
return Array.isArray(path) && path.length > 0 ? path[0] : event.target;
|
|
4
|
+
};
|
|
5
|
+
export const isCustom = (event) => {
|
|
6
|
+
const target = getEventTarget(event);
|
|
7
|
+
return !target || !target.matches;
|
|
8
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diplodoc/folding-headings-extension",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Folding headings extension for Diplodoc platform ",
|
|
5
5
|
"main": "build/plugin/index.js",
|
|
6
6
|
"types": "build/plugin/index.d.ts",
|
|
@@ -34,27 +34,27 @@
|
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "run-p build:*",
|
|
37
|
-
"build:declarations": "tsc
|
|
37
|
+
"build:declarations": "tsc -p tsconfig.publish.json",
|
|
38
38
|
"build:js": "node ./esbuild/build.mjs",
|
|
39
39
|
"prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build",
|
|
40
40
|
"test": "vitest run --config vitest.config.mjs",
|
|
41
41
|
"test:watch": "vitest --config vitest.config.mjs",
|
|
42
42
|
"test:coverage": "vitest run --coverage",
|
|
43
43
|
"typecheck": "tsc --noEmit",
|
|
44
|
-
"lint": "lint
|
|
45
|
-
"lint:fix": "lint
|
|
46
|
-
"pre-commit": "lint
|
|
47
|
-
"prepare": "husky"
|
|
44
|
+
"lint": "lint",
|
|
45
|
+
"lint:fix": "lint fix",
|
|
46
|
+
"pre-commit": "lint-staged",
|
|
47
|
+
"prepare": "husky || true",
|
|
48
|
+
"lock": "npm install --no-workspaces --package-lock-only --ignore-scripts"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
|
-
"@diplodoc/lint": "^1.14.1",
|
|
51
|
-
"@diplodoc/tsconfig": "^1.0.2",
|
|
52
51
|
"@diplodoc/utils": "^2.2.2",
|
|
53
52
|
"@types/markdown-it": "^13.0.9",
|
|
54
53
|
"@vitest/coverage-v8": "^3.2.4",
|
|
55
54
|
"markdown-it": "^13.0.0",
|
|
56
55
|
"npm-run-all": "^4.1.5",
|
|
57
56
|
"typescript": "^5.3.3",
|
|
58
|
-
"vitest": "^3.2.4"
|
|
57
|
+
"vitest": "^3.2.4",
|
|
58
|
+
"@diplodoc/infra": "2.2.1"
|
|
59
59
|
}
|
|
60
60
|
}
|
package/build/test/data.d.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
export declare const sectionsHtmlResult: string;
|
|
2
|
-
export declare const commonHeadingTokens: ({
|
|
3
|
-
type: string;
|
|
4
|
-
tag: string;
|
|
5
|
-
attrs: null;
|
|
6
|
-
map: number[];
|
|
7
|
-
nesting: number;
|
|
8
|
-
level: number;
|
|
9
|
-
children: null;
|
|
10
|
-
content: string;
|
|
11
|
-
markup: string;
|
|
12
|
-
info: string;
|
|
13
|
-
meta: null;
|
|
14
|
-
block: boolean;
|
|
15
|
-
hidden: boolean;
|
|
16
|
-
} | {
|
|
17
|
-
type: string;
|
|
18
|
-
tag: string;
|
|
19
|
-
attrs: null;
|
|
20
|
-
map: number[];
|
|
21
|
-
nesting: number;
|
|
22
|
-
level: number;
|
|
23
|
-
children: {
|
|
24
|
-
type: string;
|
|
25
|
-
tag: string;
|
|
26
|
-
attrs: null;
|
|
27
|
-
map: null;
|
|
28
|
-
nesting: number;
|
|
29
|
-
level: number;
|
|
30
|
-
children: null;
|
|
31
|
-
content: string;
|
|
32
|
-
markup: string;
|
|
33
|
-
info: string;
|
|
34
|
-
meta: null;
|
|
35
|
-
block: boolean;
|
|
36
|
-
hidden: boolean;
|
|
37
|
-
}[];
|
|
38
|
-
content: string;
|
|
39
|
-
markup: string;
|
|
40
|
-
info: string;
|
|
41
|
-
meta: null;
|
|
42
|
-
block: boolean;
|
|
43
|
-
hidden: boolean;
|
|
44
|
-
} | {
|
|
45
|
-
type: string;
|
|
46
|
-
tag: string;
|
|
47
|
-
attrs: null;
|
|
48
|
-
map: null;
|
|
49
|
-
nesting: number;
|
|
50
|
-
level: number;
|
|
51
|
-
children: null;
|
|
52
|
-
content: string;
|
|
53
|
-
markup: string;
|
|
54
|
-
info: string;
|
|
55
|
-
meta: null;
|
|
56
|
-
block: boolean;
|
|
57
|
-
hidden: boolean;
|
|
58
|
-
})[];
|
|
59
|
-
export declare const foldingHeadingTokens: ({
|
|
60
|
-
type: string;
|
|
61
|
-
tag: string;
|
|
62
|
-
attrs: string[][];
|
|
63
|
-
map: null;
|
|
64
|
-
nesting: number;
|
|
65
|
-
level: number;
|
|
66
|
-
children: null;
|
|
67
|
-
content: string;
|
|
68
|
-
markup: string;
|
|
69
|
-
info: string;
|
|
70
|
-
meta: null;
|
|
71
|
-
block: boolean;
|
|
72
|
-
hidden: boolean;
|
|
73
|
-
} | {
|
|
74
|
-
type: string;
|
|
75
|
-
tag: string;
|
|
76
|
-
attrs: null;
|
|
77
|
-
map: number[];
|
|
78
|
-
nesting: number;
|
|
79
|
-
level: number;
|
|
80
|
-
children: null;
|
|
81
|
-
content: string;
|
|
82
|
-
markup: string;
|
|
83
|
-
info: string;
|
|
84
|
-
meta: {
|
|
85
|
-
folding: boolean;
|
|
86
|
-
};
|
|
87
|
-
block: boolean;
|
|
88
|
-
hidden: boolean;
|
|
89
|
-
} | {
|
|
90
|
-
type: string;
|
|
91
|
-
tag: string;
|
|
92
|
-
attrs: null;
|
|
93
|
-
map: number[];
|
|
94
|
-
nesting: number;
|
|
95
|
-
level: number;
|
|
96
|
-
children: {
|
|
97
|
-
type: string;
|
|
98
|
-
tag: string;
|
|
99
|
-
attrs: null;
|
|
100
|
-
map: null;
|
|
101
|
-
nesting: number;
|
|
102
|
-
level: number;
|
|
103
|
-
children: null;
|
|
104
|
-
content: string;
|
|
105
|
-
markup: string;
|
|
106
|
-
info: string;
|
|
107
|
-
meta: null;
|
|
108
|
-
block: boolean;
|
|
109
|
-
hidden: boolean;
|
|
110
|
-
}[];
|
|
111
|
-
content: string;
|
|
112
|
-
markup: string;
|
|
113
|
-
info: string;
|
|
114
|
-
meta: null;
|
|
115
|
-
block: boolean;
|
|
116
|
-
hidden: boolean;
|
|
117
|
-
} | {
|
|
118
|
-
type: string;
|
|
119
|
-
tag: string;
|
|
120
|
-
attrs: null;
|
|
121
|
-
map: null;
|
|
122
|
-
nesting: number;
|
|
123
|
-
level: number;
|
|
124
|
-
children: null;
|
|
125
|
-
content: string;
|
|
126
|
-
markup: string;
|
|
127
|
-
info: string;
|
|
128
|
-
meta: {
|
|
129
|
-
folding: boolean;
|
|
130
|
-
};
|
|
131
|
-
block: boolean;
|
|
132
|
-
hidden: boolean;
|
|
133
|
-
} | {
|
|
134
|
-
type: string;
|
|
135
|
-
tag: string;
|
|
136
|
-
attrs: null;
|
|
137
|
-
map: null;
|
|
138
|
-
nesting: number;
|
|
139
|
-
level: number;
|
|
140
|
-
children: null;
|
|
141
|
-
content: string;
|
|
142
|
-
markup: string;
|
|
143
|
-
info: string;
|
|
144
|
-
meta: null;
|
|
145
|
-
block: boolean;
|
|
146
|
-
hidden: boolean;
|
|
147
|
-
})[];
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|