@kerebron/extension-odt 0.5.2 → 0.5.4

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 (61) hide show
  1. package/README.md +2 -2
  2. package/esm/ExtensionOdt.d.ts.map +1 -1
  3. package/esm/ExtensionOdt.js +28 -77
  4. package/esm/ExtensionOdt.js.map +1 -1
  5. package/esm/OdtParser.d.ts +41 -7
  6. package/esm/OdtParser.d.ts.map +1 -1
  7. package/esm/OdtParser.js +26 -19
  8. package/esm/OdtParser.js.map +1 -1
  9. package/esm/lists.d.ts +9 -9
  10. package/esm/lists.d.ts.map +1 -1
  11. package/esm/lists.js +21 -7
  12. package/esm/lists.js.map +1 -1
  13. package/esm/node_handlers/basic_node_handlers.d.ts +1 -0
  14. package/esm/node_handlers/basic_node_handlers.d.ts.map +1 -1
  15. package/esm/node_handlers/basic_node_handlers.js +77 -9
  16. package/esm/node_handlers/basic_node_handlers.js.map +1 -1
  17. package/esm/node_handlers/list_node_handlers.d.ts.map +1 -1
  18. package/esm/node_handlers/list_node_handlers.js +61 -65
  19. package/esm/node_handlers/list_node_handlers.js.map +1 -1
  20. package/esm/postprocess/convertCodeParagraphsToCodeBlocks.d.ts.map +1 -1
  21. package/esm/postprocess/convertCodeParagraphsToCodeBlocks.js +40 -66
  22. package/esm/postprocess/convertCodeParagraphsToCodeBlocks.js.map +1 -1
  23. package/esm/postprocess/convertMathMl.d.ts.map +1 -1
  24. package/esm/postprocess/convertMathMl.js +1 -1
  25. package/esm/postprocess/convertMathMl.js.map +1 -1
  26. package/esm/postprocess/fixContinuedLists.d.ts.map +1 -1
  27. package/esm/postprocess/fixContinuedLists.js +80 -67
  28. package/esm/postprocess/fixContinuedLists.js.map +1 -1
  29. package/esm/postprocess/fixListsLevels.d.ts +3 -0
  30. package/esm/postprocess/fixListsLevels.d.ts.map +1 -0
  31. package/esm/postprocess/fixListsLevels.js +62 -0
  32. package/esm/postprocess/fixListsLevels.js.map +1 -0
  33. package/esm/postprocess/mergeCodeBlocks.d.ts +3 -0
  34. package/esm/postprocess/mergeCodeBlocks.d.ts.map +1 -0
  35. package/esm/postprocess/mergeCodeBlocks.js +83 -0
  36. package/esm/postprocess/mergeCodeBlocks.js.map +1 -0
  37. package/esm/postprocess/postProcess.d.ts.map +1 -1
  38. package/esm/postprocess/postProcess.js +5 -2
  39. package/esm/postprocess/postProcess.js.map +1 -1
  40. package/esm/postprocess/removeUnusedBookmarks.d.ts +1 -1
  41. package/esm/postprocess/removeUnusedBookmarks.d.ts.map +1 -1
  42. package/esm/postprocess/removeUnusedBookmarks.js +16 -19
  43. package/esm/postprocess/removeUnusedBookmarks.js.map +1 -1
  44. package/esm/postprocess/urlRewrite.d.ts +4 -0
  45. package/esm/postprocess/urlRewrite.d.ts.map +1 -0
  46. package/esm/postprocess/urlRewrite.js +60 -0
  47. package/esm/postprocess/urlRewrite.js.map +1 -0
  48. package/package.json +3 -3
  49. package/src/ExtensionOdt.ts +30 -114
  50. package/src/OdtParser.ts +86 -31
  51. package/src/lists.ts +24 -10
  52. package/src/node_handlers/basic_node_handlers.ts +82 -10
  53. package/src/node_handlers/list_node_handlers.ts +91 -90
  54. package/src/postprocess/convertCodeParagraphsToCodeBlocks.ts +44 -86
  55. package/src/postprocess/convertMathMl.ts +1 -2
  56. package/src/postprocess/fixContinuedLists.ts +95 -78
  57. package/src/postprocess/fixListsLevels.ts +93 -0
  58. package/src/postprocess/mergeCodeBlocks.ts +114 -0
  59. package/src/postprocess/postProcess.ts +5 -1
  60. package/src/postprocess/removeUnusedBookmarks.ts +33 -21
  61. package/src/postprocess/urlRewrite.ts +95 -0
@@ -6,24 +6,19 @@ import {
6
6
  Extension,
7
7
  type UrlRewriter,
8
8
  } from '@kerebron/editor';
9
- import {
10
- init_debug,
11
- parse_content,
12
- parse_styles,
13
- unzip,
14
- } from '@kerebron/odt-wasm';
15
9
 
16
10
  import { OdtParser, OdtParserConfig } from './OdtParser.js';
17
11
  import { getDefaultsPostProcessFilters } from './postprocess/postProcess.js';
18
12
  import { Command } from '@kerebron/editor/commands';
19
- import { InputRulesPlugin } from '@kerebron/editor/plugins/input-rules';
13
+ import { EditorState, Transaction } from 'prosemirror-state';
14
+ import { urlRewrite } from './postprocess/urlRewrite.js';
20
15
 
21
16
  export interface OdtConfig extends OdtParserConfig {
22
17
  debug?: boolean;
23
18
  postProcessCommands?: Command[];
24
19
  }
25
20
 
26
- init_debug();
21
+ let odtWasm: Record<string, any> | undefined = undefined;
27
22
 
28
23
  export class ExtensionOdt extends Extension {
29
24
  name = 'odt';
@@ -37,14 +32,19 @@ export class ExtensionOdt extends Extension {
37
32
  editor: CoreEditor,
38
33
  schema: Schema,
39
34
  ): Record<string, Converter> {
40
- const config = this.config;
41
-
42
35
  const odtConverter = {
43
36
  fromDoc: async (document: Node): Promise<Uint8Array> => {
44
37
  throw new Error('Not implemented');
45
38
  },
46
39
  toDoc: async (buffer: Uint8Array): Promise<Node> => {
47
- const { doc, filesMap } = odtConverter.odtToJson(buffer);
40
+ if (!odtWasm) {
41
+ odtWasm = this.config.debug
42
+ ? await import('@kerebron/odt-wasm/debug')
43
+ : await import('@kerebron/odt-wasm');
44
+ odtWasm = await odtWasm.init();
45
+ }
46
+
47
+ const { doc, filesMap } = odtConverter.odtToJson(buffer, odtWasm);
48
48
 
49
49
  const filterCommands = getDefaultsPostProcessFilters({
50
50
  doc,
@@ -53,128 +53,44 @@ export class ExtensionOdt extends Extension {
53
53
  this.config.postProcessCommands || [],
54
54
  );
55
55
 
56
- const plugin = editor.state.plugins.find((plugin) =>
57
- plugin instanceof InputRulesPlugin
58
- );
59
- if (plugin) {
60
- // plugin.props.
61
- }
62
-
63
- const subEditor = editor.clone();
64
- subEditor.setDocument(doc.toJSON());
56
+ let state = EditorState.create({ doc });
57
+ const dispatch = (tr: Transaction) => {
58
+ state = state.apply(tr);
59
+ };
65
60
 
66
- let modified = false;
67
61
  if (this.urlFromRewriter) {
68
- const imageNodes: Array<{ node: Node; pos: number }> = [];
69
- subEditor.getDocument().descendants((node, pos) => {
70
- if (node.type.name === 'image') {
71
- imageNodes.push({ node, pos });
72
- }
73
- });
74
-
75
- const linkNodes: Array<{ node: Node; pos: number }> = [];
76
- subEditor.getDocument().descendants((node, pos) => {
77
- if (node.marks.find((mark) => mark.type.name === 'link')) {
78
- linkNodes.push({ node, pos });
79
- }
80
- });
81
-
82
- const tr = subEditor.state.tr;
83
-
84
- for (const { node, pos } of linkNodes) {
85
- const linkMark = node.marks.find((mark) =>
86
- mark.type.name === 'link'
87
- );
88
- if (!linkMark) {
89
- continue;
90
- }
91
- let href = linkMark.attrs.href || '';
92
- href = await this.urlFromRewriter(href, {
93
- type: 'A',
94
- dest: 'kerebron',
95
- });
96
- if (href !== linkMark.attrs.href) {
97
- const newMarks = node.marks.map((mark) => {
98
- if (mark.type.name === 'link') {
99
- const markType = this.editor.schema.marks['link'];
100
- return markType.create({ ...mark.attrs, href });
101
- }
102
- return mark;
103
- });
104
-
105
- const nodeType = this.editor.schema.nodes[node.type.name];
106
- let replaceNode;
107
- if (nodeType.isText) {
108
- replaceNode = this.editor.schema.text(
109
- node.text || '',
110
- newMarks,
111
- );
112
- } else {
113
- replaceNode = nodeType.create(
114
- node.attrs,
115
- node.content,
116
- newMarks,
117
- );
118
- }
119
- tr.replaceWith(
120
- tr.mapping.map(pos),
121
- tr.mapping.map(pos + node.nodeSize),
122
- replaceNode,
123
- );
124
- }
125
- }
126
-
127
- for (const { node, pos } of imageNodes) {
128
- let src = node.attrs.src || '';
129
-
130
- src = await this.urlFromRewriter(src, {
131
- type: 'IMG',
132
- dest: 'kerebron',
133
- filesMap,
134
- });
135
-
136
- if (src !== node.attrs.src) {
137
- const nodeType = this.editor.schema.nodes[node.type.name];
138
- const replaceNode = nodeType.create(
139
- { ...node.attrs, src },
140
- node.content,
141
- node.marks,
142
- );
143
- tr.replaceWith(
144
- tr.mapping.map(pos),
145
- tr.mapping.map(pos + node.nodeSize),
146
- replaceNode,
147
- );
148
- }
149
- }
150
- subEditor.dispatchTransaction(tr), modified = true;
62
+ await urlRewrite(this.urlFromRewriter, filesMap, state, dispatch);
151
63
  }
152
64
 
153
65
  if (filterCommands.length > 0) {
154
66
  for (const filter of filterCommands) {
155
67
  filter(
156
- subEditor.state,
157
- (tr) => subEditor.dispatchTransaction(tr),
68
+ state,
69
+ (tr) => dispatch(tr),
158
70
  );
159
71
  }
160
- modified = true;
161
72
  }
162
73
 
163
- if (modified) {
164
- return subEditor.getDocument();
74
+ if (this.config.debug) {
75
+ const event = new CustomEvent('odt:pmdoc:filtered', {
76
+ detail: {
77
+ doc: state.doc,
78
+ },
79
+ });
80
+ this.editor.dispatchEvent(event);
165
81
  }
166
82
 
167
- return doc;
83
+ return state.doc;
168
84
  },
169
- odtToJson: (buffer: Uint8Array) => {
170
- const files = unzip(buffer);
85
+ odtToJson: (buffer: Uint8Array, odtWasm: any) => {
86
+ const files = odtWasm.unzip(buffer);
171
87
  const filesMap: Record<string, Uint8Array> = {};
172
88
  for (const k of files.keys()) {
173
89
  filesMap[k] = Uint8Array.from(files.get(k));
174
90
  }
175
91
 
176
- const stylesTree = parse_styles(files.get('styles.xml'));
177
- const contentTree = parse_content(files.get('content.xml'));
92
+ const stylesTree = odtWasm.parse_styles(files.get('styles.xml'));
93
+ const contentTree = odtWasm.parse_content(files.get('content.xml'));
178
94
 
179
95
  if (this.config.debug) {
180
96
  const event = new CustomEvent('odt:parsed', {
package/src/OdtParser.ts CHANGED
@@ -6,7 +6,6 @@ import {
6
6
  import { getListNodesHandlers } from './node_handlers/list_node_handlers.js';
7
7
  import { getTableNodesHandlers } from './node_handlers/table_node_handlers.js';
8
8
  import { ListTracker } from './lists.js';
9
- import { type UrlRewriter } from '@kerebron/editor';
10
9
 
11
10
  const COURIER_FONTS = ['Courier New', 'Courier', 'Roboto Mono'];
12
11
 
@@ -16,39 +15,95 @@ export interface OdtElement {
16
15
 
17
16
  export type NodeHandler = (ctx: OdtStashContext, value: any) => void;
18
17
 
19
- interface ListStyle {
20
- '@name': string;
18
+ export interface ListLLevelLabelAlignment {
19
+ '@margin-left'?: string;
21
20
  }
22
21
 
23
- interface Style {
24
- '@name': string;
22
+ export interface ListLevelProperties {
23
+ 'list-level-label-alignment': ListLLevelLabelAlignment;
24
+ }
25
+
26
+ export interface ListLevelStyleBullet {
27
+ '@level': number;
28
+ 'list-level-properties': ListLevelProperties;
29
+ }
30
+
31
+ export interface ListLevelStyleNumber {
32
+ '@level': number;
33
+ '@start-value'?: number;
34
+ '@num-format': string;
35
+ 'list-level-properties': ListLevelProperties;
36
+ }
37
+
38
+ export interface ListStyle {
39
+ '@name'?: string;
40
+ 'list-level-style-bullet': ListLevelStyleBullet[];
41
+ 'list-level-style-number': ListLevelStyleNumber[];
42
+ }
43
+
44
+ export interface TextProperty {
45
+ '@font-name'?: string;
46
+ '@font-weight'?: string;
47
+ '@font-style'?: string;
48
+ '@font-size'?: string;
49
+ '@text-underline-style'?: string;
50
+ '@color'?: string;
51
+ }
52
+
53
+ export interface ParagraphProperty {
54
+ '@break-before'?: string;
55
+ '@break-after'?: string;
56
+ '@margin-left'?: string;
57
+ }
58
+
59
+ export interface Style {
60
+ '@name'?: string;
25
61
  '@parent-style-name'?: string;
26
62
  styles: string[];
63
+ 'text-properties'?: TextProperty;
64
+ 'paragraph-properties'?: TextProperty;
27
65
  }
28
66
 
29
- interface StylesTree {
67
+ export interface StylesTree {
30
68
  styles: {
31
69
  'list-style': Array<ListStyle>;
32
70
  'style': Array<Style>;
33
71
  };
34
72
  }
35
73
 
36
- interface AutomaticStyles {
74
+ export interface AutomaticStyles {
37
75
  'style': Array<Style>;
38
76
  }
39
77
 
40
- export function resolveStyle(
78
+ export function resolveListStyle(
41
79
  stylesTree: StylesTree,
42
80
  automaticStyles: AutomaticStyles,
43
81
  name: string,
44
- ): Style {
45
- let style: Style;
82
+ ): ListStyle {
83
+ let style: ListStyle | undefined;
84
+
85
+ style = stylesTree.styles['list-style'].find((item) =>
86
+ item['@name'] === name
87
+ );
46
88
 
47
89
  if (!style) {
48
- style = stylesTree.styles['list-style'].find((item) =>
49
- item['@name'] === name
50
- );
90
+ style = {
91
+ '@name': name,
92
+ 'list-level-style-number': [],
93
+ 'list-level-style-bullet': [],
94
+ };
51
95
  }
96
+
97
+ return style;
98
+ }
99
+
100
+ export function resolveStyle(
101
+ stylesTree: StylesTree,
102
+ automaticStyles: AutomaticStyles,
103
+ name: string,
104
+ ): Style {
105
+ let style: Style | undefined;
106
+
52
107
  if (!style) {
53
108
  style = stylesTree.styles['style'].find((item) => item['@name'] === name);
54
109
  }
@@ -59,6 +114,7 @@ export function resolveStyle(
59
114
  if (!style) {
60
115
  style = {
61
116
  '@name': name,
117
+ styles: [],
62
118
  };
63
119
  }
64
120
 
@@ -72,7 +128,7 @@ export function resolveStyle(
72
128
  );
73
129
  if (parentStyle) {
74
130
  const styles = [...style['styles'], ...parentStyle['styles']];
75
- for (const key in style) {
131
+ for (const key of Object.keys(style) as (keyof Style)[]) {
76
132
  if (typeof style[key] === 'undefined') {
77
133
  delete style[key];
78
134
  }
@@ -198,6 +254,10 @@ export class OdtStashContext {
198
254
  this.current.content = [];
199
255
  }
200
256
 
257
+ public dropNode() {
258
+ this.unstash();
259
+ }
260
+
201
261
  public closeNode(type: string, attrs = {}, marks = Mark.none) {
202
262
  const node = this.createNode(type, attrs, marks);
203
263
  this.unstash();
@@ -264,7 +324,9 @@ export class OdtStashContext {
264
324
  this.automaticStyles,
265
325
  element['@style-name'],
266
326
  )
267
- : {};
327
+ : {
328
+ styles: [],
329
+ };
268
330
 
269
331
  return style;
270
332
  }
@@ -291,21 +353,14 @@ export class OdtParser {
291
353
  ...getBasicNodesHandlers(),
292
354
  ...getListNodesHandlers(),
293
355
  ...getTableNodesHandlers(),
294
-
295
- 'change-start': {
296
- // custom(state) {
297
- // state.textMarks.add({
298
- // markName: 'change',
299
- // markAttributes: {},
300
- // });
301
- // },
302
- },
303
- 'change-end': {
304
- // custom(state) {
305
- // state.textMarks.forEach((x) =>
306
- // x.markName === 'change' ? state.textMarks.delete(x) : x
307
- // );
308
- // },
356
+ 'g': () => { // Test is: embedded-diagram-example.odt
357
+ // DrawG draw:g
358
+ const node = ctx.createText(
359
+ 'INSTEAD OF EMBEDDED DIAGRAM ABOVE USE EMBEDDED DIAGRAM FROM DRIVE AND PUT LINK TO IT IN THE DESCRIPTION. See: https://github.com/mieweb/wikiGDrive/issues/353',
360
+ );
361
+ if (node) {
362
+ ctx.current.content.push(node);
363
+ }
309
364
  },
310
365
  'frame': (ctx: OdtStashContext, odtElement: any) => {
311
366
  if (odtElement.object && odtElement.object['@href']) {
@@ -322,7 +377,7 @@ export class OdtParser {
322
377
  }
323
378
  }
324
379
  if (odtElement.image && odtElement.image['@href']) { // TODO links rewrite
325
- const alt = odtElement.description?.value || '';
380
+ const alt = odtElement.desc?.['$value'] || '';
326
381
  const src = odtElement.image['@href'];
327
382
  ctx.openNode();
328
383
  ctx.closeNode('image', {
package/src/lists.ts CHANGED
@@ -1,10 +1,8 @@
1
- import { OdtElement } from './OdtParser.js';
2
-
3
1
  export class ListNumbering {
4
2
  levels: { [level: number]: number } = {};
5
- levelNodes: { [level: number]: Node } = {};
3
+ forceStart: { [level: number]: boolean } = {};
6
4
 
7
- constructor() {
5
+ constructor(public readonly id: string) {
8
6
  for (let i = 0; i < 20; i++) {
9
7
  this.levels[i] = 1;
10
8
  }
@@ -16,20 +14,36 @@ export class ListNumbering {
16
14
  }
17
15
  }
18
16
 
19
- setLevelNode(level: number, node: Node) {
20
- this.levelNodes[level] = node;
17
+ clone(id: string): ListNumbering {
18
+ const retVal = new ListNumbering(id);
19
+
20
+ retVal.levels = structuredClone(retVal.levels);
21
+ retVal.forceStart = structuredClone(retVal.forceStart);
22
+
23
+ return retVal;
21
24
  }
22
25
  }
23
26
 
24
27
  export interface List {
25
28
  level: number;
26
- odtElement: OdtElement;
29
+ id?: string;
30
+ styleName: string;
27
31
  }
28
32
 
29
33
  export class ListTracker {
30
34
  listStack: List[] = [];
31
35
 
32
- listNumberings: Map<string, ListNumbering> = new Map<string, ListNumbering>();
33
- lastNumbering?: ListNumbering;
34
- preserveMinLevel = 999;
36
+ pushList(id?: string, styleName = ''): List {
37
+ const list: List = {
38
+ id,
39
+ styleName,
40
+ level: this.listStack.length + 1,
41
+ };
42
+ this.listStack.push(list);
43
+ return list;
44
+ }
45
+
46
+ getCurrentList(): List {
47
+ return this.listStack[this.listStack.length - 1];
48
+ }
35
49
  }
@@ -1,5 +1,22 @@
1
+ import { NESTING_CLOSING, NESTING_OPENING } from '@kerebron/editor';
1
2
  import { iterateChildren, NodeHandler, OdtStashContext } from '../OdtParser.js';
2
3
 
4
+ export function inchesToMm(value: string): number {
5
+ if (!value) {
6
+ return 0;
7
+ }
8
+ if (value.endsWith('pt')) {
9
+ return parseFloat(value.substring(0, value.length - 2)) * 0.3528;
10
+ }
11
+ if (value.endsWith('in')) {
12
+ return parseFloat(value.substring(0, value.length - 2)) * 25.4;
13
+ }
14
+ if (value.endsWith('em')) {
15
+ return parseFloat(value.substring(0, value.length - 2)) / 0.125 * 25.4;
16
+ }
17
+ return 0;
18
+ }
19
+
3
20
  export function getInlineNodesHandlers(): Record<string, NodeHandler> {
4
21
  return {
5
22
  '$text': (ctx: OdtStashContext, value: any) => {
@@ -23,23 +40,21 @@ export function getInlineNodesHandlers(): Record<string, NodeHandler> {
23
40
  }
24
41
  },
25
42
  'rect': (ctx: OdtStashContext, odtElement: any) => {
26
- // if (odtElement['@rel-width'] === '100%') {
27
- // ctx.openNode();
28
- // ctx.closeNode('hr');
29
- // }
43
+ if (odtElement['@rel-width'] === '100%') {
44
+ ctx.openNode();
45
+ ctx.closeNode('hr');
46
+ }
30
47
  },
31
48
  'line-break': (ctx: OdtStashContext, odtElement: any) => {
32
49
  ctx.openNode();
33
50
  ctx.closeNode('br');
34
51
  },
35
52
  'soft-page-break': (ctx: OdtStashContext, odtElement: any) => {
36
- ctx.openNode();
37
- ctx.closeNode('br');
38
53
  },
39
54
 
40
55
  'bookmark': (ctx: OdtStashContext, element: any) => { // bookmark for parent para
41
56
  ctx.openNode();
42
- ctx.closeNode('bookmark-node', {
57
+ ctx.closeNode('node_bookmark', {
43
58
  id: element['@name'],
44
59
  });
45
60
  },
@@ -59,6 +74,20 @@ export function getInlineNodesHandlers(): Record<string, NodeHandler> {
59
74
  // : x
60
75
  // );
61
76
  },
77
+ 'change-start': (ctx: OdtStashContext, element: any) => {
78
+ ctx.openNode();
79
+ ctx.closeNode('comment', {
80
+ id: element['@change-id'],
81
+ nesting: NESTING_OPENING,
82
+ });
83
+ },
84
+ 'change-end': (ctx: OdtStashContext, element: any) => {
85
+ ctx.openNode();
86
+ ctx.closeNode('comment', {
87
+ id: element['@change-id'],
88
+ nesting: NESTING_CLOSING,
89
+ });
90
+ },
62
91
  };
63
92
  }
64
93
 
@@ -74,7 +103,7 @@ export function getBasicNodesHandlers(): Record<string, NodeHandler> {
74
103
  );
75
104
  },
76
105
  'p': (ctx: OdtStashContext, value: any) => {
77
- const attrs = {};
106
+ const attrs: Record<string, any> = {};
78
107
 
79
108
  const style = ctx.getElementStyle(value);
80
109
  const heading = style.styles.find((item) =>
@@ -103,13 +132,56 @@ export function getBasicNodesHandlers(): Record<string, NodeHandler> {
103
132
  },
104
133
 
105
134
  'table-of-content': (ctx: OdtStashContext, value: any) => {
106
- ctx.openNode();
135
+ const levels: number[] = [];
136
+
137
+ let prevMarginLeft = -1;
107
138
  for (const pElem of value['index-body']['p']) {
139
+ const style = ctx.getElementStyle(pElem) as any;
140
+ let marginLeft = 0;
141
+ if ('paragraph-properties' in style) {
142
+ marginLeft = inchesToMm(
143
+ style['paragraph-properties']['@margin-left'],
144
+ );
145
+ }
146
+
147
+ if (prevMarginLeft < marginLeft) {
148
+ if (levels.length > 0) {
149
+ ctx.openNode(); // list_item
150
+ }
151
+ ctx.openNode(); // bullet_list
152
+ levels.push(marginLeft);
153
+ } else {
154
+ for (let i = levels.length - 1; i >= 0; i--) {
155
+ if (levels[i] > marginLeft) {
156
+ ctx.closeNode('bullet_list', { odtMarginLeft: marginLeft });
157
+ if (i > 0) {
158
+ ctx.closeNode('list_item');
159
+ }
160
+ levels.pop();
161
+ } else {
162
+ break;
163
+ }
164
+ }
165
+ }
108
166
  ctx.openNode();
109
167
  ctx.handle('p', pElem);
110
168
  ctx.closeNode('list_item');
169
+
170
+ prevMarginLeft = marginLeft;
171
+ }
172
+
173
+ for (let i = levels.length - 1; i >= 0; i--) {
174
+ const marginLeft = levels.pop();
175
+ if (i > 0) {
176
+ ctx.closeNode('bullet_list', { odtMarginLeft: marginLeft });
177
+ ctx.closeNode('list_item');
178
+ } else {
179
+ ctx.closeNode('bullet_list', {
180
+ odtMarginLeft: marginLeft,
181
+ toc: true,
182
+ });
183
+ }
111
184
  }
112
- ctx.closeNode('bullet_list');
113
185
  },
114
186
 
115
187
  'span': (ctx: OdtStashContext, value: any) => {