openui-telegram 0.0.0-stage → 1.0.1

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.
@@ -0,0 +1,42 @@
1
+ import { WidgetRuntime } from './runtime.js';
2
+ import type { SavedState } from './runtime.js';
3
+ export interface WidgetFrame {
4
+ i: string;
5
+ v: number;
6
+ r: number;
7
+ h: string;
8
+ c: ReturnType<WidgetRuntime['clientRecords']>;
9
+ f: Record<string, unknown>;
10
+ l: Record<string, string>;
11
+ e: Record<string, Record<string, string>>;
12
+ /** CSS dependencies of the rendered components, including render-only components. */
13
+ s?: string[];
14
+ }
15
+ export type WidgetInput = [instance: string, version: number, revision: number, kind: 'a', action: string] | [
16
+ instance: string,
17
+ version: number,
18
+ revision: number,
19
+ kind: 'v',
20
+ field: string,
21
+ value: unknown
22
+ ];
23
+ /** Persist together with the current session source and runtime state. */
24
+ export interface WidgetCheckpoint {
25
+ instanceId: string;
26
+ version: number;
27
+ revision: number;
28
+ initialFrame?: WidgetFrame;
29
+ }
30
+ /** No Bot API, timers, retries or storage. The application routes a message to its controller. */
31
+ export declare function createWidgetController(runtime: WidgetRuntime, options?: {
32
+ initialFrame?: WidgetFrame;
33
+ checkpoint?: WidgetCheckpoint;
34
+ }): {
35
+ runtime: WidgetRuntime;
36
+ frame(): Promise<WidgetFrame>;
37
+ handle(data: string): Promise<boolean>;
38
+ encode(frame: WidgetFrame, base?: WidgetFrame | undefined): string;
39
+ save(): SavedState;
40
+ checkpoint(): WidgetCheckpoint;
41
+ dispose(): void;
42
+ };
@@ -0,0 +1,61 @@
1
+ import { WidgetRuntime } from './runtime.js';
2
+ import { encodeTransport, utf8Bytes, TransportSizeError } from './transport.js';
3
+ import { createWidgetUpdate } from './wire.js';
4
+ /** No Bot API, timers, retries or storage. The application routes a message to its controller. */
5
+ export function createWidgetController(runtime, options = {}) {
6
+ const saved = options.checkpoint;
7
+ if (saved && (options.initialFrame || saved.instanceId !== runtime.instanceId || saved.version !== runtime.version ||
8
+ !Number.isSafeInteger(saved.revision) || saved.revision < -1 || saved.initialFrame && saved.initialFrame.r > saved.revision))
9
+ throw new Error('Checkpoint does not match the runtime or delivery revision');
10
+ const initialFrame = saved?.initialFrame ?? options.initialFrame;
11
+ if (initialFrame && (initialFrame.i !== runtime.instanceId || initialFrame.v > runtime.version))
12
+ throw new Error('Bootstrap belongs to a different interface or newer program');
13
+ let revision = saved?.revision ?? initialFrame?.r ?? -1;
14
+ let busy = false;
15
+ return {
16
+ runtime,
17
+ async frame() {
18
+ const h = await runtime.render();
19
+ const styles = runtime.requiredStyles();
20
+ return { i: runtime.instanceId, v: runtime.version, r: ++revision, h,
21
+ c: runtime.clientRecords(), f: runtime.fieldValues(), e: runtime.formErrors(), l: runtime.nativeLinks,
22
+ ...(styles.length ? { s: styles } : {}) };
23
+ },
24
+ async handle(data) {
25
+ const bytes = utf8Bytes(data);
26
+ if (!bytes || bytes > 64)
27
+ throw new TransportSizeError('request', bytes, 64);
28
+ const input = JSON.parse(data);
29
+ if (!Array.isArray(input) || input[0] !== runtime.instanceId || input[1] !== runtime.version || input[2] !== revision)
30
+ throw new Error('Stale input or wrong interface instance/version');
31
+ if (busy)
32
+ throw new Error('An action is already executing; application controls delivery order');
33
+ busy = true;
34
+ try {
35
+ if (input[3] === 'a' && input.length === 5 && typeof input[4] === 'string')
36
+ return await runtime.click(input[4]);
37
+ else if (input[3] === 'v' && input.length === 6 && typeof input[4] === 'string') {
38
+ runtime.setFieldHandle(input[4], input[5]);
39
+ return true;
40
+ }
41
+ else
42
+ throw new Error('Invalid widget input');
43
+ }
44
+ finally {
45
+ busy = false;
46
+ }
47
+ },
48
+ encode(frame, base = initialFrame) {
49
+ if (!base)
50
+ return encodeTransport(frame, 'response');
51
+ const update = createWidgetUpdate(base, frame);
52
+ const delta = JSON.stringify(update), full = JSON.stringify(frame);
53
+ return encodeTransport(utf8Bytes(delta) < utf8Bytes(full) ? update : frame, 'response');
54
+ },
55
+ save() { return runtime.snapshot(); },
56
+ checkpoint() {
57
+ return structuredClone({ instanceId: runtime.instanceId, version: runtime.version, revision, initialFrame });
58
+ },
59
+ dispose() { runtime.dispose(); },
60
+ };
61
+ }
package/dist/dom.js ADDED
@@ -0,0 +1,72 @@
1
+ /** Reconcile using native nodes. Unchanged nodes keep listeners, canvas contexts and local state. */
2
+ const rendered = new WeakMap();
3
+ const remember = (node, source) => {
4
+ rendered.set(node, source.cloneNode(true));
5
+ for (const [index, child] of Array.from(node.childNodes).entries())
6
+ remember(child, source.childNodes[index]);
7
+ };
8
+ /** Discard lifecycle-owned DOM before this boundary is reused by another instance. */
9
+ export function restoreDOM(element) {
10
+ const source = rendered.get(element);
11
+ const replacement = source.cloneNode(true);
12
+ remember(replacement, source);
13
+ element.replaceWith(replacement);
14
+ }
15
+ export function reconcileDOM(root, html) {
16
+ const desired = root.ownerDocument.createElement('div');
17
+ desired.innerHTML = html;
18
+ const key = (node) => node.nodeType === 1 ?
19
+ node.getAttribute('data-oui-key') ?? node.getAttribute('data-oui-component') ??
20
+ node.getAttribute('data-oui-node') ?? node.getAttribute('id') : null;
21
+ const compatible = (a, b) => a.nodeType === b.nodeType &&
22
+ (a.nodeType !== 1 || a.tagName === b.tagName && a.namespaceURI === b.namespaceURI);
23
+ const morph = (a, b) => {
24
+ const previous = rendered.get(a);
25
+ if (previous?.isEqualNode(b))
26
+ return;
27
+ if (a.nodeType !== 1) {
28
+ if (a.nodeValue !== b.nodeValue)
29
+ a.nodeValue = b.nodeValue;
30
+ return;
31
+ }
32
+ const target = a, source = b;
33
+ const before = previous;
34
+ for (const attr of Array.from(before?.attributes ?? target.attributes))
35
+ if (!source.hasAttribute(attr.name))
36
+ target.removeAttribute(attr.name);
37
+ for (const attr of Array.from(source.attributes))
38
+ if (!before || before.getAttribute(attr.name) !== attr.value) {
39
+ if (attr.namespaceURI)
40
+ target.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
41
+ else
42
+ target.setAttribute(attr.name, attr.value);
43
+ }
44
+ children(target, source);
45
+ };
46
+ const children = (target, source) => {
47
+ const old = Array.from(target.childNodes).filter(node => !rendered.has(target) || rendered.has(node)), used = new Set();
48
+ const keyed = new Map(old.filter(n => key(n) !== null).map(n => [key(n), n]));
49
+ for (const [index, desiredNode] of Array.from(source.childNodes).entries()) {
50
+ const nodeKey = key(desiredNode);
51
+ let node = nodeKey !== null ? keyed.get(nodeKey) : old.find(n => !used.has(n) && key(n) === null && compatible(n, desiredNode));
52
+ if (node && (used.has(node) || !compatible(node, desiredNode)))
53
+ node = undefined;
54
+ if (!node) {
55
+ node = desiredNode.cloneNode(true);
56
+ remember(node, desiredNode);
57
+ }
58
+ else
59
+ morph(node, desiredNode);
60
+ rendered.set(node, desiredNode.cloneNode(true));
61
+ used.add(node);
62
+ const at = target.childNodes[index] ?? null;
63
+ if (at !== node)
64
+ target.insertBefore(node, at);
65
+ }
66
+ for (const node of old)
67
+ if (!used.has(node))
68
+ target.removeChild(node);
69
+ rendered.set(target, source.cloneNode(true));
70
+ };
71
+ children(root, desired);
72
+ }
@@ -0,0 +1,19 @@
1
+ import type { Plugin } from 'esbuild';
2
+ import type { Library, ParseResult } from '@openuidev/lang-core';
3
+ import type { WidgetComponent } from './runtime.js';
4
+ export interface WidgetClientFeatures {
5
+ specialized: boolean;
6
+ /** Bundled lifecycle implementations; render-only components run in the application. */
7
+ components: string[];
8
+ /** Component styles retained for the initial program and explicitly reserved patches. */
9
+ styles: string[];
10
+ lifecycle: boolean;
11
+ fields: boolean;
12
+ forms: boolean;
13
+ mount: boolean;
14
+ events: boolean;
15
+ text: boolean;
16
+ }
17
+ export declare function widgetClientFeatures(parsed: ParseResult, library: Library<WidgetComponent>, initialTypes: string[], extra?: string[], specialized?: boolean): WidgetClientFeatures;
18
+ /** Only widget bundles are transformed; application implementations remain intact. */
19
+ export declare function hostClientPlugin(features: WidgetClientFeatures, entry?: string): Plugin;
@@ -0,0 +1,183 @@
1
+ import { parse } from 'acorn';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { resolve, extname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { transform } from 'esbuild';
6
+ /** Unknown callback shapes retain the full context, including aliases/rest/arguments. */
7
+ function contextServices(callback) {
8
+ const full = { fields: true, forms: true, events: true, text: true }, empty = { fields: false, forms: false, events: false, text: false };
9
+ if (!callback)
10
+ return empty;
11
+ let fn;
12
+ const source = callback.toString();
13
+ if (/\barguments\b/.test(source))
14
+ return full;
15
+ try {
16
+ fn = parse(`(${source})`, { ecmaVersion: 'latest' }).body[0].expression;
17
+ }
18
+ catch {
19
+ try {
20
+ fn = parse(`({${source}})`, { ecmaVersion: 'latest' }).body[0].expression.properties[0].value;
21
+ }
22
+ catch {
23
+ return full;
24
+ }
25
+ }
26
+ const parameter = fn.params[0];
27
+ if (!parameter)
28
+ return empty;
29
+ if (parameter.type !== 'ObjectPattern' || parameter.properties.some((p) => p.type === 'RestElement'))
30
+ return full;
31
+ const keys = parameter.properties.map((p) => p.computed ? null : p.key.name ?? p.key.value);
32
+ return keys.includes(null) ? full : { fields: keys.includes('field'), forms: keys.includes('form'), events: keys.includes('on'), text: keys.includes('text') };
33
+ }
34
+ export function widgetClientFeatures(parsed, library, initialTypes, extra = [], specialized = true) {
35
+ const names = new Set([...initialTypes, ...extra]);
36
+ for (const name of extra)
37
+ if (!Object.hasOwn(library.components, name))
38
+ throw new TypeError(`Unknown client component ${name}`);
39
+ const schema = library.toJSONSchema();
40
+ let opaque = false;
41
+ const visit = (value) => {
42
+ if (!value || typeof value !== 'object')
43
+ return;
44
+ if (value.type === 'element')
45
+ names.add(value.typeName);
46
+ if (value.k === 'Comp') {
47
+ if (Object.hasOwn(library.components, value.name))
48
+ names.add(value.name);
49
+ // A custom function returning arbitrary data may manufacture elements.
50
+ const returns = schema.functions?.[value.name]?.returns;
51
+ if (Object.hasOwn(library.functions ?? {}, value.name) && !['string', 'number', 'boolean', 'null', 'integer'].includes(returns?.type))
52
+ opaque = true;
53
+ }
54
+ for (const child of Object.values(value))
55
+ if (Array.isArray(child))
56
+ child.forEach(visit);
57
+ else
58
+ visit(child);
59
+ };
60
+ visit(parsed);
61
+ if (['StateRef', 'RuntimeRef'].includes(parsed.rootExpression?.k ?? ''))
62
+ opaque = true;
63
+ if (!specialized || opaque)
64
+ Object.keys(library.components).forEach(name => names.add(name));
65
+ const components = [...names].filter(name => {
66
+ const component = library.components[name]?.component;
67
+ return component?.mount || component?.update;
68
+ }).sort();
69
+ let fields = !specialized, forms = !specialized, events = !specialized, text = !specialized;
70
+ for (const name of components)
71
+ for (const callback of [library.components[name].component.mount, library.components[name].component.update]) {
72
+ const services = contextServices(callback);
73
+ fields ||= services.fields;
74
+ forms ||= services.forms;
75
+ events ||= services.events;
76
+ text ||= services.text;
77
+ }
78
+ const styles = [...names].filter(name => library.components[name]?.component?.css).sort();
79
+ return { specialized, components, styles, lifecycle: !specialized || components.length > 0, fields, forms, events, text,
80
+ mount: !specialized || components.some(name => !!library.components[name].component.mount) };
81
+ }
82
+ const pure = (node) => !!node && (['Literal', 'FunctionExpression', 'ArrowFunctionExpression'].includes(node.type) ||
83
+ node.type === 'ArrayExpression' && node.elements.every((n) => !n || pure(n)) ||
84
+ node.type === 'ObjectExpression' && node.properties.every((p) => p.type === 'Property' && !p.computed && p.kind === 'init' && pure(p.value)));
85
+ const usesReceiver = (node) => !!node && typeof node === 'object' &&
86
+ (node.type === 'ThisExpression' || Object.values(node).some(value => Array.isArray(value) ? value.some(usesReceiver) : usesReceiver(value)));
87
+ /** Only widget bundles are transformed; application implementations remain intact. */
88
+ export function hostClientPlugin(features, entry) {
89
+ return { name: 'widget-client-specialization', setup(build) {
90
+ build.onLoad({ filter: /[\\/]client\.js$/ }, async ({ path }) => {
91
+ if (resolve(path) !== resolve(fileURLToPath(new URL('./client.js', import.meta.url))))
92
+ return;
93
+ const source = await readFile(path, 'utf8');
94
+ const ast = parse(source, { ecmaVersion: 'latest', sourceType: 'module' });
95
+ const values = { CLIENT_LIFECYCLE: features.lifecycle, CLIENT_FIELDS: features.fields, CLIENT_FORMS: features.forms,
96
+ CLIENT_MOUNT: features.mount, CLIENT_CLEANUPS: features.mount || features.events, CLIENT_EVENTS: features.events, CLIENT_TEXT: features.text };
97
+ const edits = [];
98
+ const visit = (node) => {
99
+ if (!node || typeof node !== 'object')
100
+ return;
101
+ if (node.type === 'ImportDeclaration' && node.source.value === './client-features.js') {
102
+ edits.push({ start: node.start, end: node.end, text: '' });
103
+ return;
104
+ }
105
+ if (node.type === 'Identifier' && Object.hasOwn(values, node.name)) {
106
+ edits.push({ start: node.start, end: node.end, text: String(values[node.name]) });
107
+ return;
108
+ }
109
+ for (const value of Object.values(node))
110
+ if (Array.isArray(value))
111
+ value.forEach(visit);
112
+ else if (value && typeof value === 'object')
113
+ visit(value);
114
+ };
115
+ visit(ast);
116
+ let contents = source;
117
+ for (const edit of edits.sort((a, b) => b.start - a.start))
118
+ contents = contents.slice(0, edit.start) + edit.text + contents.slice(edit.end);
119
+ return { contents, loader: 'js' };
120
+ });
121
+ if (!features.specialized || !entry)
122
+ return;
123
+ build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async ({ path }) => {
124
+ if (resolve(path) !== resolve(entry))
125
+ return;
126
+ const original = await readFile(path, 'utf8');
127
+ const extension = extname(path);
128
+ const loader = extension === '.tsx' ? 'tsx' : extension === '.jsx' ? 'jsx' : /\.[cm]?ts$/.test(extension) ? 'ts' : 'js';
129
+ const source = (await transform(original, { loader, target: 'es2023' })).code;
130
+ const ast = parse(source, { ecmaVersion: 'latest', sourceType: 'module' });
131
+ // The registry may be used as an internal dependency (including computed
132
+ // lookups). Keep that module intact rather than break a callback/helper.
133
+ let registryUsed = false;
134
+ const references = (node, parent) => {
135
+ if (!node || typeof node !== 'object')
136
+ return;
137
+ if (node.type === 'Identifier' && node.name === 'components' &&
138
+ !(parent?.type === 'VariableDeclarator' && parent.id === node) && parent?.type !== 'ExportSpecifier' &&
139
+ !(parent?.type === 'Property' && parent.key === node && !parent.computed && !parent.shorthand))
140
+ registryUsed = true;
141
+ for (const value of Object.values(node))
142
+ if (Array.isArray(value))
143
+ value.forEach(child => references(child, node));
144
+ else if (value && typeof value === 'object')
145
+ references(value, node);
146
+ };
147
+ references(ast);
148
+ if (registryUsed)
149
+ return { contents: source, loader: 'js' };
150
+ const edits = [];
151
+ for (const top of ast.body) {
152
+ const declaration = top.declaration ?? top;
153
+ if (declaration.type !== 'VariableDeclaration' || declaration.kind !== 'const')
154
+ continue;
155
+ for (const item of declaration.declarations) {
156
+ if (item.id.name !== 'components' || item.init?.type !== 'ObjectExpression')
157
+ continue;
158
+ const properties = item.init.properties.filter((property) => {
159
+ if (property.type !== 'Property' || property.computed || property.kind !== 'init')
160
+ return true;
161
+ const name = property.key.name ?? property.key.value;
162
+ return features.components.includes(name) || !pure(property.value);
163
+ }).map((property) => {
164
+ if (property.type !== 'Property' || property.value.type !== 'ObjectExpression')
165
+ return source.slice(property.start, property.end);
166
+ if (property.value.properties.some((p) => p.type === 'Property' &&
167
+ ['mount', 'update'].includes(p.key.name ?? p.key.value) && usesReceiver(p.value)))
168
+ return source.slice(property.start, property.end);
169
+ const body = property.value.properties.filter((p) => p.type !== 'Property' || p.computed ||
170
+ !['render', 'css', 'reactive'].includes(p.key.name ?? p.key.value) || !pure(p.value));
171
+ return source.slice(property.start, property.value.start) + '{' + body.map((p) => source.slice(p.start, p.end)).join(',') + '}';
172
+ });
173
+ edits.push({ start: item.init.start, end: item.init.end, text: '{' + properties.join(',') + '}' });
174
+ }
175
+ }
176
+ let contents = source;
177
+ // A module has at most one top-level const binding named components.
178
+ for (const edit of edits)
179
+ contents = contents.slice(0, edit.start) + edit.text + contents.slice(edit.end);
180
+ return { contents, loader: 'js' };
181
+ });
182
+ } };
183
+ }
@@ -0,0 +1,15 @@
1
+ export { compileWidget } from './compiler.js';
2
+ export type { CompileOptions } from './compiler.js';
3
+ export type { WidgetClientFeatures } from './host-client-bundle.js';
4
+ export { createInterface, defineWidgetComponent, WidgetSession } from './session.js';
5
+ export { WidgetRuntime, runtimeLibrary, text } from './runtime.js';
6
+ export type { Diagnostic, Operation, OperationReply, SavedState, ComponentContext, ClientContext, WidgetComponent, RuntimeLibrary, RuntimeOptions, FieldDescriptor } from './runtime.js';
7
+ export { TELEGRAM_PROFILE, TransportSizeError, encodeTransport, utf8Bytes } from './transport.js';
8
+ export type { StateUpdate, TelegramEnvironment } from './transport.js';
9
+ export { createWidgetController } from './controller.js';
10
+ export type { WidgetFrame, WidgetInput, WidgetCheckpoint } from './controller.js';
11
+ export { responseParts } from './message.js';
12
+ export type { ResponsePart } from './message.js';
13
+ export { createWidgetUpdate } from './wire.js';
14
+ export { applyWidgetUpdate } from './wire-apply.js';
15
+ export type { WidgetUpdate, FrameChange } from './wire-apply.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { compileWidget } from './compiler.js';
2
+ export { createInterface, defineWidgetComponent, WidgetSession } from './session.js';
3
+ export { WidgetRuntime, runtimeLibrary, text } from './runtime.js';
4
+ export { TELEGRAM_PROFILE, TransportSizeError, encodeTransport, utf8Bytes } from './transport.js';
5
+ export { createWidgetController } from './controller.js';
6
+ export { responseParts } from './message.js';
7
+ export { createWidgetUpdate } from './wire.js';
8
+ export { applyWidgetUpdate } from './wire-apply.js';
@@ -0,0 +1,39 @@
1
+ /** Emit data literals, never executable input strings. Unlike JSON syntax used as
2
+ * JavaScript, computed __proto__ keys preserve own properties without changing prototypes. */
3
+ export function javascriptData(value, ancestors = new Set()) {
4
+ if (value === null)
5
+ return 'null';
6
+ if (value === undefined)
7
+ return 'void 0';
8
+ if (typeof value === 'string')
9
+ return JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
10
+ if (typeof value === 'boolean')
11
+ return String(value);
12
+ if (typeof value === 'number')
13
+ return Object.is(value, -0) ? '-0' : String(value);
14
+ if (typeof value !== 'object' || ancestors.has(value))
15
+ throw new TypeError('Bootstrap data must be acyclic data values');
16
+ if (Object.getOwnPropertySymbols(value).length)
17
+ throw new TypeError('Symbol properties cannot be embedded');
18
+ ancestors.add(value);
19
+ try {
20
+ if (Array.isArray(value)) {
21
+ const elements = Array.from({ length: value.length }, (_, i) => Object.hasOwn(value, i) ? javascriptData(value[i], ancestors) : '');
22
+ return '[' + elements.join(',') + (value.length && !Object.hasOwn(value, value.length - 1) ? ',' : '') + ']';
23
+ }
24
+ const prototype = Object.getPrototypeOf(value);
25
+ if (prototype !== null && prototype !== Object.prototype)
26
+ throw new TypeError('Bootstrap objects must be plain data');
27
+ const properties = Object.entries(Object.getOwnPropertyDescriptors(value)).map(([key, descriptor]) => {
28
+ if (!('value' in descriptor))
29
+ throw new TypeError('Accessor properties cannot be embedded');
30
+ const name = key === '__proto__' ? `[${javascriptData(key)}]` : javascriptData(key);
31
+ return `${name}:${javascriptData(descriptor.value, ancestors)}`;
32
+ });
33
+ const object = '{' + properties.join(',') + '}';
34
+ return prototype === null ? `Object.assign(Object.create(null),${object})` : object;
35
+ }
36
+ finally {
37
+ ancestors.delete(value);
38
+ }
39
+ }
@@ -0,0 +1,10 @@
1
+ export interface ResponsePart {
2
+ kind: 'text' | 'code';
3
+ language?: string;
4
+ content: string;
5
+ raw: string;
6
+ complete: boolean;
7
+ }
8
+ /** Preserve prose and code separately. Language parsing still goes through upstream,
9
+ * using the original content, not a reconstruction of these presentation segments. */
10
+ export declare function responseParts(content: string): ResponsePart[];
@@ -0,0 +1,44 @@
1
+ /** Preserve prose and code separately. Language parsing still goes through upstream,
2
+ * using the original content, not a reconstruction of these presentation segments. */
3
+ export function responseParts(content) {
4
+ const parts = [];
5
+ let raw = '', body = '', language, fenced = false, quoted = false;
6
+ const flush = (complete) => {
7
+ if (raw)
8
+ parts.push({ kind: fenced ? 'code' : 'text', language, content: fenced ? body : raw, raw, complete });
9
+ raw = '';
10
+ body = '';
11
+ };
12
+ for (const line of content.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
13
+ const fence = !quoted && /^[ \t]*```[ \t]*([\w.+-]*)[ \t]*(.*?)[ \t]*\r?\n?$/.exec(line);
14
+ if (fence) {
15
+ if (fenced) {
16
+ raw += line;
17
+ flush(true);
18
+ fenced = false;
19
+ language = undefined;
20
+ }
21
+ else {
22
+ flush(true);
23
+ fenced = true;
24
+ language = fence[1];
25
+ raw = line;
26
+ body = fence[2] ? fence[2] + '\n' : '';
27
+ }
28
+ }
29
+ else {
30
+ raw += line;
31
+ if (fenced) {
32
+ body += line;
33
+ for (let i = 0; i < line.length; i++) {
34
+ if (line[i] === '\\' && quoted)
35
+ i++;
36
+ else if (line[i] === '"')
37
+ quoted = !quoted;
38
+ }
39
+ }
40
+ }
41
+ }
42
+ flush(!fenced);
43
+ return parts;
44
+ }