@pipelex/mthds-form 0.12.0 → 0.13.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.
- package/README.md +9 -0
- package/dist/{chunk-V7T4DM63.js → chunk-7KQXGG6J.js} +6 -4
- package/dist/chunk-7KQXGG6J.js.map +1 -0
- package/dist/{chunk-4KX6YTLW.js → chunk-D2EQVWGN.js} +113 -21
- package/dist/chunk-D2EQVWGN.js.map +1 -0
- package/dist/{chunk-4KGQ65TQ.js → chunk-G6SFMTL4.js} +3 -3
- package/dist/{chunk-4KGQ65TQ.js.map → chunk-G6SFMTL4.js.map} +1 -1
- package/dist/{chunk-IUWLOSAM.js → chunk-IR3OFSGD.js} +3 -3
- package/dist/{chunk-IUWLOSAM.js.map → chunk-IR3OFSGD.js.map} +1 -1
- package/dist/{chunk-PM55AHRO.js → chunk-MJQ7CXUB.js} +6 -3
- package/dist/chunk-MJQ7CXUB.js.map +1 -0
- package/dist/{chunk-VS5JMFUV.js → chunk-RRTI7AIR.js} +2 -2
- package/dist/chunk-RRTI7AIR.js.map +1 -0
- package/dist/core/gate.js +2 -2
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +5 -5
- package/dist/core/readiness.d.ts +9 -1
- package/dist/core/readiness.js +1 -1
- package/dist/core/save-plan.d.ts +13 -4
- package/dist/core/save-plan.js +2 -2
- package/dist/core/stuff-files.d.ts +7 -1
- package/dist/core/stuff-files.js +1 -1
- package/dist/core/values.js +2 -2
- package/dist/generative/index.js +4 -4
- package/dist/generative/index.js.map +1 -1
- package/dist/react/index.d.ts +58 -7
- package/dist/react/index.js +4 -4
- package/package.json +1 -1
- package/dist/chunk-4KX6YTLW.js.map +0 -1
- package/dist/chunk-PM55AHRO.js.map +0 -1
- package/dist/chunk-V7T4DM63.js.map +0 -1
- package/dist/chunk-VS5JMFUV.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/readiness.ts"],"names":[],"mappings":"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","file":"chunk-PM55AHRO.js","sourcesContent":["/**\n * Value readiness - the helpers the runner uses to decide whether the Run\n * button may fire and which inputs are still missing.\n */\n\nimport type { RunField } from './descriptor';\nimport { hasOwnProp, ownProp } from './own-property';\n\n/**\n * How deep `isFilled` walks a value before it stops looking, and the answer it\n * gives when it does.\n *\n * The walk is over CALLER-SUPPLIED data - a request body on a public run\n * endpoint, or a form's state - so its depth is whatever the caller sent, and\n * the schema is no bound on it: `pruneEmptyOptionals` copies a property the\n * schema does not declare straight through, so a key ajv never walks reaches\n * this function unwalked. Unbounded, a value a few thousand levels deep threw\n * `RangeError` out of the middle of a host's server gate - a gate whose whole\n * contract is that it returns a verdict instead of throwing.\n *\n * **The cap answers `false`, and that direction is the deliberate half.** Past\n * the cap this function cannot tell whether there is anything down there, and\n * the kernel's rule throughout is that an unanswerable absence fails closed.\n * The cost of the other choice is a run the method cannot use, started (and\n * billed) on a payload no schema validated. The cost of this one is nearly\n * nothing: `isFilled` combines branches with `some`, so a refused over-deep\n * branch only loses its own vote - a real value anywhere beside it still reads\n * filled - and no concept structure declares nesting anywhere near this deep,\n * so a value that reaches the cap is not one the method had a slot for.\n */\nconst MAX_FILLED_DEPTH = 64;\n\n/** True when a value is meaningfully filled (not empty / blank / empty container). */\nexport function isFilled(value: unknown): boolean {\n return walkFilled(value, 0, undefined);\n}\n\n/**\n * `isFilled`'s walk, carrying the depth and the objects already judged.\n *\n * `seen` is a cycle guard first - a value that references itself would\n * otherwise recurse to the cap on every branch - and a memo second. Both uses\n * rest on the same fact: an object reached twice within ONE call answered\n * `false` the first time, because a `true` short-circuits every `some` above it\n * and the walk never gets back down here. So the map can only ever hold empty\n * subtrees, which is what makes it safe to keep entries after unwinding -\n * without that, a value shaped like a diamond chain costs exponential time.\n *\n * **It records the DEPTH each object was judged at, and that is not a detail.**\n * A `false` here has two causes - nothing was down there, or the cap stopped\n * the walk - and only the first travels. An object whose children sit one level\n * past the cap answers `false` at depth 63 and is perfectly filled at depth 5;\n * memoizing identity alone then reported a genuinely filled input as missing\n * the moment a shallower path reached the same object second. So a recorded\n * answer is reused only for a revisit at the SAME depth or deeper, where the\n * walk could see no more than the first one did, and a strictly shallower\n * arrival walks again. That keeps the cycle guard exact (depth rises along\n * every path, so a cycle always revisits deeper) and keeps the memo bounded:\n * an object's recorded depth strictly falls each time it is re-walked, over a\n * range the cap fixes, so the diamond stays linear in all but a constant.\n */\nfunction walkFilled(value: unknown, depth: number, seen: Map<object, number> | undefined): boolean {\n if (value == null) return false;\n // Whitespace is not a value. The Run button lit up over a required text input\n // holding three spaces, ajv passed it (a content model carries no\n // `minLength`), and the run reached the runtime with nothing in it. The same\n // test is what makes a blank OPTIONAL input a real absence on the wire rather\n // than a supplied empty string - see `apiInputsFromSchemaData`.\n if (typeof value === 'string') return value.trim() !== '';\n // No concept in the taxonomy holds a function, so one here arrived by a route\n // that is already wrong - most plausibly an inherited `Object.prototype`\n // member read off a plain object by a colliding input name. `ownProp` closes\n // that route at every lookup; this branch is the backstop for the ones that\n // arrive by another.\n if (typeof value === 'function') return false;\n if (typeof value !== 'object') return true;\n\n if (depth >= MAX_FILLED_DEPTH) return false;\n const visited = seen ?? new Map<object, number>();\n const judgedAt = visited.get(value);\n if (judgedAt !== undefined && judgedAt <= depth) return false;\n visited.set(value, depth);\n\n if (Array.isArray(value)) return value.some((item) => walkFilled(item, depth + 1, visited));\n const obj = value as Record<string, unknown>;\n // A file value is { url } - filled when the url is set.\n if (hasOwnProp(obj, 'url')) return walkFilled(obj.url, depth + 1, visited);\n return Object.values(obj).some((child) => walkFilled(child, depth + 1, visited));\n}\n\n/**\n * True when a slot holds something AND every required field inside it has a\n * value.\n *\n * The two halves are both load-bearing, and the first one is the one that is\n * easy to leave out. A structure is satisfied only if there is something in it:\n * a struct whose concept declares no required child used to be *vacuously*\n * satisfied by an absent value - every child passed the `!f.required` test, so\n * `every` returned true over a value that was not there. Readiness reported\n * nothing missing and the Run button lit up, while the value bridge omitted the\n * untouched structure and the gate then rejected the run on the combined\n * schema's `required` list, naming an input the form had just declared ready.\n *\n * `isFilled` is the SAME predicate `toRjsf` uses to decide whether a structure\n * collapses to an absence (`values.ts`), which is what makes the two halves\n * agree by construction rather than by parallel edits: readiness calls an input\n * present exactly when the bridge keeps it. Absence is what a singular slot\n * expresses, so a required one has to be touched - the button stays dark until\n * the user puts a value somewhere inside, and then the whole shell travels,\n * empty children and all, so a required child left blank still fails loudly.\n *\n * Its own recursion follows the DESCRIPTOR, not the value, so it needs no depth\n * cap of its own: a `RunField` tree only exists because `buildRunFields`\n * finished walking a contract's schema, which bounds it. `isFilled` is where\n * the caller's data is walked, and where the cap lives.\n */\nexport function fieldFilled(field: RunField, value: unknown): boolean {\n if (field.kind === 'object') {\n if (!isFilled(value)) return false;\n const obj = (value ?? {}) as Record<string, unknown>;\n return field.fields.every((f) => !f.required || fieldFilled(f, ownProp(obj, f.name)));\n }\n if (field.kind === 'list') {\n // Too many items is not filled, and this is asked of EVERY list, not only\n // one carrying a lower bound. `ListField` will not add past the bound, so\n // it answers the list that arrived from somewhere else - a restored draft,\n // a host seeding values, a bespoke surface calling these predicates over\n // data it did not render. Asking it only inside the `itemCount` branch\n // meant a model stating `maxItems` alone published its ceiling on the\n // descriptor and enforced it nowhere: the button stayed live on a list ajv\n // was about to refuse. Naming it in `missing` is a small stretch of that\n // word, and the alternative is the disagreement.\n if (\n field.maxItemCount !== undefined &&\n Array.isArray(value) &&\n value.length > field.maxItemCount\n )\n return false;\n // A list the method gave a count to (`Concept[N]`) is satisfied only by that\n // many items, each of them filled. Answering it by `isFilled` alone - \"is\n // there anything in the array?\" - left the button live on a `[3]` slot\n // holding two, and the gate then refused it on the very count the method\n // declared. `itemCount` comes off the same `minItems` ajv reads, so the two\n // halves phrase one rule.\n if (field.itemCount !== undefined) {\n if (!Array.isArray(value) || value.length < field.itemCount) return false;\n return value.every((item) => fieldFilled(field.item, item));\n }\n // A VARIABLE list never gates on being empty (see `mustBeFilled`) - a plural\n // slot's empty form IS the empty list. But a row the user STARTED owes its\n // concept every field the concept declares, which is the same rule a touched\n // optional input follows one level up, and `isFilled` alone does not say it:\n // a row holding `{name: 'x', other: ''}` reads filled by `some`, satisfies\n // ajv's `required` (a present empty string is present), and went out as an\n // item the runtime cannot use. An untouched row stays an absence and still\n // blocks nothing.\n if (Array.isArray(value))\n return (\n value.every((item) => !isFilled(item) || fieldFilled(field.item, item)) && isFilled(value)\n );\n }\n return isFilled(value);\n}\n\nexport interface Readiness {\n total: number;\n ready: number;\n missing: string[];\n}\n\n/**\n * True when the user must put a value in this field before the run can start.\n *\n * Distinct from `required`, which also drives layout (a required field always\n * shows; an optional one may collapse). A **variable-plural** field stays\n * required - it is a first-class input and keeps its place in the form - but it\n * never gates: a plural slot is never \"absent\" in MTHDS, its empty form IS the\n * empty list, and no method can declare \"at least one item\". A **fixed-count**\n * list (`[N]`) is the exception the wire now states, and it gates like any other\n * slot - there the method HAS ruled the empty form out.\n *\n * **For a field this package built, the answer comes from `inputMustBeFilled`**\n * (`contracts.ts`) - the SAME predicate the method viewer's Run button, its ajv\n * `required` list and its missing-inputs toast all read. `buildRunFields`\n * stamps it onto `gating`. This used to be re-derived here as\n * `required && kind !== 'list'`: the same rule expressed against the MAPPED\n * field instead of the contract, and the two could disagree - `kind` is `'list'`\n * for a `conceptRef` ending in `[]` OR an array schema, while the contract's\n * plural test is the array schema alone, so a plural input carrying a non-array\n * schema gated on one surface and not the other.\n *\n * The shape heuristic survives only as the fallback for a **hand-authored**\n * `RunField` - story fixtures and unit tests, which have no contract to consult\n * and therefore nothing to disagree with. Every field a host renders comes from\n * `buildRunFields` and carries `gating`, so a host is on the contract's answer\n * alone. (The fallback cannot see a fixed count either - one more thing only the\n * contract knows.)\n */\nexport function mustBeFilled(field: RunField): boolean {\n return field.gating ?? (field.required && field.kind !== 'list');\n}\n\n/**\n * How many top-level inputs stand between the current values and a run.\n *\n * Two kinds count, and the second is easy to miss. An input the method DEMANDS\n * counts always (`mustBeFilled`). An input the method left optional counts once\n * the user has put something in it - because a structure that has been touched\n * owes its concept every field the concept declares, and the gate enforces\n * exactly that. Left out, an optional struct opened and half-filled was ignored\n * here and refused there: the button stayed live and the run came back rejected\n * on a required child, which is the same disagreement the demanded case had.\n *\n * So an optional input moves INTO the denominator the moment it is touched, and\n * out of `missing` as soon as it is complete: 3 of 3 while it is untouched, 3\n * of 4 once it is started, 4 of 4 once it is done. `total` answers \"how many\n * things stand between me and Run\", which is the number a host is displaying.\n */\nexport function computeReadiness(fields: RunField[], values: Record<string, unknown>): Readiness {\n const gating = fields.filter((f) => mustBeFilled(f) || isFilled(ownProp(values, f.name)));\n const missing = gating\n .filter((f) => !fieldFilled(f, ownProp(values, f.name)))\n .map((f) => f.title ?? f.name);\n return { total: gating.length, ready: gating.length - missing.length, missing };\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/save-plan.ts"],"names":["extension"],"mappings":"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","file":"chunk-V7T4DM63.js","sourcesContent":["import { dataUrlExtensions, readDataUrl, viewableUrl } from './native-content';\nimport { collectStuffFiles, type StuffFile, type StuffFileKind } from './stuff-files';\nimport type { RunField } from './descriptor';\n\n/**\n * What saving a result MEANS, decided with no DOM: which files, what each is\n * called, what type it is, and where its bytes are.\n *\n * ## Why the plan is separate from the delivery\n *\n * Handing bytes to a reader is the part that depends on where the view runs.\n * An ordinary browser tab saves through an object URL and a clicked link; a\n * view inside a sandboxed frame is refused all of that and has to ask its host\n * to do it, typically by passing links and inline contents across a bridge that\n * fetches the links itself. What to save is the same answer in both places, so\n * it is computed once, here, and a delivery only has to carry it out. The\n * default browser delivery lives in `./react`; a host supplies its own through\n * `ResultEnvProvider`'s `saveFiles`. See docs/result-view.md § \"Saving a\n * result\".\n *\n * ## Why a planned file carries a URL rather than bytes\n *\n * A host's download bridge takes links and fetches them itself, which is also\n * what keeps a cross-origin fetch out of a frame that cannot make one. So a\n * stored file is planned as the URL the gate admitted, and only the default\n * browser delivery fetches it. Content the stuff carries inline (the JSON copy\n * of the data, an HTML page) is planned as its text.\n */\n\n/** What a planned file is: one of the stuff's own files, or the JSON copy of its data. */\nexport type SaveFileKind = StuffFileKind | 'data';\n\ninterface SaveFileCommon {\n /**\n * What to save it as: a bare file name, never a path. The file's own\n * `filename` when it states one, else the base name and the file's place in\n * the result (`report-output-figures-0-image.png`). No two files in one plan\n * share a name, compared without case.\n */\n name: string;\n /**\n * The media type. For a `data:` URL it is the type the gate admitted; for any\n * other URL it is what the payload states, or what the name's extension\n * implies, or `application/octet-stream` when nothing says.\n */\n mimeType: string;\n kind: SaveFileKind;\n /** Where it sat in the result, dotted (`output.figures.0.image`); the root's name for the JSON copy. */\n path: string;\n}\n\n/**\n * One file to hand to the reader: exactly one of `url` and `text` is set.\n *\n * `url` is the string the URL gate returned, after the host's resolver was\n * asked first, so a delivery never acts on a member nothing judged. It may be a\n * root-relative path, which a planner with no document cannot resolve; the\n * result view makes one absolute against its own document before a delivery\n * sees it. `text` is content the result carries inline.\n */\nexport type SaveFile =\n | (SaveFileCommon & { url: string; text?: undefined })\n | (SaveFileCommon & { text: string; url?: undefined });\n\n/** A file the result holds that cannot be saved, because no URL the gate admits was found for it. */\nexport interface UnavailableFile {\n /** What it would have been saved as, so the reader can be told which one. */\n name: string;\n kind: StuffFileKind;\n path: string;\n}\n\n/** Everything saving a whole result involves. */\nexport interface SavePlan {\n /** What to deliver, in the order the result shows it, the JSON copy last. */\n files: SaveFile[];\n /**\n * What the result holds and cannot be delivered. The JSON copy still carries\n * each one's reference, so the reader keeps it even without the bytes.\n */\n unavailable: UnavailableFile[];\n}\n\n/** A file a delivery did not hand over, and why when it knows. */\nexport interface SaveFailure {\n file: SaveFile;\n reason?: string;\n}\n\n/**\n * What a delivery reports. Every planned file not listed in `failed` was\n * delivered, so an empty list means everything went out.\n */\nexport interface SaveResult {\n failed: SaveFailure[];\n}\n\n/**\n * How files reach the reader. The default saves them in the browser; a host\n * whose view runs in a sandboxed frame supplies one that asks the host to.\n *\n * It resolves rather than rejects when a file does not go out, and names that\n * file in `failed`, so the control that asked can tell the reader. A throw or\n * a rejection is read as every file having failed.\n */\nexport type SaveFiles = (files: readonly SaveFile[]) => Promise<SaveResult>;\n\nexport interface SavePlanOptions {\n /** Names the JSON copy (`<baseName>.json`) and prefixes a file that has no name of its own. */\n baseName: string;\n /** The same resolver the result view paints through, asked before the payload's own URLs. */\n resolveUrl?: (url: string) => string | undefined;\n}\n\n/**\n * Everything a stuff contains, planned for saving.\n *\n * The data goes down as JSON, and any image, document or HTML page inside it\n * goes down as ITSELF: a JSON file holding a URL to a picture is not the\n * picture, and a reader who saves a report with three attachments wants the\n * three attachments.\n *\n * The JSON copy is planned whenever the stuff is anything more than one file. A\n * result that IS one image has nothing left over once the image is saved, so\n * `{\"url\": …}` beside it would be a second file saying less than the first. That\n * holds only when the image can be saved: a bare file the gate refuses leaves\n * the JSON copy as the one thing the reader gets, because it still carries the\n * reference.\n */\nexport function planStuffSave(field: RunField, value: unknown, options: SavePlanOptions): SavePlan {\n const found = collectStuffFiles(field, value);\n const files: SaveFile[] = [];\n const unavailable: UnavailableFile[] = [];\n for (const file of found) {\n const planned = planFileSave(file, options);\n if (planned) files.push(planned);\n else\n unavailable.push({ name: nameFor(file, options.baseName), kind: file.kind, path: file.path });\n }\n\n // A stuff that is nothing BUT one file, read from the collector rather than\n // from `field.kind`: a `native.Html` node's kind is `object`, and the whole\n // stuff is still the page.\n const isBareFile = found.length === 1 && found[0]?.path === field.name;\n if (isBareFile && files.length === 1) return { files, unavailable };\n\n const jsonName = `${safeName(options.baseName) || 'result'}.json`;\n return {\n files: [\n ...distinctNames(files, jsonName),\n {\n name: jsonName,\n mimeType: 'application/json',\n kind: 'data',\n path: field.name,\n text: jsonCopy(value),\n },\n ],\n unavailable,\n };\n}\n\n/**\n * The plan's files with no two sharing a name, and none taking the JSON copy's.\n *\n * A name derived from a file's place is unique by construction, but a name the\n * payload states is not: two documents a method wrote as `scan.pdf`, or one it\n * called `report.json` beside the JSON copy of `report`. A browser suffixes a\n * duplicate download itself, but a host's delivery may write the names as given,\n * into a folder or an archive, where the second file silently replaces the\n * first. So the first file keeps its name and each later one gets a number\n * before its extension (`scan-2.pdf`), compared without case, as the file\n * systems readers mostly use compare them.\n *\n * That is the whole plan's concern: a file's own button saves one file, which\n * collides with nothing, under its own name.\n */\nfunction distinctNames(files: readonly SaveFile[], reserved: string): SaveFile[] {\n const taken = new Set([reserved.toLowerCase()]);\n return files.map((file) => {\n let name = file.name;\n for (let count = 2; taken.has(name.toLowerCase()); count += 1) {\n name = numbered(file.name, count);\n }\n taken.add(name.toLowerCase());\n return name === file.name ? file : { ...file, name };\n });\n}\n\n/** `scan.pdf` as `scan-2.pdf`: the number before the extension, so the type still reads. */\nfunction numbered(name: string, count: number): string {\n const extension = extensionOf(name);\n return extension\n ? `${name.slice(0, -(extension.length + 1))}-${count}.${name.slice(-extension.length)}`\n : `${name}-${count}`;\n}\n\n/**\n * One file, planned on its own — what a file's own download button saves.\n *\n * `undefined` when the file cannot be saved: a stored reference the host's\n * resolver does not answer and no URL the gate admits beside it. The button is\n * not drawn then, so a reader is never offered a download that cannot happen.\n */\nexport function planFileSave(file: StuffFile, options: SavePlanOptions): SaveFile | undefined {\n const common = { kind: file.kind, path: file.path };\n if (file.text !== undefined) {\n return {\n ...common,\n name: nameFor(file, options.baseName),\n mimeType: 'text/html',\n text: file.text,\n };\n }\n // Judged by the same gate the result view paints through, in the same\n // durability order: the host's resolver over the durable reference first,\n // then `public_url`, then the reference itself. What the gate returns is what\n // is planned, never the raw member.\n const url =\n viewableUrl(options.resolveUrl?.(file.url ?? '')) ??\n viewableUrl(file.publicUrl) ??\n viewableUrl(file.url);\n if (!url) return undefined;\n const encoded = readDataUrl(url);\n if (encoded) {\n // A `data:` URL is its own file, and the gate admitted it for its declared\n // type alone. That type names it: the payload's `filename` and the tail of\n // the payload are both the payload's say, and a `data:image/png` URL saved\n // under `image.html` is a page that runs when the reader opens it.\n return {\n ...common,\n name: nameFor(file, options.baseName, dataUrlExtensions(encoded.mediaType)),\n mimeType: encoded.mediaType,\n url,\n };\n }\n const name = nameFor(file, options.baseName);\n return { ...common, name, mimeType: file.mimeType ?? typeForName(name), url };\n}\n\n/** The type a name's extension implies, for a stored file whose payload states none. */\nconst TYPE_FOR_EXTENSION: Readonly<Record<string, string>> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n avif: 'image/avif',\n svg: 'image/svg+xml',\n pdf: 'application/pdf',\n html: 'text/html',\n};\n\nfunction typeForName(name: string): string {\n const extension = name.match(/\\.([A-Za-z0-9]{1,8})$/)?.[1]?.toLowerCase();\n return (extension && TYPE_FOR_EXTENSION[extension]) || 'application/octet-stream';\n}\n\n/**\n * What a saved file is called.\n *\n * The file's own `filename` wins when it has one: it is the name the producer\n * chose and the reader recognises. Otherwise the descriptor path names it\n * (`report-report-attachments-2`), which is unlovely but unambiguous, since two\n * images from one result must not both land as `image.png` and overwrite each\n * other in the download folder.\n *\n * `allowed` is set for a `data:` URL, whose admitted type is the only thing\n * allowed to decide the extension: a name carrying one of those extensions\n * keeps it, and any other extension is replaced by the type's own.\n */\nfunction nameFor(file: StuffFile, base: string, allowed?: readonly string[]): string {\n const own = file.filename ? safeName(file.filename) : '';\n const stem = own || `${safeName(base) || 'result'}-${file.path.replace(/\\./g, '-')}`;\n const stated = own ? extensionOf(own) : '';\n if (allowed) {\n if (stated && allowed.includes(stated)) return stem;\n const bare = stated ? stem.slice(0, -(stated.length + 1)) : stem;\n const extension = allowed[0];\n return extension ? `${bare}.${extension}` : bare;\n }\n if (own) return own;\n const extension = file.extension ?? extensionOf(pathOf(file.url ?? ''));\n return extension ? `${stem}.${extension}` : stem;\n}\n\n/** The extension a name ends in, lowercased, or none. Never guessed from the bytes. */\nfunction extensionOf(name: string): string {\n return name.match(/\\.([A-Za-z0-9]{1,8})$/)?.[1]?.toLowerCase() ?? '';\n}\n\n/** A URL without its query and fragment, so `x.png?sig=…` still ends in `.png`. */\nfunction pathOf(url: string): string {\n return url.split(/[?#]/)[0] ?? url;\n}\n\n/**\n * A name safe to hand a delivery: its last path segment, without control\n * characters, trimmed. A browser sanitises a download name itself, but a host's\n * bridge may write it to disk as given, and a payload's `filename` is the\n * payload's say. Empty when nothing is left.\n */\nfunction safeName(name: string): string {\n const printable = Array.from(name)\n .filter((character) => {\n const code = character.charCodeAt(0);\n return code > 0x1f && code !== 0x7f;\n })\n .join('');\n const trimmed = (printable.split(/[/\\\\]/).pop() ?? '').trim();\n return trimmed === '.' || trimmed === '..' ? '' : trimmed;\n}\n\n/**\n * The data as JSON, the way the JSON view shows it. A BigInt, which a JSON\n * parser configured to produce one can hand over, is written as its digits\n * rather than failing the whole save.\n */\nfunction jsonCopy(value: unknown): string {\n const text = JSON.stringify(\n value,\n (_key, member: unknown) => (typeof member === 'bigint' ? member.toString() : member),\n 2,\n );\n return text ?? 'null';\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/stuff-files.ts"],"names":["content"],"mappings":"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","file":"chunk-VS5JMFUV.js","sourcesContent":["import { ownProp } from './own-property';\nimport {\n isNativeHtmlNode,\n readDocumentContent,\n readHtmlContent,\n readImageContent,\n} from './native-content';\nimport type { RunField } from './descriptor';\n\n/**\n * One file a stuff carries: what to fetch, and what to call it once saved.\n */\nexport interface StuffFile {\n /**\n * The durable reference or URL the value states, for a file held BY\n * REFERENCE. Absent for inline content, which needs no fetch.\n */\n url?: string;\n /**\n * The content itself, for a file the stuff carries INLINE — markup, today.\n * Exactly one of `url` and `text` is set.\n */\n text?: string;\n /** The extension to save inline content under, without the dot. */\n extension?: string;\n /**\n * The URL that works outside this session, when the value carries one. A host\n * whose display URL is session-scoped (an owner-checked proxy route) needs\n * this for anything that leaves the browser.\n */\n publicUrl?: string;\n /** The file's own name when it states one — otherwise the caller derives it. */\n filename?: string;\n mimeType?: string;\n /** Where it sat, dotted: `report.attachments.2.scan`. Names a saved file. */\n path: string;\n kind: StuffFileKind;\n}\n\n/**\n * What a file is: a picture, a document, or a page of markup the stuff carries\n * inline. It is the descriptor's statement (the node's kind, or its concept for\n * `native.Html`), never a reading of the bytes.\n */\nexport type StuffFileKind = 'image' | 'document' | 'markup';\n\n/**\n * One file value, read as the file it is declared to be — or `undefined` when\n * the value holds nothing to save (no URL, no markup).\n *\n * The walk below calls this for every file node it reaches, and the result view\n * calls it for the one file a control is drawn beside, so a file's own download\n * button and the whole-result download read the value the same way. `kind` is\n * the caller's statement from the descriptor; this never inspects the value to\n * decide what it is.\n */\nexport function readStuffFile(\n kind: StuffFileKind,\n value: unknown,\n path: string,\n): StuffFile | undefined {\n if (kind === 'markup') {\n const content = readHtmlContent(value);\n return content?.innerHtml\n ? { text: content.innerHtml, extension: 'html', path, kind }\n : undefined;\n }\n const content = kind === 'image' ? readImageContent(value) : readDocumentContent(value);\n if (!content?.url) return undefined;\n return {\n url: content.url,\n publicUrl: content.publicUrl,\n filename: content.filename,\n mimeType: content.mimeType,\n path,\n kind,\n };\n}\n\n/**\n * Every file inside a stuff, found by walking the DESCRIPTOR — never the value.\n *\n * The distinction is the whole point. A payload walk would have to decide what\n * a file is by looking at it: an object with a `url`, a string that ends in\n * `.pdf`. That is the guessing this package exists to remove, and it is wrong in\n * both directions — a method whose output legitimately has a `url` field of its\n * own is not a document, and a stored image whose reference carries no extension\n * is. The descriptor already states `kind: \"image\"` or `kind: \"document\"`, so\n * the walk reads the declaration and consults the value only for the members\n * that declaration promises.\n *\n * Ordering is the authored one, depth-first, which is the order the rendered\n * view puts them in — so a download of three images numbers them the way the\n * reader saw them.\n */\nexport function collectStuffFiles(field: RunField, value: unknown): StuffFile[] {\n const found: StuffFile[] = [];\n walk(field, value, [], found);\n return found;\n}\n\nfunction walk(field: RunField, raw: unknown, path: string[], out: StuffFile[]): void {\n const value = unwrapContent(field, raw);\n const here = path.length > 0 ? path : [field.name];\n\n // Markup, before the kind switch, exactly as the renderer takes it: a\n // `native.Html` node's KIND is `object` (its content model has two members),\n // so the switch below would walk into it looking for files and find none.\n //\n // It is a file even though nothing stores it as one. The stuff IS a page —\n // the reader is looking at a rendered report — and handing them a JSON\n // envelope with the page inside it as a string is the same mistake as handing\n // them a URL instead of a picture. `isNativeHtmlNode` asks the descriptor\n // whether the concept refines `native.Html`, which is the standard's own way\n // to ask; it never looks at the value to decide.\n if (isNativeHtmlNode(field)) {\n const file = readStuffFile('markup', value, here.join('.'));\n if (file) out.push(file);\n return;\n }\n\n switch (field.kind) {\n case 'image':\n case 'document': {\n const file = readStuffFile(field.kind, value, here.join('.'));\n if (file) out.push(file);\n return;\n }\n case 'object': {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return;\n const record = value as Record<string, unknown>;\n for (const child of field.fields) {\n walk(child, ownProp(record, child.name), [...here, child.name], out);\n }\n return;\n }\n case 'list': {\n if (!Array.isArray(value)) return;\n // The element descriptor's own `name` is unused by the standard, so the\n // index is what identifies an entry — the same thing the rendered list\n // shows beside each row.\n value.forEach((entry, index) => walk(field.item, entry, [...here, String(index)], out));\n return;\n }\n default:\n // Every other kind is a scalar. It cannot contain a file, and a `native`\n // node with no declared members (an `unknown` kind — a composite, say)\n // states nothing a walk could follow, so there is nothing to look at.\n return;\n }\n}\n\n/**\n * The one unwrap path, matching the renderer's: a content model is opened by\n * the property the schema named, and by nothing else.\n */\nfunction unwrapContent(field: RunField, value: unknown): unknown {\n if (!field.contentKey) return value;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return value;\n const unwrapped = ownProp(value as Record<string, unknown>, field.contentKey);\n return unwrapped === undefined ? value : unwrapped;\n}\n"]}
|