@pretextbook/web-editor 0.4.3 → 0.5.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.
@@ -97,6 +97,13 @@ export declare function ensureLatexSectionWrapper(content: string, type: Documen
97
97
  * - For `\begin{section}` style: updates the `\title{…}` inside.
98
98
  */
99
99
  export declare function updateLatexSectionTitle(content: string, newTitle: string): string;
100
+ /**
101
+ * Derive a LaTeX section's title directly from its header — the code
102
+ * editor's source-of-truth content — mirroring the two header styles
103
+ * {@link updateLatexSectionTitle} writes. Returns `null` when no header is
104
+ * found (introduction/conclusion have none), so callers leave title as-is.
105
+ */
106
+ export declare function extractLatexDivisionTitle(content: string): string | null;
100
107
  /** Create a new blank LaTeX section as a `Division`. */
101
108
  export declare function createNewLatexSection(title?: string): DocumentSection;
102
109
  /** Create a blank LaTeX introduction. */
@@ -123,6 +130,22 @@ export declare function getSectionAttributes(content: string): {
123
130
  xmlId: string;
124
131
  label: string;
125
132
  };
133
+ /**
134
+ * Derive a division's title, type, `xml:id`, and `label` directly from its
135
+ * full PreTeXt source — the code editor's content, wrapper tag included.
136
+ * Used to keep the TOC in sync when the user edits these directly in the
137
+ * source rather than through the metadata dropdown form.
138
+ *
139
+ * Returns `null` when `content` isn't well-formed XML or its root element
140
+ * isn't a recognised division tag (both common mid-edit), so callers can
141
+ * skip the update rather than clobbering existing metadata with junk.
142
+ */
143
+ export declare function extractDivisionMetadata(content: string): {
144
+ title: string;
145
+ type: DivisionType;
146
+ xmlId: string;
147
+ label: string;
148
+ } | null;
126
149
  /**
127
150
  * Update the title, tag name (type), `xml:id`, and `label` of a section.
128
151
  *
@@ -201,6 +224,26 @@ export declare function removeDivisionRef(content: string, xmlId: string): strin
201
224
  * - `afterXmlId` moves it immediately after that ref.
202
225
  */
203
226
  export declare function moveDivisionRef(content: string, xmlId: string, afterXmlId: string | null): string;
227
+ /**
228
+ * Rename an existing `<plus:* ref="oldXmlId"/>` placeholder in-place to point
229
+ * at `newXmlId`, also updating the `*` tag name to `newType` if it changed.
230
+ * Unlike {@link moveDivisionRef}, the placeholder's position is left
231
+ * untouched — only its `ref` value and element name are rewritten.
232
+ *
233
+ * Used to keep a parent division's child placeholder in sync when the
234
+ * child's own `xml:id`/type are edited directly in its source, so the
235
+ * rename doesn't orphan the child from its parent.
236
+ *
237
+ * Returns `content` unchanged if no placeholder for `oldXmlId` is found.
238
+ */
239
+ export declare function renameDivisionRef(content: string, oldXmlId: string, newXmlId: string, newType: DivisionType): string;
240
+ /**
241
+ * Find the division in `divisions` whose content contains a
242
+ * `<plus:* ref="xmlId"/>` placeholder for `xmlId` — i.e. `xmlId`'s parent in
243
+ * the division tree. Returns `null` if `xmlId` is unplaced (orphaned) or is
244
+ * the root.
245
+ */
246
+ export declare function findDivisionParent(divisions: Division[], xmlId: string): Division | null;
204
247
  /**
205
248
  * Rewrite `content` so its `<plus:* ref="..."/>` placeholders appear in the
206
249
  * order given by `orderedXmlIds`.
@@ -1,12 +1,24 @@
1
1
  /**
2
2
  * Per-instance Zustand store for the Editors component.
3
3
  *
4
- * ARCHITECTURE NOTE — host data always wins:
5
- * Editors.tsx syncs all controlled props into the store on every render via
6
- * syncState(). This means when the host pushes fresh `divisions` (e.g. after a
7
- * refetchOnWindowFocus), every deep component automatically re-renders with the
8
- * new data. Never let store mutations "win" over incoming props — always call
9
- * the host callbacks and let the host's state update propagate back in.
4
+ * ARCHITECTURE NOTE — the store owns the live editing buffer:
5
+ * `createEditorStore(init)` seeds the editing buffer (`divisions`, `title`,
6
+ * `docinfo`, `activeDivisionId`, …) from the host's initial props *once*.
7
+ * After that, the store is authoritative for what's being edited:
8
+ * Internal edit actions (`setDivisionContent`, `patchDivision`, `setTitle`,
9
+ * …) update the store optimistically and the host callbacks are fired
10
+ * purely as notifications (so the host can persist/autosave). A host is no
11
+ * longer required to echo every edit back as new props for it to display.
12
+ * • Genuine external updates (a save that reconciles server-assigned ids, or
13
+ * swapping to a different project) still win: Editors.tsx detects when a
14
+ * controlled prop actually changes since the last render and calls
15
+ * `applyExternalUpdate()` to overwrite the buffer. A stale prop that the
16
+ * host simply never updated is NOT re-applied, so it can't clobber a local
17
+ * edit.
18
+ *
19
+ * Derived/config fields that are never edited locally (`source`, `sourceFormat`,
20
+ * `projectAssets`, `projectType`, `rootDivisionId`, …) are still mirrored from
21
+ * props every render via `syncState()`.
10
22
  *
11
23
  * Callback stability: createEditorStore returns a `bindCallbacks` function that
12
24
  * EditorsInner calls from useLayoutEffect after every render. Store actions
@@ -24,6 +36,20 @@ export type DivisionChanges = {
24
36
  sourceFormat?: SourceFormat;
25
37
  label?: string | null;
26
38
  };
39
+ /**
40
+ * A batch of editing-buffer fields the host has genuinely changed (an external
41
+ * reset). Only the provided fields are overwritten in the store; omitted
42
+ * fields keep their current — possibly locally edited — value.
43
+ */
44
+ export interface ExternalUpdate {
45
+ divisions?: Division[];
46
+ rootDivisionId?: string;
47
+ activeDivisionId?: string | null;
48
+ title?: string;
49
+ docinfo?: string;
50
+ commonDocinfo?: string;
51
+ useCommonDocinfo?: boolean;
52
+ }
27
53
  type ModalKey = "isLatexDialogOpen" | "isConvertDialogOpen" | "isDocinfoEditorOpen" | "isAssetPickerOpen";
28
54
  /**
29
55
  * All callbacks wired by Editors.tsx that deep components need to call.
@@ -71,21 +97,36 @@ export interface EditorStoreState {
71
97
  isConvertDialogOpen: boolean;
72
98
  isDocinfoEditorOpen: boolean;
73
99
  isAssetPickerOpen: boolean;
74
- internalTitle: string;
75
- internalDocinfo: string;
76
- internalCommonDocinfo: string;
77
- internalUseCommonDocinfo: boolean;
78
100
  editingId: string | null;
79
101
  editDraft: EditDraft | null;
80
102
  /** Sync a batch of derived/controlled data from Editors into the store. */
81
103
  syncState: (partial: Partial<EditorSyncableState>) => void;
104
+ /** Apply a genuine external update from the host (host wins). */
105
+ applyExternalUpdate: (partial: ExternalUpdate) => void;
106
+ /** Optimistically set a division's content in the local pool. */
107
+ setDivisionContent: (xmlId: string, content: string) => void;
108
+ /** Optimistically patch a division's metadata (title/type/xml:id/format). */
109
+ patchDivision: (xmlId: string, changes: DivisionChanges) => void;
110
+ /** Optimistically add a division to the local pool (no-op if it exists). */
111
+ addDivisionToPool: (division: Division) => void;
112
+ /** Optimistically remove a division from the local pool. */
113
+ removeDivisionFromPool: (xmlId: string) => void;
114
+ /** Set the active (open-for-editing) division id. */
115
+ setActiveDivisionId: (id: string | null) => void;
116
+ /** Optimistically set the document title. */
117
+ setTitle: (title: string) => void;
118
+ /** Optimistically set the docinfo-related fields together. */
119
+ setDocinfo: (info: {
120
+ docinfo: string;
121
+ commonDocinfo: string;
122
+ useCommonDocinfo: boolean;
123
+ }) => void;
82
124
  setShowFullPreview: (show: boolean) => void;
83
125
  setActiveTab: (tab: "editor" | "preview") => void;
84
126
  setIsNarrowScreen: (narrow: boolean) => void;
85
127
  setIsTocCollapsed: (value: boolean | ((prev: boolean) => boolean)) => void;
86
128
  openModal: (modal: ModalKey) => void;
87
129
  closeModal: (modal: ModalKey) => void;
88
- setInternalTitle: (title: string) => void;
89
130
  selectSection: (id: string) => void;
90
131
  addSection: (afterId: string | null) => void;
91
132
  removeSection: (id: string) => void;
@@ -102,7 +143,7 @@ export interface EditorStoreState {
102
143
  feedbackSubmit: (feedback: FeedbackSubmission) => void;
103
144
  }
104
145
  /** The subset of EditorStoreState that Editors.tsx syncs on each render. */
105
- export type EditorSyncableState = Pick<EditorStoreState, "source" | "sourceFormat" | "projectAssets" | "libraryAssets" | "title" | "docinfo" | "commonDocinfo" | "useCommonDocinfo" | "projectType" | "projectUrl" | "divisions" | "rootDivisionId" | "activeDivisionId" | "canConvertToPretext" | "activeEditorSource" | "hasFeedback" | "internalDocinfo" | "internalCommonDocinfo" | "internalUseCommonDocinfo">;
146
+ export type EditorSyncableState = Pick<EditorStoreState, "source" | "sourceFormat" | "projectAssets" | "libraryAssets" | "title" | "docinfo" | "commonDocinfo" | "useCommonDocinfo" | "projectType" | "projectUrl" | "divisions" | "rootDivisionId" | "activeDivisionId" | "canConvertToPretext" | "activeEditorSource" | "hasFeedback">;
106
147
  export interface EditorStoreInit {
107
148
  source: string;
108
149
  sourceFormat: SourceFormat;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pretextbook/web-editor",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "A web-based editor for PreTeXt documents, with simple preview functionality",
6
6
  "keywords": [