@pretextbook/import 0.9.0 → 0.10.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 (44) hide show
  1. package/dist/convert-worker.cjs +1 -1
  2. package/dist/convert-worker.js +1 -1
  3. package/dist/{import-mode-DuPFMu97.cjs → import-mode-CV7gCDzF.cjs} +1 -1
  4. package/dist/{import-mode-BaaBvzcy.js → import-mode-DQ8g5P8P.js} +1 -1
  5. package/dist/import.css +1 -1
  6. package/dist/index.cjs +3 -3
  7. package/dist/index.d.ts +9 -5
  8. package/dist/index.js +523 -216
  9. package/dist/lib/assets/images.d.ts +49 -0
  10. package/dist/lib/detect-snippet-format.d.ts +32 -0
  11. package/dist/lib/insert/dedupe-ids.d.ts +36 -0
  12. package/dist/lib/insert/destination.d.ts +21 -0
  13. package/dist/lib/insert/index.d.ts +4 -0
  14. package/dist/lib/insert/prepare.d.ts +44 -0
  15. package/dist/lib/insert/retarget.d.ts +35 -0
  16. package/dist/lib/layout/xml-scan.d.ts +28 -0
  17. package/dist/lib/pandoc/formats.d.ts +40 -0
  18. package/dist/lib/pandoc/index.d.ts +3 -0
  19. package/dist/lib/pandoc/pandoc-engine.d.ts +24 -0
  20. package/dist/lib/pandoc/remote-pandoc.d.ts +74 -0
  21. package/dist/lib/pool/division-pool.d.ts +5 -0
  22. package/dist/lib/pool/index.d.ts +5 -1
  23. package/dist/lib/pool/place-divisions.d.ts +25 -0
  24. package/dist/lib/pool/placeholders.d.ts +4 -0
  25. package/dist/lib/pool/refs.d.ts +8 -0
  26. package/dist/lib/pool/serialize-destination.d.ts +24 -0
  27. package/dist/lib/pool/serialize-files.d.ts +1 -2
  28. package/dist/lib/pool/serialize-insert-records.d.ts +22 -0
  29. package/dist/lib/pool/serialize-insert.d.ts +44 -0
  30. package/dist/lib/pool/serialize-plus.d.ts +4 -4
  31. package/dist/lib/pool/serialize-records.d.ts +11 -0
  32. package/dist/lib/pretext-divisions.d.ts +25 -0
  33. package/dist/lib/select/divisions.d.ts +44 -0
  34. package/dist/lib/types.d.ts +82 -2
  35. package/dist/lib/upload.d.ts +56 -4
  36. package/dist/react/import-wizard.d.ts +30 -1
  37. package/dist/react/index.d.ts +1 -1
  38. package/dist/react.cjs +2 -2
  39. package/dist/react.js +658 -511
  40. package/dist/upload-BnRj1gU1.cjs +135 -0
  41. package/dist/upload-H_OCZBBn.js +7648 -0
  42. package/package.json +1 -1
  43. package/dist/upload-BmErkg8I.js +0 -7140
  44. package/dist/upload-BtS22hIH.cjs +0 -135
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The default `external` directory name, matching what `renderPublicationPtx`
3
+ * writes into the publication file it generates. Relative to the main source
4
+ * file's own directory, which is how PreTeXt reads it.
5
+ */
6
+ export declare const DEFAULT_EXTERNAL_DIR = "external";
7
+ /**
8
+ * Where an image lands, given the directory holding the main source file.
9
+ *
10
+ * Paths are flattened to the basename: `@source` is relative to one flat
11
+ * external directory, so the tree the archive happened to use cannot survive.
12
+ */
13
+ export declare function imageAssetPath(sourceDir: string, externalDir: string, fileName: string): string;
14
+ export interface RoutedImages {
15
+ /** Output path for each original upload path. */
16
+ pathByOriginal: Record<string, string>;
17
+ /**
18
+ * What each `<image source="…"/>` should become, keyed by the basename the
19
+ * document is most likely to name the image by. Values are relative to the
20
+ * external directory, which is what `@source` means.
21
+ */
22
+ sourceByBaseName: Record<string, string>;
23
+ }
24
+ /**
25
+ * Decide where every image goes, deduplicating names that collide once the
26
+ * paths are flattened: an archive's `chapters/figs/plot.png` and
27
+ * `appendix/figs/plot.png` are two different images with one name.
28
+ */
29
+ export declare function routeImageAssets(originalPaths: readonly string[], sourceDir: string, externalDir?: string): RoutedImages;
30
+ export interface RewriteImageSourcesResult {
31
+ source: string;
32
+ /** Original → rewritten, for each reference that moved. */
33
+ rewritten: Array<{
34
+ from: string;
35
+ to: string;
36
+ }>;
37
+ /** References naming a file the upload did not carry. */
38
+ unresolved: string[];
39
+ }
40
+ /**
41
+ * Point every `@source` at the image's new home.
42
+ *
43
+ * Matched by basename rather than by full path: a document says
44
+ * `source="figures/plot.png"` while the archive held it at
45
+ * `chapters/figures/plot.png`, and the basename is the part both agree on. A
46
+ * reference naming a file the upload did not carry is left exactly as written —
47
+ * it may point at something the author will supply later — and reported.
48
+ */
49
+ export declare function rewriteImageSources(pretextSource: string, sourceByBaseName: Record<string, string>): RewriteImageSourcesResult;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Format detection for a *snippet* — a pasted exercise, a copied paragraph —
3
+ * as opposed to a whole file.
4
+ *
5
+ * `detectSourceFormat` answers a different question: given a file, is this a
6
+ * LaTeX document? Its markers are document furniture — `\documentclass`,
7
+ * `\begin{document}`, `\section` — none of which appear in the fragment someone
8
+ * copies out of the middle of one. Asking it about `Let $G$ be a \emph{group}`
9
+ * gets "pretext", because it recognises nothing and falls back.
10
+ *
11
+ * So this detector reads the grain of the markup instead: math delimiters,
12
+ * backslash commands, list bullets, emphasis runs. It scores both languages and
13
+ * takes the winner, provided the winner clears a floor — a single weak hint is
14
+ * not enough, because guessing wrong mangles text the author meant to keep
15
+ * verbatim. Declining is always safe; the caller falls back to a plain paste.
16
+ */
17
+ /** Formats a snippet can be converted from. */
18
+ export type SnippetFormat = "latex" | "markdown";
19
+ export interface SnippetFormatScores {
20
+ latex: number;
21
+ markdown: number;
22
+ }
23
+ /** The raw scores, exposed so a host can explain a decision in a log. */
24
+ export declare function scoreSnippetFormats(text: string): SnippetFormatScores;
25
+ /**
26
+ * Which converter a snippet warrants, or `undefined` to leave it alone.
27
+ *
28
+ * Ties go to LaTeX: the two languages overlap mainly on `*` and `_`, and in a
29
+ * PreTeXt document the thing being pasted with a backslash or a dollar sign in
30
+ * it is overwhelmingly TeX.
31
+ */
32
+ export declare function detectSnippetFormat(text: string): SnippetFormat | undefined;
@@ -0,0 +1,36 @@
1
+ export interface XmlIdRename {
2
+ from: string;
3
+ to: string;
4
+ }
5
+ export interface DedupeXmlIdsOptions {
6
+ /** Every `xml:id` already live in the host project. */
7
+ takenIds: ReadonlySet<string>;
8
+ /**
9
+ * Ref of the fragment's outermost division, when it has one — available to
10
+ * the naming policy as a natural namespace for the ids beneath it.
11
+ */
12
+ fragmentRef?: string;
13
+ }
14
+ export interface DedupeXmlIdsResult {
15
+ /** The fragment with colliding ids — and references to them — renamed. */
16
+ source: string;
17
+ /** Every rename performed, in document order, for the host to surface. */
18
+ renamed: XmlIdRename[];
19
+ }
20
+ export interface IdRenameContext {
21
+ /** The colliding id exactly as it appeared in the fragment. */
22
+ original: string;
23
+ /** `original` coerced into a valid ref; equal to `original` when it already was one. */
24
+ base: string;
25
+ /** True when a candidate name is spoken for — by the host, or by a rename already made. */
26
+ isTaken: (candidate: string) => boolean;
27
+ /** Ref of the fragment's outermost division, when known. */
28
+ fragmentRef?: string;
29
+ }
30
+ /**
31
+ * Rename every `xml:id` in `source` that collides with the host project (or
32
+ * with an earlier id in the fragment itself), rewriting internal references to
33
+ * match. Ids that are unique and valid are left exactly as the author wrote
34
+ * them — a rename an author did not ask for is a rename they have to undo.
35
+ */
36
+ export declare function dedupeXmlIds(source: string, options: DedupeXmlIdsOptions): DedupeXmlIdsResult;
@@ -0,0 +1,21 @@
1
+ import { PretextDivisionTag } from '../pretext-divisions';
2
+ /** Import as a project of its own — scaffold, manifest, publication file. */
3
+ export interface ProjectDestination {
4
+ kind: "project";
5
+ }
6
+ /** Import into a document that already exists. */
7
+ export interface InsertDestination {
8
+ kind: "insert";
9
+ /** Division level the imported document becomes (`subsection`, …). */
10
+ targetTag: PretextDivisionTag;
11
+ /** Every `xml:id` already live in the host project. */
12
+ takenIds: ReadonlySet<string>;
13
+ /**
14
+ * Directory of the file receiving the include, with a trailing slash (`""`
15
+ * for the workspace root). New files land beside it, so the hrefs written
16
+ * into the parent are bare filenames.
17
+ */
18
+ hrefBase: string;
19
+ }
20
+ export type ImportDestination = ProjectDestination | InsertDestination;
21
+ export declare const PROJECT_DESTINATION: ProjectDestination;
@@ -0,0 +1,4 @@
1
+ export { retargetFragment, retargetFragmentToDepth, type RetargetFragmentResult, } from './retarget';
2
+ export { dedupeXmlIds, type DedupeXmlIdsOptions, type DedupeXmlIdsResult, type IdRenameContext, type XmlIdRename, } from './dedupe-ids';
3
+ export { PROJECT_DESTINATION, type ImportDestination, type InsertDestination, type ProjectDestination, } from './destination';
4
+ export { prepareInsertSource, type PreparedInsertSource } from './prepare';
@@ -0,0 +1,44 @@
1
+ import { CleaningWarning } from '../clean/warnings';
2
+ import { XmlIdRename } from './dedupe-ids';
3
+ import { RetargetFragmentResult } from './retarget';
4
+ import { InsertDestination } from './destination';
5
+ export interface PreparedInsertSource {
6
+ /** The document to hand to `buildDivisionPool`, retargeted and deduped. */
7
+ source: string;
8
+ /**
9
+ * True when the document's `<book>`/`<article>` wrapper is scaffolding the
10
+ * import should discard, its children being the units actually inserted.
11
+ * False when the wrapper carries a title of its own and becomes the
12
+ * inserted division. The serializer reads this; the pool never sees it.
13
+ */
14
+ unwrapRoot: boolean;
15
+ retarget: RetargetFragmentResult;
16
+ renamed: XmlIdRename[];
17
+ warnings: CleaningWarning[];
18
+ }
19
+ /**
20
+ * The shallowest split an insert can be laid out at.
21
+ *
22
+ * Dropping the wrapper makes the root's children the units being inserted, and
23
+ * a unit with no file of its own is a unit that never arrives: at level 0 the
24
+ * pool holds nothing but the root, and the root is exactly what was dropped.
25
+ * So unwrapping forces one level of split, whatever the split dial says.
26
+ */
27
+ export declare function minimumInsertSplitLevel(unwrapRoot: boolean): number;
28
+ /**
29
+ * Retarget and de-collide a converted document so it can join a host project.
30
+ *
31
+ * The result's `source` is still a whole document — the pool builder is left
32
+ * exactly as it is, and `unwrapRoot` tells the serializer what to do with the
33
+ * root division it produces.
34
+ */
35
+ export interface PrepareInsertOptions {
36
+ /**
37
+ * True when the author cherry-picked divisions (SPEC §9, step 6), which
38
+ * forces the unwrap: once they have named the divisions they want, the
39
+ * wrapper is no longer the document but the container they picked *from*, and
40
+ * keeping it would insert a level nobody asked for.
41
+ */
42
+ hasSelection?: boolean;
43
+ }
44
+ export declare function prepareInsertSource(pretextSource: string, destination: InsertDestination, options?: PrepareInsertOptions): PreparedInsertSource;
@@ -0,0 +1,35 @@
1
+ import { LadderTag, PretextDivisionTag } from '../pretext-divisions';
2
+ export interface RetargetFragmentResult {
3
+ /** The fragment with every ladder tag shifted. */
4
+ source: string;
5
+ /** Rungs each ladder tag moved down; 0 when the fragment was left alone. */
6
+ delta: number;
7
+ /** The shallowest ladder tag the fragment contained, if any. */
8
+ topTag?: LadderTag;
9
+ /** Distinct source tags that overflowed into `<paragraphs>`, shallowest first. */
10
+ overflowed: LadderTag[];
11
+ }
12
+ /**
13
+ * Retarget `source` so its outermost division becomes `targetTag`.
14
+ *
15
+ * The delta is measured from the *shallowest* ladder tag anywhere in the
16
+ * fragment, not from whatever happens to come first. That choice carries an
17
+ * invariant worth having: since every other ladder tag is at least as deep as
18
+ * the shallowest one, no tag is ever promoted above `targetTag`. Inserted
19
+ * content can therefore only ever nest *inside* its attachment point — it can
20
+ * never climb out of it and break the host document's structure.
21
+ *
22
+ * Off-ladder divisions (`<exercises>`, `<appendix>`, `<worksheet>`) pass
23
+ * through untouched, and anything pushed past `<subsubsection>` becomes
24
+ * `<paragraphs>`.
25
+ */
26
+ export declare function retargetFragment(source: string, targetTag: PretextDivisionTag): RetargetFragmentResult;
27
+ /**
28
+ * `retargetFragment` by rung index rather than tag name. Inserting a document
29
+ * *under* a division shifts its contents to the rung below, and below
30
+ * `<subsubsection>` there is no tag to name — only overflow — so the depth
31
+ * (which may be `DIVISION_LADDER.length`) says what no tag can.
32
+ *
33
+ * A negative depth means "no ladder target", and the fragment is left alone.
34
+ */
35
+ export declare function retargetFragmentToDepth(source: string, targetDepth: number): RetargetFragmentResult;
@@ -12,3 +12,31 @@ export declare function findTopLevelElements(source: string, name: string): XmlE
12
12
  export declare function findTopLevelElementsMatching(source: string, matches: (name: string) => boolean): XmlElementSpan[];
13
13
  export declare function findFirstElement(source: string, name: string): XmlElementSpan | null;
14
14
  export declare function findAnyElement(source: string, name: string): XmlElementSpan | null;
15
+ /**
16
+ * One tag occurrence — an open, close, or self-closing tag — rather than a
17
+ * whole element. The element finders above pair tags up; a rewriter that
18
+ * renames elements or edits attributes wants them unpaired, at every depth,
19
+ * with the source span each occupies.
20
+ */
21
+ export interface XmlTagOccurrence {
22
+ name: string;
23
+ kind: "open" | "close" | "selfclose";
24
+ /** Index of the tag's opening `<`. */
25
+ start: number;
26
+ /** Index just past the tag's `>`. */
27
+ end: number;
28
+ /** Parsed attributes; always empty for a closing tag. */
29
+ attributes: Record<string, string>;
30
+ }
31
+ /**
32
+ * Every tag in `source`, in document order, at any nesting depth. Comments,
33
+ * CDATA, processing instructions and declarations are skipped, so a `<section>`
34
+ * mentioned inside a comment is not reported (and so cannot be rewritten).
35
+ */
36
+ export declare function findTagOccurrences(source: string): XmlTagOccurrence[];
37
+ /**
38
+ * The text of an element's own `<title>`, with nested markup stripped, or `""`
39
+ * when it has none. Top-level only, so a division does not pick up the title of
40
+ * a theorem inside it.
41
+ */
42
+ export declare function elementTitleText(inner: string): string;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * What pandoc will read on our behalf.
3
+ *
4
+ * These tables mirror `PANDOC_EXTENSIONS` / `PANDOC_BINARY_FORMATS` in the
5
+ * pretext-plus-build server (`app.py`). Keeping a copy here is deliberate: the
6
+ * upload step has to decide what to put in the file picker's `accept` list
7
+ * *before* any request is made, and it should reject a file the server would
8
+ * only 400 on. Drift is therefore possible — if the server's allowlist grows,
9
+ * add the extension here too.
10
+ */
11
+ /** Pandoc reader name, as the server's `from` field expects it. */
12
+ export type PandocInputFormat = string;
13
+ /**
14
+ * Extension → pandoc reader. The server infers the same mapping from an
15
+ * upload's filename, so sending `from` is technically redundant; we send it
16
+ * anyway so an unmappable extension fails here, with a message naming the
17
+ * file, rather than as an opaque 400.
18
+ */
19
+ export declare const PANDOC_EXTENSION_FORMATS: Record<string, PandocInputFormat>;
20
+ /** Formats that are zip containers: they must travel as a file upload. */
21
+ export declare const PANDOC_BINARY_FORMATS: Set<string>;
22
+ /**
23
+ * Extensions offered in the pandoc engine's file picker.
24
+ *
25
+ * `.xml` is mapped above but deliberately absent here: the server reads `.xml`
26
+ * as DocBook, while in a PreTeXt tool an `.xml` upload is almost always PreTeXt
27
+ * — which the built-in engine already handles correctly and losslessly. A user
28
+ * who really has DocBook can still rename it or pass an explicit `from`.
29
+ *
30
+ * Archives are absent too: the endpoint converts exactly one file, and the
31
+ * built-in engine is the one that unpacks projects.
32
+ */
33
+ export declare const PANDOC_ACCEPT_EXTENSIONS: string[];
34
+ /** The extension of `fileName`, lowercased, including the dot. `""` if none. */
35
+ export declare function fileExtension(fileName: string): string;
36
+ /**
37
+ * The pandoc reader for an uploaded file, or `undefined` when its extension
38
+ * names no format we accept.
39
+ */
40
+ export declare function pandocFormatForFileName(fileName: string): PandocInputFormat | undefined;
@@ -0,0 +1,3 @@
1
+ export { PANDOC_ACCEPT_EXTENSIONS, PANDOC_BINARY_FORMATS, PANDOC_EXTENSION_FORMATS, fileExtension, pandocFormatForFileName, type PandocInputFormat, } from './formats';
2
+ export { createPandocEngine, type PandocBridge, type PandocEngineOptions, } from './pandoc-engine';
3
+ export { createRemotePandocEngine, describeRemotePandocFailure, extractPandocErrorDetail, DEFAULT_REMOTE_PANDOC_TIMEOUT_MS, type RemotePandocEngineOptions, } from './remote-pandoc';
@@ -0,0 +1,24 @@
1
+ import { ImportProjectOptions } from '../upload';
2
+ import { ImportEngine } from '../../react/import-wizard';
3
+ /**
4
+ * Runs pandoc on one uploaded file and resolves with the PreTeXt it produced.
5
+ * Should reject with a message fit to show the user; the wizard's error screen
6
+ * prints it verbatim.
7
+ */
8
+ export type PandocBridge = (file: File, options: ImportProjectOptions) => Promise<string>;
9
+ export interface PandocEngineOptions {
10
+ /** Where pandoc actually runs. */
11
+ convertToPretext: PandocBridge;
12
+ id?: string;
13
+ label?: string;
14
+ description?: string;
15
+ acceptExtensions?: string[];
16
+ }
17
+ /**
18
+ * Build an `ImportEngine` from a pandoc transport.
19
+ *
20
+ * The engine implements only `convertFile`, never `prepare`, which is what
21
+ * makes the wizard skip its Sources step: pandoc converts exactly one file, so
22
+ * there is no root to choose and no second format on offer.
23
+ */
24
+ export declare function createPandocEngine(options: PandocEngineOptions): ImportEngine;
@@ -0,0 +1,74 @@
1
+ import { ImportEngine } from '../../react/import-wizard';
2
+ import { PandocInputFormat } from './formats';
3
+ /**
4
+ * The server's own conversion limit is 25s, after which it answers `504`. The
5
+ * client's limit sits above it so that in the ordinary case the server's
6
+ * explanatory response wins the race and the user sees a real message rather
7
+ * than a bare abort.
8
+ */
9
+ export declare const DEFAULT_REMOTE_PANDOC_TIMEOUT_MS = 30000;
10
+ export interface RemotePandocEngineOptions {
11
+ /** Full URL of the `/pandoc/` endpoint, or of a host proxy that fronts it. */
12
+ url: string;
13
+ /** Shared build token, sent as the `token` form field. Omit when proxying. */
14
+ token?: string;
15
+ /** Extra request headers — a CSRF token, say. Never set `Content-Type`:
16
+ * `FormData` sets it with the multipart boundary the server needs. */
17
+ headers?: Record<string, string>;
18
+ /** Passed through to `fetch`; use `"same-origin"` for a proxy on the host. */
19
+ credentials?: RequestCredentials;
20
+ /**
21
+ * Ask for a whole document rather than a fragment. Defaults to `true`, and
22
+ * should stay that way: the endpoint's own default is a fragment, and the
23
+ * import pipeline needs a `<pretext>` root to lay a project out around.
24
+ */
25
+ standalone?: boolean;
26
+ /** Override the pandoc reader instead of deriving it from the extension. */
27
+ from?: PandocInputFormat;
28
+ /** Abort after this long. Defaults to `DEFAULT_REMOTE_PANDOC_TIMEOUT_MS`. */
29
+ timeoutMs?: number;
30
+ /** Injectable `fetch`, for tests and non-browser hosts. */
31
+ fetchImpl?: typeof fetch;
32
+ id?: string;
33
+ label?: string;
34
+ description?: string;
35
+ acceptExtensions?: string[];
36
+ }
37
+ /**
38
+ * Pull the useful text out of an error body.
39
+ *
40
+ * The endpoint answers `422` with an HTML blob — `<h2>Pandoc conversion
41
+ * failed</h2><pre>…stderr…</pre>` — while every other failure is plain text.
42
+ * The `<pre>` is the part worth showing.
43
+ */
44
+ export declare function extractPandocErrorDetail(body: string): string;
45
+ /**
46
+ * Turn a failed response into the sentence the wizard's error screen shows.
47
+ *
48
+ * The statuses split into two audiences, and that split drives the wording:
49
+ *
50
+ * | Status | Cause | Whose problem |
51
+ * | ------- | ------------------------------------------------ | ------------- |
52
+ * | 400 | format the server won't accept | the author's |
53
+ * | 401/403 | bad or missing token | the host's |
54
+ * | 404 | wrong URL, or a proxy route that isn't mounted | the host's |
55
+ * | 413 | upload over the server's size cap | the author's |
56
+ * | 422 | pandoc failed; body is its stderr | the author's |
57
+ * | 503 | the writer submodule is missing on the server | the host's |
58
+ * | 504 | conversion exceeded the server's 25s limit | the author's |
59
+ *
60
+ * A misconfigured deploy is not something an author can act on, so those cases
61
+ * say plainly that the file is fine and the service is not, rather than showing
62
+ * "Invalid token" and leaving them to guess. The cases they *can* act on name
63
+ * the next move instead.
64
+ *
65
+ * The message lands in a single `<p>` on the wizard's error screen, under a
66
+ * heading that already says "Import failed" and beside a button that already
67
+ * says "Try Another File" — so: no newlines, no repeating either of those, and
68
+ * one or two sentences at most.
69
+ */
70
+ export declare function describeRemotePandocFailure(status: number, body: string, fileName: string): string;
71
+ /**
72
+ * Build an `ImportEngine` that converts uploads on a remote pandoc server.
73
+ */
74
+ export declare function createRemotePandocEngine(options: RemotePandocEngineOptions): ImportEngine;
@@ -15,6 +15,11 @@ export interface BuildDivisionPoolOptions {
15
15
  splitSections?: boolean;
16
16
  /** Binary assets keyed by their original (input) path. */
17
17
  assets?: Record<string, Uint8Array>;
18
+ /**
19
+ * Refs the pool must not mint, because something outside it already owns
20
+ * them — the live `xml:id`s of a host project being inserted into (§9.3).
21
+ */
22
+ takenIds?: ReadonlySet<string>;
18
23
  }
19
24
  /**
20
25
  * Reconcile the split options into a single depth. `splitChapters` and
@@ -1,4 +1,8 @@
1
1
  export { buildDivisionPool, resolveSplitLevel, sanitizeRef, type BuildDivisionPoolOptions, type BuildDivisionPoolResult, } from './division-pool';
2
2
  export { buildNativeDivisionPool, type BuildNativeDivisionPoolOptions, } from './native-pool';
3
3
  export { serializeProjectToFiles, divisionChildRefs, type SerializeProjectFilesOptions, type SerializedProjectFiles, } from './serialize-files';
4
- export { serializeProjectToPlusPayload } from './serialize-plus';
4
+ export { serializeProjectToRecords, assetToRecord, divisionToRecord, bytesToBase64, guessContentType, } from './serialize-records';
5
+ export { serializeProjectToPlusPayload, recordsToPlusPayload, } from './serialize-plus';
6
+ export { serializeInsertToRecords, type SerializedInsertRecords, } from './serialize-insert-records';
7
+ export { serializeInsertFiles, type SerializeInsertOptions, type SerializedInsert, } from './serialize-insert';
8
+ export { serializeForDestination, type SerializeForDestinationOptions, type SerializedForDestination, } from './serialize-destination';
@@ -0,0 +1,25 @@
1
+ import { ImportedDivision, ImportedProject } from '../types';
2
+ export interface PlacedDivision {
3
+ division: ImportedDivision;
4
+ filePath: string;
5
+ }
6
+ export interface PlacementResult {
7
+ placed: PlacedDivision[];
8
+ /** Href for each division's file, relative to the including file. */
9
+ hrefByRef: Map<string, string>;
10
+ }
11
+ export interface PlaceDivisionsOptions {
12
+ /** Divisions to place directly in `directory`, in document order. */
13
+ entries: ImportedDivision[];
14
+ /** Directory the entries land in, with a trailing slash (`""` for the root). */
15
+ directory: string;
16
+ /** xml:id the entries hang off, used to trim redundant filename prefixes. */
17
+ parentXmlId: string;
18
+ }
19
+ /**
20
+ * Walk the pool from `entries`, giving every division reachable from them a
21
+ * file. Divisions reachable from no placeholder at all — the multi-root case
22
+ * (SPEC §3.3/§4.1) — are placed alongside the entries afterwards, so nothing
23
+ * silently disappears; they just aren't included anywhere.
24
+ */
25
+ export declare function placeDivisions(project: ImportedProject, options: PlaceDivisionsOptions): PlacementResult;
@@ -0,0 +1,4 @@
1
+ /** The refs of a division's direct children, in document order. */
2
+ export declare function divisionChildRefs(content: string): string[];
3
+ /** Rewrite each placeholder to the `<xi:include>` of the file it landed in. */
4
+ export declare function replacePlaceholders(content: string, hrefByRef: Map<string, string>): string;
@@ -9,6 +9,14 @@ export declare function sanitizeRef(raw: string): string;
9
9
  /** Hands out refs, deduplicating with `-2`, `-3`, … suffixes. */
10
10
  export declare class RefPool {
11
11
  private used;
12
+ /**
13
+ * `taken` seeds the pool with names it must not hand out. An import into an
14
+ * existing project passes the host's live ids, so a *generated* fallback
15
+ * (`sec-01`) cannot collide with one either — the ids already written into
16
+ * the source are settled earlier, by `dedupeXmlIds`, but these are minted
17
+ * here and would otherwise never be checked against the host.
18
+ */
19
+ constructor(taken?: Iterable<string>);
12
20
  claim(preferred: string): string;
13
21
  has(ref: string): boolean;
14
22
  }
@@ -0,0 +1,24 @@
1
+ import { ImportDestination } from '../insert/destination';
2
+ import { ImportedProject } from '../types';
3
+ import { SerializeProjectFilesOptions } from './serialize-files';
4
+ export interface SerializeForDestinationOptions {
5
+ /** Scaffolding paths; the `project` destination only. */
6
+ layout?: SerializeProjectFilesOptions;
7
+ /**
8
+ * Whether to drop the document wrapper in favour of its children; the
9
+ * `insert` destination only. Comes from `prepareInsertSource`, which decides
10
+ * it before the pool is built.
11
+ */
12
+ unwrapRoot?: boolean;
13
+ }
14
+ export interface SerializedForDestination {
15
+ files: Record<string, string>;
16
+ /**
17
+ * `<xi:include>` elements for the host to splice into the file at the
18
+ * cursor. Always empty for the `project` destination, which writes its own
19
+ * includes into the main file instead.
20
+ */
21
+ includes: string[];
22
+ pathByRef: Record<string, string>;
23
+ }
24
+ export declare function serializeForDestination(project: ImportedProject, destination: ImportDestination, options?: SerializeForDestinationOptions): SerializedForDestination;
@@ -1,4 +1,5 @@
1
1
  import { ImportedProject } from '../types';
2
+ export { divisionChildRefs } from './placeholders';
2
3
  export interface SerializeProjectFilesOptions {
3
4
  mainSourcePath?: string;
4
5
  publicationPath?: string;
@@ -15,8 +16,6 @@ export interface SerializedProjectFiles {
15
16
  */
16
17
  pathByRef: Record<string, string>;
17
18
  }
18
- /** The refs of a division's direct children, in document order. */
19
- export declare function divisionChildRefs(content: string): string[];
20
19
  /**
21
20
  * Serialize the division pool to a project file tree. Binary assets are not
22
21
  * included — they are carried separately on `ImportedProject.assets` and
@@ -0,0 +1,22 @@
1
+ import { AssetRecord, DivisionRecord, ImportedProject } from '../types';
2
+ import { SerializeInsertOptions } from './serialize-insert';
3
+ export interface SerializedInsertRecords {
4
+ /** New division rows, the inserted units first. */
5
+ divisions: DivisionRecord[];
6
+ assets: AssetRecord[];
7
+ /**
8
+ * `<plus:TYPE ref="…"/>` elements for the host to write into the source of
9
+ * the division receiving the insert — the record-world counterpart of the
10
+ * `<xi:include>` a file host splices into the parent file.
11
+ */
12
+ placeholders: string[];
13
+ }
14
+ /**
15
+ * Serialize an insert as records.
16
+ *
17
+ * Every division in the pool belongs to the import — the pool is built from the
18
+ * pruned, prepared source and holds nothing else — so the only division that
19
+ * can be left out is the document wrapper, when it was dropped in favour of its
20
+ * children.
21
+ */
22
+ export declare function serializeInsertToRecords(project: ImportedProject, options: Pick<SerializeInsertOptions, "targetTag" | "unwrapRoot">): SerializedInsertRecords;
@@ -0,0 +1,44 @@
1
+ import { PretextDivisionTag } from '../pretext-divisions';
2
+ import { ImportedDivision, ImportedProject } from '../types';
3
+ export interface SerializeInsertOptions {
4
+ /** Division level the imported document becomes. */
5
+ targetTag: PretextDivisionTag;
6
+ /**
7
+ * Directory of the file receiving the include, with a trailing slash. New
8
+ * files land here, so hrefs are bare filenames.
9
+ */
10
+ hrefBase: string;
11
+ /** Drop the document wrapper and insert its children instead (§9.2). */
12
+ unwrapRoot: boolean;
13
+ }
14
+ export interface SerializedInsert {
15
+ /** New files to write, keyed by workspace-relative path. */
16
+ files: Record<string, string>;
17
+ /**
18
+ * The `<xi:include>` elements to splice into the parent file, in order.
19
+ * Usually one; a titleless document that is nothing but a pile of divisions
20
+ * contributes one per division (SPEC §9, story 3).
21
+ */
22
+ includes: string[];
23
+ pathByRef: Record<string, string>;
24
+ }
25
+ /**
26
+ * Serialize the division pool as an insertion into an existing project.
27
+ *
28
+ * `project.docinfo` is not written: a fragment joining a document inherits the
29
+ * document's docinfo, and a second one would be invalid. Callers that care
30
+ * report it; the pool keeps it either way.
31
+ */
32
+ /**
33
+ * The divisions the host actually receives, in order.
34
+ *
35
+ * When the wrapper survives it is retyped to the attach level first: it is
36
+ * becoming a `<subsection>`, and both the filename and the record's own tag
37
+ * come from its type, so `article-homework-3.ptx` is not what an author asked
38
+ * for. When the wrapper is dropped, its children are the entries.
39
+ */
40
+ export declare function insertEntries(project: ImportedProject, options: Pick<SerializeInsertOptions, "targetTag" | "unwrapRoot">): {
41
+ root: ImportedDivision;
42
+ entries: ImportedDivision[];
43
+ };
44
+ export declare function serializeInsertFiles(project: ImportedProject, options: SerializeInsertOptions): SerializedInsert;
@@ -1,5 +1,5 @@
1
- import { ImportedProject, PlusProjectPayload } from '../types';
2
- /**
3
- * Serialize the division pool to a pretext-plus import payload.
4
- */
1
+ import { ImportedProject, PlusProjectPayload, ProjectRecords } from '../types';
2
+ /** Rename the record projection's fields to the endpoint's own. */
3
+ export declare function recordsToPlusPayload(records: ProjectRecords): PlusProjectPayload;
4
+ /** Serialize the division pool straight to a pretext-plus import payload. */
5
5
  export declare function serializeProjectToPlusPayload(project: ImportedProject): PlusProjectPayload;
@@ -0,0 +1,11 @@
1
+ import { AssetRecord, DivisionRecord, ImportedAsset, ImportedProject, ProjectRecords } from '../types';
2
+ export declare function guessContentType(fileName: string): string;
3
+ /**
4
+ * Base64-encode bytes without `btoa`/`Buffer`, so it runs identically in the
5
+ * browser and the VS Code extension host.
6
+ */
7
+ export declare function bytesToBase64(bytes: Uint8Array): string;
8
+ export declare function assetToRecord(asset: ImportedAsset): AssetRecord;
9
+ export declare function divisionToRecord(division: ImportedProject["divisions"][number]): DivisionRecord;
10
+ /** Serialize the whole division pool as records. */
11
+ export declare function serializeProjectToRecords(project: ImportedProject): ProjectRecords;
@@ -7,3 +7,28 @@ export type PretextDivisionTag = (typeof PRETEXT_DIVISION_TAGS)[number];
7
7
  export declare function isDivisionTag(name: string): name is PretextDivisionTag;
8
8
  export declare function filePrefixForDivision(type: string): string;
9
9
  export declare function isSingletonDivision(type: string): boolean;
10
+ /**
11
+ * The depth-indexed divisions, outermost first (SPEC §9.3). Retargeting an
12
+ * imported fragment onto a chosen level shifts tags along this ladder. Every
13
+ * other division tag names a *role* rather than a depth — an `<exercises>` is
14
+ * an exercises wherever it sits — so retargeting leaves them alone.
15
+ *
16
+ * Matches `LATEX_DIVISION_COMMANDS` (latex-split.ts) rung for rung, since the
17
+ * LaTeX sectioning commands are where most imported hierarchies come from.
18
+ */
19
+ export declare const DIVISION_LADDER: readonly ["part", "chapter", "section", "subsection", "subsubsection"];
20
+ export type LadderTag = (typeof DIVISION_LADDER)[number];
21
+ /**
22
+ * Where content pushed past the bottom of the ladder lands. `<paragraphs>` is
23
+ * deliberately absent from `PRETEXT_DIVISION_TAGS`, so overflow stays inline
24
+ * and can never be split into a file of its own — the overflow rule and the
25
+ * splitter agree without either referring to the other (SPEC §9.3).
26
+ */
27
+ export declare const LADDER_OVERFLOW_TAG = "paragraphs";
28
+ /** Rung index of a tag on the ladder, or `-1` when it is off the ladder. */
29
+ export declare function ladderDepth(name: string): number;
30
+ /**
31
+ * Shift a tag `delta` rungs down the ladder. Off-ladder tags pass through
32
+ * untouched; anything pushed past the last rung becomes `<paragraphs>`.
33
+ */
34
+ export declare function shiftLadderTag(name: string, delta: number): string;