@difizen/libro-toc 0.0.2-alpha.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/LICENSE +21 -0
- package/README.md +1 -0
- package/es/cell-toc-provider.d.ts +9 -0
- package/es/cell-toc-provider.d.ts.map +1 -0
- package/es/cell-toc-provider.js +46 -0
- package/es/index.d.ts +6 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +5 -0
- package/es/index.less +66 -0
- package/es/libro-toc-color-registry.d.ts +6 -0
- package/es/libro-toc-color-registry.d.ts.map +1 -0
- package/es/libro-toc-color-registry.js +46 -0
- package/es/module.d.ts +14 -0
- package/es/module.d.ts.map +1 -0
- package/es/module.js +40 -0
- package/es/provider/html.d.ts +39 -0
- package/es/provider/html.d.ts.map +1 -0
- package/es/provider/html.js +54 -0
- package/es/provider/markdown-toc-provider.d.ts +11 -0
- package/es/provider/markdown-toc-provider.d.ts.map +1 -0
- package/es/provider/markdown-toc-provider.js +43 -0
- package/es/provider/markdown.d.ts +23 -0
- package/es/provider/markdown.d.ts.map +1 -0
- package/es/provider/markdown.js +142 -0
- package/es/provider/output-toc-provider.d.ts +10 -0
- package/es/provider/output-toc-provider.d.ts.map +1 -0
- package/es/provider/output-toc-provider.js +67 -0
- package/es/toc-collapse-service.d.ts +10 -0
- package/es/toc-collapse-service.d.ts.map +1 -0
- package/es/toc-collapse-service.js +141 -0
- package/es/toc-configuration.d.ts +7 -0
- package/es/toc-configuration.d.ts.map +1 -0
- package/es/toc-configuration.js +34 -0
- package/es/toc-contribution.d.ts +11 -0
- package/es/toc-contribution.d.ts.map +1 -0
- package/es/toc-contribution.js +63 -0
- package/es/toc-manager.d.ts +10 -0
- package/es/toc-manager.d.ts.map +1 -0
- package/es/toc-manager.js +32 -0
- package/es/toc-protocol.d.ts +107 -0
- package/es/toc-protocol.d.ts.map +1 -0
- package/es/toc-protocol.js +35 -0
- package/es/toc-provider.d.ts +37 -0
- package/es/toc-provider.d.ts.map +1 -0
- package/es/toc-provider.js +181 -0
- package/es/toc-view.d.ts +33 -0
- package/es/toc-view.d.ts.map +1 -0
- package/es/toc-view.js +245 -0
- package/package.json +62 -0
- package/src/cell-toc-provider.ts +31 -0
- package/src/index.less +66 -0
- package/src/index.ts +5 -0
- package/src/libro-toc-color-registry.ts +27 -0
- package/src/module.ts +58 -0
- package/src/provider/html.ts +61 -0
- package/src/provider/markdown-toc-provider.ts +34 -0
- package/src/provider/markdown.ts +182 -0
- package/src/provider/output-toc-provider.ts +53 -0
- package/src/toc-collapse-service.ts +96 -0
- package/src/toc-configuration.ts +22 -0
- package/src/toc-contribution.ts +34 -0
- package/src/toc-manager.ts +27 -0
- package/src/toc-protocol.ts +130 -0
- package/src/toc-provider.ts +154 -0
- package/src/toc-view.tsx +225 -0
package/src/module.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { MarkdownModule } from '@difizen/libro-markdown';
|
|
2
|
+
import { LibroRenderMimeModule } from '@difizen/libro-rendermime';
|
|
3
|
+
import { ManaModule } from '@difizen/mana-app';
|
|
4
|
+
|
|
5
|
+
import { LibroCellTOCProvider } from './cell-toc-provider.js';
|
|
6
|
+
import { LibroTocColorRegistry } from './libro-toc-color-registry.js';
|
|
7
|
+
import { MarkDownCellTOCProvider } from './provider/markdown-toc-provider.js';
|
|
8
|
+
import { OutputTOCProvider } from './provider/output-toc-provider.js';
|
|
9
|
+
import { TOCCollapseService } from './toc-collapse-service.js';
|
|
10
|
+
import { TOCSettingContribution } from './toc-configuration.js';
|
|
11
|
+
import { LibroTocSlotContribution } from './toc-contribution.js';
|
|
12
|
+
import { LibroTOCManager } from './toc-manager.js';
|
|
13
|
+
import { CellTOCProviderContribution, TOCProviderOption } from './toc-protocol.js';
|
|
14
|
+
import { LibroTOCProvider, LibroTOCProviderFactory } from './toc-provider.js';
|
|
15
|
+
import { TOCView } from './toc-view.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 不带output支持
|
|
19
|
+
*/
|
|
20
|
+
export const LibroBaseTOCModule = ManaModule.create()
|
|
21
|
+
.contribution(CellTOCProviderContribution)
|
|
22
|
+
.register(
|
|
23
|
+
TOCSettingContribution,
|
|
24
|
+
TOCView,
|
|
25
|
+
LibroTOCProvider,
|
|
26
|
+
{
|
|
27
|
+
token: LibroTOCProviderFactory,
|
|
28
|
+
useFactory: (ctx) => {
|
|
29
|
+
return (option) => {
|
|
30
|
+
const child = ctx.container.createChild();
|
|
31
|
+
child.register({
|
|
32
|
+
token: TOCProviderOption,
|
|
33
|
+
useValue: option,
|
|
34
|
+
});
|
|
35
|
+
return child.get(LibroTOCProvider);
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
LibroTOCManager,
|
|
40
|
+
LibroCellTOCProvider,
|
|
41
|
+
MarkDownCellTOCProvider,
|
|
42
|
+
TOCCollapseService,
|
|
43
|
+
LibroTocColorRegistry,
|
|
44
|
+
)
|
|
45
|
+
.dependOn(MarkdownModule);
|
|
46
|
+
/**
|
|
47
|
+
* 标准的notebook TOC
|
|
48
|
+
*/
|
|
49
|
+
export const LibroTOCModule = ManaModule.create()
|
|
50
|
+
.register(OutputTOCProvider)
|
|
51
|
+
.dependOn(LibroBaseTOCModule, LibroRenderMimeModule);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* toc在内容区右侧
|
|
55
|
+
*/
|
|
56
|
+
export const LibroTOCOnContentModule = ManaModule.create().register(
|
|
57
|
+
LibroTocSlotContribution,
|
|
58
|
+
);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { IHeading } from '../toc-protocol.js';
|
|
2
|
+
import { HeadingType } from '../toc-protocol.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HTML heading
|
|
6
|
+
*/
|
|
7
|
+
export interface IHTMLHeading extends IHeading {
|
|
8
|
+
/**
|
|
9
|
+
* HTML id
|
|
10
|
+
*/
|
|
11
|
+
id?: string | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parse a HTML string for headings.
|
|
15
|
+
*
|
|
16
|
+
* ### Notes
|
|
17
|
+
* The html string is not sanitized - use with caution
|
|
18
|
+
*
|
|
19
|
+
* @param html HTML string to parse
|
|
20
|
+
* @param force Whether to ignore HTML headings with class jp-toc-ignore and tocSkip or not
|
|
21
|
+
* @returns Extracted headings
|
|
22
|
+
*/
|
|
23
|
+
export function getHTMLHeadings(html: string, type?: HeadingType): IHTMLHeading[] {
|
|
24
|
+
const container: HTMLDivElement = document.createElement('div');
|
|
25
|
+
container.innerHTML = html;
|
|
26
|
+
|
|
27
|
+
const headings = new Array<IHTMLHeading>();
|
|
28
|
+
const headers = Array.from(container.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
|
29
|
+
for (const h of headers) {
|
|
30
|
+
const level = parseInt(h.tagName[1], 10);
|
|
31
|
+
|
|
32
|
+
headings.push({
|
|
33
|
+
text: h.textContent ?? '',
|
|
34
|
+
level,
|
|
35
|
+
id: h?.getAttribute('id'),
|
|
36
|
+
skip: h.classList.contains('jp-toc-ignore') || h.classList.contains('tocSkip'),
|
|
37
|
+
type: type ?? HeadingType.HTML,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return headings;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const HTMLMimeType = 'text/html';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns whether a MIME type corresponds to either HTML.
|
|
47
|
+
*
|
|
48
|
+
* @param mime - MIME type string
|
|
49
|
+
* @returns boolean indicating whether a provided MIME type corresponds to either HTML
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* const bool = isHTML('text/html');
|
|
53
|
+
* // returns true
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* const bool = isHTML('text/plain');
|
|
57
|
+
* // returns false
|
|
58
|
+
*/
|
|
59
|
+
export function isHTML(mime: string): boolean {
|
|
60
|
+
return mime === HTMLMimeType;
|
|
61
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { LibroMarkdownCellModel } from '@difizen/libro-core';
|
|
2
|
+
import type { CellView } from '@difizen/libro-core';
|
|
3
|
+
import { MarkdownParser } from '@difizen/libro-markdown';
|
|
4
|
+
import { inject, singleton, watch } from '@difizen/mana-app';
|
|
5
|
+
|
|
6
|
+
import type { CellTOCProvider } from '../toc-protocol.js';
|
|
7
|
+
import { HeadingType, CellTOCProviderContribution } from '../toc-protocol.js';
|
|
8
|
+
|
|
9
|
+
import { getHTMLHeadings } from './html.js';
|
|
10
|
+
|
|
11
|
+
@singleton({ contrib: [CellTOCProviderContribution] })
|
|
12
|
+
export class MarkDownCellTOCProvider implements CellTOCProviderContribution {
|
|
13
|
+
protected readonly markdownParser: MarkdownParser;
|
|
14
|
+
constructor(@inject(MarkdownParser) markdownParser: MarkdownParser) {
|
|
15
|
+
this.markdownParser = markdownParser;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
canHandle(cell: CellView) {
|
|
19
|
+
return LibroMarkdownCellModel.is(cell.model) ? 100 : 0;
|
|
20
|
+
}
|
|
21
|
+
factory(cell: CellView): CellTOCProvider {
|
|
22
|
+
return {
|
|
23
|
+
getHeadings: () => {
|
|
24
|
+
return getHTMLHeadings(
|
|
25
|
+
this.markdownParser.render(cell.model.value, { cellId: cell.model.id }),
|
|
26
|
+
HeadingType.Markdown,
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
updateWatcher: (update) => {
|
|
30
|
+
return watch(cell.model, 'value', update);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { IHeading } from '../toc-protocol.js';
|
|
2
|
+
import { HeadingType } from '../toc-protocol.js';
|
|
3
|
+
|
|
4
|
+
interface IHeader {
|
|
5
|
+
/**
|
|
6
|
+
* Heading text.
|
|
7
|
+
*/
|
|
8
|
+
text: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Heading level.
|
|
12
|
+
*/
|
|
13
|
+
level: number;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Raw string containing the heading
|
|
17
|
+
*/
|
|
18
|
+
raw: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether the heading is marked to skip or not
|
|
22
|
+
*/
|
|
23
|
+
skip: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Markdown heading
|
|
28
|
+
*/
|
|
29
|
+
export interface IMarkdownHeading extends IHeading {
|
|
30
|
+
/**
|
|
31
|
+
* Heading line
|
|
32
|
+
*/
|
|
33
|
+
line: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parses the provided string and returns a list of headings.
|
|
38
|
+
*
|
|
39
|
+
* @param text - Input text
|
|
40
|
+
* @returns List of headings
|
|
41
|
+
*/
|
|
42
|
+
export function getHeadings(text: string): IMarkdownHeading[] {
|
|
43
|
+
// Split the text into lines:
|
|
44
|
+
const lines = text.split('\n');
|
|
45
|
+
|
|
46
|
+
// Iterate over the lines to get the header level and text for each line:
|
|
47
|
+
const headings = new Array<IMarkdownHeading>();
|
|
48
|
+
let isCodeBlock;
|
|
49
|
+
let lineIdx = 0;
|
|
50
|
+
|
|
51
|
+
// Don't check for Markdown headings if in a YAML frontmatter block.
|
|
52
|
+
// We can only start a frontmatter block on the first line of the file.
|
|
53
|
+
// At other positions in a markdown file, '---' represents a horizontal rule.
|
|
54
|
+
if (lines[lineIdx] === '---') {
|
|
55
|
+
// Search for another '---' and treat that as the end of the frontmatter.
|
|
56
|
+
// If we don't find one, treat the file as containing no frontmatter.
|
|
57
|
+
for (
|
|
58
|
+
let frontmatterEndLineIdx = lineIdx + 1;
|
|
59
|
+
frontmatterEndLineIdx < lines.length;
|
|
60
|
+
frontmatterEndLineIdx++
|
|
61
|
+
) {
|
|
62
|
+
if (lines[frontmatterEndLineIdx] === '---') {
|
|
63
|
+
lineIdx = frontmatterEndLineIdx + 1;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (; lineIdx < lines.length; lineIdx++) {
|
|
70
|
+
const line = lines[lineIdx];
|
|
71
|
+
|
|
72
|
+
if (line === '') {
|
|
73
|
+
// Bail early
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Don't check for Markdown headings if in a code block
|
|
78
|
+
if (line.startsWith('```')) {
|
|
79
|
+
isCodeBlock = !isCodeBlock;
|
|
80
|
+
}
|
|
81
|
+
if (isCodeBlock) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const heading = parseHeading(line, lines[lineIdx + 1]); // append the next line to capture alternative style Markdown headings
|
|
86
|
+
|
|
87
|
+
if (heading) {
|
|
88
|
+
headings.push({
|
|
89
|
+
...heading,
|
|
90
|
+
line: lineIdx,
|
|
91
|
+
type: HeadingType.Markdown,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return headings;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether a MIME type corresponds to a Markdown flavor.
|
|
100
|
+
*/
|
|
101
|
+
export function isMarkdown(mime: string): boolean {
|
|
102
|
+
return [
|
|
103
|
+
'text/x-ipythongfm',
|
|
104
|
+
'text/x-markdown',
|
|
105
|
+
'text/x-gfm',
|
|
106
|
+
'text/markdown',
|
|
107
|
+
].includes(mime);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Ignore title with html tag with a class name equal to `jp-toc-ignore` or `tocSkip`
|
|
112
|
+
*/
|
|
113
|
+
const skipHeading =
|
|
114
|
+
/<\w+\s(.*?\s)?class="(.*?\s)?(jp-toc-ignore|tocSkip)(\s.*?)?"(\s.*?)?>/;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parses a heading, if one exists, from a provided string.
|
|
118
|
+
* @param line - Line to parse
|
|
119
|
+
* @param nextLine - The line after the one to parse
|
|
120
|
+
* @returns heading info
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ### Foo
|
|
124
|
+
* const out = parseHeading('### Foo\n');
|
|
125
|
+
* // returns {'text': 'Foo', 'level': 3}
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* const out = parseHeading('Foo\n===\n');
|
|
129
|
+
* // returns {'text': 'Foo', 'level': 1}
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* <h4>Foo</h4>
|
|
133
|
+
* const out = parseHeading('<h4>Foo</h4>\n');
|
|
134
|
+
* // returns {'text': 'Foo', 'level': 4}
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* const out = parseHeading('Foo');
|
|
138
|
+
* // returns null
|
|
139
|
+
*/
|
|
140
|
+
function parseHeading(line: string, nextLine?: string): IHeader | null {
|
|
141
|
+
// Case: Markdown heading
|
|
142
|
+
let match = line.match(/^([#]{1,6}) (.*)/);
|
|
143
|
+
if (match) {
|
|
144
|
+
return {
|
|
145
|
+
text: cleanTitle(match[2]),
|
|
146
|
+
level: match[1].length,
|
|
147
|
+
raw: line,
|
|
148
|
+
skip: skipHeading.test(match[0]),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Case: Markdown heading (alternative style)
|
|
152
|
+
if (nextLine) {
|
|
153
|
+
match = nextLine.match(/^ {0,3}([=]{2,}|[-]{2,})\s*$/);
|
|
154
|
+
if (match) {
|
|
155
|
+
return {
|
|
156
|
+
text: cleanTitle(line),
|
|
157
|
+
level: match[1][0] === '=' ? 1 : 2,
|
|
158
|
+
raw: [line, nextLine].join('\n'),
|
|
159
|
+
skip: skipHeading.test(line),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Case: HTML heading (WARNING: this is not particularly robust, as HTML headings can span multiple lines)
|
|
164
|
+
match = line.match(/<h([1-6]).*>(.*)<\/h\1>/i);
|
|
165
|
+
if (match) {
|
|
166
|
+
return {
|
|
167
|
+
text: match[2],
|
|
168
|
+
level: parseInt(match[1], 10),
|
|
169
|
+
skip: skipHeading.test(match[0]),
|
|
170
|
+
raw: line,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function cleanTitle(heading: string): string {
|
|
178
|
+
// take special care to parse Markdown links into raw text
|
|
179
|
+
return heading.replace(/\[(.+)\]\(.+\)/g, '$1');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export const MarkdownMimeType = 'text/markdown';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { MultilineString } from '@difizen/libro-common';
|
|
2
|
+
import { concatMultilineString } from '@difizen/libro-common';
|
|
3
|
+
import type { CellView } from '@difizen/libro-core';
|
|
4
|
+
import { ExecutableCellView } from '@difizen/libro-core';
|
|
5
|
+
import { RenderMimeRegistry } from '@difizen/libro-rendermime';
|
|
6
|
+
import { inject, singleton } from '@difizen/mana-app';
|
|
7
|
+
import { watch } from '@difizen/mana-app';
|
|
8
|
+
|
|
9
|
+
import type { CellTOCProvider } from '../toc-protocol.js';
|
|
10
|
+
import { CellTOCProviderContribution } from '../toc-protocol.js';
|
|
11
|
+
|
|
12
|
+
import { getHTMLHeadings } from './html.js';
|
|
13
|
+
import { isMarkdown, MarkdownMimeType } from './markdown.js';
|
|
14
|
+
|
|
15
|
+
@singleton({ contrib: [CellTOCProviderContribution] })
|
|
16
|
+
export class OutputTOCProvider implements CellTOCProviderContribution {
|
|
17
|
+
@inject(RenderMimeRegistry) renderMimeRegistry: RenderMimeRegistry;
|
|
18
|
+
canHandle(cell: CellView) {
|
|
19
|
+
return ExecutableCellView.is(cell) ? 100 : 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
factory(cell: CellView): CellTOCProvider {
|
|
23
|
+
if (!ExecutableCellView.is(cell)) {
|
|
24
|
+
throw new Error('expected EditorCellView');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
getHeadings: () => {
|
|
29
|
+
return cell.outputArea.outputs
|
|
30
|
+
.filter((item) => {
|
|
31
|
+
const defaultRenderMimeType =
|
|
32
|
+
this.renderMimeRegistry.preferredMimeType(item);
|
|
33
|
+
if (!defaultRenderMimeType) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return isMarkdown(defaultRenderMimeType);
|
|
37
|
+
})
|
|
38
|
+
.map((item) => {
|
|
39
|
+
const html = this.renderMimeRegistry.markdownParser?.render(
|
|
40
|
+
concatMultilineString(item.data[MarkdownMimeType] as MultilineString),
|
|
41
|
+
{ cellId: cell.model.id },
|
|
42
|
+
);
|
|
43
|
+
const head = getHTMLHeadings(html ?? '');
|
|
44
|
+
return head;
|
|
45
|
+
})
|
|
46
|
+
.flat();
|
|
47
|
+
},
|
|
48
|
+
updateWatcher: (update) => {
|
|
49
|
+
return watch(cell.outputArea, 'outputs', update);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { CellView } from '@difizen/libro-core';
|
|
2
|
+
import {
|
|
3
|
+
CellCollapsible,
|
|
4
|
+
DefaultCollapseService,
|
|
5
|
+
CollapseService,
|
|
6
|
+
} from '@difizen/libro-core';
|
|
7
|
+
import { prop } from '@difizen/mana-app';
|
|
8
|
+
import { inject, transient } from '@difizen/mana-app';
|
|
9
|
+
|
|
10
|
+
import { LibroTOCManager } from './toc-manager.js';
|
|
11
|
+
import { HeadingType } from './toc-protocol.js';
|
|
12
|
+
|
|
13
|
+
@transient({ token: CollapseService })
|
|
14
|
+
export class TOCCollapseService extends DefaultCollapseService {
|
|
15
|
+
@inject(LibroTOCManager) libroTOCManager: LibroTOCManager;
|
|
16
|
+
|
|
17
|
+
@prop()
|
|
18
|
+
override collapserVisible = true;
|
|
19
|
+
|
|
20
|
+
override setHeadingCollapse(cell: CellView, collapsing: boolean) {
|
|
21
|
+
if (!CellCollapsible.is(cell)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
cell.headingCollapsed = collapsing;
|
|
25
|
+
const currentIndex = this.view.model.cells.findIndex(
|
|
26
|
+
(item) => item.model.id === cell.model.id,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (currentIndex < 0) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const childNumber = this.getCollapsibleChildNumber(cell);
|
|
34
|
+
cell.collapsibleChildNumber = childNumber;
|
|
35
|
+
|
|
36
|
+
const childCells = this.view.model.cells.filter(
|
|
37
|
+
(_item, index) => index > currentIndex && index <= currentIndex + childNumber,
|
|
38
|
+
);
|
|
39
|
+
if (collapsing === true) {
|
|
40
|
+
childCells.forEach((item) => {
|
|
41
|
+
item.collapsedHidden = collapsing;
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < childNumber) {
|
|
46
|
+
const element = childCells[i];
|
|
47
|
+
element.collapsedHidden = false;
|
|
48
|
+
/**
|
|
49
|
+
* 展开时子项的折叠不需要展开
|
|
50
|
+
*/
|
|
51
|
+
if (CellCollapsible.is(element) && element.headingCollapsed) {
|
|
52
|
+
i = i + element.collapsibleChildNumber + 1;
|
|
53
|
+
} else {
|
|
54
|
+
i = i + 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override getCollapsibleChildNumber(cell: CellView) {
|
|
61
|
+
const providerList = this.libroTOCManager
|
|
62
|
+
.getTOCProvider(this.view)
|
|
63
|
+
.getCellTocProviderList();
|
|
64
|
+
const withMaxLevel = providerList
|
|
65
|
+
.map((item) => {
|
|
66
|
+
let maxLevel = 100;
|
|
67
|
+
if (item.tocProvider) {
|
|
68
|
+
// 最大的标题是level最小的
|
|
69
|
+
const headings = item.tocProvider
|
|
70
|
+
?.getHeadings()
|
|
71
|
+
.filter((heading) => heading.type === HeadingType.Markdown);
|
|
72
|
+
|
|
73
|
+
if (headings.length > 0) {
|
|
74
|
+
maxLevel = Math.min(...headings.map((heading) => heading.level));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { ...item, maxLevel };
|
|
78
|
+
})
|
|
79
|
+
.map((item, index, array) => {
|
|
80
|
+
let childNumber = 0;
|
|
81
|
+
for (let i = index + 1; i < array.length; i++) {
|
|
82
|
+
const element = array[i];
|
|
83
|
+
if (item.maxLevel < element.maxLevel) {
|
|
84
|
+
childNumber = childNumber + 1;
|
|
85
|
+
} else {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { ...item, childNumber };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const number =
|
|
93
|
+
withMaxLevel.find((item) => item.cellId === cell.model.id)?.childNumber ?? 0;
|
|
94
|
+
return number;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ConfigurationContribution } from '@difizen/mana-app';
|
|
2
|
+
import type { ConfigurationNode } from '@difizen/mana-app';
|
|
3
|
+
import { singleton } from '@difizen/mana-app';
|
|
4
|
+
import { l10n } from '@difizen/mana-l10n';
|
|
5
|
+
|
|
6
|
+
export const TOCVisible: ConfigurationNode<boolean> = {
|
|
7
|
+
id: 'libro.toc.visible',
|
|
8
|
+
description: l10n.t('是否显示侧边的TOC'),
|
|
9
|
+
title: 'TOC',
|
|
10
|
+
type: 'checkbox',
|
|
11
|
+
defaultValue: true,
|
|
12
|
+
schema: {
|
|
13
|
+
type: 'boolean',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
@singleton({ contrib: ConfigurationContribution })
|
|
18
|
+
export class TOCSettingContribution implements ConfigurationContribution {
|
|
19
|
+
registerConfigurations() {
|
|
20
|
+
return [TOCVisible];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LibroView,
|
|
3
|
+
LibroExtensionSlotFactory,
|
|
4
|
+
LibroSlot,
|
|
5
|
+
} from '@difizen/libro-core';
|
|
6
|
+
import { LibroExtensionSlotContribution } from '@difizen/libro-core';
|
|
7
|
+
import { ViewManager } from '@difizen/mana-app';
|
|
8
|
+
import { inject, singleton } from '@difizen/mana-app';
|
|
9
|
+
|
|
10
|
+
import { TOCView } from './toc-view.js';
|
|
11
|
+
|
|
12
|
+
@singleton({ contrib: [LibroExtensionSlotContribution] })
|
|
13
|
+
export class LibroTocSlotContribution implements LibroExtensionSlotContribution {
|
|
14
|
+
@inject(ViewManager) viewManager: ViewManager;
|
|
15
|
+
protected viewMap: Map<string, TOCView> = new Map();
|
|
16
|
+
|
|
17
|
+
public readonly slot: LibroSlot = 'right';
|
|
18
|
+
|
|
19
|
+
factory: LibroExtensionSlotFactory = async (libro: LibroView) => {
|
|
20
|
+
const view = await this.viewManager.getOrCreateView(TOCView, {
|
|
21
|
+
parentId: libro.id,
|
|
22
|
+
});
|
|
23
|
+
view.parent = libro;
|
|
24
|
+
this.viewMap.set(libro.id, view);
|
|
25
|
+
view.onDisposed(() => {
|
|
26
|
+
this.viewMap.delete(libro.id);
|
|
27
|
+
});
|
|
28
|
+
return view;
|
|
29
|
+
};
|
|
30
|
+
// viewOpenOption = {
|
|
31
|
+
// reveal: true,
|
|
32
|
+
// order: 'a',
|
|
33
|
+
// };
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { LibroView } from '@difizen/libro-core';
|
|
2
|
+
import { inject, singleton } from '@difizen/mana-app';
|
|
3
|
+
|
|
4
|
+
import type { LibroTOCProvider } from './toc-provider.js';
|
|
5
|
+
import { LibroTOCProviderFactory } from './toc-provider.js';
|
|
6
|
+
|
|
7
|
+
@singleton()
|
|
8
|
+
export class LibroTOCManager {
|
|
9
|
+
protected tocProviderMap = new Map<LibroView, LibroTOCProvider>();
|
|
10
|
+
protected libroTOCProviderFactory: LibroTOCProviderFactory;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
@inject(LibroTOCProviderFactory) libroTOCProviderFactory: LibroTOCProviderFactory,
|
|
14
|
+
) {
|
|
15
|
+
this.libroTOCProviderFactory = libroTOCProviderFactory;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getTOCProvider(view: LibroView): LibroTOCProvider {
|
|
19
|
+
let provider = this.tocProviderMap.get(view);
|
|
20
|
+
if (provider) {
|
|
21
|
+
return provider;
|
|
22
|
+
}
|
|
23
|
+
provider = this.libroTOCProviderFactory({ view });
|
|
24
|
+
this.tocProviderMap.set(view, provider);
|
|
25
|
+
return provider;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { CellView } from '@difizen/libro-core';
|
|
2
|
+
import type { View } from '@difizen/mana-app';
|
|
3
|
+
import type { Disposable } from '@difizen/mana-app';
|
|
4
|
+
import { Syringe } from '@difizen/mana-app';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Table of Contents configuration
|
|
8
|
+
*
|
|
9
|
+
* #### Notes
|
|
10
|
+
* A document model may ignore some of those options.
|
|
11
|
+
*/
|
|
12
|
+
export interface TOCOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Base level for the highest headings
|
|
15
|
+
*/
|
|
16
|
+
baseNumbering: number;
|
|
17
|
+
/**
|
|
18
|
+
* Maximal depth of headings to display
|
|
19
|
+
*/
|
|
20
|
+
maximalDepth: number;
|
|
21
|
+
/**
|
|
22
|
+
* Whether to number first-level headings or not.
|
|
23
|
+
*/
|
|
24
|
+
numberingH1: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Whether to number headings in document or not.
|
|
27
|
+
*/
|
|
28
|
+
numberHeaders: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Whether to include cell outputs in headings or not.
|
|
31
|
+
*/
|
|
32
|
+
includeOutput: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Whether to synchronize heading collapse state between the ToC and the document or not.
|
|
35
|
+
*/
|
|
36
|
+
syncCollapseState: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Default table of content configuration
|
|
41
|
+
*/
|
|
42
|
+
export const defaultConfig: TOCOptions = {
|
|
43
|
+
baseNumbering: 1,
|
|
44
|
+
maximalDepth: 4,
|
|
45
|
+
numberingH1: true,
|
|
46
|
+
numberHeaders: false,
|
|
47
|
+
includeOutput: true,
|
|
48
|
+
syncCollapseState: false,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Interface describing a heading.
|
|
53
|
+
*/
|
|
54
|
+
export interface IHeading {
|
|
55
|
+
/**
|
|
56
|
+
* Type of heading
|
|
57
|
+
*/
|
|
58
|
+
type: HeadingType;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Heading text.
|
|
62
|
+
*/
|
|
63
|
+
text: string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* HTML heading level.
|
|
67
|
+
*/
|
|
68
|
+
level: number;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Heading prefix.
|
|
72
|
+
*/
|
|
73
|
+
prefix?: string | null;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Dataset to add to the item node
|
|
77
|
+
*/
|
|
78
|
+
dataset?: Record<string, string>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether the heading is marked to skip or not
|
|
82
|
+
*/
|
|
83
|
+
skip?: boolean;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Index of the output containing the heading
|
|
87
|
+
*/
|
|
88
|
+
outputIndex?: number;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* HTML id
|
|
92
|
+
*/
|
|
93
|
+
id?: string | null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Type of headings
|
|
98
|
+
*/
|
|
99
|
+
export enum HeadingType {
|
|
100
|
+
/**
|
|
101
|
+
* Heading from HTML output
|
|
102
|
+
*/
|
|
103
|
+
HTML,
|
|
104
|
+
/**
|
|
105
|
+
* Heading from Markdown cell or Markdown output
|
|
106
|
+
*/
|
|
107
|
+
Markdown,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface CellTOCProvider {
|
|
111
|
+
getHeadings: () => IHeading[];
|
|
112
|
+
updateWatcher: (fn: () => void) => Disposable;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface CellTOCProviderContribution {
|
|
116
|
+
canHandle: (cell: CellView) => number;
|
|
117
|
+
factory: (cell: CellView) => CellTOCProvider;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const CellTOCProviderContribution = Syringe.defineToken(
|
|
121
|
+
'CellTOCProviderContribution',
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
export interface TOCProviderOption {
|
|
125
|
+
/**
|
|
126
|
+
* libro view
|
|
127
|
+
*/
|
|
128
|
+
view: View;
|
|
129
|
+
}
|
|
130
|
+
export const TOCProviderOption = Symbol('TOCProviderOption');
|