@segnavia/markdown-editor 0.0.0-stage → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ import { isDiagramFence, isGithubAlertBlock } from "@segnavia/html";
2
+ import { Node } from "@tiptap/core";
3
+ //#region src/markdown-rich-locked.ts
4
+ const LOCKED_BLOCK_NAME = "lockedBlock";
5
+ function lockedFenceLabel(lockedFences, language) {
6
+ return lockedFences[language?.toLowerCase() ?? ""];
7
+ }
8
+ function matchFrontmatter(src) {
9
+ return /^---[ \t]*\n[\s\S]*?\n---[ \t]*(?:\n|$)/.exec(src)?.[0];
10
+ }
11
+ function matchDirectiveContainer(src) {
12
+ const firstNl = src.indexOf("\n");
13
+ if (firstNl < 0) return void 0;
14
+ const open = /^(:{3,})[^\n]*$/.exec(src.slice(0, firstNl));
15
+ if (!open) return void 0;
16
+ const fenceLen = open[1]?.length ?? 0;
17
+ let pos = firstNl + 1;
18
+ while (pos <= src.length) {
19
+ const nextNl = src.indexOf("\n", pos);
20
+ const lineEnd = nextNl < 0 ? src.length : nextNl;
21
+ if ((/^(:{3,})[ \t]*$/.exec(src.slice(pos, lineEnd))?.[1]?.length ?? 0) >= fenceLen) return src.slice(0, nextNl < 0 ? src.length : nextNl + 1);
22
+ if (nextNl < 0) return void 0;
23
+ pos = nextNl + 1;
24
+ }
25
+ }
26
+ function isHtmlComment(raw) {
27
+ return /^<!--[\s\S]*-->$/.test(raw.replace(/\n+$/, ""));
28
+ }
29
+ function isLockedToken(token, lockedFences = {}) {
30
+ if (token.type === "frontmatter" || token.type === "directive") return true;
31
+ if (token.type === "code" && (isDiagramFence(token.lang) || Boolean(lockedFenceLabel(lockedFences, token.lang)))) return true;
32
+ if (token.type === "blockquote" && isGithubAlertBlock(token.raw)) return true;
33
+ return token.type === "html" && !isHtmlComment(token.raw);
34
+ }
35
+ function isSilentToken(token) {
36
+ return token.type === "def" || token.type === "html" && isHtmlComment(token.raw);
37
+ }
38
+ function lockedNode(source, label) {
39
+ return {
40
+ type: LOCKED_BLOCK_NAME,
41
+ attrs: {
42
+ source,
43
+ label: label ?? null,
44
+ dir: "auto"
45
+ }
46
+ };
47
+ }
48
+ const LockedBlock = Node.create({
49
+ name: LOCKED_BLOCK_NAME,
50
+ group: "block",
51
+ atom: true,
52
+ selectable: true,
53
+ draggable: false,
54
+ addAttributes() {
55
+ return {
56
+ source: { default: "" },
57
+ label: { default: null },
58
+ dir: { default: "auto" }
59
+ };
60
+ },
61
+ parseHTML() {
62
+ return [{ tag: "div[data-locked-block]" }];
63
+ },
64
+ renderHTML({ HTMLAttributes }) {
65
+ return ["div", {
66
+ ...HTMLAttributes,
67
+ "data-locked-block": "",
68
+ dir: "auto"
69
+ }];
70
+ },
71
+ renderMarkdown(node) {
72
+ return typeof node.attrs?.source === "string" ? node.attrs.source : "";
73
+ },
74
+ markdownTokenizer: {
75
+ name: "lockedFence",
76
+ level: "block",
77
+ start(src) {
78
+ return src.startsWith("---") || src.startsWith(":::") ? 0 : -1;
79
+ },
80
+ tokenize(src, tokens) {
81
+ if (tokens.length === 0) {
82
+ const frontmatter = matchFrontmatter(src);
83
+ if (frontmatter) return {
84
+ type: "frontmatter",
85
+ raw: frontmatter
86
+ };
87
+ }
88
+ const directive = matchDirectiveContainer(src);
89
+ return directive ? {
90
+ type: "directive",
91
+ raw: directive
92
+ } : void 0;
93
+ }
94
+ }
95
+ });
96
+ //#endregion
97
+ export { LOCKED_BLOCK_NAME, LockedBlock, isLockedToken, isSilentToken, lockedFenceLabel, lockedNode };
@@ -0,0 +1,182 @@
1
+ import { isLockedToken, isSilentToken, lockedFenceLabel, lockedNode } from "./markdown-rich-locked.js";
2
+ import { richMarkdownManager, richSchema } from "./markdown-rich-extensions.js";
3
+ import { Node } from "@tiptap/pm/model";
4
+ //#region src/markdown-rich-save.ts
5
+ let cachedOriginal;
6
+ const objectKeys = /* @__PURE__ */ new WeakMap();
7
+ function isRecord(value) {
8
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+ function keptAttrs(attrs) {
11
+ const kept = Object.fromEntries(Object.entries(attrs).filter(([, value]) => value !== null && value !== void 0 && (!Array.isArray(value) || value.length > 0)));
12
+ return Object.keys(kept).length ? kept : void 0;
13
+ }
14
+ function canonical(node) {
15
+ const next = { type: node.type };
16
+ if (typeof node.text === "string") next.text = node.text;
17
+ const attrs = node.attrs && isRecord(node.attrs) ? keptAttrs(node.attrs) : void 0;
18
+ if (attrs) next.attrs = attrs;
19
+ if (node.marks?.length) next.marks = node.marks.map((mark) => canonical(mark));
20
+ if (node.content?.length) next.content = node.content.map(canonical);
21
+ return next;
22
+ }
23
+ function asJson(node) {
24
+ return node instanceof Node ? node.toJSON() : node;
25
+ }
26
+ function nodeKey(node) {
27
+ const cached = objectKeys.get(node);
28
+ if (cached !== void 0) return cached;
29
+ const key = JSON.stringify(canonical(asJson(node)));
30
+ objectKeys.set(node, key);
31
+ return key;
32
+ }
33
+ const isEmptyParagraph = (node) => {
34
+ const json = asJson(node);
35
+ return json.type === "paragraph" && !json.content?.length && !json.text;
36
+ };
37
+ const dropTrailingEmptyParagraph = (nodes) => {
38
+ const last = nodes.at(-1);
39
+ return last && isEmptyParagraph(last) ? nodes.slice(0, -1) : nodes;
40
+ };
41
+ const restoreAmpersands = (markdown) => markdown.replace(/&amp;(?![a-zA-Z][a-zA-Z0-9]*;)/g, "&");
42
+ function parseBlock(token, raw, lockedFences) {
43
+ if (isSilentToken(token)) return [];
44
+ if (isLockedToken(token, lockedFences)) return [lockedNode(raw, lockedFenceLabel(lockedFences, token.lang))];
45
+ const parsed = richMarkdownManager().parse(raw);
46
+ return Node.fromJSON(richSchema(), parsed).toJSON().content ?? [];
47
+ }
48
+ function blocks(markdown, lockedFences = {}) {
49
+ const out = [];
50
+ const tokens = richMarkdownManager().instance.lexer(markdown);
51
+ let at = 0;
52
+ let prevEnd = 0;
53
+ for (const token of tokens) {
54
+ const start = at;
55
+ at += token.raw.length;
56
+ if (token.type === "space") continue;
57
+ const raw = token.raw.replace(/\n+$/, "");
58
+ const nodes = parseBlock(token, raw, lockedFences);
59
+ out.push({
60
+ raw,
61
+ sep: markdown.slice(prevEnd, start),
62
+ nodes,
63
+ keys: nodes.map(nodeKey)
64
+ });
65
+ prevEnd = start + raw.length;
66
+ }
67
+ return out;
68
+ }
69
+ function fencesKey(lockedFences) {
70
+ return JSON.stringify(Object.entries(lockedFences ?? {}).sort(([left], [right]) => left.localeCompare(right)));
71
+ }
72
+ function originalEntry(original, options) {
73
+ const nextFencesKey = fencesKey(options.lockedFences);
74
+ if (cachedOriginal?.original === original && cachedOriginal.fencesKey === nextFencesKey && cachedOriginal.bodyTransform === options.bodyTransform) return cachedOriginal;
75
+ const split = options.bodyTransform?.split(original);
76
+ const editableBlocks = blocks(split?.editable ?? original, options.lockedFences);
77
+ const nodes = editableBlocks.flatMap((block) => block.nodes);
78
+ const keys = editableBlocks.flatMap((block) => block.keys);
79
+ const indicesByFirstKey = /* @__PURE__ */ new Map();
80
+ for (const [index, block] of editableBlocks.entries()) {
81
+ const first = block.keys[0];
82
+ if (first === void 0) continue;
83
+ const indices = indicesByFirstKey.get(first) ?? [];
84
+ indices.push(index);
85
+ indicesByFirstKey.set(first, indices);
86
+ }
87
+ cachedOriginal = {
88
+ original,
89
+ fencesKey: nextFencesKey,
90
+ bodyTransform: options.bodyTransform,
91
+ split,
92
+ blocks: editableBlocks,
93
+ nodes,
94
+ keys,
95
+ indicesByFirstKey
96
+ };
97
+ return cachedOriginal;
98
+ }
99
+ function firstAtOrAfter(indices, minimum) {
100
+ let low = 0;
101
+ let high = indices.length;
102
+ while (low < high) {
103
+ const middle = low + high >>> 1;
104
+ if ((indices[middle] ?? Number.POSITIVE_INFINITY) < minimum) low = middle + 1;
105
+ else high = middle;
106
+ }
107
+ return indices[low] ?? -1;
108
+ }
109
+ function serializeNodes(nodes) {
110
+ return restoreAmpersands(richMarkdownManager().serialize({
111
+ type: "doc",
112
+ content: nodes.map(asJson)
113
+ }).trim());
114
+ }
115
+ function matchingBlockIndex(entry, keys, keyIndex, blockIndex) {
116
+ const currentKey = keys[keyIndex];
117
+ if (currentKey === void 0) return -1;
118
+ const candidates = entry.indicesByFirstKey.get(currentKey) ?? [];
119
+ let hit = firstAtOrAfter(candidates, blockIndex);
120
+ let block = entry.blocks[hit];
121
+ while (hit >= 0 && !block?.keys.every((key, offset) => keys[keyIndex + offset] === key)) {
122
+ hit = firstAtOrAfter(candidates, hit + 1);
123
+ block = entry.blocks[hit];
124
+ }
125
+ return hit;
126
+ }
127
+ function stitch(entry, edited, keys) {
128
+ const bs = entry.blocks;
129
+ let out = "";
130
+ let last = -1;
131
+ let blockIndex = 0;
132
+ let keyIndex = 0;
133
+ const pending = [];
134
+ const emitNew = () => {
135
+ if (!pending.length) return;
136
+ out += (out ? "\n\n" : bs[blockIndex]?.sep ?? "") + serializeNodes(pending.splice(0));
137
+ last = -1;
138
+ };
139
+ const emitOld = (hit) => {
140
+ const block = bs[hit];
141
+ if (!block) throw new Error(`Cannot stitch missing rich Markdown block ${hit}.`);
142
+ out += out ? last === hit - 1 ? block.sep : "\n\n" : block.sep;
143
+ out += block.raw;
144
+ last = hit;
145
+ };
146
+ const emitReplacement = (end) => {
147
+ let firstEditable = blockIndex;
148
+ while (firstEditable < end && bs[firstEditable]?.nodes.length === 0) {
149
+ emitOld(firstEditable);
150
+ firstEditable++;
151
+ }
152
+ emitNew();
153
+ for (let index = firstEditable; index < end; index++) if (bs[index]?.nodes.length === 0) emitOld(index);
154
+ };
155
+ while (keyIndex < keys.length) {
156
+ const hit = matchingBlockIndex(entry, keys, keyIndex, blockIndex);
157
+ if (hit < 0) {
158
+ const node = edited[keyIndex++];
159
+ if (node) pending.push(node);
160
+ continue;
161
+ }
162
+ emitReplacement(hit);
163
+ emitOld(hit);
164
+ keyIndex += bs[hit]?.nodes.length ?? 0;
165
+ blockIndex = hit + 1;
166
+ }
167
+ emitReplacement(bs.length);
168
+ return `${out.replace(/\n+$/, "")}\n`;
169
+ }
170
+ function parseRichDocument(markdown, options = {}) {
171
+ return dropTrailingEmptyParagraph(originalEntry(markdown, options).nodes);
172
+ }
173
+ function saveRichDocument(original, edited, options = {}) {
174
+ const prepared = dropTrailingEmptyParagraph(edited);
175
+ const entry = originalEntry(original, options);
176
+ const keys = prepared.map(nodeKey);
177
+ if (entry.keys.length === keys.length && entry.keys.every((key, index) => key === keys[index])) return original;
178
+ const saved = stitch(entry, prepared, keys);
179
+ return entry.split && options.bodyTransform ? options.bodyTransform.join(saved, entry.split.retained) : saved;
180
+ }
181
+ //#endregion
182
+ export { parseRichDocument, saveRichDocument };
@@ -0,0 +1,119 @@
1
+ import { MARKDOWN_EDITOR_KEYS } from "./markdown-editor-policy.js";
2
+ import { defaultKeymap, history, historyKeymap, redoDepth, undoDepth } from "@codemirror/commands";
3
+ import { Decoration, EditorView, keymap } from "@codemirror/view";
4
+ import { markdown } from "@codemirror/lang-markdown";
5
+ import { HighlightStyle, LanguageDescription, syntaxHighlighting } from "@codemirror/language";
6
+ import { languages } from "@codemirror/language-data";
7
+ import { EditorState, Prec } from "@codemirror/state";
8
+ import { Tag, styleTags, tags } from "@lezer/highlight";
9
+ import { GFM } from "@lezer/markdown";
10
+ //#region src/markdown-source.ts
11
+ const mermaidLanguage = LanguageDescription.of({
12
+ name: "Mermaid",
13
+ alias: ["mermaid"],
14
+ load: () => import("codemirror-lang-mermaid").then((module) => module.mermaid())
15
+ });
16
+ function codeLanguage(info) {
17
+ if (info.toLowerCase() === "mermaid") return mermaidLanguage;
18
+ return LanguageDescription.matchLanguageName(languages, info, true);
19
+ }
20
+ const inlineCode = Tag.define(tags.monospace);
21
+ const autoDirectionLine = Decoration.line({ attributes: { dir: "auto" } });
22
+ const lineDirections = EditorView.decorations.of((view) => {
23
+ const ranges = [];
24
+ let previous = -1;
25
+ for (const visible of view.visibleRanges) for (let position = visible.from; position <= visible.to;) {
26
+ const line = view.state.doc.lineAt(position);
27
+ if (line.from !== previous) ranges.push(autoDirectionLine.range(line.from));
28
+ previous = line.from;
29
+ position = line.to + 1;
30
+ }
31
+ return Decoration.set(ranges);
32
+ });
33
+ const highlight = HighlightStyle.define([
34
+ {
35
+ tag: tags.comment,
36
+ color: "var(--segnavia-markdown-comment,inherit)"
37
+ },
38
+ {
39
+ tag: [
40
+ tags.keyword,
41
+ tags.processingInstruction,
42
+ tags.meta
43
+ ],
44
+ color: "var(--segnavia-markdown-keyword,inherit)"
45
+ },
46
+ {
47
+ tag: [tags.string, tags.regexp],
48
+ color: "var(--segnavia-markdown-string,inherit)"
49
+ },
50
+ {
51
+ tag: [
52
+ tags.number,
53
+ tags.bool,
54
+ tags.atom
55
+ ],
56
+ color: "var(--segnavia-markdown-literal,inherit)"
57
+ },
58
+ {
59
+ tag: [tags.heading, tags.strong],
60
+ color: "var(--segnavia-markdown-heading,inherit)"
61
+ },
62
+ {
63
+ tag: [tags.link, tags.url],
64
+ color: "var(--segnavia-markdown-link,inherit)"
65
+ },
66
+ {
67
+ tag: [inlineCode, tags.monospace],
68
+ color: "var(--segnavia-markdown-code,inherit)"
69
+ },
70
+ {
71
+ tag: [
72
+ tags.punctuation,
73
+ tags.bracket,
74
+ tags.labelName,
75
+ tags.contentSeparator
76
+ ],
77
+ color: "var(--segnavia-markdown-mark,inherit)"
78
+ },
79
+ {
80
+ tag: tags.invalid,
81
+ color: "var(--segnavia-markdown-invalid,inherit)"
82
+ }
83
+ ]);
84
+ function markdownSourceState(doc, callbacks) {
85
+ const actions = {
86
+ save: callbacks.save,
87
+ "toggle-mode": callbacks.toggleMode
88
+ };
89
+ return EditorState.create({
90
+ doc,
91
+ extensions: [
92
+ history(),
93
+ Prec.highest(keymap.of(MARKDOWN_EDITOR_KEYS.map((binding) => ({
94
+ key: binding.key,
95
+ preventDefault: true,
96
+ run: () => {
97
+ actions[binding.action]();
98
+ return true;
99
+ }
100
+ })))),
101
+ keymap.of([...defaultKeymap, ...historyKeymap]),
102
+ EditorView.lineWrapping,
103
+ lineDirections,
104
+ EditorView.perLineTextDirection.of(true),
105
+ markdown({
106
+ extensions: [GFM, { props: [styleTags({ InlineCode: inlineCode })] }],
107
+ codeLanguages: codeLanguage
108
+ }),
109
+ syntaxHighlighting(highlight),
110
+ EditorView.updateListener.of((update) => {
111
+ if (update.docChanged) callbacks.change(update.state.doc.toString());
112
+ if (update.transactions.length) callbacks.state(undoDepth(update.state) > 0, redoDepth(update.state) > 0);
113
+ }),
114
+ EditorView.contentAttributes.of({ "aria-label": "Markdown body" })
115
+ ]
116
+ });
117
+ }
118
+ //#endregion
119
+ export { markdownSourceState };
@@ -0,0 +1,13 @@
1
+ //#region src/markdown-title.d.ts
2
+ export type LeadingHeadingSplit = Readonly<{
3
+ title: string | null;
4
+ editable: string;
5
+ retained: string;
6
+ }>;
7
+ /** Splits the first nonblank level-one ATX heading from the editable body. */
8
+ export declare function splitLeadingHeading(markdown: string): LeadingHeadingSplit;
9
+ /** Joins an editable body to the exact leading-heading source retained by the split. */
10
+ export declare function joinLeadingHeading(editable: string, retained: string): string;
11
+ /** A title field is always one line, including pasted text. */
12
+ export declare function normalizeMarkdownTitle(value: string): string;
13
+ //#endregion
@@ -0,0 +1,43 @@
1
+ //#region src/markdown-title.ts
2
+ const LEADING_HEADING = /^#(?!#)[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/;
3
+ /** Splits the first nonblank level-one ATX heading from the editable body. */
4
+ function splitLeadingHeading(markdown) {
5
+ const lines = markdown.split("\n");
6
+ let index = 0;
7
+ while (index < lines.length && lines[index]?.trim() === "") index++;
8
+ const line = lines[index];
9
+ const title = (line === void 0 ? null : LEADING_HEADING.exec(line))?.[1] || null;
10
+ if (!title) return {
11
+ title: null,
12
+ editable: markdown,
13
+ retained: ""
14
+ };
15
+ const heading = lines.slice(0, index + 1).join("\n");
16
+ const retained = markdown[heading.length] === "\n" ? `${heading}\n` : heading;
17
+ return {
18
+ title,
19
+ editable: markdown.slice(retained.length),
20
+ retained
21
+ };
22
+ }
23
+ /** Joins an editable body to the exact leading-heading source retained by the split. */
24
+ function joinLeadingHeading(editable, retained) {
25
+ return `${retained}${retained && editable && !retained.endsWith("\n") ? "\n" : ""}${editable}`;
26
+ }
27
+ /** Replaces the retained heading while preserving blank lines before it and its line break. */
28
+ function retainedLeadingHeading(title, previous) {
29
+ if (title === null || title === "") return "";
30
+ if (!previous.retained) {
31
+ if (!previous.editable) return `# ${title}`;
32
+ return `# ${title}${previous.editable.startsWith("\n") ? "\n" : "\n\n"}`;
33
+ }
34
+ const retainedWithoutBreak = previous.retained.endsWith("\n") ? previous.retained.slice(0, -1) : previous.retained;
35
+ const headingStart = retainedWithoutBreak.lastIndexOf("\n") + 1;
36
+ return `${retainedWithoutBreak.slice(0, headingStart)}# ${title}${previous.retained.endsWith("\n") ? "\n" : ""}`;
37
+ }
38
+ /** A title field is always one line, including pasted text. */
39
+ function normalizeMarkdownTitle(value) {
40
+ return value.replace(/[\r\n]+/g, " ");
41
+ }
42
+ //#endregion
43
+ export { joinLeadingHeading, normalizeMarkdownTitle, retainedLeadingHeading, splitLeadingHeading };
package/package.json CHANGED
@@ -1,6 +1,74 @@
1
1
  {
2
2
  "name": "@segnavia/markdown-editor",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.42.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/modstudio/segnavia.git",
9
+ "directory": "packages/markdown-editor"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "sideEffects": [
15
+ "./src/markdown-editor.ts",
16
+ "./dist/markdown-editor.js",
17
+ "./src/markdown-rich-editor.ts",
18
+ "./dist/markdown-rich-editor.js"
19
+ ],
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "@segnavia/source": "./src/markdown-editor.ts",
29
+ "types": "./dist/markdown-editor.d.ts",
30
+ "default": "./dist/markdown-editor.js"
31
+ }
32
+ },
33
+ "dependencies": {
34
+ "@segnavia/format": "0.42.0",
35
+ "@segnavia/frame": "0.42.0",
36
+ "@segnavia/html": "0.42.0"
37
+ },
38
+ "peerDependencies": {
39
+ "@codemirror/commands": "^6.11.1",
40
+ "@codemirror/lang-markdown": "^6.5.2",
41
+ "@codemirror/language": "^6.13.1",
42
+ "@codemirror/language-data": "^6.5.2",
43
+ "@codemirror/state": "^6.7.6",
44
+ "@codemirror/view": "^6.43.15",
45
+ "@lezer/highlight": "^1.2.5",
46
+ "@lezer/markdown": "^1.8.1",
47
+ "@tiptap/core": "^3.32.0",
48
+ "@tiptap/extension-image": "^3.32.0",
49
+ "@tiptap/extension-list": "^3.32.0",
50
+ "@tiptap/extension-table": "^3.32.0",
51
+ "@tiptap/markdown": "^3.32.0",
52
+ "@tiptap/pm": "^3.32.0",
53
+ "@tiptap/starter-kit": "^3.32.0",
54
+ "codemirror-lang-mermaid": "^0.5.0"
55
+ },
56
+ "devDependencies": {
57
+ "@codemirror/commands": "6.11.1",
58
+ "@codemirror/lang-markdown": "6.5.2",
59
+ "@codemirror/language": "6.13.1",
60
+ "@codemirror/language-data": "6.5.2",
61
+ "@codemirror/state": "6.7.6",
62
+ "@codemirror/view": "6.43.15",
63
+ "@lezer/highlight": "1.2.5",
64
+ "@lezer/markdown": "1.8.1",
65
+ "@tiptap/core": "3.32.0",
66
+ "@tiptap/extension-image": "3.32.0",
67
+ "@tiptap/extension-list": "3.32.0",
68
+ "@tiptap/extension-table": "3.32.0",
69
+ "@tiptap/markdown": "3.32.0",
70
+ "@tiptap/pm": "3.32.0",
71
+ "@tiptap/starter-kit": "3.32.0",
72
+ "codemirror-lang-mermaid": "0.5.0"
73
+ }
74
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.