d718c5a372
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled
904 lines
25 KiB
JavaScript
904 lines
25 KiB
JavaScript
import { fromMarkdown } from "https://esm.sh/mdast-util-from-markdown@2.0.0";
|
|
import { toMarkdown as mdastUtilToMarkdown } from "https://esm.sh/mdast-util-to-markdown@2.1.2";
|
|
import Quill from "https://esm.sh/quill@2.0.3";
|
|
|
|
/**
|
|
* @typedef {Object} QuillAttributes
|
|
* @property {boolean} [bold] - Whether the text is bold.
|
|
* @property {boolean} [italic] - Whether the text is italic.
|
|
* @property {string} [link] - URL if the text is a link.
|
|
* @property {number} [header] - Header level (1-3).
|
|
* @property {string} [list] - List type ('ordered' or 'bullet').
|
|
* @property {boolean} [blockquote] - Whether the text is in a blockquote.
|
|
* @property {string} [code-block] - Code language if in a code block.
|
|
* @property {string} [alt] - Alt text for images.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} QuillOperation
|
|
* @property {string|Object} [insert] - Content to insert (string or object with image URL).
|
|
* @property {number} [delete] - Number of characters to delete.
|
|
* @property {number} [retain] - Number of characters to retain.
|
|
* @property {QuillAttributes} [attributes] - Formatting attributes.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} QuillDelta
|
|
* @property {Array<QuillOperation>} ops - Array of operations in the delta.
|
|
*/
|
|
|
|
/**
|
|
* Converts Quill Delta object to a Markdown string using mdast.
|
|
* @param {QuillDelta} delta - Quill Delta object (https://quilljs.com/docs/delta/).
|
|
* @returns {string} - Markdown representation.
|
|
*/
|
|
function deltaToMarkdown(delta) {
|
|
const mdastTree = deltaToMdast(delta);
|
|
const options = {
|
|
bullet: "*",
|
|
listItemIndent: "one",
|
|
handlers: {},
|
|
unknownHandler: (node) => {
|
|
console.warn(`Unknown node type encountered: ${node.type}`, node);
|
|
return false;
|
|
},
|
|
};
|
|
return mdastUtilToMarkdown(mdastTree, options);
|
|
}
|
|
|
|
/**
|
|
* Creates a div to replace the textarea and prepares it for Quill.
|
|
* @param {HTMLTextAreaElement} textarea - The original textarea.
|
|
* @returns {HTMLDivElement} - The div element created for the Quill editor.
|
|
*/
|
|
function createAndReplaceTextarea(textarea) {
|
|
const editorDiv = document.createElement("div");
|
|
editorDiv.className = "mb-3";
|
|
editorDiv.style.height = "250px";
|
|
|
|
const label = textarea.closest("label");
|
|
if (!label) {
|
|
textarea.parentNode.insertBefore(editorDiv, textarea);
|
|
} else {
|
|
label.parentNode.insertBefore(editorDiv, label.nextSibling);
|
|
}
|
|
// Hide the original textarea, but keep it focusable for validation
|
|
textarea.style = "transform: scale(0); position: absolute; opacity: 0;";
|
|
return editorDiv;
|
|
}
|
|
|
|
/**
|
|
* Returns the toolbar options array configured for Markdown compatibility.
|
|
* @returns {Array<Array<any>>} - Quill toolbar options.
|
|
*/
|
|
function getMarkdownToolbarOptions() {
|
|
return [
|
|
[{ header: 1 }, { header: 2 }, { header: 3 }],
|
|
["bold", "italic", "code"],
|
|
["link", "image", "blockquote", "code-block"],
|
|
[{ list: "ordered" }, { list: "bullet" }],
|
|
["clean"],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Initializes a Quill editor instance on a given div.
|
|
* @param {HTMLDivElement} editorDiv - The div element for the editor.
|
|
* @param {Array<Array<any>>} toolbarOptions - The toolbar configuration.
|
|
* @param {string} initialValue - The initial content for the editor.
|
|
* @returns {Quill} - The initialized Quill instance.
|
|
*/
|
|
function initializeQuillEditor(
|
|
editorDiv,
|
|
toolbarOptions,
|
|
initialValue,
|
|
readOnly,
|
|
) {
|
|
const quill = new Quill(editorDiv, {
|
|
theme: "snow",
|
|
modules: {
|
|
toolbar: toolbarOptions,
|
|
},
|
|
readOnly: readOnly,
|
|
formats: [
|
|
"bold",
|
|
"italic",
|
|
"link",
|
|
"header",
|
|
"list",
|
|
"blockquote",
|
|
"code",
|
|
"code-block",
|
|
"image",
|
|
],
|
|
});
|
|
if (initialValue) {
|
|
const delta = markdownToDelta(initialValue);
|
|
quill.setContents(delta);
|
|
}
|
|
return quill;
|
|
}
|
|
|
|
/**
|
|
* Converts Markdown string to a Quill Delta object.
|
|
* @param {string} markdown - The markdown string to convert.
|
|
* @returns {QuillDelta} - Quill Delta representation.
|
|
*/
|
|
function markdownToDelta(markdown) {
|
|
try {
|
|
const mdastTree = fromMarkdown(markdown);
|
|
return mdastToDelta(mdastTree);
|
|
} catch (error) {
|
|
console.error("Error parsing markdown:", error);
|
|
return { ops: [{ insert: markdown }] };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts MDAST to Quill Delta.
|
|
* @param {MdastNode} tree - The MDAST tree to convert.
|
|
* @returns {QuillDelta} - Quill Delta representation.
|
|
*/
|
|
function mdastToDelta(tree) {
|
|
const delta = { ops: [] };
|
|
if (!tree || !tree.children) return delta;
|
|
|
|
for (const node of tree.children) {
|
|
traverseMdastNode(node, delta);
|
|
}
|
|
|
|
return delta;
|
|
}
|
|
|
|
/**
|
|
* Recursively traverse MDAST nodes and convert to Delta operations.
|
|
* @param {MdastNode} node - The MDAST node to process.
|
|
* @param {QuillDelta} delta - The Delta object to append operations to.
|
|
* @param {QuillAttributes} [attributes={}] - The current attributes to apply.
|
|
*/
|
|
function traverseMdastNode(node, delta, attributes = {}) {
|
|
if (!node) return;
|
|
|
|
switch (node.type) {
|
|
case "root":
|
|
for (const child of node.children || []) {
|
|
traverseMdastNode(child, delta);
|
|
}
|
|
break;
|
|
|
|
case "paragraph": {
|
|
for (const child of node.children || []) {
|
|
traverseMdastNode(child, delta, attributes);
|
|
}
|
|
const pLineAttributes = {};
|
|
if (attributes.blockquote) {
|
|
pLineAttributes.blockquote = true;
|
|
}
|
|
delta.ops.push({ insert: "\n", attributes: pLineAttributes });
|
|
break;
|
|
}
|
|
|
|
case "heading": {
|
|
const headingContentAttributes = { ...attributes, header: node.depth };
|
|
for (const child of node.children || []) {
|
|
traverseMdastNode(child, delta, headingContentAttributes);
|
|
}
|
|
const headingLineAttributes = { header: node.depth };
|
|
if (attributes.blockquote) {
|
|
headingLineAttributes.blockquote = true;
|
|
}
|
|
delta.ops.push({ insert: "\n", attributes: headingLineAttributes });
|
|
break;
|
|
}
|
|
|
|
case "text":
|
|
delta.ops.push({ insert: node.value || "", attributes });
|
|
break;
|
|
|
|
case "strong":
|
|
for (const child of node.children || []) {
|
|
traverseMdastNode(child, delta, { ...attributes, bold: true });
|
|
}
|
|
break;
|
|
|
|
case "emphasis":
|
|
for (const child of node.children || []) {
|
|
traverseMdastNode(child, delta, { ...attributes, italic: true });
|
|
}
|
|
break;
|
|
|
|
case "link":
|
|
for (const child of node.children || []) {
|
|
traverseMdastNode(child, delta, { ...attributes, link: node.url });
|
|
}
|
|
break;
|
|
|
|
case "image":
|
|
delta.ops.push({
|
|
insert: { image: node.url },
|
|
attributes: { alt: node.alt || "" },
|
|
});
|
|
break;
|
|
|
|
case "list":
|
|
for (const child of node.children || []) {
|
|
traverseMdastNode(child, delta, {
|
|
...attributes,
|
|
list: node.ordered ? "ordered" : "bullet",
|
|
});
|
|
}
|
|
break;
|
|
|
|
case "listItem": {
|
|
// biome-ignore lint/correctness/noUnusedVariables: object destructuring with a spread
|
|
const { list, ...listItemChildrenAttributes } = attributes;
|
|
|
|
for (const child of node.children || []) {
|
|
traverseMdastNode(child, delta, listItemChildrenAttributes);
|
|
}
|
|
|
|
// Attributes for the listItem's newline (e.g., { list: 'bullet', blockquote: true })
|
|
// are in `attributes` passed to this `listItem` case.
|
|
{
|
|
const lastOp = delta.ops[delta.ops.length - 1];
|
|
if (lastOp && lastOp.insert === "\n") {
|
|
lastOp.attributes = { ...lastOp.attributes, ...attributes };
|
|
} else {
|
|
delta.ops.push({ insert: "\n", attributes });
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "blockquote":
|
|
for (const child of node.children || []) {
|
|
traverseMdastNode(child, delta, { ...attributes, blockquote: true });
|
|
}
|
|
break;
|
|
|
|
case "code": {
|
|
// mdast 'code' is a block
|
|
const codeBlockLineFormat = { "code-block": node.lang || true };
|
|
if (attributes.blockquote) {
|
|
codeBlockLineFormat.blockquote = true;
|
|
}
|
|
|
|
const textInCodeAttributes = {};
|
|
if (attributes.blockquote) {
|
|
// Text lines also get blockquote if active
|
|
textInCodeAttributes.blockquote = true;
|
|
}
|
|
|
|
const lines = (node.value || "").split("\n");
|
|
for (const lineText of lines) {
|
|
delta.ops.push({ insert: lineText, attributes: textInCodeAttributes });
|
|
delta.ops.push({ insert: "\n", attributes: codeBlockLineFormat });
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "inlineCode":
|
|
delta.ops.push({
|
|
insert: node.value || "",
|
|
attributes: { ...attributes, code: true },
|
|
});
|
|
break;
|
|
|
|
default:
|
|
if (node.children) {
|
|
for (const child of node.children) {
|
|
traverseMdastNode(child, delta, attributes);
|
|
}
|
|
} else if (node.value) {
|
|
delta.ops.push({ insert: node.value, attributes });
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attaches a submit event listener to the form to update the hidden textarea.
|
|
* @param {HTMLFormElement|null} form - The form containing the editor.
|
|
* @param {HTMLTextAreaElement} textarea - The original (hidden) textarea.
|
|
* @param {Quill} quill - The Quill editor instance.
|
|
* @returns {void}
|
|
*/
|
|
function updateTextareaOnSubmit(form, textarea, quill) {
|
|
if (!form) {
|
|
console.warn(
|
|
"Textarea not inside a form, submission handling skipped for:",
|
|
textarea.name || textarea.id,
|
|
);
|
|
return;
|
|
}
|
|
form.addEventListener("submit", (event) => {
|
|
const delta = quill.getContents();
|
|
const markdownContent = deltaToMarkdown(delta);
|
|
textarea.value = markdownContent;
|
|
console.log(
|
|
`${textarea.name}:\n${markdownContent}\ntransformed from delta:\n${JSON.stringify(delta, null, 2)}`,
|
|
);
|
|
if (textarea.required && !markdownContent) {
|
|
textarea.setCustomValidity(`${textarea.name} cannot be empty`);
|
|
quill.once("text-change", (delta) => {
|
|
textarea.value = deltaToMarkdown(delta);
|
|
textarea.setCustomValidity("");
|
|
});
|
|
quill.focus();
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Loads the Quill CSS stylesheet.
|
|
* @returns {void}
|
|
*/
|
|
function loadQuillStylesheet() {
|
|
const link = document.createElement("link");
|
|
link.rel = "stylesheet";
|
|
link.href = "https://esm.sh/quill@2.0.3/dist/quill.snow.css";
|
|
document.head.appendChild(link);
|
|
}
|
|
|
|
/**
|
|
* Handles errors during editor initialization.
|
|
* @param {HTMLTextAreaElement} textarea - The textarea that failed initialization.
|
|
* @param {Error} error - The error that occurred.
|
|
* @returns {void}
|
|
*/
|
|
function handleEditorInitError(textarea, error) {
|
|
console.error("Failed to initialize Quill for textarea:", textarea, error);
|
|
textarea.style.display = "";
|
|
const errorMsg = document.createElement("p");
|
|
errorMsg.textContent = "Failed to load rich text editor.";
|
|
errorMsg.style.color = "red";
|
|
textarea.parentNode.insertBefore(errorMsg, textarea.nextSibling);
|
|
}
|
|
|
|
/**
|
|
* Sets up a single editor for a textarea.
|
|
* @param {HTMLTextAreaElement} textarea - The textarea to replace with an editor.
|
|
* @param {Array<Array<any>>} toolbarOptions - The toolbar configuration.
|
|
* @returns {boolean} - Whether the setup was successful.
|
|
*/
|
|
function setupSingleEditor(textarea, toolbarOptions) {
|
|
if (textarea.dataset.quillInitialized === "true") {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const initialValue = textarea.value;
|
|
const readOnly = textarea.readOnly || textarea.disabled;
|
|
const form = textarea.closest("form");
|
|
const editorDiv = createAndReplaceTextarea(textarea);
|
|
const quill = initializeQuillEditor(
|
|
editorDiv,
|
|
toolbarOptions,
|
|
initialValue,
|
|
readOnly,
|
|
);
|
|
updateTextareaOnSubmit(form, textarea, quill);
|
|
textarea.dataset.quillInitialized = "true";
|
|
return true;
|
|
} catch (error) {
|
|
handleEditorInitError(textarea, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes Quill editors for all textareas in the document.
|
|
* @returns {void}
|
|
*/
|
|
function initializeEditors() {
|
|
loadQuillStylesheet();
|
|
|
|
const textareas = document.getElementsByTagName("textarea");
|
|
if (textareas.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const toolbarOptions = getMarkdownToolbarOptions();
|
|
let initializedCount = 0;
|
|
|
|
for (const textarea of textareas) {
|
|
if (setupSingleEditor(textarea, toolbarOptions)) {
|
|
initializedCount++;
|
|
}
|
|
}
|
|
|
|
if (initializedCount > 0) {
|
|
console.log(
|
|
`Successfully initialized Quill for ${initializedCount} textareas.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// MDAST conversion functions
|
|
/**
|
|
* @typedef {Object} MdastNode
|
|
* @property {string} type - The type of the node.
|
|
* @property {Array<MdastNode>} [children] - Child nodes.
|
|
* @property {string} [value] - Text value for text nodes.
|
|
* @property {string} [url] - URL for link and image nodes.
|
|
* @property {string} [title] - Title for image nodes.
|
|
* @property {string} [alt] - Alt text for image nodes.
|
|
* @property {number} [depth] - Depth for heading nodes.
|
|
* @property {boolean} [ordered] - Whether the list is ordered.
|
|
* @property {boolean} [spread] - Whether the list is spread.
|
|
* @property {string} [lang] - Language for code blocks.
|
|
*/
|
|
|
|
/**
|
|
* Converts a Quill Delta to a MDAST (Markdown Abstract Syntax Tree).
|
|
* @param {QuillDelta} delta - The Quill Delta to convert.
|
|
* @returns {MdastNode} - The root MDAST node.
|
|
*/
|
|
function deltaToMdast(delta) {
|
|
const mdast = createRootNode();
|
|
/** @type {MdastNode|null} */
|
|
let currentParagraph = null;
|
|
/** @type {MdastNode|null} */
|
|
let currentList = null;
|
|
let textBuffer = "";
|
|
|
|
for (const op of delta.ops) {
|
|
if (isImageInsert(op)) {
|
|
if (!currentParagraph) {
|
|
currentParagraph = createParagraphNode();
|
|
}
|
|
currentParagraph.children.push(createImageNode(op));
|
|
}
|
|
if (typeof op.insert !== "string") continue;
|
|
|
|
const text = op.insert;
|
|
const attributes = op.attributes || {};
|
|
|
|
// Handle newlines within text content
|
|
if (text.includes("\n") && text !== "\n") {
|
|
const lines = text.split("\n");
|
|
|
|
// Process all lines except the last one as complete lines
|
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
const line = lines[i];
|
|
if (line.length > 0) {
|
|
// Add text to current paragraph
|
|
if (!currentParagraph) {
|
|
currentParagraph = createParagraphNode();
|
|
}
|
|
const nodes = createTextNodes(line, attributes);
|
|
currentParagraph.children.push(...nodes);
|
|
textBuffer = line;
|
|
}
|
|
|
|
// Process line break
|
|
currentList = processLineBreak(
|
|
mdast,
|
|
currentParagraph,
|
|
attributes,
|
|
textBuffer,
|
|
currentList,
|
|
);
|
|
|
|
currentParagraph = null;
|
|
textBuffer = "";
|
|
}
|
|
|
|
// Add the last line to the buffer without processing the line break yet
|
|
const lastLine = lines[lines.length - 1];
|
|
if (lastLine.length > 0) {
|
|
if (!currentParagraph) {
|
|
currentParagraph = createParagraphNode();
|
|
}
|
|
const nodes = createTextNodes(lastLine, attributes);
|
|
currentParagraph.children.push(...nodes);
|
|
textBuffer = lastLine;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (text === "\n") {
|
|
currentList = processLineBreak(
|
|
mdast,
|
|
currentParagraph,
|
|
attributes,
|
|
textBuffer,
|
|
currentList,
|
|
);
|
|
|
|
// Reset paragraph and buffer after processing line break
|
|
currentParagraph = null;
|
|
textBuffer = "";
|
|
continue;
|
|
}
|
|
|
|
// Process regular text
|
|
const nodes = createTextNodes(text, attributes);
|
|
|
|
if (!currentParagraph) {
|
|
currentParagraph = createParagraphNode();
|
|
}
|
|
|
|
textBuffer += text;
|
|
currentParagraph.children.push(...nodes);
|
|
}
|
|
|
|
if (currentParagraph) {
|
|
mdast.children.push(currentParagraph);
|
|
}
|
|
|
|
return mdast;
|
|
}
|
|
|
|
/**
|
|
* Creates a root MDAST node.
|
|
* @returns {MdastNode} - The root node.
|
|
*/
|
|
function createRootNode() {
|
|
return {
|
|
type: "root",
|
|
children: [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a paragraph MDAST node.
|
|
* @returns {MdastNode} - The paragraph node.
|
|
*/
|
|
function createParagraphNode() {
|
|
return {
|
|
type: "paragraph",
|
|
children: [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Checks if an operation is an image insertion.
|
|
* @param {Object} op - The operation to check.
|
|
* @returns {boolean} - Whether the operation is an image insertion.
|
|
*/
|
|
function isImageInsert(op) {
|
|
return typeof op.insert === "object" && op.insert.image;
|
|
}
|
|
|
|
/**
|
|
* Creates an image MDAST node.
|
|
* @param {Object} op - The operation containing the image.
|
|
* @returns {MdastNode} - The image node.
|
|
*/
|
|
function createImageNode(op) {
|
|
return {
|
|
type: "image",
|
|
url: op.insert.image,
|
|
title: op.attributes?.alt || "",
|
|
alt: op.attributes?.alt || "",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a text MDAST node with optional formatting.
|
|
* @param {string} text - The text content.
|
|
* @param {Object} attributes - The formatting attributes.
|
|
* @returns {MdastNode[]} - The formatted text nodes.
|
|
*/
|
|
function createTextNodes(text, attributes) {
|
|
let nodes = text.split("\n").flatMap((value, i) => [
|
|
...(i > 0 ? [{ type: "break" }] : []),
|
|
{
|
|
type: "text",
|
|
value,
|
|
},
|
|
]);
|
|
|
|
if (attributes.bold) {
|
|
nodes = [wrapNodesWith(nodes, "strong")];
|
|
}
|
|
|
|
if (attributes.italic) {
|
|
nodes = [wrapNodesWith(nodes, "emphasis")];
|
|
}
|
|
|
|
if (attributes.link) {
|
|
nodes = [{ ...wrapNodesWith(nodes, "link"), url: attributes.link }];
|
|
}
|
|
|
|
return nodes;
|
|
}
|
|
|
|
/**
|
|
* Wraps a node with a formatting container.
|
|
* @param {MdastNode[]} children - The node to wrap.
|
|
* @param {string} type - The type of container.
|
|
* @returns {MdastNode} - The wrapped node.
|
|
*/
|
|
function wrapNodesWith(children, type) {
|
|
return {
|
|
type: type,
|
|
children,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Processes a line break in the Delta.
|
|
* @param {MdastNode} mdast - The root MDAST node.
|
|
* @param {MdastNode|null} currentParagraph - The current paragraph being built.
|
|
* @param {Object} attributes - The attributes for the line.
|
|
* @param {string} textBuffer - The text buffer for the current line.
|
|
* @param {MdastNode|null} currentList - The current list being built.
|
|
* @returns {MdastNode|null} - The updated current list.
|
|
*/
|
|
function processLineBreak(
|
|
mdast,
|
|
currentParagraph,
|
|
attributes,
|
|
textBuffer,
|
|
currentList,
|
|
) {
|
|
if (!currentParagraph) {
|
|
return handleEmptyLineWithAttributes(mdast, attributes, currentList);
|
|
}
|
|
|
|
if (attributes.header) {
|
|
processHeaderLineBreak(mdast, textBuffer, attributes);
|
|
return null;
|
|
}
|
|
|
|
if (attributes["code-block"]) {
|
|
processCodeBlockLineBreak(mdast, textBuffer, attributes);
|
|
return currentList;
|
|
}
|
|
|
|
if (attributes.list) {
|
|
return processListLineBreak(
|
|
mdast,
|
|
currentParagraph,
|
|
attributes,
|
|
currentList,
|
|
);
|
|
}
|
|
|
|
if (attributes.blockquote) {
|
|
processBlockquoteLineBreak(mdast, currentParagraph);
|
|
return currentList;
|
|
}
|
|
|
|
// Default case: regular paragraph
|
|
mdast.children.push(currentParagraph);
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Handles an empty line with special attributes.
|
|
* @param {MdastNode} mdast - The root MDAST node.
|
|
* @param {Object} attributes - The attributes for the line.
|
|
* @param {MdastNode|null} currentList - The current list being built.
|
|
* @returns {MdastNode|null} - The updated current list.
|
|
*/
|
|
function handleEmptyLineWithAttributes(mdast, attributes, currentList) {
|
|
if (attributes["code-block"]) {
|
|
mdast.children.push(createEmptyCodeBlock(attributes));
|
|
return currentList;
|
|
}
|
|
|
|
if (attributes.list) {
|
|
const list = ensureList(mdast, attributes, currentList);
|
|
list.children.push(createEmptyListItem());
|
|
return list;
|
|
}
|
|
|
|
if (attributes.blockquote) {
|
|
mdast.children.push(createEmptyBlockquote());
|
|
return currentList;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Creates an empty code block MDAST node.
|
|
* @param {Object} attributes - The attributes for the code block.
|
|
* @returns {MdastNode} - The code block node.
|
|
*/
|
|
function createEmptyCodeBlock(attributes) {
|
|
return {
|
|
type: "code",
|
|
value: "",
|
|
lang:
|
|
attributes["code-block"] === "plain" ? null : attributes["code-block"],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates an empty list item MDAST node.
|
|
* @returns {MdastNode} - The list item node.
|
|
*/
|
|
function createEmptyListItem() {
|
|
return {
|
|
type: "listItem",
|
|
spread: false,
|
|
children: [{ type: "paragraph", children: [] }],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates an empty blockquote MDAST node.
|
|
* @returns {MdastNode} - The blockquote node.
|
|
*/
|
|
function createEmptyBlockquote() {
|
|
return {
|
|
type: "blockquote",
|
|
children: [{ type: "paragraph", children: [] }],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Processes a header line break.
|
|
* @param {MdastNode} mdast - The root MDAST node.
|
|
* @param {string} textBuffer - The text buffer for the current line.
|
|
* @param {Object} attributes - The attributes for the line.
|
|
* @returns {void}
|
|
*/
|
|
function processHeaderLineBreak(mdast, textBuffer, attributes) {
|
|
const lines = textBuffer.split("\n");
|
|
|
|
if (lines.length > 1) {
|
|
const lastLine = lines.pop();
|
|
const previousLines = lines.join("\n");
|
|
|
|
if (previousLines) {
|
|
mdast.children.push({
|
|
type: "paragraph",
|
|
children: [{ type: "text", value: previousLines }],
|
|
});
|
|
}
|
|
|
|
mdast.children.push({
|
|
type: "heading",
|
|
depth: attributes.header,
|
|
children: [{ type: "text", value: lastLine }],
|
|
});
|
|
} else {
|
|
mdast.children.push({
|
|
type: "heading",
|
|
depth: attributes.header,
|
|
children: [{ type: "text", value: textBuffer }],
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes a code block line break.
|
|
* @param {MdastNode} mdast - The root MDAST node.
|
|
* @param {string} textBuffer - The text buffer for the current line.
|
|
* @param {Object} attributes - The attributes for the line.
|
|
* @returns {void}
|
|
*/
|
|
function processCodeBlockLineBreak(mdast, textBuffer, attributes) {
|
|
const lang =
|
|
attributes["code-block"] === "plain" ? null : attributes["code-block"];
|
|
|
|
// Find the last code block with the same language
|
|
let lastCodeBlock = null;
|
|
for (let i = mdast.children.length - 1; i >= 0; i--) {
|
|
const child = mdast.children[i];
|
|
if (child.type === "code" && child.lang === lang) {
|
|
lastCodeBlock = child;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (lastCodeBlock) {
|
|
// Append to existing code block with same language
|
|
lastCodeBlock.value += `\n${textBuffer}`;
|
|
} else {
|
|
// Create new code block
|
|
mdast.children.push({
|
|
type: "code",
|
|
value: textBuffer,
|
|
lang,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensures a list exists in the MDAST.
|
|
* @param {MdastNode} mdast - The root MDAST node.
|
|
* @param {Object} attributes - The attributes for the line.
|
|
* @param {MdastNode|null} currentList - The current list being built.
|
|
* @returns {MdastNode} - The list node.
|
|
*/
|
|
function ensureList(mdast, attributes, currentList) {
|
|
const isOrderedList = attributes.list === "ordered";
|
|
|
|
// If there's no current list or the list type doesn't match
|
|
if (!currentList || currentList.ordered !== isOrderedList) {
|
|
// Check if the last child is a list of the correct type
|
|
const lastChild = mdast.children[mdast.children.length - 1];
|
|
if (
|
|
lastChild &&
|
|
lastChild.type === "list" &&
|
|
lastChild.ordered === isOrderedList
|
|
) {
|
|
// Use the last list if it matches the type
|
|
return lastChild;
|
|
}
|
|
|
|
// Create a new list
|
|
const newList = {
|
|
type: "list",
|
|
ordered: isOrderedList,
|
|
spread: false,
|
|
children: [],
|
|
};
|
|
mdast.children.push(newList);
|
|
return newList;
|
|
}
|
|
|
|
return currentList;
|
|
}
|
|
|
|
/**
|
|
* Processes a list line break.
|
|
* @param {MdastNode} mdast - The root MDAST node.
|
|
* @param {MdastNode} currentParagraph - The current paragraph being built.
|
|
* @param {Object} attributes - The attributes for the line.
|
|
* @param {MdastNode|null} currentList - The current list being built.
|
|
* @returns {MdastNode} - The updated list node.
|
|
*/
|
|
function processListLineBreak(
|
|
mdast,
|
|
currentParagraph,
|
|
attributes,
|
|
currentList,
|
|
) {
|
|
const list = ensureList(mdast, attributes, currentList);
|
|
|
|
// Check if this list item already exists to avoid duplication
|
|
const paragraphContent = JSON.stringify(currentParagraph.children);
|
|
const isDuplicate = list.children.some(
|
|
(item) =>
|
|
item.children?.length === 1 &&
|
|
JSON.stringify(item.children[0].children) === paragraphContent,
|
|
);
|
|
|
|
if (!isDuplicate) {
|
|
const listItem = {
|
|
type: "listItem",
|
|
spread: false,
|
|
children: [currentParagraph],
|
|
};
|
|
|
|
list.children.push(listItem);
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
/**
|
|
* Processes a blockquote line break.
|
|
* @param {MdastNode} mdast - The root MDAST node.
|
|
* @param {MdastNode} currentParagraph - The current paragraph being built.
|
|
* @returns {void}
|
|
*/
|
|
function processBlockquoteLineBreak(mdast, currentParagraph) {
|
|
// Look for an existing blockquote with identical content to avoid duplication
|
|
const paragraphContent = JSON.stringify(currentParagraph.children);
|
|
const existingBlockquote = mdast.children.find(
|
|
(child) =>
|
|
child.type === "blockquote" &&
|
|
child.children?.length === 1 &&
|
|
JSON.stringify(child.children[0].children) === paragraphContent,
|
|
);
|
|
|
|
if (!existingBlockquote) {
|
|
mdast.children.push({
|
|
type: "blockquote",
|
|
children: [currentParagraph],
|
|
});
|
|
}
|
|
}
|
|
|
|
// Main execution
|
|
document.addEventListener("DOMContentLoaded", initializeEditors);
|