@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
@@ -0,0 +1,154 @@
1
+ import type { LibroView } from '@difizen/libro-core';
2
+ import type { Disposable, Event } from '@difizen/mana-app';
3
+ import { inject, notEmpty, prop, transient, watch } from '@difizen/mana-app';
4
+ import { Emitter, DisposableCollection } from '@difizen/mana-app';
5
+
6
+ import { LibroCellTOCProvider } from './cell-toc-provider.js';
7
+ import type { CellTOCProvider, IHeading } from './toc-protocol.js';
8
+ import { TOCProviderOption } from './toc-protocol.js';
9
+
10
+ export type LibroTOCProviderFactory = (option: TOCProviderOption) => LibroTOCProvider;
11
+ export const LibroTOCProviderFactory = Symbol('LibroTOCProviderFactory');
12
+
13
+ @transient()
14
+ export class LibroTOCProvider implements Disposable {
15
+ protected libroCellTOCProvider: LibroCellTOCProvider;
16
+
17
+ @prop() protected providerMap = new Map<string, CellTOCProvider>();
18
+
19
+ @prop() headings: IHeading[] = [];
20
+
21
+ @prop()
22
+ protected view: LibroView;
23
+
24
+ protected toDispose = new DisposableCollection();
25
+ protected toDisposeWatcher = new DisposableCollection();
26
+
27
+ disposed = false;
28
+
29
+ protected activeCellChangeEmitter = new Emitter<IHeading>();
30
+
31
+ get activeCellChange(): Event<IHeading> {
32
+ return this.activeCellChangeEmitter.event;
33
+ }
34
+
35
+ constructor(
36
+ @inject(TOCProviderOption) option: TOCProviderOption,
37
+ @inject(LibroCellTOCProvider) libroCellTOCProvider: LibroCellTOCProvider,
38
+ ) {
39
+ this.view = option.view as LibroView;
40
+ this.libroCellTOCProvider = libroCellTOCProvider;
41
+
42
+ this.initUpdateWatch();
43
+ this.updateTOC();
44
+ }
45
+
46
+ protected initUpdateWatch() {
47
+ this.toDispose.push(
48
+ watch(this.view.model, 'activeIndex', this.handleActiveCellChange),
49
+ );
50
+ this.toDispose.push(watch(this.view.model, 'cells', this.onCellsChanged));
51
+ this.setupUpdaterWatcher();
52
+ }
53
+
54
+ protected setupUpdaterWatcher() {
55
+ this.toDisposeWatcher.dispose();
56
+ this.toDisposeWatcher = new DisposableCollection();
57
+ this.getCellTocProviders().map((item) => {
58
+ this.toDisposeWatcher.push(item.updateWatcher(this.updateTOC));
59
+ });
60
+ }
61
+
62
+ protected handleActiveCellChange = () => {
63
+ const header = this.getHeadingByCellIndex(this.view.model.activeIndex);
64
+ if (header) {
65
+ this.activeCellChangeEmitter.fire(header);
66
+ }
67
+ };
68
+
69
+ protected updateTOC = () => {
70
+ this.headings = this.getHeadings();
71
+ };
72
+
73
+ protected onActiveCellChanged = () => {
74
+ this.updateTOC();
75
+ };
76
+
77
+ protected onCellsChanged = () => {
78
+ this.setupUpdaterWatcher();
79
+ this.updateTOC();
80
+ };
81
+
82
+ protected onContentChanged = () => {
83
+ this.updateTOC();
84
+ };
85
+
86
+ getCellTocProviderList() {
87
+ if (!this.view) {
88
+ return [];
89
+ }
90
+ const cells = this.view.model.cells;
91
+ return cells.map((cell) => {
92
+ let tocProvider: CellTOCProvider | undefined;
93
+ if (this.providerMap.has(cell.id)) {
94
+ tocProvider = this.providerMap.get(cell.id);
95
+ } else {
96
+ tocProvider = this.libroCellTOCProvider.createCellTOCProvider(cell);
97
+ }
98
+
99
+ if (tocProvider) {
100
+ this.providerMap.set(cell.id, tocProvider);
101
+ }
102
+ return { cellId: cell.model.id, tocProvider };
103
+ });
104
+ }
105
+
106
+ protected getCellTocProviders() {
107
+ return this.getCellTocProviderList()
108
+ .map((item) => item.tocProvider)
109
+ .filter(notEmpty);
110
+ }
111
+
112
+ protected getHeadings() {
113
+ return this.getCellTocProviderList()
114
+ .map((item) => {
115
+ if (item.tocProvider !== undefined) {
116
+ const headings = item.tocProvider.getHeadings();
117
+ headings.forEach(
118
+ (heading) =>
119
+ (heading.dataset = { ...heading.dataset, cellId: item.cellId }),
120
+ );
121
+ return headings;
122
+ }
123
+ return;
124
+ })
125
+ .filter(notEmpty)
126
+ .flat();
127
+ }
128
+
129
+ selectCellByHeading(heading: IHeading) {
130
+ const cellId = heading?.dataset?.['cellId'];
131
+ if (!cellId) {
132
+ return;
133
+ }
134
+ const cell = this.view.model.cells.find((item) => item.model.id === cellId);
135
+ if (cell) {
136
+ this.view.selectCell(cell);
137
+ }
138
+ }
139
+
140
+ getHeadingByCellIndex(index: number) {
141
+ const cell = this.view.model.cells[index];
142
+ return this.headings.find((item) => item?.dataset?.['cellId'] === cell.model.id);
143
+ }
144
+
145
+ dispose() {
146
+ if (this.disposed) {
147
+ return;
148
+ }
149
+ this.toDispose.dispose();
150
+ this.toDisposeWatcher.dispose();
151
+ this.providerMap.clear();
152
+ this.disposed = true;
153
+ }
154
+ }
@@ -0,0 +1,225 @@
1
+ import { ArrowDown, ArrowRight } from '@difizen/libro-core';
2
+ import type { DisplayView, LibroView } from '@difizen/libro-core';
3
+ import { ConfigurationService } from '@difizen/mana-app';
4
+ import { getOrigin, prop, useInject } from '@difizen/mana-app';
5
+ import { BaseView, view, ViewInstance } from '@difizen/mana-app';
6
+ import { inject, transient } from '@difizen/mana-app';
7
+ import { l10n } from '@difizen/mana-l10n';
8
+ import React, { useRef } from 'react';
9
+
10
+ import { TOCVisible } from './toc-configuration.js';
11
+ import { LibroTOCManager } from './toc-manager.js';
12
+ import type { IHeading } from './toc-protocol.js';
13
+ import type { LibroTOCProvider } from './toc-provider.js';
14
+ import './index.less';
15
+
16
+ interface DisplayHeading extends IHeading {
17
+ hasChild: boolean;
18
+ visible: boolean;
19
+ }
20
+
21
+ interface TocItemProps {
22
+ heading: DisplayHeading;
23
+ active: boolean;
24
+ onClick: (heading: DisplayHeading) => void;
25
+ onToggle: () => void;
26
+ headingCollapsed: boolean;
27
+ }
28
+
29
+ const TocItem: React.FC<TocItemProps> = ({
30
+ heading,
31
+ onClick,
32
+ active,
33
+ headingCollapsed,
34
+ onToggle,
35
+ }) => {
36
+ const instance = useInject<TOCView>(ViewInstance);
37
+ const { id, text, level, hasChild, visible } = heading;
38
+ if (!visible) {
39
+ return null;
40
+ }
41
+ const handleClick = () => {
42
+ if (!id) {
43
+ return;
44
+ }
45
+
46
+ const element = document.getElementById(id);
47
+ if (element) {
48
+ element.scrollIntoView();
49
+ }
50
+ onClick(heading);
51
+ };
52
+ return (
53
+ <>
54
+ <div
55
+ className={`markdown-toc-container-anchor ${active ? 'active' : ''}`}
56
+ style={{
57
+ paddingLeft: instance.getHeadingIndentSize(level) + (hasChild ? 0 : 20),
58
+ }}
59
+ onClick={handleClick}
60
+ >
61
+ {hasChild && (
62
+ <span
63
+ onClick={(e) => {
64
+ e.stopPropagation();
65
+ onToggle();
66
+ }}
67
+ className="libro-toc-collapsed"
68
+ >
69
+ {headingCollapsed ? <ArrowRight /> : <ArrowDown />}
70
+ </span>
71
+ )}
72
+
73
+ {text}
74
+ </div>
75
+ <div className={`markdown-toc-container-anchor-shot ${active ? 'active' : ''}`} />
76
+ </>
77
+ );
78
+ };
79
+
80
+ export const TocRender = () => {
81
+ const instance = useInject<TOCView>(ViewInstance);
82
+ const containRef = useRef<HTMLDivElement>(null);
83
+
84
+ const handleClick = (heading: IHeading) => {
85
+ instance.activeHeading = heading;
86
+ instance.tocProvider?.selectCellByHeading(heading);
87
+ };
88
+
89
+ return (
90
+ <div className="markdown-toc-container" ref={containRef}>
91
+ <div className="markdown-toc-container-title">{instance.tocTitle}</div>
92
+ {instance.getDisplayHeadings().map((heading) => {
93
+ const collapsed = instance.isHeadingCollapsed(heading);
94
+ return (
95
+ <TocItem
96
+ active={instance.activeHeading?.id === heading.id}
97
+ heading={heading}
98
+ key={heading.id}
99
+ onClick={handleClick}
100
+ headingCollapsed={collapsed}
101
+ onToggle={() => {
102
+ if (heading.id) {
103
+ instance.headingCollapseState.set(heading.id, !collapsed);
104
+ }
105
+ }}
106
+ />
107
+ );
108
+ })}
109
+ </div>
110
+ );
111
+ };
112
+
113
+ @transient()
114
+ @view('libro-toc-view')
115
+ export class TOCView extends BaseView implements DisplayView {
116
+ parent: LibroView | undefined = undefined;
117
+ protected configurationService: ConfigurationService;
118
+
119
+ override view = TocRender;
120
+
121
+ @prop()
122
+ isDisplay: boolean;
123
+
124
+ @prop()
125
+ tocProvider?: LibroTOCProvider;
126
+
127
+ @prop()
128
+ activeHeading: IHeading | undefined;
129
+
130
+ protected libroTOCManager: LibroTOCManager;
131
+
132
+ @prop()
133
+ tocTitle: string = l10n.t('大纲');
134
+
135
+ @prop()
136
+ headingCollapseState = new Map<string, boolean>();
137
+
138
+ constructor(
139
+ @inject(LibroTOCManager) libroTOCManager: LibroTOCManager,
140
+ @inject(ConfigurationService) configurationService: ConfigurationService,
141
+ ) {
142
+ super();
143
+ this.libroTOCManager = libroTOCManager;
144
+ this.configurationService = configurationService;
145
+ this.configurationService
146
+ .get(TOCVisible)
147
+ .then((value) => {
148
+ this.isDisplay = value;
149
+ return;
150
+ })
151
+ .catch(() => {
152
+ //
153
+ });
154
+ }
155
+
156
+ getHeadingIndentSize(level: number): number {
157
+ return level * 12;
158
+ }
159
+
160
+ override onViewMount() {
161
+ if (!this.tocProvider && this.parent) {
162
+ getOrigin(this.parent.initialized)
163
+ .then(() => {
164
+ this.tocProvider = this.libroTOCManager.getTOCProvider(this.parent!);
165
+ this.tocProvider.activeCellChange((header) => {
166
+ this.activeHeading = header;
167
+ });
168
+ return;
169
+ })
170
+ .catch(() => {
171
+ //
172
+ });
173
+ }
174
+ }
175
+
176
+ getDisplayHeadings(): DisplayHeading[] {
177
+ const headings = this.tocProvider?.headings ?? [];
178
+
179
+ return headings.map((item, index) => {
180
+ return {
181
+ ...item,
182
+ visible: this.isHeadingVisible(item, index, headings),
183
+ hasChild: this.hasChildren(item, index, headings),
184
+ };
185
+ });
186
+ }
187
+
188
+ protected isHeadingVisible(heading: IHeading, index: number, list: IHeading[]) {
189
+ if (index === 0) {
190
+ return true;
191
+ }
192
+ let headingCollapsed = false;
193
+ let parent = heading;
194
+ for (let i = index - 1; i >= 0; i--) {
195
+ const current = list[i];
196
+ if (current.level < parent.level) {
197
+ parent = current;
198
+ if (this.isHeadingCollapsed(parent)) {
199
+ headingCollapsed = true;
200
+ break;
201
+ }
202
+ }
203
+ }
204
+
205
+ return !headingCollapsed;
206
+ }
207
+
208
+ isHeadingCollapsed(heading: IHeading) {
209
+ return heading.id ? this.headingCollapseState.get(heading.id) ?? false : false;
210
+ }
211
+
212
+ protected hasChildren(current: IHeading, index: number, list: IHeading[]) {
213
+ if (index === list.length - 1) {
214
+ return false;
215
+ }
216
+ if (current.level < list[index + 1].level) {
217
+ return true;
218
+ }
219
+ return false;
220
+ }
221
+
222
+ override dispose() {
223
+ this.tocProvider?.dispose();
224
+ }
225
+ }