@ai-matrx/rich-content 0.0.0 → 0.2.2
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/CHANGELOG.md +72 -0
- package/LICENSE +1 -1
- package/README.md +58 -3
- package/dist/display/blocks/csv/CsvBlock.d.ts +10 -0
- package/dist/display/blocks/csv/CsvBlock.js +245 -0
- package/dist/display/blocks/table/parseMarkdownTable.d.ts +20 -0
- package/dist/display/blocks/table/parseMarkdownTable.js +35 -0
- package/dist/display/chat-markdown/citations/remarkMatrxCite.d.ts +33 -0
- package/dist/display/chat-markdown/citations/remarkMatrxCite.js +71 -0
- package/dist/display/chat-markdown/matrx-variables/remarkMatrxVariable.d.ts +29 -0
- package/dist/display/chat-markdown/matrx-variables/remarkMatrxVariable.js +71 -0
- package/dist/display/chat-markdown/page-break/remarkMatrxPageBreak.d.ts +55 -0
- package/dist/display/chat-markdown/page-break/remarkMatrxPageBreak.js +108 -0
- package/dist/display/chat-markdown/rehypeSafeRawHtml.d.ts +59 -0
- package/dist/display/chat-markdown/rehypeSafeRawHtml.js +140 -0
- package/dist/display/markdown-classification/processors/bock-processors/parse-markdown-table.d.ts +14 -0
- package/dist/display/markdown-classification/processors/bock-processors/parse-markdown-table.js +171 -0
- package/dist/display/markdown-classification/processors/types.d.ts +120 -0
- package/dist/display/markdown-classification/processors/types.js +10 -0
- package/dist/display/markdown-classification/processors/utils/client-blocks.d.ts +169 -0
- package/dist/display/markdown-classification/processors/utils/client-blocks.js +0 -0
- package/dist/display/markdown-classification/processors/utils/content-splitter-core.d.ts +223 -0
- package/dist/display/markdown-classification/processors/utils/content-splitter-core.js +1791 -0
- package/dist/display/markdown-classification/processors/utils/content-splitter-v2.d.ts +15 -0
- package/dist/display/markdown-classification/processors/utils/content-splitter-v2.js +19 -0
- package/dist/display/markdown-classification/processors/utils/expand-text-blocks.d.ts +9 -0
- package/dist/display/markdown-classification/processors/utils/expand-text-blocks.js +36 -0
- package/dist/display/markdown-classification/processors/utils/gfm-table-lines.d.ts +5 -0
- package/dist/display/markdown-classification/processors/utils/gfm-table-lines.js +40 -0
- package/dist/display/markdown-classification/processors/utils/gfm-table-vector-inputs.d.ts +16 -0
- package/dist/display/markdown-classification/processors/utils/gfm-table-vector-inputs.js +385 -0
- package/dist/display/markdown-classification/processors/utils/gfm-table-vectors.d.ts +43 -0
- package/dist/display/markdown-classification/processors/utils/gfm-table-vectors.js +33 -0
- package/dist/display/markdown-classification/processors/utils/table-data-parser.d.ts +9 -0
- package/dist/display/markdown-classification/processors/utils/table-data-parser.js +30 -0
- package/dist/display/markdown-classification/processors/utils/thinking-parser.d.ts +7 -0
- package/dist/display/markdown-classification/processors/utils/thinking-parser.js +70 -0
- package/dist/display/render-block.d.ts +23 -0
- package/dist/display/render-block.js +0 -0
- package/dist/display/themes.d.ts +1023 -0
- package/dist/display/themes.js +1105 -0
- package/dist/display/types.d.ts +32 -0
- package/dist/display/types.js +0 -0
- package/dist/host/ContentIrHostBoundary.d.ts +8 -0
- package/dist/host/ContentIrHostBoundary.js +15 -0
- package/dist/host/ErrorActions.d.ts +4 -0
- package/dist/host/ErrorActions.js +11 -0
- package/dist/host/NestedContent.d.ts +3 -0
- package/dist/host/NestedContent.js +12 -0
- package/dist/host/ResourcePeek.d.ts +6 -0
- package/dist/host/ResourcePeek.js +14 -0
- package/dist/host/component-registry.d.ts +2 -0
- package/dist/host/component-registry.js +25 -0
- package/dist/host/diagnostics.d.ts +2 -0
- package/dist/host/diagnostics.js +15 -0
- package/dist/host/index.d.ts +3 -0
- package/dist/host/index.js +18 -0
- package/dist/host/kind-incident.d.ts +3 -0
- package/dist/host/kind-incident.js +12 -0
- package/dist/host/kind-registry.d.ts +2 -0
- package/dist/host/kind-registry.js +35 -0
- package/dist/host/people.d.ts +3 -0
- package/dist/host/people.js +12 -0
- package/dist/host/registry.d.ts +27 -0
- package/dist/host/registry.js +55 -0
- package/dist/host/splitter-envelopes.d.ts +2 -0
- package/dist/host/splitter-envelopes.js +7 -0
- package/dist/host/tables.d.ts +6 -0
- package/dist/host/tables.js +8 -0
- package/dist/host/toast.d.ts +2 -0
- package/dist/host/toast.js +57 -0
- package/dist/host/types.d.ts +157 -0
- package/dist/host/types.js +0 -0
- package/dist/host/wikilinks.d.ts +15 -0
- package/dist/host/wikilinks.js +23 -0
- package/dist/kinds/host/route-env.d.ts +33 -0
- package/dist/kinds/host/route-env.js +16 -0
- package/dist/kinds/react/KindEscapedNotice.d.ts +14 -0
- package/dist/kinds/react/KindEscapedNotice.js +72 -0
- package/dist/kinds/react/ProvisionalKindBoundary.d.ts +19 -0
- package/dist/kinds/react/ProvisionalKindBoundary.js +17 -0
- package/dist/kinds/react/ensure-kind-renderable.d.ts +14 -0
- package/dist/kinds/react/ensure-kind-renderable.js +45 -0
- package/dist/kinds/react/kind-problems.d.ts +62 -0
- package/dist/kinds/react/kind-problems.js +134 -0
- package/dist/kinds/react/kind-route.d.ts +23 -0
- package/dist/kinds/react/kind-route.js +24 -0
- package/dist/kinds/react/loading/infer-loading-slug.d.ts +58 -0
- package/dist/kinds/react/loading/infer-loading-slug.js +100 -0
- package/dist/kinds/react/loading/kind-loading-components.d.ts +48 -0
- package/dist/kinds/react/loading/kind-loading-components.js +447 -0
- package/dist/kinds/react/loading/kind-loading-registry.d.ts +21 -0
- package/dist/kinds/react/loading/kind-loading-registry.js +62 -0
- package/dist/kinds/react/loading/kind-loading-slugs.d.ts +18 -0
- package/dist/kinds/react/loading/kind-loading-slugs.js +34 -0
- package/dist/kinds/react/loading/kind-loading.types.d.ts +79 -0
- package/dist/kinds/react/loading/kind-loading.types.js +31 -0
- package/dist/kinds/react/loading/resolve-loading-slug.d.ts +29 -0
- package/dist/kinds/react/loading/resolve-loading-slug.js +51 -0
- package/dist/kinds/react/partial-kind-route.d.ts +32 -0
- package/dist/kinds/react/partial-kind-route.js +27 -0
- package/dist/kinds/react/registry-versioned.d.ts +54 -0
- package/dist/kinds/react/registry-versioned.js +18 -0
- package/dist/kinds/react/route-at-version.d.ts +38 -0
- package/dist/kinds/react/route-at-version.js +78 -0
- package/dist/kinds/react/slot/KindSlot.d.ts +87 -0
- package/dist/kinds/react/slot/KindSlot.js +65 -0
- package/dist/kinds/react/use-registry-repaint.d.ts +6 -0
- package/dist/kinds/react/use-registry-repaint.js +36 -0
- package/dist/kinds/react/useIrNode.d.ts +9 -0
- package/dist/kinds/react/useIrNode.js +46 -0
- package/dist/kinds/react/useLiveJsonRegion.d.ts +15 -0
- package/dist/kinds/react/useLiveJsonRegion.js +84 -0
- package/dist/kinds/redux/render-block-envelope.d.ts +14 -0
- package/dist/kinds/redux/render-block-envelope.js +26 -0
- package/dist/kinds/registry/kind-correctors.d.ts +63 -0
- package/dist/kinds/registry/kind-correctors.js +94 -0
- package/dist/kinds/registry/legacy-quiz-envelope.d.ts +1 -0
- package/dist/kinds/registry/legacy-quiz-envelope.js +1 -0
- package/dist/kinds/registry/region-envelope-memo.d.ts +45 -0
- package/dist/kinds/registry/region-envelope-memo.js +94 -0
- package/dist/kinds/registry/registry-debug.d.ts +38 -0
- package/dist/kinds/registry/registry-debug.js +39 -0
- package/dist/markdown-core/MarkdownCore.d.ts +6 -0
- package/dist/markdown-core/MarkdownCore.js +32 -0
- package/dist/markdown-core/MarkdownCoreImpl.d.ts +3 -0
- package/dist/markdown-core/MarkdownCoreImpl.js +27 -0
- package/dist/markdown-core/MarkdownCoreServer.d.ts +4 -0
- package/dist/markdown-core/MarkdownCoreServer.js +26 -0
- package/dist/markdown-core/heading-anchors-context.d.ts +2 -0
- package/dist/markdown-core/heading-anchors-context.js +11 -0
- package/dist/markdown-core/heading-anchors-props.d.ts +2 -0
- package/dist/markdown-core/heading-anchors-props.js +6 -0
- package/dist/markdown-core/markdown-core-html.d.ts +3 -0
- package/dist/markdown-core/markdown-core-html.js +33 -0
- package/dist/markdown-core/markdown-core-presets.d.ts +22 -0
- package/dist/markdown-core/markdown-core-presets.js +118 -0
- package/dist/markdown-core/markdown-core-types.d.ts +43 -0
- package/dist/markdown-core/markdown-core-types.js +0 -0
- package/dist/markdown-core/math/vendor/micromark-math-math-flow.d.ts +15 -0
- package/dist/markdown-core/math/vendor/micromark-math-math-flow.js +143 -0
- package/dist/markdown-core/math/vendor/micromark-math-math-text.d.ts +24 -0
- package/dist/markdown-core/math/vendor/micromark-math-math-text.js +127 -0
- package/dist/markdown-core/math/vendor/micromark-math-syntax.d.ts +30 -0
- package/dist/markdown-core/math/vendor/micromark-math-syntax.js +15 -0
- package/dist/markdown-core/math/vendor/rehype-katex.d.ts +15 -0
- package/dist/markdown-core/math/vendor/rehype-katex.js +83 -0
- package/dist/markdown-core/math/vendor/remark-math.d.ts +9 -0
- package/dist/markdown-core/math/vendor/remark-math.js +20 -0
- package/dist/markdown-core/math-normalizer.d.ts +57 -0
- package/dist/markdown-core/math-normalizer.js +315 -0
- package/dist/markdown-core/plain-title.d.ts +2 -0
- package/dist/markdown-core/plain-title.js +1 -0
- package/dist/markdown-core/stream-heal.d.ts +2 -0
- package/dist/markdown-core/stream-heal.js +184 -0
- package/dist/markdown-core/streaming-context.d.ts +2 -0
- package/dist/markdown-core/streaming-context.js +11 -0
- package/dist/markdown-core/syntax/callout-types.d.ts +9 -0
- package/dist/markdown-core/syntax/callout-types.js +101 -0
- package/dist/markdown-core/syntax/containers.d.ts +17 -0
- package/dist/markdown-core/syntax/containers.js +353 -0
- package/dist/markdown-core/syntax/document-numbering.d.ts +52 -0
- package/dist/markdown-core/syntax/document-numbering.js +184 -0
- package/dist/markdown-core/syntax/elements/CalloutIcon.d.ts +3 -0
- package/dist/markdown-core/syntax/elements/CalloutIcon.js +41 -0
- package/dist/markdown-core/syntax/elements/CrossRef.d.ts +12 -0
- package/dist/markdown-core/syntax/elements/CrossRef.js +57 -0
- package/dist/markdown-core/syntax/elements/CsvFence.d.ts +4 -0
- package/dist/markdown-core/syntax/elements/CsvFence.js +18 -0
- package/dist/markdown-core/syntax/elements/DetailsElements.d.ts +12 -0
- package/dist/markdown-core/syntax/elements/DetailsElements.js +38 -0
- package/dist/markdown-core/syntax/elements/DocumentNumbering.d.ts +20 -0
- package/dist/markdown-core/syntax/elements/DocumentNumbering.js +37 -0
- package/dist/markdown-core/syntax/elements/DocumentPropertiesPanel.d.ts +4 -0
- package/dist/markdown-core/syntax/elements/DocumentPropertiesPanel.js +46 -0
- package/dist/markdown-core/syntax/elements/InDocAnchor.d.ts +11 -0
- package/dist/markdown-core/syntax/elements/InDocAnchor.js +87 -0
- package/dist/markdown-core/syntax/elements/MarkdownSourceEdit.d.ts +24 -0
- package/dist/markdown-core/syntax/elements/MarkdownSourceEdit.js +28 -0
- package/dist/markdown-core/syntax/elements/MatrxTabs.d.ts +10 -0
- package/dist/markdown-core/syntax/elements/MatrxTabs.js +59 -0
- package/dist/markdown-core/syntax/elements/MentionChip.d.ts +7 -0
- package/dist/markdown-core/syntax/elements/MentionChip.js +69 -0
- package/dist/markdown-core/syntax/elements/NestedBody.d.ts +3 -0
- package/dist/markdown-core/syntax/elements/NestedBody.js +13 -0
- package/dist/markdown-core/syntax/elements/TableOfContents.d.ts +5 -0
- package/dist/markdown-core/syntax/elements/TableOfContents.js +44 -0
- package/dist/markdown-core/syntax/elements/TaskCheckbox.d.ts +7 -0
- package/dist/markdown-core/syntax/elements/TaskCheckbox.js +63 -0
- package/dist/markdown-core/syntax/elements/WikiEmbed.d.ts +7 -0
- package/dist/markdown-core/syntax/elements/WikiEmbed.js +68 -0
- package/dist/markdown-core/syntax/elements/WikiLink.d.ts +10 -0
- package/dist/markdown-core/syntax/elements/WikiLink.js +114 -0
- package/dist/markdown-core/syntax/elements/core-syntax-elements.d.ts +22 -0
- package/dist/markdown-core/syntax/elements/core-syntax-elements.js +70 -0
- package/dist/markdown-core/syntax/elements/find-in-document.d.ts +3 -0
- package/dist/markdown-core/syntax/elements/find-in-document.js +20 -0
- package/dist/markdown-core/syntax/elements/wiki-target.d.ts +5 -0
- package/dist/markdown-core/syntax/elements/wiki-target.js +8 -0
- package/dist/markdown-core/syntax/frontmatter.d.ts +26 -0
- package/dist/markdown-core/syntax/frontmatter.js +72 -0
- package/dist/markdown-core/syntax/gfm-cell-pipes.d.ts +11 -0
- package/dist/markdown-core/syntax/gfm-cell-pipes.js +4 -0
- package/dist/markdown-core/syntax/inline-syntax.d.ts +26 -0
- package/dist/markdown-core/syntax/inline-syntax.js +163 -0
- package/dist/markdown-core/syntax/linear-scan.d.ts +46 -0
- package/dist/markdown-core/syntax/linear-scan.js +129 -0
- package/dist/markdown-core/syntax/mdast-helpers.d.ts +48 -0
- package/dist/markdown-core/syntax/mdast-helpers.js +48 -0
- package/dist/markdown-core/syntax/mentions.d.ts +2 -0
- package/dist/markdown-core/syntax/mentions.js +42 -0
- package/dist/markdown-core/syntax/names.d.ts +6 -0
- package/dist/markdown-core/syntax/names.js +18 -0
- package/dist/markdown-core/syntax/prepare-syntax-source.d.ts +1 -0
- package/dist/markdown-core/syntax/prepare-syntax-source.js +78 -0
- package/dist/markdown-core/syntax/rehype-matrx-syntax.d.ts +9 -0
- package/dist/markdown-core/syntax/rehype-matrx-syntax.js +180 -0
- package/dist/markdown-core/syntax/remark-inline-only.d.ts +23 -0
- package/dist/markdown-core/syntax/remark-inline-only.js +25 -0
- package/dist/markdown-core/syntax/remark-matrx-syntax.d.ts +7 -0
- package/dist/markdown-core/syntax/remark-matrx-syntax.js +351 -0
- package/dist/markdown-core/syntax/remark-table-code-pipes.d.ts +7 -0
- package/dist/markdown-core/syntax/remark-table-code-pipes.js +23 -0
- package/dist/markdown-core/syntax/stream-heal-syntax.d.ts +2 -0
- package/dist/markdown-core/syntax/stream-heal-syntax.js +108 -0
- package/dist/markdown-core/syntax/task-source.d.ts +10 -0
- package/dist/markdown-core/syntax/task-source.js +34 -0
- package/dist/markdown-core/syntax/useDocumentProperties.d.ts +3 -0
- package/dist/markdown-core/syntax/useDocumentProperties.js +8 -0
- package/dist/styles.css +0 -0
- package/dist/utils/addressWithoutNavigating.d.ts +68 -0
- package/dist/utils/addressWithoutNavigating.js +60 -0
- package/dist/utils/client-lazy.d.ts +13 -0
- package/dist/utils/client-lazy.js +24 -0
- package/dist/utils/decision-options.d.ts +21 -0
- package/dist/utils/decision-options.js +32 -0
- package/dist/utils/gfm-lexer.d.ts +5 -0
- package/dist/utils/gfm-lexer.js +10 -0
- package/dist/utils/json.d.ts +63 -0
- package/dist/utils/json.js +22 -0
- package/dist/utils/plain-title.d.ts +25 -0
- package/dist/utils/plain-title.js +51 -0
- package/dist/utils/table-source.d.ts +62 -0
- package/dist/utils/table-source.js +191 -0
- package/dist/utils/youtube.d.ts +52 -0
- package/dist/utils/youtube.js +137 -0
- package/package.json +132 -5
|
@@ -0,0 +1,1791 @@
|
|
|
1
|
+
import { RESERVED_PREFIX, tryDecodeDirective } from "@ai-matrx/content-ir";
|
|
2
|
+
import { isJsonObject } from "../../../../utils/json.js";
|
|
3
|
+
import { parseDecisionOptionsFromBody } from "../../../../utils/decision-options.js";
|
|
4
|
+
import {
|
|
5
|
+
DIRECTIVE_CONTAINER_OPEN,
|
|
6
|
+
DirectiveContainerTracker
|
|
7
|
+
} from "@ai-matrx/content-ir/source";
|
|
8
|
+
import { TITLED_IMAGE_LINE } from "@ai-matrx/content-ir/source";
|
|
9
|
+
import { continuesTable, isGfmDelimiterRow, opensTable, tableContainerIndent, tableStartsAt } from "./gfm-table-lines.js";
|
|
10
|
+
import {
|
|
11
|
+
parseYouTubeUrl,
|
|
12
|
+
isYouTubeThumbnailUrl,
|
|
13
|
+
youTubeWatchUrl
|
|
14
|
+
} from "../../../../utils/youtube.js";
|
|
15
|
+
import {
|
|
16
|
+
recognizeOurFileUrl,
|
|
17
|
+
mightBeOurFileUrl
|
|
18
|
+
} from "@ai-matrx/media/files/engine/media/our-file-sources";
|
|
19
|
+
import {
|
|
20
|
+
normalizeRecoveredContainerPiece,
|
|
21
|
+
splitAroundEmbeddedKindJson
|
|
22
|
+
} from "@ai-matrx/content-ir/surfaces";
|
|
23
|
+
import {
|
|
24
|
+
isJsonFenceLanguage,
|
|
25
|
+
jsonKindSignal,
|
|
26
|
+
bareRegionBreaksIntoProse
|
|
27
|
+
} from "@ai-matrx/content-ir/surfaces";
|
|
28
|
+
import { liftQuotedKindRegions } from "@ai-matrx/content-ir/surfaces";
|
|
29
|
+
import {
|
|
30
|
+
decodeDoubleEncodedKindText,
|
|
31
|
+
unescapeMarkdownKindJson
|
|
32
|
+
} from "@ai-matrx/content-ir/surfaces";
|
|
33
|
+
import { unwrapKindImageAlt } from "@ai-matrx/content-ir/surfaces";
|
|
34
|
+
import { IR_ENVELOPE_KEY } from "@ai-matrx/content-ir";
|
|
35
|
+
import { ALLOWED_RAW_HTML_TAGS } from "../../../chat-markdown/rehypeSafeRawHtml.js";
|
|
36
|
+
import { isPageBreakLine } from "@ai-matrx/print/directives";
|
|
37
|
+
import { readXmlTag } from "@ai-matrx/content-ir/source";
|
|
38
|
+
import { FENCE_META_KEY, splitFenceInfo } from "@ai-matrx/content-ir/source";
|
|
39
|
+
import {
|
|
40
|
+
closeFence,
|
|
41
|
+
fenceOpenerOf,
|
|
42
|
+
isHtmlBlockTagName,
|
|
43
|
+
XmlContainerTracker
|
|
44
|
+
} from "@ai-matrx/content-ir/source";
|
|
45
|
+
import { indexOutsideInlineCode } from "@ai-matrx/content-ir/source";
|
|
46
|
+
import {
|
|
47
|
+
findBalancedXmlClose,
|
|
48
|
+
initialXmlBalance
|
|
49
|
+
} from "@ai-matrx/content-ir/source";
|
|
50
|
+
const NO_SPLITTER_ENVELOPES = {
|
|
51
|
+
withIrEnvelope: (_source, metadata) => metadata,
|
|
52
|
+
fenceRegion: () => null,
|
|
53
|
+
xmlRegion: () => null
|
|
54
|
+
};
|
|
55
|
+
const DIRECTIVE_HEAD_RE = new RegExp(
|
|
56
|
+
String.raw`^\{\s*"__kind"\s*:\s*"${RESERVED_PREFIX}`
|
|
57
|
+
);
|
|
58
|
+
function looksLikeDirectiveHead(content) {
|
|
59
|
+
return DIRECTIVE_HEAD_RE.test(content.trimStart());
|
|
60
|
+
}
|
|
61
|
+
function countStructuralBraces(source, open = "{", close = "}") {
|
|
62
|
+
let opens = 0;
|
|
63
|
+
let closes = 0;
|
|
64
|
+
let inString = false;
|
|
65
|
+
let escaped = false;
|
|
66
|
+
for (const char of source) {
|
|
67
|
+
if (inString) {
|
|
68
|
+
if (escaped) escaped = false;
|
|
69
|
+
else if (char === "\\") escaped = true;
|
|
70
|
+
else if (char === '"') inString = false;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (char === '"') inString = true;
|
|
74
|
+
else if (char === open) opens++;
|
|
75
|
+
else if (char === close) closes++;
|
|
76
|
+
}
|
|
77
|
+
return { opens, closes };
|
|
78
|
+
}
|
|
79
|
+
function blockHasResolvedRootKind(block) {
|
|
80
|
+
const envelope = block.metadata?.[IR_ENVELOPE_KEY];
|
|
81
|
+
if (typeof envelope !== "object" || envelope === null) return false;
|
|
82
|
+
const root = envelope.root;
|
|
83
|
+
if (typeof root !== "object" || root === null) return false;
|
|
84
|
+
const kind = root.kind;
|
|
85
|
+
return typeof kind === "string" && kind.length > 0;
|
|
86
|
+
}
|
|
87
|
+
function isJsonFenceBlock(block) {
|
|
88
|
+
return block.type === "code" && isJsonFenceLanguage(block.language);
|
|
89
|
+
}
|
|
90
|
+
function isQuotedSourceFenceBlock(block) {
|
|
91
|
+
return block.type === "code" && block.metadata?.genericXmlContainer !== true && !isJsonFenceLanguage(block.language);
|
|
92
|
+
}
|
|
93
|
+
function recoverEmbeddedKindJsonBlocksWith(input, envelopes) {
|
|
94
|
+
const recovered = [];
|
|
95
|
+
for (const block of input) {
|
|
96
|
+
if (block.type === "artifact" || block.type === "matrx" || block.type === "matrx_file" || // An XML TAG — closed or never closed — is STRUCTURE, not quoted
|
|
97
|
+
// source (the owner's ruling (b), round 3): a kind inside it is data
|
|
98
|
+
// and leaves the card below, exactly as the live stream lifts it. A
|
|
99
|
+
// ```xml FENCE stays quoted source (`isQuotedSourceFenceBlock`).
|
|
100
|
+
blockHasResolvedRootKind(block) || // A fence of another language (```ts, ```xml, ```markdown …) is the
|
|
101
|
+
// model QUOTING SOURCE: a kind inside it stays as written (the owner's
|
|
102
|
+
// ruling, 2026-09-30 — the same definition the leaf gate reads).
|
|
103
|
+
isQuotedSourceFenceBlock(block)) {
|
|
104
|
+
recovered.push(block);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const genericXml = block.metadata?.genericXmlContainer === true;
|
|
108
|
+
const pieces = splitAroundEmbeddedKindJson(block.content, {
|
|
109
|
+
excludeLiteralContexts: genericXml,
|
|
110
|
+
liftJsonFences: genericXml,
|
|
111
|
+
// Prose, tables and sections: an inline code span is quoted source.
|
|
112
|
+
// A JSON fence's body is JSON — a backtick inside a string is no span.
|
|
113
|
+
excludeQuotedSource: !genericXml && !isJsonFenceBlock(block)
|
|
114
|
+
});
|
|
115
|
+
if (pieces.length === 1 && pieces[0]?.type === "container") {
|
|
116
|
+
recovered.push(block);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
let followsKind = false;
|
|
120
|
+
for (const piece of pieces) {
|
|
121
|
+
if (piece.type === "chrome") continue;
|
|
122
|
+
if (piece.type === "residual") {
|
|
123
|
+
recovered.push({
|
|
124
|
+
type: "code",
|
|
125
|
+
content: piece.json,
|
|
126
|
+
language: "json",
|
|
127
|
+
metadata: envelopes.withIrEnvelope(piece.json, void 0)
|
|
128
|
+
});
|
|
129
|
+
followsKind = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (piece.type === "kind") {
|
|
133
|
+
followsKind = true;
|
|
134
|
+
if (looksLikeDirectiveHead(piece.content)) {
|
|
135
|
+
recovered.push({
|
|
136
|
+
type: "matrx",
|
|
137
|
+
content: piece.content,
|
|
138
|
+
language: "json"
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
recovered.push({
|
|
143
|
+
type: "code",
|
|
144
|
+
content: piece.content,
|
|
145
|
+
language: "json",
|
|
146
|
+
metadata: envelopes.withIrEnvelope(piece.content, void 0)
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const content = normalizeRecoveredContainerPiece(
|
|
151
|
+
piece.content,
|
|
152
|
+
block.type,
|
|
153
|
+
followsKind
|
|
154
|
+
);
|
|
155
|
+
followsKind = false;
|
|
156
|
+
if (!content) continue;
|
|
157
|
+
recovered.push({
|
|
158
|
+
...block,
|
|
159
|
+
content,
|
|
160
|
+
// An XML piece stays STRUCTURE (`isQuotedSourceXmlBlock`) — the card
|
|
161
|
+
// draws whatever kind it still holds as the kind.
|
|
162
|
+
metadata: genericXml ? { genericXmlContainer: true } : void 0
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return recovered;
|
|
167
|
+
}
|
|
168
|
+
const SPECIAL_CODE_LANGUAGES = [
|
|
169
|
+
"transcript",
|
|
170
|
+
"tasks",
|
|
171
|
+
"structured_info",
|
|
172
|
+
"questionnaire",
|
|
173
|
+
"flashcards",
|
|
174
|
+
"cooking_recipe",
|
|
175
|
+
"mermaid",
|
|
176
|
+
"svg",
|
|
177
|
+
"chart",
|
|
178
|
+
"map",
|
|
179
|
+
"stats",
|
|
180
|
+
"diff",
|
|
181
|
+
// html + react are DELIVERABLES (a webpage / a live component), not throwaway
|
|
182
|
+
// code snippets — promote them so they materialize into persistent artifacts
|
|
183
|
+
// (render-by-id, no re-creation), unlike a bare ```code fence.
|
|
184
|
+
"html",
|
|
185
|
+
"react",
|
|
186
|
+
// A ```matrx fence carries one Matrx Envelope (reference / secret / directive
|
|
187
|
+
// / validation). Promoted to a first-class block so MatrxEnvelopeBlock renders
|
|
188
|
+
// it (chips for references, a muted card otherwise) instead of raw JSON. The
|
|
189
|
+
// in-content position only resolves reference/secret — see MATRX_REFERENCES.md.
|
|
190
|
+
"matrx"
|
|
191
|
+
];
|
|
192
|
+
const CODE_LANGUAGE_ALIASES = {
|
|
193
|
+
mmd: "mermaid",
|
|
194
|
+
// jsx/tsx render + materialize as a live React component (same as ```react).
|
|
195
|
+
jsx: "react",
|
|
196
|
+
tsx: "react"
|
|
197
|
+
};
|
|
198
|
+
function normalizeCodeLanguage(language) {
|
|
199
|
+
if (!language) return language;
|
|
200
|
+
const lower = language.toLowerCase();
|
|
201
|
+
return CODE_LANGUAGE_ALIASES[lower] ?? lower;
|
|
202
|
+
}
|
|
203
|
+
const JSON_BLOCK_PATTERNS = {
|
|
204
|
+
quiz: {
|
|
205
|
+
rootKey: "quiz_title",
|
|
206
|
+
validate: (parsed) => isJsonObject(parsed) && parsed.quiz_title && Array.isArray(parsed.multiple_choice) && parsed.multiple_choice.length > 0
|
|
207
|
+
},
|
|
208
|
+
presentation: {
|
|
209
|
+
rootKey: "presentation",
|
|
210
|
+
validate: (parsed) => isJsonObject(parsed) && isJsonObject(parsed.presentation) && Array.isArray(parsed.presentation.slides)
|
|
211
|
+
},
|
|
212
|
+
decision_tree: {
|
|
213
|
+
rootKey: "decision_tree",
|
|
214
|
+
validate: (parsed) => isJsonObject(parsed) && isJsonObject(parsed.decision_tree) && parsed.decision_tree.title && parsed.decision_tree.root
|
|
215
|
+
},
|
|
216
|
+
comparison_table: {
|
|
217
|
+
rootKey: "comparison",
|
|
218
|
+
validate: (parsed) => isJsonObject(parsed) && isJsonObject(parsed.comparison) && parsed.comparison.title && Array.isArray(parsed.comparison.items) && Array.isArray(parsed.comparison.criteria)
|
|
219
|
+
},
|
|
220
|
+
diagram: {
|
|
221
|
+
rootKey: "diagram",
|
|
222
|
+
validate: (parsed) => isJsonObject(parsed) && isJsonObject(parsed.diagram) && parsed.diagram.title && Array.isArray(parsed.diagram.nodes)
|
|
223
|
+
},
|
|
224
|
+
math_problem: {
|
|
225
|
+
rootKey: "math_problem",
|
|
226
|
+
validate: (parsed) => isJsonObject(parsed) && parsed.math_problem && typeof parsed.math_problem === "object"
|
|
227
|
+
},
|
|
228
|
+
// A clickable reference to a platform entity (agent, note, task, file, …).
|
|
229
|
+
// Deliberately forgiving: any object under the key with a string `type`
|
|
230
|
+
// qualifies — the renderer handles unknown types via a neutral fallback.
|
|
231
|
+
item_presentation: {
|
|
232
|
+
rootKey: "item_presentation",
|
|
233
|
+
validate: (parsed) => isJsonObject(parsed) && isJsonObject(parsed.item_presentation) && typeof parsed.item_presentation.type === "string"
|
|
234
|
+
},
|
|
235
|
+
// An output-schema proposal — what the "JSON Schema Generator" agent emits:
|
|
236
|
+
// `{ name: string, schema: object, strict?: boolean }`. Recognized so the FE
|
|
237
|
+
// can offer "Apply to an agent" (writes agent.definition.output_schema). The validate
|
|
238
|
+
// is deliberately strict (string `name` AND object `schema`) so ordinary JSON
|
|
239
|
+
// that merely begins with a "name" key never misfires into this block.
|
|
240
|
+
schema_proposal: {
|
|
241
|
+
rootKey: "name",
|
|
242
|
+
validate: (parsed) => isJsonObject(parsed) && typeof parsed.name === "string" && parsed.schema !== null && typeof parsed.schema === "object" && !Array.isArray(parsed.schema)
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
function extractFirstJsonKey(content) {
|
|
246
|
+
const match = content.trimStart().match(/^\{\s*"([^"]+)"/);
|
|
247
|
+
return match ? match[1] : null;
|
|
248
|
+
}
|
|
249
|
+
function detectJsonBlockType(content) {
|
|
250
|
+
const trimmed = content.trim();
|
|
251
|
+
if (trimmed.startsWith("{")) {
|
|
252
|
+
try {
|
|
253
|
+
const parsed = JSON.parse(trimmed);
|
|
254
|
+
if (tryDecodeDirective(parsed)) return "matrx";
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (looksLikeDirectiveHead(content)) return "matrx";
|
|
259
|
+
const firstKey = extractFirstJsonKey(content);
|
|
260
|
+
if (!firstKey) return null;
|
|
261
|
+
if (firstKey === "matrx_version") return "matrx";
|
|
262
|
+
for (const [type, pattern] of Object.entries(JSON_BLOCK_PATTERNS)) {
|
|
263
|
+
if (pattern.rootKey === firstKey) {
|
|
264
|
+
return type;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
function containsPlaceholderText(content) {
|
|
270
|
+
return /\[\s*(array|object|string|number|boolean|description|example|etc|list|item)[^\]]*\]/i.test(
|
|
271
|
+
content
|
|
272
|
+
) || /:\s*"?\[?(array|list) of /i.test(content) || /:\s*"?object with /i.test(content) || /:\s*"?<[a-z_]+>/i.test(content) || /:\s*"?\.\.\."?/.test(content);
|
|
273
|
+
}
|
|
274
|
+
function validateJsonBlock(content, type) {
|
|
275
|
+
if (type === "matrx") {
|
|
276
|
+
let trimmed2 = content.trim();
|
|
277
|
+
trimmed2 = trimmed2.replace(/```+\s*$/, "").trim();
|
|
278
|
+
try {
|
|
279
|
+
const parsed = JSON.parse(trimmed2);
|
|
280
|
+
if (tryDecodeDirective(parsed)) {
|
|
281
|
+
return { isComplete: true, shouldShow: true };
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
isComplete: false,
|
|
287
|
+
shouldShow: looksLikeDirectiveHead(trimmed2) || extractFirstJsonKey(trimmed2) === "matrx_version"
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
let trimmed = content.trim();
|
|
291
|
+
trimmed = trimmed.replace(/```+\s*$/, "").trim();
|
|
292
|
+
if (containsPlaceholderText(trimmed)) {
|
|
293
|
+
return { isComplete: false, shouldShow: false };
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const parsed = JSON.parse(trimmed);
|
|
297
|
+
const pattern = JSON_BLOCK_PATTERNS[type];
|
|
298
|
+
const isValid = pattern.validate(parsed);
|
|
299
|
+
if (isValid) {
|
|
300
|
+
return {
|
|
301
|
+
isComplete: true,
|
|
302
|
+
shouldShow: true,
|
|
303
|
+
metadata: { isComplete: true }
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
isComplete: true,
|
|
308
|
+
shouldShow: false,
|
|
309
|
+
metadata: { isComplete: true }
|
|
310
|
+
};
|
|
311
|
+
} catch (error) {
|
|
312
|
+
}
|
|
313
|
+
const { opens: openBraces, closes: closeBraces } = countStructuralBraces(trimmed);
|
|
314
|
+
if (openBraces > closeBraces) {
|
|
315
|
+
return { isComplete: false, shouldShow: false };
|
|
316
|
+
}
|
|
317
|
+
if (trimmed.endsWith("}") && openBraces === closeBraces) {
|
|
318
|
+
return {
|
|
319
|
+
isComplete: true,
|
|
320
|
+
shouldShow: false,
|
|
321
|
+
metadata: { isComplete: true }
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (closeBraces > openBraces) {
|
|
325
|
+
return {
|
|
326
|
+
isComplete: true,
|
|
327
|
+
shouldShow: false,
|
|
328
|
+
metadata: { isComplete: true }
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return { isComplete: false, shouldShow: false };
|
|
332
|
+
}
|
|
333
|
+
const questionnaireCache = /* @__PURE__ */ new Map();
|
|
334
|
+
const flashcardCache = /* @__PURE__ */ new Map();
|
|
335
|
+
const recipeCache = /* @__PURE__ */ new Map();
|
|
336
|
+
const tableCache = /* @__PURE__ */ new Map();
|
|
337
|
+
const XML_TAG_BLOCKS = {
|
|
338
|
+
thinking: ["<thinking>", "<think>"],
|
|
339
|
+
reasoning: ["<reasoning>"],
|
|
340
|
+
info: ["<info>"],
|
|
341
|
+
task: ["<task>"],
|
|
342
|
+
database: ["<database>"],
|
|
343
|
+
private: ["<private>"],
|
|
344
|
+
plan: ["<plan>"],
|
|
345
|
+
event: ["<event>"],
|
|
346
|
+
tool: ["<tool>"],
|
|
347
|
+
questionnaire: ["<questionnaire>"],
|
|
348
|
+
flashcards: ["<flashcards>"],
|
|
349
|
+
cooking_recipe: ["<cooking_recipe>"],
|
|
350
|
+
timeline: ["<timeline>"],
|
|
351
|
+
progress_tracker: ["<progress_tracker>"],
|
|
352
|
+
troubleshooting: ["<troubleshooting>"],
|
|
353
|
+
resources: ["<resources>"],
|
|
354
|
+
research: ["<research>"]
|
|
355
|
+
};
|
|
356
|
+
const ATTRIBUTE_XML_BLOCKS = [
|
|
357
|
+
"decision",
|
|
358
|
+
"artifact",
|
|
359
|
+
// Editor pills — round-trip representation of code-editor errors and
|
|
360
|
+
// selected code snippets. Attributes carry file/line/severity/language; the
|
|
361
|
+
// body carries <message>+<surrounding_code> for errors, raw code for snippets.
|
|
362
|
+
"editor_error",
|
|
363
|
+
"editor_code_snippet",
|
|
364
|
+
// Audio citation — an agent reference to a moment in a scribe session's audio.
|
|
365
|
+
// `start`/`end` are session-relative seconds; the body is the cited label.
|
|
366
|
+
// Emitted mid-sentence (handled by detectMidLineAttributeXml), so a reply can
|
|
367
|
+
// cite the audio inline like a footnote.
|
|
368
|
+
"audiocite"
|
|
369
|
+
];
|
|
370
|
+
const KNOWN_XML_TAG_NAMES = /* @__PURE__ */ new Set([
|
|
371
|
+
...Object.values(XML_TAG_BLOCKS).flatMap(
|
|
372
|
+
(tags) => tags.map((tag) => tag.slice(1, -1))
|
|
373
|
+
),
|
|
374
|
+
...ATTRIBUTE_XML_BLOCKS
|
|
375
|
+
]);
|
|
376
|
+
function parseXmlAttributes(openingTag) {
|
|
377
|
+
const attrs = {};
|
|
378
|
+
const attrRegex = /(\w+)\s*=\s*"([^"]*)"/g;
|
|
379
|
+
let match;
|
|
380
|
+
while ((match = attrRegex.exec(openingTag)) !== null) {
|
|
381
|
+
attrs[match[1]] = match[2];
|
|
382
|
+
}
|
|
383
|
+
return attrs;
|
|
384
|
+
}
|
|
385
|
+
function detectAttributeXmlBlock(line) {
|
|
386
|
+
const trimmed = line.trim();
|
|
387
|
+
for (const type of ATTRIBUTE_XML_BLOCKS) {
|
|
388
|
+
const prefix = `<${type}`;
|
|
389
|
+
if (trimmed.startsWith(prefix) && (trimmed[prefix.length] === " " || trimmed[prefix.length] === ">")) {
|
|
390
|
+
const closeBracket = trimmed.indexOf(">");
|
|
391
|
+
if (closeBracket === -1) return null;
|
|
392
|
+
const fullOpeningTag = trimmed.slice(0, closeBracket + 1);
|
|
393
|
+
const attributes = parseXmlAttributes(fullOpeningTag);
|
|
394
|
+
return { type, fullOpeningTag, attributes };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
function detectMidLineAttributeXml(line) {
|
|
400
|
+
for (const type of ATTRIBUTE_XML_BLOCKS) {
|
|
401
|
+
const prefix = `<${type}`;
|
|
402
|
+
const idx = indexOutsideInlineCode(line, prefix);
|
|
403
|
+
if (idx === -1) continue;
|
|
404
|
+
if (line.trimStart().startsWith(prefix)) continue;
|
|
405
|
+
const afterPrefix = line[idx + prefix.length];
|
|
406
|
+
if (afterPrefix !== " " && afterPrefix !== ">") continue;
|
|
407
|
+
const closeBracket = line.indexOf(">", idx);
|
|
408
|
+
if (closeBracket === -1) continue;
|
|
409
|
+
const fullOpeningTag = line.slice(idx, closeBracket + 1);
|
|
410
|
+
const attributes = parseXmlAttributes(fullOpeningTag);
|
|
411
|
+
return { tagStart: idx, type, fullOpeningTag, attributes };
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
function extractAttributeXmlBlock(detection, startIndex, lines, rawXmlOverride) {
|
|
416
|
+
const closingTag = `</${detection.type}>`;
|
|
417
|
+
const rawLines = [];
|
|
418
|
+
const consumedLines = [];
|
|
419
|
+
let i = startIndex;
|
|
420
|
+
let foundClosingTag = false;
|
|
421
|
+
const firstLine = normalizeLine(lines[i]).trim();
|
|
422
|
+
consumedLines.push(lines[i]);
|
|
423
|
+
const afterOpening = firstLine.slice(firstLine.indexOf(">") + 1);
|
|
424
|
+
if (afterOpening.includes(closingTag)) {
|
|
425
|
+
const closingIdx = afterOpening.indexOf(closingTag);
|
|
426
|
+
const inner = afterOpening.slice(0, closingIdx).trim();
|
|
427
|
+
if (inner) rawLines.push(inner);
|
|
428
|
+
const remainderAfterClose = afterOpening.slice(closingIdx + closingTag.length).trim();
|
|
429
|
+
foundClosingTag = true;
|
|
430
|
+
i++;
|
|
431
|
+
if (remainderAfterClose) {
|
|
432
|
+
lines.splice(i, 0, remainderAfterClose);
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
if (afterOpening.trim()) rawLines.push(afterOpening.trim());
|
|
436
|
+
i++;
|
|
437
|
+
while (i < lines.length) {
|
|
438
|
+
const current = normalizeLine(lines[i]).trim();
|
|
439
|
+
const closingIdx = current.indexOf(closingTag);
|
|
440
|
+
if (closingIdx !== -1) {
|
|
441
|
+
const before = current.slice(0, closingIdx).trim();
|
|
442
|
+
if (before) rawLines.push(before);
|
|
443
|
+
const originalLine = lines[i];
|
|
444
|
+
const originalClosingIdx = originalLine.indexOf(closingTag);
|
|
445
|
+
consumedLines.push(
|
|
446
|
+
originalClosingIdx !== -1 ? originalLine.slice(0, originalClosingIdx + closingTag.length) : originalLine
|
|
447
|
+
);
|
|
448
|
+
const remainderAfterClose = current.slice(closingIdx + closingTag.length).trim();
|
|
449
|
+
foundClosingTag = true;
|
|
450
|
+
i++;
|
|
451
|
+
if (remainderAfterClose) {
|
|
452
|
+
lines.splice(i, 0, remainderAfterClose);
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
if (current === closingTag) {
|
|
457
|
+
consumedLines.push(lines[i]);
|
|
458
|
+
foundClosingTag = true;
|
|
459
|
+
i++;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
consumedLines.push(lines[i]);
|
|
463
|
+
rawLines.push(lines[i]);
|
|
464
|
+
i++;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const innerContent = rawLines.join("\n");
|
|
468
|
+
const fullXml = rawXmlOverride ?? consumedLines.join("\n");
|
|
469
|
+
if (detection.type === "editor_error" || detection.type === "editor_code_snippet" || detection.type === "audiocite") {
|
|
470
|
+
return {
|
|
471
|
+
content: innerContent,
|
|
472
|
+
nextIndex: i,
|
|
473
|
+
metadata: {
|
|
474
|
+
isComplete: foundClosingTag,
|
|
475
|
+
...detection.attributes,
|
|
476
|
+
rawXml: fullXml
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
if (detection.type === "artifact") {
|
|
481
|
+
const artifactId = detection.attributes.id || `artifact-${startIndex}`;
|
|
482
|
+
const artifactIndex = artifactId.includes("_") ? parseInt(artifactId.split("_").pop() || "0", 10) : startIndex;
|
|
483
|
+
const parsedVersion = detection.attributes.version ? parseInt(detection.attributes.version, 10) : void 0;
|
|
484
|
+
return {
|
|
485
|
+
content: innerContent,
|
|
486
|
+
nextIndex: i,
|
|
487
|
+
metadata: {
|
|
488
|
+
isComplete: foundClosingTag,
|
|
489
|
+
artifactId,
|
|
490
|
+
artifactIndex,
|
|
491
|
+
artifactType: detection.attributes.type || "text",
|
|
492
|
+
artifactTitle: detection.attributes.title || "",
|
|
493
|
+
...typeof parsedVersion === "number" && Number.isFinite(parsedVersion) ? { version: parsedVersion } : {},
|
|
494
|
+
rawXml: fullXml
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const options = parseDecisionOptionsFromBody(innerContent).map(
|
|
499
|
+
(o) => ({ id: o.id, label: o.label, text: o.text })
|
|
500
|
+
);
|
|
501
|
+
const prompt = detection.attributes.prompt || "Make a selection";
|
|
502
|
+
const decision = {
|
|
503
|
+
id: `decision-${startIndex}`,
|
|
504
|
+
prompt,
|
|
505
|
+
options
|
|
506
|
+
};
|
|
507
|
+
return {
|
|
508
|
+
content: innerContent,
|
|
509
|
+
nextIndex: i,
|
|
510
|
+
metadata: {
|
|
511
|
+
isComplete: foundClosingTag,
|
|
512
|
+
decision,
|
|
513
|
+
rawXml: fullXml
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function detectXmlBlockType(line) {
|
|
518
|
+
const trimmed = line.trim();
|
|
519
|
+
for (const [type, tags] of Object.entries(XML_TAG_BLOCKS)) {
|
|
520
|
+
const matchedTag = tags.find(
|
|
521
|
+
(tag) => trimmed === tag || trimmed.startsWith(tag)
|
|
522
|
+
);
|
|
523
|
+
if (matchedTag) {
|
|
524
|
+
return { type, matchedTag };
|
|
525
|
+
}
|
|
526
|
+
const bare = tags[0].slice(0, -1);
|
|
527
|
+
if (trimmed.startsWith(`${bare} `)) {
|
|
528
|
+
const close = trimmed.indexOf(">");
|
|
529
|
+
if (close !== -1) {
|
|
530
|
+
return {
|
|
531
|
+
type,
|
|
532
|
+
matchedTag: trimmed.slice(0, close + 1)
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
function xmlTagNameOf(matchedTag) {
|
|
540
|
+
return /^<([\w-]+)/.exec(matchedTag)?.[1] ?? matchedTag.slice(1, -1);
|
|
541
|
+
}
|
|
542
|
+
function isHtmlOrKnownTag(name) {
|
|
543
|
+
return KNOWN_XML_TAG_NAMES.has(name) || ALLOWED_RAW_HTML_TAGS.has(name.toLowerCase());
|
|
544
|
+
}
|
|
545
|
+
function leadingTagName(line) {
|
|
546
|
+
return /^<([A-Za-z_][\w.:-]*)/.exec(line.trimStart())?.[1] ?? null;
|
|
547
|
+
}
|
|
548
|
+
function isUnclosedGenericXmlOpening(source) {
|
|
549
|
+
if (isPageBreakLine(source)) return false;
|
|
550
|
+
const text = source.trimStart();
|
|
551
|
+
const prefix = /^<([A-Za-z_][\w.:-]*)(?=\s|\/|$)/.exec(text);
|
|
552
|
+
if (!prefix || isHtmlOrKnownTag(prefix[1])) return false;
|
|
553
|
+
let quote = null;
|
|
554
|
+
for (let i = prefix[0].length; i < text.length; i++) {
|
|
555
|
+
if (quote) {
|
|
556
|
+
if (text[i] === quote) quote = null;
|
|
557
|
+
} else if (text[i] === "'" || text[i] === '"') quote = text[i];
|
|
558
|
+
else if (text[i] === ">") return false;
|
|
559
|
+
}
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
function startUnrecognizedXmlContainer(line) {
|
|
563
|
+
if (isPageBreakLine(line)) return null;
|
|
564
|
+
const firstTrimmed = line.trimStart();
|
|
565
|
+
if (!firstTrimmed.startsWith("<")) return null;
|
|
566
|
+
const openingTag = readXmlTag(firstTrimmed, 0);
|
|
567
|
+
if (!openingTag || openingTag.isClosing) return null;
|
|
568
|
+
const rootTag = openingTag.tagName;
|
|
569
|
+
if (isHtmlOrKnownTag(rootTag)) return null;
|
|
570
|
+
return {
|
|
571
|
+
tracker: new XmlContainerTracker(rootTag),
|
|
572
|
+
rootStart: line.length - firstTrimmed.length
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function extractUnrecognizedXmlBlock(startIndex, lines) {
|
|
576
|
+
const firstLine = normalizeLine(lines[startIndex]);
|
|
577
|
+
const start = startUnrecognizedXmlContainer(firstLine);
|
|
578
|
+
if (!start) return null;
|
|
579
|
+
const { tracker, rootStart } = start;
|
|
580
|
+
for (let lineIndex = startIndex; lineIndex < lines.length; lineIndex++) {
|
|
581
|
+
const currentLine = normalizeLine(lines[lineIndex]);
|
|
582
|
+
const rootEnd = tracker.consumeLine(
|
|
583
|
+
currentLine,
|
|
584
|
+
lineIndex === startIndex ? rootStart : 0
|
|
585
|
+
);
|
|
586
|
+
if (rootEnd === null) continue;
|
|
587
|
+
const contentLines = lineIndex === startIndex ? [currentLine.slice(rootStart, rootEnd)] : [
|
|
588
|
+
firstLine.slice(rootStart),
|
|
589
|
+
...lines.slice(startIndex + 1, lineIndex).map(normalizeLine),
|
|
590
|
+
currentLine.slice(0, rootEnd)
|
|
591
|
+
];
|
|
592
|
+
const remainder = currentLine.slice(rootEnd).trim();
|
|
593
|
+
if (remainder) lines.splice(lineIndex + 1, 0, remainder);
|
|
594
|
+
return {
|
|
595
|
+
content: contentLines.join("\n").trim(),
|
|
596
|
+
nextIndex: lineIndex + 1,
|
|
597
|
+
metadata: { isComplete: true, genericXmlContainer: true }
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
function isCompleteUnrecognizedXmlContainer(source) {
|
|
603
|
+
const lines = source.split(/\r?\n/);
|
|
604
|
+
const extraction = extractUnrecognizedXmlBlock(0, [...lines]);
|
|
605
|
+
return extraction !== null && extraction.nextIndex >= lines.length;
|
|
606
|
+
}
|
|
607
|
+
const THINKING_OPENERS = {
|
|
608
|
+
thinking: ["<thinking>", "<think>"],
|
|
609
|
+
reasoning: ["<reasoning>"]
|
|
610
|
+
};
|
|
611
|
+
function detectOrphanThinkingClose(trimmedLine) {
|
|
612
|
+
const CLOSERS = [
|
|
613
|
+
["thinking", "</thinking>"],
|
|
614
|
+
["thinking", "</think>"],
|
|
615
|
+
["reasoning", "</reasoning>"]
|
|
616
|
+
];
|
|
617
|
+
for (const [type, closer] of CLOSERS) {
|
|
618
|
+
const idx = indexOutsideInlineCode(trimmedLine, closer);
|
|
619
|
+
if (idx === -1) continue;
|
|
620
|
+
return {
|
|
621
|
+
type,
|
|
622
|
+
before: trimmedLine.slice(0, idx).trim(),
|
|
623
|
+
remainder: trimmedLine.slice(idx + closer.length).trim()
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
function extractXmlBlock(type, matchedTag, startIndex, lines, trackFences = true) {
|
|
629
|
+
const content = [];
|
|
630
|
+
let i = startIndex;
|
|
631
|
+
let foundClosingTag = false;
|
|
632
|
+
const balance = initialXmlBalance();
|
|
633
|
+
let remainderAfterClose = "";
|
|
634
|
+
const tagName = xmlTagNameOf(matchedTag);
|
|
635
|
+
const closingTag = `</${tagName}>`;
|
|
636
|
+
const firstLine = lines[i];
|
|
637
|
+
const processedFirst = normalizeLine(firstLine).trim();
|
|
638
|
+
if (processedFirst.startsWith(matchedTag)) {
|
|
639
|
+
const afterTag = processedFirst.slice(matchedTag.length);
|
|
640
|
+
const closingIdx = afterTag.indexOf(closingTag);
|
|
641
|
+
if (closingIdx !== -1) {
|
|
642
|
+
const beforeClosing = afterTag.slice(0, closingIdx).trim();
|
|
643
|
+
if (beforeClosing) content.push(beforeClosing);
|
|
644
|
+
remainderAfterClose = afterTag.slice(closingIdx + closingTag.length).trim();
|
|
645
|
+
foundClosingTag = true;
|
|
646
|
+
i++;
|
|
647
|
+
if (remainderAfterClose) {
|
|
648
|
+
lines.splice(i, 0, remainderAfterClose);
|
|
649
|
+
}
|
|
650
|
+
const fullContent2 = content.join("\n");
|
|
651
|
+
const result2 = validateStreamingXmlBlock(
|
|
652
|
+
type,
|
|
653
|
+
fullContent2,
|
|
654
|
+
foundClosingTag
|
|
655
|
+
);
|
|
656
|
+
return {
|
|
657
|
+
content: result2.content || fullContent2,
|
|
658
|
+
nextIndex: i,
|
|
659
|
+
metadata: result2.metadata
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const afterTagTrimmed = afterTag.trim();
|
|
663
|
+
if (afterTagTrimmed) content.push(afterTagTrimmed);
|
|
664
|
+
i++;
|
|
665
|
+
}
|
|
666
|
+
while (i < lines.length) {
|
|
667
|
+
const currentTrimmed = normalizeLine(lines[i]).trim();
|
|
668
|
+
const closingIdx = findBalancedXmlClose(currentTrimmed, tagName, balance, {
|
|
669
|
+
trackFences
|
|
670
|
+
});
|
|
671
|
+
if (closingIdx !== -1) {
|
|
672
|
+
const beforeClosing = currentTrimmed.slice(0, closingIdx).trim();
|
|
673
|
+
if (beforeClosing) content.push(beforeClosing);
|
|
674
|
+
remainderAfterClose = currentTrimmed.slice(closingIdx + closingTag.length).trim();
|
|
675
|
+
foundClosingTag = true;
|
|
676
|
+
i++;
|
|
677
|
+
if (remainderAfterClose) {
|
|
678
|
+
lines.splice(i, 0, remainderAfterClose);
|
|
679
|
+
}
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
if (type === "thinking" && currentTrimmed.startsWith("### I have everything")) {
|
|
683
|
+
content.push(lines[i]);
|
|
684
|
+
i++;
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
content.push(lines[i]);
|
|
688
|
+
i++;
|
|
689
|
+
}
|
|
690
|
+
if (!foundClosingTag && trackFences && balance.fence !== null) {
|
|
691
|
+
return extractXmlBlock(type, matchedTag, startIndex, lines, false);
|
|
692
|
+
}
|
|
693
|
+
const fullContent = content.join("\n");
|
|
694
|
+
const result = validateStreamingXmlBlock(type, fullContent, foundClosingTag);
|
|
695
|
+
return {
|
|
696
|
+
content: result.content || fullContent,
|
|
697
|
+
nextIndex: i,
|
|
698
|
+
metadata: result.metadata
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function validateStreamingXmlBlock(type, content, foundClosingTag) {
|
|
702
|
+
if (!["questionnaire", "flashcards", "cooking_recipe"].includes(type)) {
|
|
703
|
+
return {
|
|
704
|
+
content,
|
|
705
|
+
metadata: { isComplete: foundClosingTag }
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
if (type === "questionnaire") {
|
|
709
|
+
return validateQuestionnaireStreaming(content, foundClosingTag);
|
|
710
|
+
}
|
|
711
|
+
if (type === "flashcards") {
|
|
712
|
+
return validateFlashcardStreaming(content, foundClosingTag);
|
|
713
|
+
}
|
|
714
|
+
if (type === "cooking_recipe") {
|
|
715
|
+
return validateRecipeStreaming(content, foundClosingTag);
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
content,
|
|
719
|
+
metadata: { isComplete: foundClosingTag }
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function validateQuestionnaireStreaming(content, foundClosingTag) {
|
|
723
|
+
const lines = content.split("\n");
|
|
724
|
+
const completeQuestions = [];
|
|
725
|
+
let currentQuestion = [];
|
|
726
|
+
let totalQuestions = 0;
|
|
727
|
+
for (let i = 0; i < lines.length; i++) {
|
|
728
|
+
const line = lines[i].trim();
|
|
729
|
+
if (line.match(/^###\s+\*\*Q\d+:/) || line.match(/^###\s+Q\d+:/) || line.match(/^\*\*Q\d+:/)) {
|
|
730
|
+
totalQuestions++;
|
|
731
|
+
if (currentQuestion.length > 0) {
|
|
732
|
+
completeQuestions.push(currentQuestion.join("\n"));
|
|
733
|
+
}
|
|
734
|
+
currentQuestion = [lines[i]];
|
|
735
|
+
} else if (line === "---") {
|
|
736
|
+
currentQuestion.push(lines[i]);
|
|
737
|
+
if (currentQuestion.length > 1) {
|
|
738
|
+
completeQuestions.push(currentQuestion.join("\n"));
|
|
739
|
+
}
|
|
740
|
+
currentQuestion = [];
|
|
741
|
+
} else if (currentQuestion.length > 0 || line.length > 0) {
|
|
742
|
+
currentQuestion.push(lines[i]);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (foundClosingTag && currentQuestion.length > 0) {
|
|
746
|
+
completeQuestions.push(currentQuestion.join("\n"));
|
|
747
|
+
}
|
|
748
|
+
const contentHash = `${completeQuestions.length}-${foundClosingTag}`;
|
|
749
|
+
const cacheKey = `questionnaire-${contentHash}`;
|
|
750
|
+
let contentToRelease = foundClosingTag ? content : completeQuestions.join("\n\n---\n\n");
|
|
751
|
+
const cached = questionnaireCache.get(cacheKey);
|
|
752
|
+
if (cached && cached.content === contentToRelease) {
|
|
753
|
+
return cached;
|
|
754
|
+
}
|
|
755
|
+
const result = {
|
|
756
|
+
content: contentToRelease,
|
|
757
|
+
metadata: {
|
|
758
|
+
isComplete: foundClosingTag,
|
|
759
|
+
completeQuestionCount: completeQuestions.length,
|
|
760
|
+
totalQuestions,
|
|
761
|
+
hasPartialContent: !foundClosingTag && currentQuestion.length > 0
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
questionnaireCache.set(cacheKey, {
|
|
765
|
+
content: contentToRelease,
|
|
766
|
+
hash: contentHash,
|
|
767
|
+
metadata: result.metadata
|
|
768
|
+
});
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
function validateFlashcardStreaming(content, foundClosingTag) {
|
|
772
|
+
const lines = content.split("\n");
|
|
773
|
+
const completeCards = [];
|
|
774
|
+
let currentCard = [];
|
|
775
|
+
let totalCards = 0;
|
|
776
|
+
let hasFront = false;
|
|
777
|
+
let hasBack = false;
|
|
778
|
+
for (let i = 0; i < lines.length; i++) {
|
|
779
|
+
const line = lines[i].trim();
|
|
780
|
+
if (line.match(/^(?:Front|Question):/i)) {
|
|
781
|
+
if (currentCard.length > 0 && hasFront && hasBack) {
|
|
782
|
+
completeCards.push(currentCard.join("\n"));
|
|
783
|
+
}
|
|
784
|
+
currentCard = [lines[i]];
|
|
785
|
+
hasFront = true;
|
|
786
|
+
hasBack = false;
|
|
787
|
+
totalCards++;
|
|
788
|
+
} else if (line.match(/^(?:Back|Answer):/i)) {
|
|
789
|
+
currentCard.push(lines[i]);
|
|
790
|
+
hasBack = true;
|
|
791
|
+
} else if (line === "---") {
|
|
792
|
+
if (currentCard.length > 0 && hasFront && hasBack) {
|
|
793
|
+
completeCards.push(currentCard.join("\n"));
|
|
794
|
+
}
|
|
795
|
+
currentCard = [];
|
|
796
|
+
hasFront = false;
|
|
797
|
+
hasBack = false;
|
|
798
|
+
} else if (currentCard.length > 0) {
|
|
799
|
+
currentCard.push(lines[i]);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (foundClosingTag && currentCard.length > 0 && hasFront && hasBack) {
|
|
803
|
+
completeCards.push(currentCard.join("\n"));
|
|
804
|
+
}
|
|
805
|
+
const contentHash = `${completeCards.length}-${foundClosingTag}`;
|
|
806
|
+
const cacheKey = `flashcards-${contentHash}`;
|
|
807
|
+
let contentToRelease = foundClosingTag ? content : completeCards.join("\n\n---\n\n");
|
|
808
|
+
const cached = flashcardCache.get(cacheKey);
|
|
809
|
+
if (cached && cached.content === contentToRelease) {
|
|
810
|
+
return cached;
|
|
811
|
+
}
|
|
812
|
+
const result = {
|
|
813
|
+
content: contentToRelease,
|
|
814
|
+
metadata: {
|
|
815
|
+
isComplete: foundClosingTag,
|
|
816
|
+
completeCardCount: completeCards.length,
|
|
817
|
+
totalCards,
|
|
818
|
+
hasPartialContent: !foundClosingTag && currentCard.length > 0
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
flashcardCache.set(cacheKey, {
|
|
822
|
+
content: contentToRelease,
|
|
823
|
+
hash: contentHash,
|
|
824
|
+
metadata: result.metadata
|
|
825
|
+
});
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
function validateRecipeStreaming(content, foundClosingTag) {
|
|
829
|
+
const hasTitle = /^###\s+.+$/m.test(content);
|
|
830
|
+
const hasIngredients = /####\s*Ingredients?:/i.test(content);
|
|
831
|
+
const hasInstructions = /####\s*Instructions?:/i.test(content);
|
|
832
|
+
const contentLength = content.length;
|
|
833
|
+
const contentHash = `${contentLength}-${foundClosingTag}-${hasTitle}-${hasIngredients}-${hasInstructions}`;
|
|
834
|
+
const cacheKey = `cooking_recipe-${contentHash}`;
|
|
835
|
+
let contentToRelease = "";
|
|
836
|
+
if (foundClosingTag) {
|
|
837
|
+
contentToRelease = content;
|
|
838
|
+
} else if (hasTitle && (hasIngredients || hasInstructions)) {
|
|
839
|
+
contentToRelease = content;
|
|
840
|
+
}
|
|
841
|
+
const cached = recipeCache.get(cacheKey);
|
|
842
|
+
if (cached && cached.content === contentToRelease) {
|
|
843
|
+
return cached;
|
|
844
|
+
}
|
|
845
|
+
const result = {
|
|
846
|
+
content: contentToRelease,
|
|
847
|
+
metadata: {
|
|
848
|
+
isComplete: foundClosingTag,
|
|
849
|
+
hasTitle,
|
|
850
|
+
hasIngredients,
|
|
851
|
+
hasInstructions,
|
|
852
|
+
hasPartialContent: !foundClosingTag && contentLength > 0
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
recipeCache.set(cacheKey, {
|
|
856
|
+
content: contentToRelease,
|
|
857
|
+
hash: contentHash,
|
|
858
|
+
metadata: result.metadata
|
|
859
|
+
});
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
862
|
+
function detectCodeBlock(line) {
|
|
863
|
+
const opener = fenceOpenerOf(line);
|
|
864
|
+
if (!opener) {
|
|
865
|
+
return { isCodeBlock: false };
|
|
866
|
+
}
|
|
867
|
+
const trimmed = line.trim();
|
|
868
|
+
const ticks = opener.ticks;
|
|
869
|
+
const { language, meta } = splitFenceInfo(trimmed.slice(ticks));
|
|
870
|
+
return { isCodeBlock: true, language, meta, ticks, char: opener.char };
|
|
871
|
+
}
|
|
872
|
+
function jsonLineForFenceReader(line) {
|
|
873
|
+
if (line.includes("```")) return line;
|
|
874
|
+
let inString = false;
|
|
875
|
+
let escaped = false;
|
|
876
|
+
for (let i = 0; i < line.length; i++) {
|
|
877
|
+
const ch = line[i];
|
|
878
|
+
if (escaped) escaped = false;
|
|
879
|
+
else if (ch === "\\" && inString) escaped = true;
|
|
880
|
+
else if (ch === '"') inString = !inString;
|
|
881
|
+
}
|
|
882
|
+
if (!inString) return line;
|
|
883
|
+
return escaped ? `${line}x"` : `${line}"`;
|
|
884
|
+
}
|
|
885
|
+
function extractCodeBlock(language, openTicks, startIndex, lines, fenceChar = "`") {
|
|
886
|
+
const jsonBody = fenceChar === "`" && (language ?? "") === "json";
|
|
887
|
+
const end = closeFence(
|
|
888
|
+
{ count: lines.length, line: (k) => jsonBody ? jsonLineForFenceReader(lines[k] ?? "") : lines[k] ?? "" },
|
|
889
|
+
startIndex - 1,
|
|
890
|
+
{ char: fenceChar, ticks: openTicks, lang: language ?? "" }
|
|
891
|
+
);
|
|
892
|
+
if (!end.closed) {
|
|
893
|
+
return {
|
|
894
|
+
content: lines.slice(startIndex).join("\n"),
|
|
895
|
+
nextIndex: lines.length + 1,
|
|
896
|
+
metadata: { language },
|
|
897
|
+
closedCleanly: false
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
const closer = lines[end.line] ?? "";
|
|
901
|
+
const bare = fenceOpenerOf(closer)?.char === fenceChar;
|
|
902
|
+
const before = bare ? "" : closer.slice(0, closer.indexOf("`".repeat(3)));
|
|
903
|
+
const body = lines.slice(startIndex, end.line);
|
|
904
|
+
if (before.trim()) body.push(before);
|
|
905
|
+
return {
|
|
906
|
+
content: body.join("\n"),
|
|
907
|
+
nextIndex: end.line + 1,
|
|
908
|
+
metadata: { language },
|
|
909
|
+
closedCleanly: bare
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function isTableSeparator(line) {
|
|
913
|
+
return isGfmDelimiterRow(normalizeLine(line).trim());
|
|
914
|
+
}
|
|
915
|
+
function detectTableStart(lines, index) {
|
|
916
|
+
return opensTable(lines, index);
|
|
917
|
+
}
|
|
918
|
+
function extractTable(startIndex, lines) {
|
|
919
|
+
const tableLines = [lines[startIndex]];
|
|
920
|
+
let i = startIndex + 1;
|
|
921
|
+
const container = tableContainerIndent(lines, startIndex);
|
|
922
|
+
while (i < lines.length && (continuesTable(normalizeLine(lines[i]), container) || i === startIndex + 1 && isTableSeparator(lines[i]))) {
|
|
923
|
+
tableLines.push(lines[i]);
|
|
924
|
+
i++;
|
|
925
|
+
}
|
|
926
|
+
const stillArriving = startIndex + 2 >= lines.length && tableLines.length >= 2 && isTableSeparator(tableLines[1]);
|
|
927
|
+
if (tableLines.length < 2 || !(tableStartsAt(lines, startIndex) || stillArriving)) {
|
|
928
|
+
return {
|
|
929
|
+
content: "",
|
|
930
|
+
nextIndex: startIndex + 1,
|
|
931
|
+
metadata: { isValid: false }
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
const tableHasEnded = i < lines.length;
|
|
935
|
+
const result = analyzeTableCompletionWithCache(tableLines, tableHasEnded);
|
|
936
|
+
return {
|
|
937
|
+
content: result.content,
|
|
938
|
+
nextIndex: i,
|
|
939
|
+
metadata: result.metadata
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
function analyzeTableCompletionWithCache(tableLines, tableHasEnded) {
|
|
943
|
+
if (tableLines.length < 2) {
|
|
944
|
+
return {
|
|
945
|
+
content: "",
|
|
946
|
+
metadata: { isComplete: false, completeRowCount: 0 }
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const headerHash = tableLines.slice(0, 2).join("|").replace(/\s+/g, "");
|
|
950
|
+
const rowCount = tableLines.length;
|
|
951
|
+
const cacheKey = `table-${headerHash}-${rowCount}-${tableHasEnded}`;
|
|
952
|
+
const state = analyzeTableCompletion(tableLines, tableHasEnded);
|
|
953
|
+
const contentToRelease = tableLines.join("\n");
|
|
954
|
+
const cached = tableCache.get(cacheKey);
|
|
955
|
+
if (cached && cached.content === contentToRelease) {
|
|
956
|
+
return cached;
|
|
957
|
+
}
|
|
958
|
+
const result = {
|
|
959
|
+
content: contentToRelease,
|
|
960
|
+
metadata: {
|
|
961
|
+
cacheKey,
|
|
962
|
+
isComplete: state.isComplete,
|
|
963
|
+
completeRowCount: state.completeRowCount,
|
|
964
|
+
totalRows: state.totalRows,
|
|
965
|
+
hasPartialContent: state.bufferRow.length > 0
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
tableCache.set(cacheKey, {
|
|
969
|
+
content: contentToRelease,
|
|
970
|
+
hash: `${state.completeRowCount}-${state.isComplete}-${rowCount}`,
|
|
971
|
+
metadata: result.metadata
|
|
972
|
+
});
|
|
973
|
+
return result;
|
|
974
|
+
}
|
|
975
|
+
function analyzeTableCompletion(tableLines, tableHasEnded) {
|
|
976
|
+
if (tableLines.length < 3) {
|
|
977
|
+
return {
|
|
978
|
+
isComplete: false,
|
|
979
|
+
completeRowCount: 0,
|
|
980
|
+
totalRows: 0,
|
|
981
|
+
bufferRow: ""
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
const dataLines = tableLines.slice(2);
|
|
985
|
+
const completeRows = [];
|
|
986
|
+
let bufferRow = "";
|
|
987
|
+
for (let i = 0; i < dataLines.length; i++) {
|
|
988
|
+
const line = dataLines[i];
|
|
989
|
+
const trimmedLine = normalizeLine(line).trim();
|
|
990
|
+
if (continuesTable(trimmedLine)) {
|
|
991
|
+
const isCompleteRow = tableHasEnded || i < dataLines.length - 1;
|
|
992
|
+
if (isCompleteRow) {
|
|
993
|
+
completeRows.push(line);
|
|
994
|
+
} else {
|
|
995
|
+
bufferRow = line;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
isComplete: tableHasEnded || bufferRow.length === 0,
|
|
1001
|
+
completeRowCount: completeRows.length,
|
|
1002
|
+
totalRows: dataLines.length,
|
|
1003
|
+
bufferRow
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
function detectImageMarkdown(line) {
|
|
1007
|
+
const trimmed = line.trim();
|
|
1008
|
+
const standardMatch = trimmed.match(
|
|
1009
|
+
/^!\[(.*?)\]\((https?:\/\/[^\s)]+)(?:\s+(?:"[^"]*"|'[^']*'))?\)/
|
|
1010
|
+
);
|
|
1011
|
+
const customMatch = trimmed.match(/\[Image URL: (https?:\/\/[^\s\]]+)\]/);
|
|
1012
|
+
if (standardMatch) {
|
|
1013
|
+
return { isImage: true, alt: standardMatch[1], src: standardMatch[2] };
|
|
1014
|
+
}
|
|
1015
|
+
if (customMatch) {
|
|
1016
|
+
return { isImage: true, alt: "Image", src: customMatch[1] };
|
|
1017
|
+
}
|
|
1018
|
+
return { isImage: false };
|
|
1019
|
+
}
|
|
1020
|
+
function countInlineImages(line) {
|
|
1021
|
+
const re = /!\[(.*?)\]\((https?:\/\/[^\s)]+)(?:\s+(?:"[^"]*"|'[^']*'))?\)/g;
|
|
1022
|
+
let count = 0;
|
|
1023
|
+
while (re.exec(line) !== null) count++;
|
|
1024
|
+
return count;
|
|
1025
|
+
}
|
|
1026
|
+
function detectVideoMarkdown(line) {
|
|
1027
|
+
const trimmed = line.trim();
|
|
1028
|
+
const customMatch = trimmed.match(/\[Video URL: (https?:\/\/[^\s\]]+)\]/);
|
|
1029
|
+
if (customMatch) {
|
|
1030
|
+
return { isVideo: true, alt: "Video", src: customMatch[1] };
|
|
1031
|
+
}
|
|
1032
|
+
return { isVideo: false };
|
|
1033
|
+
}
|
|
1034
|
+
const AUDIO_URL_EXT = /\.(mp3|wav|m4a|aac|ogg|oga|opus|flac|weba|webm)(\?[^\s)]*)?$/i;
|
|
1035
|
+
function extractAudioLink(line) {
|
|
1036
|
+
const trimmed = line.trim();
|
|
1037
|
+
const customMatch = trimmed.match(/\[Audio URL:\s*(https?:\/\/[^\s\]]+)\]/);
|
|
1038
|
+
if (customMatch) {
|
|
1039
|
+
return { src: customMatch[1], alt: "Audio" };
|
|
1040
|
+
}
|
|
1041
|
+
const linkMatch = trimmed.match(/^!?\[(.*?)\]\((https?:\/\/[^\s)]+)\)$/);
|
|
1042
|
+
if (linkMatch && AUDIO_URL_EXT.test(linkMatch[2])) {
|
|
1043
|
+
return { src: linkMatch[2], alt: linkMatch[1] || "Audio" };
|
|
1044
|
+
}
|
|
1045
|
+
if (/^https?:\/\/[^\s)]+$/.test(trimmed) && AUDIO_URL_EXT.test(trimmed)) {
|
|
1046
|
+
return { src: trimmed, alt: "Audio" };
|
|
1047
|
+
}
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
function detectAudioMarkdown(line) {
|
|
1051
|
+
const audio = extractAudioLink(line);
|
|
1052
|
+
if (audio) {
|
|
1053
|
+
return { isAudio: true, src: audio.src, alt: audio.alt };
|
|
1054
|
+
}
|
|
1055
|
+
return { isAudio: false };
|
|
1056
|
+
}
|
|
1057
|
+
function trimEmphasisEnd(text) {
|
|
1058
|
+
return text.replace(/(?:\*\*|\*|_){1,2}\s*$/, "");
|
|
1059
|
+
}
|
|
1060
|
+
function trimEmphasisStart(text) {
|
|
1061
|
+
return text.replace(/^\s*(?:\*\*|\*|_){1,2}/, "");
|
|
1062
|
+
}
|
|
1063
|
+
function detectMatrxFileMarkdown(line) {
|
|
1064
|
+
if (!mightBeOurFileUrl(line)) return { isMatrxFile: false };
|
|
1065
|
+
const linkRe = /!?\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/g;
|
|
1066
|
+
let m;
|
|
1067
|
+
while ((m = linkRe.exec(line)) !== null) {
|
|
1068
|
+
const url = m[2];
|
|
1069
|
+
if (recognizeOurFileUrl(url)) {
|
|
1070
|
+
const start = m.index;
|
|
1071
|
+
const end = m.index + m[0].length;
|
|
1072
|
+
return {
|
|
1073
|
+
isMatrxFile: true,
|
|
1074
|
+
url,
|
|
1075
|
+
label: m[1] || "",
|
|
1076
|
+
pre: trimEmphasisEnd(line.slice(0, start)),
|
|
1077
|
+
post: trimEmphasisStart(line.slice(end))
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const bareRe = /(https?:\/\/[^\s)]+)/g;
|
|
1082
|
+
while ((m = bareRe.exec(line)) !== null) {
|
|
1083
|
+
const url = m[1].replace(/[).,;]+$/, "");
|
|
1084
|
+
if (recognizeOurFileUrl(url)) {
|
|
1085
|
+
const start = m.index;
|
|
1086
|
+
const end = m.index + url.length;
|
|
1087
|
+
return {
|
|
1088
|
+
isMatrxFile: true,
|
|
1089
|
+
url,
|
|
1090
|
+
label: "",
|
|
1091
|
+
pre: trimEmphasisEnd(line.slice(0, start)),
|
|
1092
|
+
post: trimEmphasisStart(line.slice(end))
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return { isMatrxFile: false };
|
|
1097
|
+
}
|
|
1098
|
+
function detectYoutubeMarkdown(line) {
|
|
1099
|
+
const trimmed = line.trim();
|
|
1100
|
+
const linkedThumb = trimmed.match(
|
|
1101
|
+
/^\[!\[(.*?)\]\((https?:\/\/[^\s)]+?)\)\]\((https?:\/\/[^\s)]+?)\)$/
|
|
1102
|
+
);
|
|
1103
|
+
if (linkedThumb) {
|
|
1104
|
+
const parsed = parseYouTubeUrl(linkedThumb[3]);
|
|
1105
|
+
if (parsed) {
|
|
1106
|
+
return {
|
|
1107
|
+
isYoutube: true,
|
|
1108
|
+
videoId: parsed.videoId,
|
|
1109
|
+
start: parsed.start,
|
|
1110
|
+
title: linkedThumb[1] || void 0,
|
|
1111
|
+
poster: isYouTubeThumbnailUrl(linkedThumb[2]) ? linkedThumb[2] : void 0,
|
|
1112
|
+
watchUrl: youTubeWatchUrl(parsed.videoId, parsed.start)
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
const plainLink = trimmed.match(/^\[([^\]]*)\]\((https?:\/\/[^\s)]+?)\)$/);
|
|
1117
|
+
if (plainLink) {
|
|
1118
|
+
const parsed = parseYouTubeUrl(plainLink[2]);
|
|
1119
|
+
if (parsed) {
|
|
1120
|
+
return {
|
|
1121
|
+
isYoutube: true,
|
|
1122
|
+
videoId: parsed.videoId,
|
|
1123
|
+
start: parsed.start,
|
|
1124
|
+
title: plainLink[1] || void 0,
|
|
1125
|
+
watchUrl: youTubeWatchUrl(parsed.videoId, parsed.start)
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
if (/^https?:\/\/\S+$/.test(trimmed)) {
|
|
1130
|
+
const parsed = parseYouTubeUrl(trimmed);
|
|
1131
|
+
if (parsed) {
|
|
1132
|
+
return {
|
|
1133
|
+
isYoutube: true,
|
|
1134
|
+
videoId: parsed.videoId,
|
|
1135
|
+
start: parsed.start,
|
|
1136
|
+
watchUrl: youTubeWatchUrl(parsed.videoId, parsed.start)
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return { isYoutube: false };
|
|
1141
|
+
}
|
|
1142
|
+
const TREE_CHARS = /[├└│┌┐┘┬┴┤┼─]/;
|
|
1143
|
+
const ASCII_TREE_PATTERNS = /^[\s│|]*(?:[├└]|[+|][─-])/;
|
|
1144
|
+
function isTreeLine(line) {
|
|
1145
|
+
if (!line) return false;
|
|
1146
|
+
if (TREE_CHARS.test(line)) return true;
|
|
1147
|
+
return ASCII_TREE_PATTERNS.test(line) && !isGfmDelimiterRow(line);
|
|
1148
|
+
}
|
|
1149
|
+
function isMarkdownHeadingLine(line) {
|
|
1150
|
+
return /^#{1,6}\s/.test(line.trim());
|
|
1151
|
+
}
|
|
1152
|
+
function findTreeBlockStart(lines, firstTreeIndex) {
|
|
1153
|
+
let start = firstTreeIndex;
|
|
1154
|
+
for (let j = firstTreeIndex - 1; j >= 0; j--) {
|
|
1155
|
+
const trimmed = normalizeLine(lines[j]).trim();
|
|
1156
|
+
if (!trimmed) break;
|
|
1157
|
+
if (isTreeLine(trimmed)) break;
|
|
1158
|
+
if (isMarkdownHeadingLine(trimmed)) break;
|
|
1159
|
+
start = j;
|
|
1160
|
+
}
|
|
1161
|
+
return start;
|
|
1162
|
+
}
|
|
1163
|
+
function stripAccumulatedLinesFromText(text, rawLines, fromIndex, toIndex) {
|
|
1164
|
+
let result = text;
|
|
1165
|
+
for (let j = fromIndex; j < toIndex; j++) {
|
|
1166
|
+
const processed = normalizeLine(rawLines[j]);
|
|
1167
|
+
if (result.endsWith(processed + "\n")) {
|
|
1168
|
+
result = result.slice(0, -(processed.length + 1));
|
|
1169
|
+
} else if (result.endsWith(processed)) {
|
|
1170
|
+
result = result.slice(0, -processed.length);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return result;
|
|
1174
|
+
}
|
|
1175
|
+
function normalizeLine(text) {
|
|
1176
|
+
return text.trim() === "" ? "" : text;
|
|
1177
|
+
}
|
|
1178
|
+
function frontMatterLineCount(lines) {
|
|
1179
|
+
const opener = lines[0]?.replace(/^\uFEFF/, "");
|
|
1180
|
+
if (lines.length < 2 || opener !== "---" && opener !== "+++") return 0;
|
|
1181
|
+
for (let k = 1; k < lines.length; k++) {
|
|
1182
|
+
if (lines[k] === opener || opener === "---" && lines[k] === "...") return k + 1;
|
|
1183
|
+
}
|
|
1184
|
+
return 0;
|
|
1185
|
+
}
|
|
1186
|
+
const splitContentIntoBlocksWith = (mdContent, envelopes) => {
|
|
1187
|
+
const blocks = [];
|
|
1188
|
+
const lines = liftQuotedKindRegions(
|
|
1189
|
+
unwrapKindImageAlt(unescapeMarkdownKindJson(decodeDoubleEncodedKindText(mdContent)))
|
|
1190
|
+
).split(/\r?\n/);
|
|
1191
|
+
let currentText = "";
|
|
1192
|
+
let i = 0;
|
|
1193
|
+
const pendingRawXmlOverrides = /* @__PURE__ */ new Map();
|
|
1194
|
+
const frontMatterLines = frontMatterLineCount(lines);
|
|
1195
|
+
if (frontMatterLines > 0) {
|
|
1196
|
+
currentText = lines.slice(0, frontMatterLines).map(normalizeLine).join("\n") + (frontMatterLines < lines.length ? "\n" : "");
|
|
1197
|
+
i = frontMatterLines;
|
|
1198
|
+
}
|
|
1199
|
+
while (i < lines.length) {
|
|
1200
|
+
const line = lines[i];
|
|
1201
|
+
const processedLine = normalizeLine(line);
|
|
1202
|
+
const trimmedLine = processedLine.trim();
|
|
1203
|
+
if (/^\*\s*\*\s*\*\s*$/.test(trimmedLine)) {
|
|
1204
|
+
if (currentText.trim()) {
|
|
1205
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1206
|
+
currentText = "";
|
|
1207
|
+
}
|
|
1208
|
+
blocks.push({ type: "accent-divider", content: trimmedLine });
|
|
1209
|
+
i++;
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (/^#\s*={3,}\s*$/.test(trimmedLine)) {
|
|
1213
|
+
if (currentText.trim()) {
|
|
1214
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1215
|
+
currentText = "";
|
|
1216
|
+
}
|
|
1217
|
+
blocks.push({ type: "heavy-divider", content: trimmedLine });
|
|
1218
|
+
i++;
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
if (DIRECTIVE_CONTAINER_OPEN.test(processedLine)) {
|
|
1222
|
+
const tracker = new DirectiveContainerTracker(processedLine);
|
|
1223
|
+
let j = i;
|
|
1224
|
+
while (j < lines.length) {
|
|
1225
|
+
const containerLine = normalizeLine(lines[j]);
|
|
1226
|
+
currentText += containerLine + (j < lines.length - 1 ? "\n" : "");
|
|
1227
|
+
j++;
|
|
1228
|
+
if (j - 1 > i && tracker.consume(containerLine)) break;
|
|
1229
|
+
}
|
|
1230
|
+
i = j;
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
const codeCheck = detectCodeBlock(processedLine);
|
|
1234
|
+
if (codeCheck.isCodeBlock) {
|
|
1235
|
+
if (currentText.trim()) {
|
|
1236
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1237
|
+
currentText = "";
|
|
1238
|
+
}
|
|
1239
|
+
const extraction = extractCodeBlock(
|
|
1240
|
+
codeCheck.language,
|
|
1241
|
+
codeCheck.ticks ?? 3,
|
|
1242
|
+
i + 1,
|
|
1243
|
+
lines,
|
|
1244
|
+
codeCheck.char
|
|
1245
|
+
);
|
|
1246
|
+
const normalizedLanguage = normalizeCodeLanguage(codeCheck.language);
|
|
1247
|
+
if (normalizedLanguage && SPECIAL_CODE_LANGUAGES.includes(normalizedLanguage)) {
|
|
1248
|
+
const closedCleanly = extraction.closedCleanly;
|
|
1249
|
+
let fenceMetadata;
|
|
1250
|
+
if (closedCleanly) {
|
|
1251
|
+
const fenceEnvelope = envelopes.fenceRegion(
|
|
1252
|
+
normalizedLanguage,
|
|
1253
|
+
extraction.content
|
|
1254
|
+
);
|
|
1255
|
+
if (fenceEnvelope) {
|
|
1256
|
+
fenceMetadata = { [IR_ENVELOPE_KEY]: fenceEnvelope };
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
blocks.push({
|
|
1260
|
+
type: normalizedLanguage,
|
|
1261
|
+
content: extraction.content,
|
|
1262
|
+
language: codeCheck.language,
|
|
1263
|
+
...fenceMetadata ? { metadata: fenceMetadata } : {}
|
|
1264
|
+
});
|
|
1265
|
+
} else if (codeCheck.language?.toLowerCase() === "json") {
|
|
1266
|
+
const jsonType = detectJsonBlockType(extraction.content);
|
|
1267
|
+
if (jsonType) {
|
|
1268
|
+
const streamingState = validateJsonBlock(
|
|
1269
|
+
extraction.content,
|
|
1270
|
+
jsonType
|
|
1271
|
+
);
|
|
1272
|
+
blocks.push({
|
|
1273
|
+
type: jsonType,
|
|
1274
|
+
content: extraction.content,
|
|
1275
|
+
language: "json",
|
|
1276
|
+
// content-ir: complete JSON regions carry the canonical envelope
|
|
1277
|
+
// (memoized one-shot parse — identical to the streaming path's).
|
|
1278
|
+
// EXCEPT a directive: its __kind is a reserved-namespace slug that
|
|
1279
|
+
// is BY DESIGN never in kind_definition, so an __ir envelope here
|
|
1280
|
+
// fires a doomed registry fetch per message and lets the kind
|
|
1281
|
+
// route re-type the block away from MatrxEnvelopeBlock the moment
|
|
1282
|
+
// a directive-class registry row exists (adversarial finding A-8;
|
|
1283
|
+
// recoverEmbeddedKindJsonBlocks already does exactly this).
|
|
1284
|
+
metadata: jsonType === "matrx" ? streamingState.metadata : envelopes.withIrEnvelope(extraction.content, streamingState.metadata)
|
|
1285
|
+
});
|
|
1286
|
+
} else {
|
|
1287
|
+
blocks.push({
|
|
1288
|
+
type: "code",
|
|
1289
|
+
content: extraction.content,
|
|
1290
|
+
language: codeCheck.language,
|
|
1291
|
+
metadata: envelopes.withIrEnvelope(
|
|
1292
|
+
extraction.content,
|
|
1293
|
+
codeCheck.meta ? { [FENCE_META_KEY]: codeCheck.meta } : void 0
|
|
1294
|
+
)
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
} else {
|
|
1298
|
+
blocks.push({
|
|
1299
|
+
type: "code",
|
|
1300
|
+
content: extraction.content,
|
|
1301
|
+
language: codeCheck.language,
|
|
1302
|
+
...codeCheck.meta ? { metadata: { [FENCE_META_KEY]: codeCheck.meta } } : {}
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
i = extraction.nextIndex;
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
const attrXmlMatch = detectAttributeXmlBlock(processedLine);
|
|
1309
|
+
if (attrXmlMatch) {
|
|
1310
|
+
if (currentText.trim()) {
|
|
1311
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1312
|
+
currentText = "";
|
|
1313
|
+
}
|
|
1314
|
+
const rawXmlOverride = pendingRawXmlOverrides.get(trimmedLine);
|
|
1315
|
+
if (rawXmlOverride !== void 0) {
|
|
1316
|
+
pendingRawXmlOverrides.delete(trimmedLine);
|
|
1317
|
+
}
|
|
1318
|
+
const extraction = extractAttributeXmlBlock(
|
|
1319
|
+
attrXmlMatch,
|
|
1320
|
+
i,
|
|
1321
|
+
lines,
|
|
1322
|
+
rawXmlOverride
|
|
1323
|
+
);
|
|
1324
|
+
blocks.push({
|
|
1325
|
+
type: attrXmlMatch.type,
|
|
1326
|
+
content: extraction.content,
|
|
1327
|
+
metadata: extraction.metadata
|
|
1328
|
+
});
|
|
1329
|
+
i = extraction.nextIndex;
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
const xmlMatch = detectXmlBlockType(processedLine);
|
|
1333
|
+
if (xmlMatch) {
|
|
1334
|
+
if (currentText.trim()) {
|
|
1335
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1336
|
+
currentText = "";
|
|
1337
|
+
}
|
|
1338
|
+
const extraction = extractXmlBlock(
|
|
1339
|
+
xmlMatch.type,
|
|
1340
|
+
xmlMatch.matchedTag,
|
|
1341
|
+
i,
|
|
1342
|
+
lines
|
|
1343
|
+
);
|
|
1344
|
+
let xmlMetadata = extraction.metadata;
|
|
1345
|
+
if (xmlMetadata?.isComplete === true) {
|
|
1346
|
+
const xmlTag = xmlTagNameOf(xmlMatch.matchedTag);
|
|
1347
|
+
const attributed = xmlMatch.matchedTag !== `<${xmlTag}>`;
|
|
1348
|
+
const xmlEnvelope = envelopes.xmlRegion(
|
|
1349
|
+
xmlTag,
|
|
1350
|
+
// Attributes (e.g. a questionnaire `title`) live on the opener:
|
|
1351
|
+
// hand the strategy the same literal framing the accumulator does.
|
|
1352
|
+
attributed ? `${xmlMatch.matchedTag}
|
|
1353
|
+
${extraction.content}
|
|
1354
|
+
</${xmlTag}>` : extraction.content
|
|
1355
|
+
);
|
|
1356
|
+
if (xmlEnvelope) {
|
|
1357
|
+
xmlMetadata = { ...xmlMetadata, [IR_ENVELOPE_KEY]: xmlEnvelope };
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
blocks.push({
|
|
1361
|
+
type: xmlMatch.type,
|
|
1362
|
+
content: extraction.content,
|
|
1363
|
+
metadata: xmlMetadata
|
|
1364
|
+
});
|
|
1365
|
+
i = extraction.nextIndex;
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
const unrecognizedXml = extractUnrecognizedXmlBlock(i, lines);
|
|
1369
|
+
if (unrecognizedXml) {
|
|
1370
|
+
if (currentText.trim()) {
|
|
1371
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1372
|
+
currentText = "";
|
|
1373
|
+
}
|
|
1374
|
+
blocks.push({
|
|
1375
|
+
type: "code",
|
|
1376
|
+
content: unrecognizedXml.content,
|
|
1377
|
+
language: "xml",
|
|
1378
|
+
metadata: unrecognizedXml.metadata
|
|
1379
|
+
});
|
|
1380
|
+
i = unrecognizedXml.nextIndex;
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
const incompleteUnrecognizedXml = !isHtmlBlockTagName(leadingTagName(processedLine) ?? "") && (startUnrecognizedXmlContainer(processedLine) || isUnclosedGenericXmlOpening(processedLine));
|
|
1384
|
+
if (incompleteUnrecognizedXml) {
|
|
1385
|
+
if (currentText.trim()) {
|
|
1386
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1387
|
+
currentText = "";
|
|
1388
|
+
}
|
|
1389
|
+
blocks.push({
|
|
1390
|
+
type: "code",
|
|
1391
|
+
content: lines.slice(i).map(normalizeLine).join("\n").trim(),
|
|
1392
|
+
language: "xml",
|
|
1393
|
+
metadata: { isComplete: false, genericXmlContainer: true }
|
|
1394
|
+
});
|
|
1395
|
+
i = lines.length;
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
const orphanClose = detectOrphanThinkingClose(trimmedLine);
|
|
1399
|
+
if (orphanClose) {
|
|
1400
|
+
const openers = orphanClose.type === "thinking" ? ["<thinking>", "<think>"] : ["<reasoning>"];
|
|
1401
|
+
let sameLineOpener = -1;
|
|
1402
|
+
let sameLineLen = 0;
|
|
1403
|
+
for (const opener of THINKING_OPENERS[orphanClose.type]) {
|
|
1404
|
+
const idx = orphanClose.before.lastIndexOf(opener);
|
|
1405
|
+
if (idx > sameLineOpener) {
|
|
1406
|
+
sameLineOpener = idx;
|
|
1407
|
+
sameLineLen = opener.length;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (sameLineOpener >= 0 && orphanClose.before.slice(0, sameLineOpener).trim()) {
|
|
1411
|
+
currentText += processedLine + (i < lines.length - 1 ? "\n" : "");
|
|
1412
|
+
i++;
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
if (sameLineOpener >= 0) {
|
|
1416
|
+
const proseBefore = `${currentText}${orphanClose.before.slice(0, sameLineOpener)}`;
|
|
1417
|
+
if (proseBefore.trim()) {
|
|
1418
|
+
blocks.push({ type: "text", content: proseBefore.trimEnd() });
|
|
1419
|
+
}
|
|
1420
|
+
currentText = "";
|
|
1421
|
+
const span = orphanClose.before.slice(sameLineOpener + sameLineLen).trim();
|
|
1422
|
+
if (span) {
|
|
1423
|
+
blocks.push({
|
|
1424
|
+
type: orphanClose.type,
|
|
1425
|
+
content: span,
|
|
1426
|
+
metadata: { isComplete: true }
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
if (orphanClose.remainder) {
|
|
1430
|
+
lines.splice(i + 1, 0, orphanClose.remainder);
|
|
1431
|
+
}
|
|
1432
|
+
i++;
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
let openerIdx = -1;
|
|
1436
|
+
let openerLen = 0;
|
|
1437
|
+
for (const opener of openers) {
|
|
1438
|
+
const idx = currentText.lastIndexOf(opener);
|
|
1439
|
+
if (idx > openerIdx) {
|
|
1440
|
+
openerIdx = idx;
|
|
1441
|
+
openerLen = opener.length;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
if (openerIdx >= 0 && /<\/(?:thinking|think|reasoning)>/.test(currentText.slice(openerIdx + openerLen))) {
|
|
1445
|
+
openerIdx = -1;
|
|
1446
|
+
}
|
|
1447
|
+
let regionText = currentText;
|
|
1448
|
+
if (openerIdx >= 0) {
|
|
1449
|
+
const proseBefore = currentText.slice(0, openerIdx);
|
|
1450
|
+
regionText = currentText.slice(openerIdx + openerLen);
|
|
1451
|
+
if (proseBefore.trim()) {
|
|
1452
|
+
blocks.push({ type: "text", content: proseBefore.trimEnd() });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
currentText = "";
|
|
1456
|
+
const body = [regionText.trim(), orphanClose.before].filter(Boolean).join("\n");
|
|
1457
|
+
if (body) {
|
|
1458
|
+
blocks.push({
|
|
1459
|
+
type: orphanClose.type,
|
|
1460
|
+
content: body,
|
|
1461
|
+
metadata: { isComplete: true, continuation: true }
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
if (orphanClose.remainder) {
|
|
1465
|
+
lines.splice(i + 1, 0, orphanClose.remainder);
|
|
1466
|
+
}
|
|
1467
|
+
i++;
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
const youtubeCheck = detectYoutubeMarkdown(line);
|
|
1471
|
+
if (youtubeCheck.isYoutube && youtubeCheck.videoId) {
|
|
1472
|
+
if (currentText.trim()) {
|
|
1473
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1474
|
+
currentText = "";
|
|
1475
|
+
}
|
|
1476
|
+
blocks.push({
|
|
1477
|
+
type: "youtube",
|
|
1478
|
+
// Keep the original line as content so the DB round-trip
|
|
1479
|
+
// (reconstructBlockMarkdown default → content) re-detects it on reload.
|
|
1480
|
+
content: trimmedLine,
|
|
1481
|
+
src: youtubeCheck.watchUrl,
|
|
1482
|
+
metadata: {
|
|
1483
|
+
videoId: youtubeCheck.videoId,
|
|
1484
|
+
start: youtubeCheck.start,
|
|
1485
|
+
title: youtubeCheck.title,
|
|
1486
|
+
poster: youtubeCheck.poster
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
i++;
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
const imageCheck = detectImageMarkdown(line);
|
|
1493
|
+
if (imageCheck.isImage && countInlineImages(line) < 2 && !TITLED_IMAGE_LINE.test(line)) {
|
|
1494
|
+
if (currentText.trim()) {
|
|
1495
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1496
|
+
currentText = "";
|
|
1497
|
+
}
|
|
1498
|
+
blocks.push({
|
|
1499
|
+
type: "image",
|
|
1500
|
+
content: trimmedLine,
|
|
1501
|
+
src: imageCheck.src,
|
|
1502
|
+
alt: imageCheck.alt
|
|
1503
|
+
});
|
|
1504
|
+
i++;
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
const videoCheck = detectVideoMarkdown(line);
|
|
1508
|
+
if (videoCheck.isVideo) {
|
|
1509
|
+
if (currentText.trim()) {
|
|
1510
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1511
|
+
currentText = "";
|
|
1512
|
+
}
|
|
1513
|
+
blocks.push({
|
|
1514
|
+
type: "video",
|
|
1515
|
+
content: trimmedLine,
|
|
1516
|
+
src: videoCheck.src,
|
|
1517
|
+
alt: videoCheck.alt
|
|
1518
|
+
});
|
|
1519
|
+
i++;
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
const audioCheck = detectAudioMarkdown(line);
|
|
1523
|
+
if (audioCheck.isAudio) {
|
|
1524
|
+
if (currentText.trim()) {
|
|
1525
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1526
|
+
currentText = "";
|
|
1527
|
+
}
|
|
1528
|
+
blocks.push({
|
|
1529
|
+
type: "audio",
|
|
1530
|
+
content: trimmedLine,
|
|
1531
|
+
src: audioCheck.src,
|
|
1532
|
+
alt: audioCheck.alt
|
|
1533
|
+
});
|
|
1534
|
+
i++;
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
const matrxFileCheck = detectMatrxFileMarkdown(line);
|
|
1538
|
+
if (matrxFileCheck.isMatrxFile) {
|
|
1539
|
+
if (currentText.trim()) {
|
|
1540
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1541
|
+
currentText = "";
|
|
1542
|
+
}
|
|
1543
|
+
blocks.push({
|
|
1544
|
+
type: "matrx_file",
|
|
1545
|
+
// Keep the original line on content so the DB round-trip re-emits it
|
|
1546
|
+
// (reconstructBlockMarkdown default → content) and reload re-detects.
|
|
1547
|
+
content: trimmedLine,
|
|
1548
|
+
src: matrxFileCheck.url,
|
|
1549
|
+
alt: matrxFileCheck.label,
|
|
1550
|
+
metadata: {
|
|
1551
|
+
pre: matrxFileCheck.pre,
|
|
1552
|
+
post: matrxFileCheck.post,
|
|
1553
|
+
label: matrxFileCheck.label
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
i++;
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
if (detectTableStart(lines, i)) {
|
|
1560
|
+
if (currentText.trim()) {
|
|
1561
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1562
|
+
currentText = "";
|
|
1563
|
+
}
|
|
1564
|
+
const extraction = extractTable(i, lines);
|
|
1565
|
+
if (extraction.metadata?.isValid !== false) {
|
|
1566
|
+
blocks.push({
|
|
1567
|
+
type: "table",
|
|
1568
|
+
content: extraction.content,
|
|
1569
|
+
metadata: extraction.metadata
|
|
1570
|
+
});
|
|
1571
|
+
i = extraction.nextIndex;
|
|
1572
|
+
} else {
|
|
1573
|
+
currentText += processedLine + "\n";
|
|
1574
|
+
i++;
|
|
1575
|
+
}
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
if (/^\[\s*(?:\{|$)/.test(trimmedLine)) {
|
|
1579
|
+
const arrayLines = [processedLine];
|
|
1580
|
+
const first = countStructuralBraces(trimmedLine, "[", "]");
|
|
1581
|
+
let opens = first.opens;
|
|
1582
|
+
let closes = first.closes;
|
|
1583
|
+
let j = i + 1;
|
|
1584
|
+
while (j < lines.length && opens > closes) {
|
|
1585
|
+
const nextLine = normalizeLine(lines[j]);
|
|
1586
|
+
arrayLines.push(nextLine);
|
|
1587
|
+
const counts = countStructuralBraces(nextLine, "[", "]");
|
|
1588
|
+
opens += counts.opens;
|
|
1589
|
+
closes += counts.closes;
|
|
1590
|
+
j++;
|
|
1591
|
+
}
|
|
1592
|
+
const arrayContent = arrayLines.join("\n").trim();
|
|
1593
|
+
let isKindArray = false;
|
|
1594
|
+
if (opens === closes && jsonKindSignal(arrayContent) === "kind") {
|
|
1595
|
+
try {
|
|
1596
|
+
isKindArray = Array.isArray(JSON.parse(arrayContent));
|
|
1597
|
+
} catch {
|
|
1598
|
+
isKindArray = false;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
if (isKindArray) {
|
|
1602
|
+
if (currentText.trim()) {
|
|
1603
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1604
|
+
currentText = "";
|
|
1605
|
+
}
|
|
1606
|
+
blocks.push({ type: "code", content: arrayContent, language: "json" });
|
|
1607
|
+
i = j;
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
if (trimmedLine.startsWith("{")) {
|
|
1612
|
+
const jsonLines = [processedLine];
|
|
1613
|
+
const first = countStructuralBraces(trimmedLine);
|
|
1614
|
+
let openCount = first.opens;
|
|
1615
|
+
let closeCount = first.closes;
|
|
1616
|
+
let j = i + 1;
|
|
1617
|
+
while (j < lines.length && openCount > closeCount) {
|
|
1618
|
+
const nextLine = normalizeLine(lines[j]);
|
|
1619
|
+
jsonLines.push(nextLine);
|
|
1620
|
+
const counts = countStructuralBraces(nextLine);
|
|
1621
|
+
openCount += counts.opens;
|
|
1622
|
+
closeCount += counts.closes;
|
|
1623
|
+
j++;
|
|
1624
|
+
}
|
|
1625
|
+
if (openCount === closeCount && openCount > 0) {
|
|
1626
|
+
const jsonContent = jsonLines.join("\n").trim();
|
|
1627
|
+
let parsedOk = false;
|
|
1628
|
+
try {
|
|
1629
|
+
const parsed = JSON.parse(jsonContent);
|
|
1630
|
+
parsedOk = parsed !== null && typeof parsed === "object";
|
|
1631
|
+
} catch {
|
|
1632
|
+
}
|
|
1633
|
+
if (parsedOk) {
|
|
1634
|
+
if (currentText.trim()) {
|
|
1635
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1636
|
+
currentText = "";
|
|
1637
|
+
}
|
|
1638
|
+
const jsonType = detectJsonBlockType(jsonContent);
|
|
1639
|
+
if (jsonType) {
|
|
1640
|
+
const streamingState = validateJsonBlock(jsonContent, jsonType);
|
|
1641
|
+
blocks.push({
|
|
1642
|
+
type: jsonType,
|
|
1643
|
+
content: jsonContent,
|
|
1644
|
+
language: "json",
|
|
1645
|
+
// A directive block never carries __ir — see the fenced-JSON
|
|
1646
|
+
// site above (adversarial finding A-8).
|
|
1647
|
+
metadata: jsonType === "matrx" ? streamingState.metadata : envelopes.withIrEnvelope(jsonContent, streamingState.metadata)
|
|
1648
|
+
});
|
|
1649
|
+
} else {
|
|
1650
|
+
blocks.push({
|
|
1651
|
+
type: "code",
|
|
1652
|
+
content: jsonContent,
|
|
1653
|
+
language: "json",
|
|
1654
|
+
metadata: envelopes.withIrEnvelope(jsonContent, void 0)
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
i = j;
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
} else if (openCount > closeCount && // Round 11 (H1): a region whose own grammar already broke into prose
|
|
1661
|
+
// (`{\"__kind\":…}`, `{ some code`) was never JSON — the live
|
|
1662
|
+
// accumulator draws it as text (same check), so reload does too. Its
|
|
1663
|
+
// brace count never balancing must not carry the region to the end
|
|
1664
|
+
// of the message, where a LATER real kind made it "a kind region".
|
|
1665
|
+
!bareRegionBreaksIntoProse(jsonLines.join("\n"), { firstBudget: true })) {
|
|
1666
|
+
const partialJson = jsonLines.join("\n").trim();
|
|
1667
|
+
const jsonType = detectJsonBlockType(partialJson);
|
|
1668
|
+
if (jsonType) {
|
|
1669
|
+
if (currentText.trim()) {
|
|
1670
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1671
|
+
currentText = "";
|
|
1672
|
+
}
|
|
1673
|
+
blocks.push({
|
|
1674
|
+
type: jsonType,
|
|
1675
|
+
content: partialJson,
|
|
1676
|
+
language: "json",
|
|
1677
|
+
metadata: { isComplete: false }
|
|
1678
|
+
});
|
|
1679
|
+
i = j;
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
const signal = jsonKindSignal(partialJson);
|
|
1683
|
+
if (signal === "kind" || signal === "undecided" && /^\{\s*(?:"[^"]*)?$/.test(partialJson)) {
|
|
1684
|
+
if (currentText.trim()) {
|
|
1685
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1686
|
+
currentText = "";
|
|
1687
|
+
}
|
|
1688
|
+
blocks.push({
|
|
1689
|
+
type: "code",
|
|
1690
|
+
content: partialJson,
|
|
1691
|
+
language: "json",
|
|
1692
|
+
metadata: { isComplete: false }
|
|
1693
|
+
});
|
|
1694
|
+
i = j;
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
const midLineAttrXml = detectMidLineAttributeXml(processedLine);
|
|
1700
|
+
if (midLineAttrXml) {
|
|
1701
|
+
if (currentText.trim()) {
|
|
1702
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1703
|
+
currentText = "";
|
|
1704
|
+
}
|
|
1705
|
+
const textBefore = processedLine.slice(0, midLineAttrXml.tagStart).trimEnd();
|
|
1706
|
+
const tagAndRest = processedLine.slice(midLineAttrXml.tagStart);
|
|
1707
|
+
const closingTag = `</${midLineAttrXml.type}>`;
|
|
1708
|
+
const rawXmlParts = [tagAndRest];
|
|
1709
|
+
let foundClose = tagAndRest.includes(closingTag);
|
|
1710
|
+
if (!foundClose) {
|
|
1711
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1712
|
+
const jLine = lines[j];
|
|
1713
|
+
const jIdx = jLine.indexOf(closingTag);
|
|
1714
|
+
if (jIdx !== -1) {
|
|
1715
|
+
rawXmlParts.push(jLine.slice(0, jIdx + closingTag.length));
|
|
1716
|
+
foundClose = true;
|
|
1717
|
+
break;
|
|
1718
|
+
}
|
|
1719
|
+
rawXmlParts.push(jLine);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
const rawXmlOverride = rawXmlParts.join("\n");
|
|
1723
|
+
const replacements = [];
|
|
1724
|
+
if (textBefore) replacements.push(textBefore);
|
|
1725
|
+
replacements.push(tagAndRest);
|
|
1726
|
+
pendingRawXmlOverrides.set(tagAndRest.trim(), rawXmlOverride);
|
|
1727
|
+
lines.splice(i, 1, ...replacements);
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
if (isTreeLine(trimmedLine)) {
|
|
1731
|
+
let treeEnd = i + 1;
|
|
1732
|
+
while (treeEnd < lines.length) {
|
|
1733
|
+
const nextTrimmed = normalizeLine(lines[treeEnd]).trim();
|
|
1734
|
+
if (!nextTrimmed) {
|
|
1735
|
+
treeEnd++;
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
if (isTreeLine(nextTrimmed)) {
|
|
1739
|
+
treeEnd++;
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
break;
|
|
1743
|
+
}
|
|
1744
|
+
const treeLinesCount = lines.slice(i, treeEnd).filter((l) => isTreeLine(normalizeLine(l).trim())).length;
|
|
1745
|
+
if (treeLinesCount >= 3) {
|
|
1746
|
+
const treeStart = findTreeBlockStart(lines, i);
|
|
1747
|
+
if (treeStart < i) {
|
|
1748
|
+
currentText = stripAccumulatedLinesFromText(
|
|
1749
|
+
currentText,
|
|
1750
|
+
lines,
|
|
1751
|
+
treeStart,
|
|
1752
|
+
i
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
if (currentText.trim()) {
|
|
1756
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1757
|
+
currentText = "";
|
|
1758
|
+
}
|
|
1759
|
+
const treeContent = lines.slice(treeStart, treeEnd).map((l) => normalizeLine(l)).join("\n");
|
|
1760
|
+
blocks.push({ type: "tree", content: treeContent.trim() });
|
|
1761
|
+
i = treeEnd;
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
currentText += processedLine + (i < lines.length - 1 ? "\n" : "");
|
|
1766
|
+
i++;
|
|
1767
|
+
}
|
|
1768
|
+
if (currentText.trim()) {
|
|
1769
|
+
blocks.push({ type: "text", content: currentText.trimEnd() });
|
|
1770
|
+
}
|
|
1771
|
+
return recoverEmbeddedKindJsonBlocksWith(blocks, envelopes);
|
|
1772
|
+
};
|
|
1773
|
+
export {
|
|
1774
|
+
AUDIO_URL_EXT,
|
|
1775
|
+
CODE_LANGUAGE_ALIASES,
|
|
1776
|
+
NO_SPLITTER_ENVELOPES,
|
|
1777
|
+
SPECIAL_CODE_LANGUAGES,
|
|
1778
|
+
countInlineImages,
|
|
1779
|
+
detectImageMarkdown,
|
|
1780
|
+
detectJsonBlockType,
|
|
1781
|
+
detectMatrxFileMarkdown,
|
|
1782
|
+
detectVideoMarkdown,
|
|
1783
|
+
extractAudioLink,
|
|
1784
|
+
isCompleteUnrecognizedXmlContainer,
|
|
1785
|
+
isUnclosedGenericXmlOpening,
|
|
1786
|
+
normalizeCodeLanguage,
|
|
1787
|
+
parseXmlAttributes,
|
|
1788
|
+
recoverEmbeddedKindJsonBlocksWith,
|
|
1789
|
+
splitContentIntoBlocksWith,
|
|
1790
|
+
startUnrecognizedXmlContainer
|
|
1791
|
+
};
|