@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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/es/cell-toc-provider.d.ts +9 -0
  4. package/es/cell-toc-provider.d.ts.map +1 -0
  5. package/es/cell-toc-provider.js +46 -0
  6. package/es/index.d.ts +6 -0
  7. package/es/index.d.ts.map +1 -0
  8. package/es/index.js +5 -0
  9. package/es/index.less +66 -0
  10. package/es/libro-toc-color-registry.d.ts +6 -0
  11. package/es/libro-toc-color-registry.d.ts.map +1 -0
  12. package/es/libro-toc-color-registry.js +46 -0
  13. package/es/module.d.ts +14 -0
  14. package/es/module.d.ts.map +1 -0
  15. package/es/module.js +40 -0
  16. package/es/provider/html.d.ts +39 -0
  17. package/es/provider/html.d.ts.map +1 -0
  18. package/es/provider/html.js +54 -0
  19. package/es/provider/markdown-toc-provider.d.ts +11 -0
  20. package/es/provider/markdown-toc-provider.d.ts.map +1 -0
  21. package/es/provider/markdown-toc-provider.js +43 -0
  22. package/es/provider/markdown.d.ts +23 -0
  23. package/es/provider/markdown.d.ts.map +1 -0
  24. package/es/provider/markdown.js +142 -0
  25. package/es/provider/output-toc-provider.d.ts +10 -0
  26. package/es/provider/output-toc-provider.d.ts.map +1 -0
  27. package/es/provider/output-toc-provider.js +67 -0
  28. package/es/toc-collapse-service.d.ts +10 -0
  29. package/es/toc-collapse-service.d.ts.map +1 -0
  30. package/es/toc-collapse-service.js +141 -0
  31. package/es/toc-configuration.d.ts +7 -0
  32. package/es/toc-configuration.d.ts.map +1 -0
  33. package/es/toc-configuration.js +34 -0
  34. package/es/toc-contribution.d.ts +11 -0
  35. package/es/toc-contribution.d.ts.map +1 -0
  36. package/es/toc-contribution.js +63 -0
  37. package/es/toc-manager.d.ts +10 -0
  38. package/es/toc-manager.d.ts.map +1 -0
  39. package/es/toc-manager.js +32 -0
  40. package/es/toc-protocol.d.ts +107 -0
  41. package/es/toc-protocol.d.ts.map +1 -0
  42. package/es/toc-protocol.js +35 -0
  43. package/es/toc-provider.d.ts +37 -0
  44. package/es/toc-provider.d.ts.map +1 -0
  45. package/es/toc-provider.js +181 -0
  46. package/es/toc-view.d.ts +33 -0
  47. package/es/toc-view.d.ts.map +1 -0
  48. package/es/toc-view.js +245 -0
  49. package/package.json +62 -0
  50. package/src/cell-toc-provider.ts +31 -0
  51. package/src/index.less +66 -0
  52. package/src/index.ts +5 -0
  53. package/src/libro-toc-color-registry.ts +27 -0
  54. package/src/module.ts +58 -0
  55. package/src/provider/html.ts +61 -0
  56. package/src/provider/markdown-toc-provider.ts +34 -0
  57. package/src/provider/markdown.ts +182 -0
  58. package/src/provider/output-toc-provider.ts +53 -0
  59. package/src/toc-collapse-service.ts +96 -0
  60. package/src/toc-configuration.ts +22 -0
  61. package/src/toc-contribution.ts +34 -0
  62. package/src/toc-manager.ts +27 -0
  63. package/src/toc-protocol.ts +130 -0
  64. package/src/toc-provider.ts +154 -0
  65. 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');