/** * Utilities for splitting a PreTeXt article into individual sections and * merging them back into a complete document. * * Splitting always works at the `
`, ``, and * `` level inside a top-level `
` element. */ import type { Asset, SourceFormat } from "./types/editor"; import type { Division, DivisionType, DocumentSection, DocumentSectionType, DocumentSplitResult } from "./types/sections"; /** * Replace (or insert) the `` of a section XML string with `newTitle`. * Returns the updated XML string. */ export declare function updateDivisionTitle(divisionXml: string, newTitle: string): string; /** Create a new blank `<section>` as a `Division`. */ export declare function createNewSection(title?: string): DocumentSection; /** Create a blank `<introduction>` division. */ export declare function createIntroduction(): DocumentSection; /** Create a blank `<conclusion>` division. */ export declare function createConclusion(): DocumentSection; /** * Strip the outer wrapper element from a section XML string, returning just the * inner XML content (i.e. everything between `<section>` and `</section>`). * Used to show only the section body in the code editor so users can't * accidentally edit or delete the enclosing element. */ export declare function stripSectionWrapper(sectionXml: string): string; /** * Re-wrap inner XML content (as returned by the code editor) with the correct * outer element for the given section type. * * Because `DocumentSectionType` values are identical to the XML tag names, * this is simply `<${type}>inner</${type}>`. */ export declare function rewrapSection(innerXml: string, type: DocumentSectionType): string; /** * Ensure the given XML string has the correct outer element for its section * type. If the outer tag is already present it is returned unchanged; * otherwise the content is re-wrapped so that accidental deletions in the * code editor are recovered gracefully. */ export declare function ensureSectionWrapper(content: string, type: DocumentSectionType): string; export declare function splitDocument(xml: string): DocumentSplitResult; export declare function mergeDocument(wrapper: string, sections: DocumentSection[]): string; /** * Strip the outer `<chapter>` element from a chapter XML string, returning * just its inner content (title, sections, etc.). Behaves exactly like * {@link stripSectionWrapper}: the enclosing element is dropped but all * children are kept, so the user edits the chapter body without ever seeing * or editing the `<chapter>` division tag itself. */ export declare function stripChapterWrapper(chapterXml: string): string; /** * Re-wrap chapter body content (as produced by the code editor) with the * original `<chapter>` element, preserving its tag name and all attributes * (e.g. `xml:id`, `label`). * * The attributes are recovered from `originalChapterXml` — the last known * full chapter source — so that editing the body never drops them. String * concatenation (rather than re-serialising via xast) keeps this robust to * invalid inner XML while the user is mid-edit. */ export declare function rewrapChapter(innerXml: string, originalChapterXml: string): string; export declare function splitLatexDocument(latex: string): DocumentSplitResult; export declare function mergeLatexDocument(wrapper: string, sections: DocumentSection[]): string; /** * Strip the section-level header from a LaTeX section string so the code * editor shows only the body content. * * - For `\section{…}`-style sections: removes the leading `\section{…}` command. * - For `\begin{section}…\end{section}`-style: removes the wrapper tags. * - For introduction / conclusion (no header): returns as-is. */ export declare function stripLatexSectionWrapper(content: string, type: DocumentSectionType): string; /** * Re-wrap inner LaTeX content (as produced by the code editor) with the * correct section header for the given section type and title. * * - `section` type: prepends `\section{title}`. * - `introduction` / `conclusion`: returns inner content unchanged. * * Pass `originalContent` to detect whether the document uses environment style * (`\begin{section}…\end{section}`) so the same style is preserved. */ export declare function rewrapLatexSection(inner: string, type: DocumentSectionType, title: string, originalContent?: string): string; /** * Ensure the given LaTeX string has the correct section header/wrapper for its * type. If the header is already present it is returned unchanged; otherwise * it is re-wrapped using `rewrapLatexSection` so that accidental deletions in * the code editor are recovered gracefully. */ export declare function ensureLatexSectionWrapper(content: string, type: DocumentSectionType, title: string, originalContent?: string): string; /** * Replace (or insert) the section title in a LaTeX section string. * * - For command style (`\section{…}`, `\worksheet{…}`, …): updates the header * command's argument. * - For `\begin{section}` style: updates the `\title{…}` inside. */ export declare function updateLatexSectionTitle(content: string, newTitle: string): string; /** * Derive a LaTeX division's title directly from its header — the code * editor's source-of-truth content — mirroring the two header styles * {@link updateLatexSectionTitle} writes. Returns `null` when no header is * found (introduction/conclusion have none), so callers leave title as-is. */ export declare function extractLatexDivisionTitle(content: string): string | null; /** * Extract the `\label{…}` that immediately follows a LaTeX division's header * command — the LaTeX spelling of a division's `xml:id`, since * `@pretextbook/latex-pretext` maps `\label` → `xml:id`. Only the header's * label is read (a `\label` inside the body is ignored). Returns `""` when no * header label is present. */ export declare function extractLatexSectionLabel(content: string): string; /** * Update a LaTeX division's header type, title, and/or `xml:id` in place. * * - `type` rewrites the header command name (`\section{` → `\worksheet{`) so the * source reads as the division it represents. * - `title` rewrites the header command's argument. * - `xmlId` rewrites the `\label{…}` directly after the header — inserting it * when absent, removing it when `null`/empty. * * Omit a key (or pass `undefined`) to leave it unchanged. Only the * command-style header is handled — the style the code editor freezes and the * TOC form exposes for editing. This is the LaTeX analogue of * {@link updateSectionMetadata} and {@link updateMarkdownDivisionMetadata} — * same `(division, changes) => Division` shape — but LaTeX has no * representation for PreTeXt's separate `label` attribute, so `label` is * accepted (to keep the same `changes` shape) and ignored. */ export declare function updateLatexDivisionMetadata(division: Division, changes: { title?: string; type?: DivisionType; xmlId?: string | null; label?: string | null; }): Division; /** * Convert a LaTeX division's source to PreTeXt by passing the visible LaTeX * straight to `@pretextbook/latex-pretext` and using its output as-is. * * A content division's header (`\section{…}\label{…}`, `\worksheet{…}`, …) * converts to its own complete `<type xml:id="…"><title>…>` element, so the * conversion is used exactly as produced — if a header doesn't convert * correctly, that surfaces here to be fixed in the converter rather than worked * around. Root divisions (`book`/`article`/`slideshow`) hold a whole document * body that converts to a sequence of elements, so it is wrapped in the root * element (whose title/`xml:id` aren't expressed in the LaTeX body). * * Returns `null` when the conversion fails, so callers can disable the convert * action / fall back. */ export declare function latexDivisionToTaggedPretext(division: Pick<Division, "content" | "type" | "xmlId" | "title">): string | null; /** Create a new blank LaTeX section as a `Division`. */ export declare function createNewLatexSection(title?: string): DocumentSection; /** Create a blank LaTeX introduction. */ export declare function createLatexIntroduction(): DocumentSection; /** Create a blank LaTeX conclusion. */ export declare function createLatexConclusion(): DocumentSection; /** * Merge two adjacent sections into one, keeping the title of the first. * * - For PreTeXt: parses both sections and concatenates the body children * (skipping the second section's `<title>`). * - For LaTeX: strips the second section's header and appends its body. * * @param a First (absorbing) section. * @param b Second section whose content is appended to `a`. * @param isLatex Whether the document source is LaTeX. */ export declare function mergeTwoSections(a: DocumentSection, b: DocumentSection, isLatex: boolean): DocumentSection; /** * Extract `xml:id` and `label` attributes from the root element of a section * content string. Returns empty strings when the attributes are absent. */ export declare function getSectionAttributes(content: string): { xmlId: string; label: string; }; /** * Coerce a user-entered string into a value usable as an XML `xml:id` * (an NCName). Disallowed characters are replaced with `-`, and any leading * characters that can't start an NCName (digits, `-`, `.`) are stripped. * * Returns `""` when nothing valid remains — callers treat that as "reject" * since a division's `xml:id` is its identity and may not be empty. */ export declare function sanitizeXmlId(raw: string): string; /** * Derive a division's title, type, `xml:id`, and `label` directly from its * full PreTeXt source — the code editor's content, wrapper tag included. * Used to keep the TOC in sync when the user edits these directly in the * source rather than through the metadata dropdown form. * * Returns `null` when `content` isn't well-formed XML or its root element * isn't a recognised division tag (both common mid-edit), so callers can * skip the update rather than clobbering existing metadata with junk. */ export declare function extractDivisionMetadata(content: string): { title: string; type: DivisionType; xmlId: string; label: string; } | null; /** * Update the title, tag name (type), `xml:id`, and `label` of a section. * * Pass `null` for `xmlId` or `label` to remove the attribute entirely. * Omit a key (or pass `undefined`) to leave it unchanged. * * Returns a new `DocumentSection` with updated `content`, `title`, and `type`. */ export declare function updateSectionMetadata(section: DocumentSection, changes: { title?: string; type?: DocumentSectionType; xmlId?: string | null; label?: string | null; }): DocumentSection; /** * Update the `<title>`, `xml:id`, and `label` of a chapter XML string. * * Mirrors {@link updateSectionMetadata} but operates on a raw chapter source * string and never changes the element's tag name (a chapter is always a * chapter). Pass `null`/empty for `xmlId` or `label` to remove the * attribute; omit a key (or pass `undefined`) to leave it unchanged. */ export declare function updateChapterMetadata(chapterXml: string, changes: { title?: string; xmlId?: string | null; label?: string | null; }): string; /** * Parse the leading frontmatter block of a markdown division into its * structural metadata and remaining body. Returns `null` when no well-formed * frontmatter block is present (common mid-edit), so callers can skip rather * than clobber metadata with junk. */ export declare function parseMarkdownFrontmatter(content: string): { type: DivisionType; xmlId: string; label: string; body: string; } | null; /** Build a `---`-fenced frontmatter block for a markdown division. */ export declare function buildMarkdownFrontmatter(meta: { type: DivisionType; xmlId: string; label: string; }): string; /** Extract a markdown division's leading `# heading` text, or `null` if none. */ export declare function deriveMarkdownTitle(body: string): string | null; /** * Derive a markdown division's title, type, `xml:id`, and `label` directly from * its source — the frontmatter for the structural metadata and the leading * `# heading` for the title. Markdown analogue of {@link extractDivisionMetadata}; * returns `null` when the frontmatter is absent/malformed (both common * mid-edit), so callers can skip the update rather than clobber metadata. */ export declare function extractMarkdownDivisionMetadata(content: string): { title: string; type: DivisionType; xmlId: string; label: string; } | null; /** * Update the title, type (`division`), `xml:id`, and `label` of a markdown * division. Structural metadata is rewritten in the frontmatter block; a title * change rewrites the body's leading `# heading`. Markdown analogue of * {@link updateSectionMetadata} (which is XML-only and would wrongly inject a * `<title>` element). Pass `null`/empty for `label` to clear it; omit a key to * leave it unchanged. The `xml:id` is never cleared — it is the division's * identity — so an empty value falls back to the record's existing id. */ export declare function updateMarkdownDivisionMetadata(division: Division, changes: { title?: string; type?: DocumentSectionType; xmlId?: string | null; label?: string | null; }): Division; export declare function wrapDocumentAsSection(xml: string, sectionTitle?: string): DocumentSplitResult; export declare function wrapLatexDocumentAsSection(latex: string, sectionTitle?: string): DocumentSplitResult; /** * Return the ordered list of `xmlId` values referenced by * `<plus:* ref="..."/>` placeholders found in `content`. * * Only direct children are returned — the function does not recurse. * Call it for each division in the pool to build the full tree. */ export declare function parseDivisionRefs(content: string): string[]; /** * Like {@link parseDivisionRefs} but also returns the division type inferred * from the tag name (e.g. `<plus:chapter ref="x"/>` → `{ type: "chapter", xmlId: "x" }`). * Used to auto-create Division records when new refs appear in edited content. * * Only tag names in {@link DIVISION_REF_TAGS} are considered — asset * placeholders (`plus:image`, `plus:doenet`, ...) are not divisions and are * skipped. The generic `<plus:division ref="x"/>` alias falls back to type * `"section"`, matching {@link tagToType}'s default for unrecognised tags. */ export declare function parseDivisionRefsWithTypes(content: string): { xmlId: string; type: DivisionType; }[]; /** A `<plus:image ref="..."/>` / `<plus:doenet ref="..."/>` asset placeholder. */ export interface AssetRef { kind: "image" | "doenet"; ref: string; } /** * Parse every `<plus:image ref="..."/>` / `<plus:doenet ref="..."/>` asset * placeholder out of `content`, in document order, without de-duplicating. * * Asset placeholders share the `<plus:* ref="..."/>` shape used by division * refs (see {@link DIVISION_REF_TAGS}) but are deliberately parsed by a * separate, disjoint tag set so the two kinds of include are never conflated. */ export declare function parseAssetRefs(content: string): AssetRef[]; /** * Rewrite every `<plus:KIND ... ref="oldRef" ...>` placeholder in `content` to * use `newRef` instead, leaving any other attributes (e.g. `width="50%"`) * untouched. Used when an asset's `ref` is renamed, or when an unresolved * placeholder is linked to an existing asset whose ref differs — so the source * stays in sync with the project-asset pool across every division. */ export declare function renameAssetRef(content: string, kind: AssetRef["kind"], oldRef: string, newRef: string): string; /** * Remove every `<plus:KIND ref="ref"/>` placeholder for the given kind+ref from * `content`. Used when removing an unresolved placeholder (one with no backing * asset) directly from the source. */ export declare function removeAssetRef(content: string, kind: AssetRef["kind"], ref: string): string; /** * Create a minimal Division record for a given `xmlId` and `type`. * Used when the user types a new `<plus:TYPE ref="id"/>` placeholder into a * division's content and no matching Division exists in the pool yet. */ export declare function createDivisionWithId(xmlId: string, type: DivisionType, sourceFormat?: SourceFormat): Division; /** * Create blank content for a division of the given type and source format. * Used when adding a new division — the format is chosen via the TOC's * properties form before the division is first saved, so unlike editing an * existing division there's no prior source to translate: switching format * just starts over from this format's template. */ export declare function createDivisionContent(type: DivisionType, sourceFormat: SourceFormat, title: string, xmlId: string): string; /** * Insert a `<plus:TYPE ref="xmlId"/>` placeholder into `content`. * * - When `afterXmlId` is `null` the ref is appended just before the closing * tag of the outer element (or at the end of the string if none is found). * - When `afterXmlId` is provided the new ref is inserted immediately after * that ref's placeholder. If the named ref is not found, falls back to * appending. */ export declare function insertDivisionRef(content: string, xmlId: string, type: DivisionType, afterXmlId: string | null): string; /** * Remove the `<plus:* ref="xmlId"/>` placeholder for `xmlId` from `content`. * The surrounding newline/whitespace is cleaned up so the result stays tidy. */ export declare function removeDivisionRef(content: string, xmlId: string): string; /** * Move an existing `<plus:* ref="xmlId"/>` placeholder to a new position. * * Equivalent to `removeDivisionRef` followed by `insertDivisionRef`, but * preserves the original tag's element name (e.g. `plus:section` stays * `plus:section` rather than being normalised to `plus:division`). * * - `afterXmlId === null` moves the ref to the end (before the closing tag). * - `afterXmlId` moves it immediately after that ref. */ export declare function moveDivisionRef(content: string, xmlId: string, afterXmlId: string | null): string; /** * Rename an existing `<plus:* ref="oldXmlId"/>` placeholder in-place to point * at `newXmlId`, also updating the `*` tag name to `newType` if it changed. * Unlike {@link moveDivisionRef}, the placeholder's position is left * untouched — only its `ref` value and element name are rewritten. * * Used to keep a parent division's child placeholder in sync when the * child's own `xml:id`/type are edited directly in its source, so the * rename doesn't orphan the child from its parent. * * Returns `content` unchanged if no placeholder for `oldXmlId` is found. */ export declare function renameDivisionRef(content: string, oldXmlId: string, newXmlId: string, newType: DivisionType): string; /** * Find the division in `divisions` whose content contains a * `<plus:* ref="xmlId"/>` placeholder for `xmlId` — i.e. `xmlId`'s parent in * the division tree. Returns `null` if `xmlId` is unplaced (orphaned) or is * the root. */ export declare function findDivisionParent(divisions: Division[], xmlId: string): Division | null; /** * Rewrite `content` so its `<plus:* ref="..."/>` placeholders appear in the * order given by `orderedXmlIds`. * * Implemented by repeatedly moving each ref to sit immediately after its * predecessor in the desired order; because every referenced child is moved, * the final relative order of the whole group matches `orderedXmlIds` exactly * while non-ref content keeps its position. Original tag element names are * preserved (via `moveDivisionRef`). */ export declare function reorderDivisionRefs(content: string, orderedXmlIds: string[]): string; /** * Collapse expanded-empty `<plus:TAG ...></plus:TAG>` placeholders back to the * canonical self-closing `<plus:TAG .../>` form. An XML round-trip through * xast expands self-closing elements, so this is applied after editing a * division's content to keep the stored source tidy. */ export declare function normalizeSelfClosingRefs(content: string): string; /** * Return all divisions in `divisions` that are not reachable from * `rootXmlId` (and are not the root itself). * * Orphaned divisions are shown separately in the TOC so they can be placed * inside a parent division. */ export declare function getOrphanedDivisions(divisions: Division[], rootXmlId: string): Division[]; /** A division flattened into a depth-first list, annotated for tree rendering. */ export interface DivisionTreeNode { division: Division; /** Nesting depth: direct children of the start division are depth 0. */ depth: number; /** `xmlId` of the division that references this one. */ parentXmlId: string; } /** * Walk the division hierarchy starting from `startXmlId` (exclusive) and return * a depth-first–ordered flat list of descendant nodes, each annotated with its * `depth` and `parentXmlId`. * * The start division itself is not included. Cycles and missing refs are * skipped defensively. Rendering the result as a single list with * depth-based indentation reproduces the tree visually while keeping a flat * structure that a single dnd `SortableContext` can operate over. */ export declare function buildDivisionTree(divisions: Division[], startXmlId: string): DivisionTreeNode[]; /** * Return the "roots" of the orphaned (unreachable) divisions: orphans that are * not referenced by any other orphan. Each orphan root heads its own dangling * subtree, so the TOC can render unplaced material as trees rather than a flat * jumble of every disconnected descendant. */ export declare function getOrphanRoots(divisions: Division[], rootXmlId: string): Division[]; /** * Resolve the root division and recursively expand every * `<plus:* ref="..."/>` placeholder it (transitively) contains, converting * any LaTeX/Markdown divisions to PreTeXt along the way. Returns the bare * root element (e.g. `<book>...</book>`) — *not* wrapped in `<pretext>` and * without `<docinfo>`. * * This is the body half of a full document. Most callers that want an * actual buildable/persistable document should use * {@link assembleFullProjectSource} instead; this lower-level function * remains for callers (like the division-scoped preview path) that need to * compose the resolved body further before wrapping it themselves. */ export declare function assembleProjectSource(divisions: Division[], rootXmlId: string, assets?: Asset[]): string; /** * Assemble the complete PreTeXt document for a project: the root division, * fully resolved (every `<plus:* ref="..."/>` placeholder expanded and any * LaTeX/Markdown divisions converted to PreTeXt), wrapped in the outer * `<pretext>` element with `<docinfo>` inserted as its sibling. * * This is the same shape produced for a root-division preview build, and is * what a host application should persist as "the full source" and send to a * build server (e.g. `https://build.pretext.plus`) to produce the final * rendered document — the `divisions` pool itself is never a valid build * input, since it's a flat list of fragments rather than a single document * tree. */ export declare function assembleFullProjectSource(divisions: Division[], rootXmlId: string, docinfo: string, assets?: Asset[]): string; /** * Wrap a single division's own tagged XML (e.g. * `<section xml:id="...">...</section>`) into a standalone PreTeXt fragment * document suitable for a build-server preview of just that division. * * This function itself never expands `<plus:* ref="..."/>` placeholders — * the real build server has no notion of that placeholder syntax, so callers * must resolve them first (e.g. via {@link assembleProjectSource}) before * passing `divisionXml` in. Passing unresolved refs produces invalid PreTeXt * and a build failure. * * `divisionType` determines the minimal wrapper needed around `divisionXml`: * root types (`book`/`article`/`slideshow`) need none, `chapter`/`part` are * wrapped in a bare `<book>`, and everything else in a bare `<article>`. * The PreTeXt schema requires `<book>`/`<article>` to have a `<title>` as * their first child, so a wrapper built here uses `wrapperTitle` for that — * without it the build server's schema validation rejects the document, * produces no output, and 500s. * `docinfo` (the full `<docinfo>...</docinfo>` element, or `""`) is inserted * as a sibling of the root element inside `<pretext>`, matching real PreTeXt * document shape. */ export declare function wrapDivisionForPreview(divisionType: DivisionType, divisionXml: string, docinfo: string, wrapperTitle: string): string; export declare function normalizeDivisionsOnLoad(divisions: Division[], rootDivisionId: string | undefined, projectType: "article" | "book" | undefined, projectTitle?: string): Division[];