From 27e9fbfd1b037d89e60ff8de81453419905a7580 Mon Sep 17 00:00:00 2001 From: Renee <95993773+renee-k@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:46:29 -0400 Subject: [PATCH] Fix the skipped test cases in the PreTeXt package (#103) Necessary macro subs and environment subs to create valid PreTeXt from LaTeX. --- .../break-on-boundaries.ts | 33 +--- .../create-table-from-tabular.ts | 111 +++++++++++ .../pre-conversion-subs/environment-subs.ts | 148 +++++--------- .../libs/pre-conversion-subs/macro-subs.ts | 185 ++++++++--------- .../report-unsupported-macro-katex.ts | 19 +- .../libs/pre-conversion-subs/utils.ts | 46 +++++ .../libs/pretext-subs/to-pretext.ts | 96 +++++++-- .../libs/split-for-pars.ts | 6 - ...> unified-latex-plugin-to-pretext-like.ts} | 186 ++++++++++++++++-- .../libs/unified-latex-plugin-to-pretext.ts | 49 ++++- .../libs/unified-latex-wrap-pars.ts | 16 +- .../tests/convert-to-pretext.test.ts | 2 + .../report-unsupported-macro-katex.test.ts | 15 +- .../tests/unified-latex-to-pretext.test.ts | 149 +++++++------- .../tests/unified-latex-to-xml-like.test.ts | 15 +- 15 files changed, 699 insertions(+), 377 deletions(-) create mode 100644 packages/unified-latex-to-pretext/libs/pre-conversion-subs/create-table-from-tabular.ts create mode 100644 packages/unified-latex-to-pretext/libs/pre-conversion-subs/utils.ts rename packages/unified-latex-to-pretext/libs/{unified-latex-plugin-to-xml-like.ts => unified-latex-plugin-to-pretext-like.ts} (51%) diff --git a/packages/unified-latex-to-pretext/libs/pre-conversion-subs/break-on-boundaries.ts b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/break-on-boundaries.ts index 83cf2b38..be9684cb 100644 --- a/packages/unified-latex-to-pretext/libs/pre-conversion-subs/break-on-boundaries.ts +++ b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/break-on-boundaries.ts @@ -13,12 +13,13 @@ import { } from "@unified-latex/unified-latex-util-split"; import { visit } from "@unified-latex/unified-latex-util-visit"; import { VFileMessage } from "vfile-message"; +import { makeWarningMessage } from "./utils"; /** * All the divisions, where each item is {division macro, mapped environment}. * Note that this is ordered from the "largest" division to the "smallest" division. */ -const divisions: { division: string; mappedEnviron: string }[] = [ +export const divisions: { division: string; mappedEnviron: string }[] = [ { division: "part", mappedEnviron: "_part" }, { division: "chapter", mappedEnviron: "_chapter" }, { division: "section", mappedEnviron: "_section" }, @@ -34,7 +35,7 @@ const isDivisionMacro = match.createMacroMatcher( ); // check if an environment is a newly created environment -const isMappedEnviron = match.createEnvironmentMatcher( +export const isMappedEnviron = match.createEnvironmentMatcher( divisions.map((x) => x.mappedEnviron) ); @@ -54,29 +55,15 @@ export function breakOnBoundaries(ast: Ast.Ast): { messages: VFileMessage[] } { return anyMacro(child) && isDivisionMacro(child); }) ) { - const message = new VFileMessage( - "Warning: hoisted out of a group, which might break the LaTeX code." + // add a warning message + messagesLst.messages.push( + makeWarningMessage( + node, + "Warning: hoisted out of a group, which might break the LaTeX code.", + "break-on-boundaries" + ) ); - // add the position of the group if available - if (node.position) { - message.line = node.position.start.line; - message.column = node.position.start.column; - message.position = { - start: { - line: node.position.start.line, - column: node.position.start.column, - }, - end: { - line: node.position.end.line, - column: node.position.end.column, - }, - }; - } - - message.source = "LatexConversion"; - messagesLst.messages.push(message); - return node.content; } } diff --git a/packages/unified-latex-to-pretext/libs/pre-conversion-subs/create-table-from-tabular.ts b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/create-table-from-tabular.ts new file mode 100644 index 00000000..e51494ab --- /dev/null +++ b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/create-table-from-tabular.ts @@ -0,0 +1,111 @@ +import * as Ast from "@unified-latex/unified-latex-types"; +import { htmlLike } from "@unified-latex/unified-latex-util-html-like"; +import { + parseTabularSpec, + TabularColumn, +} from "@unified-latex/unified-latex-ctan/package/tabularx"; +import { parseAlignEnvironment } from "@unified-latex/unified-latex-util-align"; +import { getArgsContent } from "@unified-latex/unified-latex-util-arguments"; +import { trim } from "@unified-latex/unified-latex-util-trim"; + +type Attributes = Record>; + +/** + * Convert env into a tabular in PreTeXt. + */ +export function createTableFromTabular(env: Ast.Environment) { + const tabularBody = parseAlignEnvironment(env.content); + const args = getArgsContent(env); + let columnSpecs: TabularColumn[] = []; + try { + columnSpecs = parseTabularSpec(args[1] || []); + } catch (e) {} + + // for the tabular tag + const attributes: Attributes = {}; + + // we only need the col tags if one of the columns aren't left aligned/have a border + let notLeftAligned: boolean = false; + + // stores which columns have borders to the right + // number is the column's index in columnSpecs + const columnRightBorder: Record = {}; + + const tableBody = tabularBody.map((row) => { + const content = row.cells.map((cell, i) => { + const columnSpec = columnSpecs[i]; + + if (columnSpec) { + const { alignment } = columnSpec; + + // this will need to be in the tabular tag + if ( + columnSpec.pre_dividers.some( + (div) => div.type === "vert_divider" + ) + ) { + attributes["left"] = "minor"; + } + + // check if the column has a right border + if ( + columnSpec.post_dividers.some( + (div) => div.type === "vert_divider" + ) + ) { + columnRightBorder[i] = true; + } + + // check if the default alignment isn't used + if (alignment.alignment !== "left") { + notLeftAligned = true; + } + } + + // trim whitespace off cell + trim(cell); + + return htmlLike({ + tag: "cell", + content: cell, + }); + }); + return htmlLike({ tag: "row", content }); + }); + + // add col tags if needed + if (notLeftAligned || Object.values(columnRightBorder).some((b) => b)) { + // go backwards since adding col tags to the front of the tableBody list + // otherwise, col tags will be in the reversed order + for (let i = columnSpecs.length; i >= 0; i--) { + const columnSpec = columnSpecs[i]; + + if (!columnSpec) { + continue; + } + + const colAttributes: Attributes = {}; + const { alignment } = columnSpec; + + // add h-align attribute if not default + if (alignment.alignment !== "left") { + colAttributes["halign"] = alignment.alignment; + } + + // if there is a right border add it + if (columnRightBorder[i] === true) { + colAttributes["right"] = "minor"; + } + + tableBody.unshift( + htmlLike({ tag: "col", attributes: colAttributes }) + ); + } + } + + return htmlLike({ + tag: "tabular", + content: tableBody, + attributes: attributes, + }); +} diff --git a/packages/unified-latex-to-pretext/libs/pre-conversion-subs/environment-subs.ts b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/environment-subs.ts index e3e017a3..7f6ab9a6 100644 --- a/packages/unified-latex-to-pretext/libs/pre-conversion-subs/environment-subs.ts +++ b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/environment-subs.ts @@ -1,19 +1,12 @@ -import cssesc from "cssesc"; -import { - parseTabularSpec, - TabularColumn, -} from "@unified-latex/unified-latex-ctan/package/tabularx"; import { htmlLike } from "@unified-latex/unified-latex-util-html-like"; import * as Ast from "@unified-latex/unified-latex-types"; -import { parseAlignEnvironment } from "@unified-latex/unified-latex-util-align"; -import { - getArgsContent, - getNamedArgsContent, -} from "@unified-latex/unified-latex-util-arguments"; +import { getNamedArgsContent } from "@unified-latex/unified-latex-util-arguments"; import { match } from "@unified-latex/unified-latex-util-match"; -import { printRaw } from "@unified-latex/unified-latex-util-print-raw"; import { wrapPars } from "../wrap-pars"; import { VisitInfo } from "@unified-latex/unified-latex-util-visit"; +import { VFile } from "vfile"; +import { makeWarningMessage } from "./utils"; +import { createTableFromTabular } from "./create-table-from-tabular"; const ITEM_ARG_NAMES_REG = ["label"] as const; const ITEM_ARG_NAMES_BEAMER = [null, "label", null] as const; @@ -48,139 +41,92 @@ function getItemArgs(node: Ast.Macro): ItemArgs { return ret as ItemArgs; } -function enumerateFactory(parentTag = "ol", className = "enumerate") { +function enumerateFactory(parentTag = "ol") { return function enumerateToHtml(env: Ast.Environment) { // The body of an enumerate has already been processed and all relevant parts have // been attached to \item macros as arguments. const items = env.content.filter((node) => match.macro(node, "item")); + + // Figure out if there any manually-specified item labels. If there are, + // we need to add a title tag + let isDescriptionList = false; + const content = items.flatMap((node) => { if (!match.macro(node) || !node.args) { return []; } - const attributes: Record> = - {}; - // Figure out if there any manually-specified item labels. If there are, - // we need to specify a custom list-style-type. // We test the open mark to see if an optional argument was actually supplied. const namedArgs = getItemArgs(node); + + // if there are custom markers, don't want the title tag to be wrapped in pars + // so we wrap the body first + namedArgs.body = wrapPars(namedArgs.body); + + // check if a custom marker is used if (namedArgs.label != null) { - const formattedLabel = cssesc(printRaw(namedArgs.label || [])); - attributes.style = { - // Note the space after `formattedLabel`. That is on purpose! - "list-style-type": formattedLabel - ? `'${formattedLabel} '` - : "none", - }; + isDescriptionList = true; + + // add title tag containing custom marker + namedArgs.body.unshift( + htmlLike({ + tag: "title", + content: namedArgs.label, + }) + ); } const body = namedArgs.body; + return htmlLike({ tag: "li", - content: wrapPars(body), - attributes, + content: body, }); }); return htmlLike({ - tag: parentTag, - attributes: { className }, + tag: isDescriptionList ? "dl" : parentTag, content, }); }; } -function createCenteredElement(env: Ast.Environment) { - return htmlLike({ - tag: "center", - attributes: { className: "center" }, - content: env.content, - }); -} - -function createTableFromTabular(env: Ast.Environment) { - const tabularBody = parseAlignEnvironment(env.content); - const args = getArgsContent(env); - let columnSpecs: TabularColumn[] = []; - try { - columnSpecs = parseTabularSpec(args[1] || []); - } catch (e) {} - - const tableBody = tabularBody.map((row) => { - const content = row.cells.map((cell, i) => { - const columnSpec = columnSpecs[i]; - const styles: Record = {}; - if (columnSpec) { - const { alignment } = columnSpec; - if (alignment.alignment === "center") { - styles["text-align"] = "center"; - } - if (alignment.alignment === "right") { - styles["text-align"] = "right"; - } - if ( - columnSpec.pre_dividers.some( - (div) => div.type === "vert_divider" - ) - ) { - styles["border-left"] = "1px solid"; - } - if ( - columnSpec.post_dividers.some( - (div) => div.type === "vert_divider" - ) - ) { - styles["border-right"] = "1px solid"; - } - } - return htmlLike( - Object.keys(styles).length > 0 - ? { - tag: "td", - content: cell, - attributes: { style: styles }, - } - : { - tag: "td", - content: cell, - } - ); - }); - return htmlLike({ tag: "tr", content }); - }); +/** + * Remove the env environment by returning the content in env only. + */ +function removeEnv(env: Ast.Environment, info: VisitInfo, file?: VFile) { + // add warning + file?.message( + makeWarningMessage( + env, + `Warning: There is no equivalent tag for \"${env.env}\", so the ${env.env} environment was removed.`, + "environment-subs" + ) + ); - return htmlLike({ - tag: "table", - content: [ - htmlLike({ - tag: "tbody", - content: tableBody, - }), - ], - attributes: { className: "tabular" }, - }); + return env.content; } /** * Rules for replacing a macro with an html-like macro - * that will render has html when printed. + * that will render has pretext when printed. */ export const environmentReplacements: Record< string, ( node: Ast.Environment, - info: VisitInfo - ) => Ast.Macro | Ast.String | Ast.Environment + info: VisitInfo, + file?: VFile + ) => Ast.Macro | Ast.String | Ast.Environment | Ast.Node[] > = { enumerate: enumerateFactory("ol"), - itemize: enumerateFactory("ul", "itemize"), - center: createCenteredElement, + itemize: enumerateFactory("ul"), + center: removeEnv, tabular: createTableFromTabular, quote: (env) => { return htmlLike({ tag: "blockquote", content: env.content, - attributes: { className: "environment quote" }, }); }, }; diff --git a/packages/unified-latex-to-pretext/libs/pre-conversion-subs/macro-subs.ts b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/macro-subs.ts index 5398ee13..93e37f13 100644 --- a/packages/unified-latex-to-pretext/libs/pre-conversion-subs/macro-subs.ts +++ b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/macro-subs.ts @@ -1,18 +1,22 @@ -import { xcolorMacroToHex } from "@unified-latex/unified-latex-ctan/package/xcolor"; import { htmlLike } from "@unified-latex/unified-latex-util-html-like"; import * as Ast from "@unified-latex/unified-latex-types"; import { getArgsContent } from "@unified-latex/unified-latex-util-arguments"; import { printRaw } from "@unified-latex/unified-latex-util-print-raw"; import { VisitInfo } from "@unified-latex/unified-latex-util-visit"; +import { VFile } from "unified-lint-rule/lib"; +import { makeWarningMessage, emptyStringWithWarningFactory } from "./utils"; /** * Factory function that generates html-like macros that wrap their contents. + * warningMessage is a warning for any latex macros that don't have an equivalent + * pretext tag. */ function factory( tag: string, + warningMessage: string = "", attributes?: Record -): (macro: Ast.Macro) => Ast.Macro { - return (macro) => { +): (macro: Ast.Macro, info: VisitInfo, file?: VFile) => Ast.Macro { + return (macro, info, file) => { if (!macro.args) { throw new Error( `Found macro to replace but couldn't find content ${printRaw( @@ -20,6 +24,17 @@ function factory( )}` ); } + + // add a warning message to the file if needed + if (warningMessage && file) { + const message = makeWarningMessage( + macro, + `Warning: There is no equivalent tag for \"${macro.content}\", \"${tag}\" was used as a replacement.`, + "macro-subs" + ); + file.message(message, message.position, message.source); + } + // Assume the meaningful argument is the last argument. This // ensures that we can convert for default packages as well as // packages like beamer, which may add optional arguments. @@ -32,10 +47,7 @@ function factory( function createHeading(tag: string, attrs = {}) { return (macro: Ast.Macro) => { const args = getArgsContent(macro); - const starred = !!args[0]; - const attributes: Record = starred - ? { className: "starred" } - : {}; + const attributes: Record = {}; if (attrs) { Object.assign(attributes, attrs); @@ -51,33 +63,44 @@ function createHeading(tag: string, attrs = {}) { export const macroReplacements: Record< string, - (node: Ast.Macro, info: VisitInfo) => Ast.Node + (node: Ast.Macro, info: VisitInfo, file?: VFile) => Ast.Node > = { emph: factory("em"), - textrm: factory("span", { className: "textrm" }), - textsf: factory("span", { className: "textsf" }), - texttt: factory("span", { className: "texttt" }), - textsl: factory("span", { className: "textsl" }), - textit: factory("i", { className: "textit" }), - textbf: factory("b", { className: "textbf" }), - underline: factory("span", { className: "underline" }), - mbox: factory("span", { className: "mbox" }), - phantom: factory("span", { className: "phantom" }), - part: createHeading("title"), - chapter: createHeading("title"), - section: createHeading("title"), - subsection: createHeading("title"), - subsubsection: createHeading("title"), - paragraph: createHeading("title"), - subparagraph: createHeading("title"), - appendix: createHeading("title"), + textrm: factory( + "em", + `Warning: There is no equivalent tag for \"textrm\", \"em\" was used as a replacement.` + ), + textsf: factory( + "em", + `Warning: There is no equivalent tag for \"textsf\", \"em\" was used as a replacement.` + ), + texttt: factory( + "em", + `Warning: There is no equivalent tag for \"textsf\", \"em\" was used as a replacement.` + ), + textsl: factory( + "em", + `Warning: There is no equivalent tag for \"textsl\", \"em\" was used as a replacement.` + ), + textit: factory("em"), + textbf: factory("alert"), + underline: factory( + "em", + `Warning: There is no equivalent tag for \"underline\", \"em\" was used as a replacement.` + ), + mbox: emptyStringWithWarningFactory( + `Warning: There is no equivalent tag for \"mbox\", an empty Ast.String was used as a replacement.` + ), + phantom: emptyStringWithWarningFactory( + `Warning: There is no equivalent tag for \"phantom\", an empty Ast.String was used as a replacement.` + ), + appendix: createHeading("appendix"), url: (node) => { const args = getArgsContent(node); const url = printRaw(args[0] || "#"); return htmlLike({ - tag: "a", + tag: "url", attributes: { - className: "url", href: url, }, content: [{ type: "string", content: url }], @@ -87,9 +110,8 @@ export const macroReplacements: Record< const args = getArgsContent(node); const url = printRaw(args[1] || "#"); return htmlLike({ - tag: "a", + tag: "url", attributes: { - className: "href", href: url, }, content: args[2] || [], @@ -99,95 +121,42 @@ export const macroReplacements: Record< const args = getArgsContent(node); const url = "#" + printRaw(args[0] || ""); return htmlLike({ - tag: "a", + tag: "url", attributes: { - className: "href", href: url, }, content: args[1] || [], }); }, - "\\": () => - htmlLike({ - tag: "br", - attributes: { className: "linebreak" }, - }), - vspace: (node) => { - const args = getArgsContent(node); - return htmlLike({ - tag: "div", - attributes: { - className: "vspace", - "data-amount": printRaw(args[1] || []), - }, - content: [], - }); - }, - hspace: (node) => { - const args = getArgsContent(node); - return htmlLike({ - tag: "span", - attributes: { - className: "vspace", - "data-amount": printRaw(args[1] || []), - }, - content: [], - }); - }, - textcolor: (node) => { - const args = getArgsContent(node); - const computedColor = xcolorMacroToHex(node); - const color = computedColor.hex; - - if (color) { - return htmlLike({ - tag: "span", - attributes: { style: `color: ${color};` }, - content: args[2] || [], - }); - } else { - // If we couldn't compute the color, it's probably a named - // color that wasn't supplied. In this case, we fall back to a css variable - return htmlLike({ - tag: "span", - attributes: { - style: `color: var(${computedColor.cssVarName});`, - }, - content: args[2] || [], - }); - } - }, - textsize: (node) => { - const args = getArgsContent(node); - const textSize = printRaw(args[0] || []); - return htmlLike({ - tag: "span", - attributes: { - className: `textsize-${textSize}`, - }, - content: args[1] || [], - }); - }, - makebox: (node) => { - const args = getArgsContent(node); - return htmlLike({ - tag: "span", - attributes: { - className: `latex-box`, - style: "display: inline-block;", - }, - content: args[3] || [], - }); - }, - noindent: () => ({ type: "string", content: "" }), + "\\": emptyStringWithWarningFactory( + `Warning: There is no equivalent tag for \"\\\", an empty Ast.String was used as a replacement.` + ), + vspace: emptyStringWithWarningFactory( + `Warning: There is no equivalent tag for \"vspace\", an empty Ast.String was used as a replacement.` + ), + hspace: emptyStringWithWarningFactory( + `Warning: There is no equivalent tag for \"hspace\", an empty Ast.String was used as a replacement.` + ), + textcolor: factory( + "em", + `Warning: There is no equivalent tag for \"textcolor\", \"em\" was used as a replacement.` + ), + textsize: emptyStringWithWarningFactory( + `Warning: There is no equivalent tag for \"textsize\", an empty Ast.String was used as a replacement.` + ), + makebox: emptyStringWithWarningFactory( + `Warning: There is no equivalent tag for \"makebox\", an empty Ast.String was used as a replacement.` + ), + noindent: emptyStringWithWarningFactory( + `Warning: There is no equivalent tag for \"noindent\", an empty Ast.String was used as a replacement.` + ), includegraphics: (node) => { const args = getArgsContent(node); - const src = printRaw(args[args.length - 1] || []); + const source = printRaw(args[args.length - 1] || []); return htmlLike({ - tag: "img", + tag: "image", attributes: { - className: "includegraphics", - src, + source, }, content: [], }); diff --git a/packages/unified-latex-to-pretext/libs/pre-conversion-subs/report-unsupported-macro-katex.ts b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/report-unsupported-macro-katex.ts index 38651036..152e1f4d 100644 --- a/packages/unified-latex-to-pretext/libs/pre-conversion-subs/report-unsupported-macro-katex.ts +++ b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/report-unsupported-macro-katex.ts @@ -2,12 +2,16 @@ import * as Ast from "@unified-latex/unified-latex-types"; import { anyMacro, match } from "@unified-latex/unified-latex-util-match"; import { visit } from "@unified-latex/unified-latex-util-visit"; import { KATEX_SUPPORT } from "./katex-subs"; +import { VFileMessage } from "vfile-message"; +import { makeWarningMessage } from "./utils"; /** * Return a list of macros used in ast that are unsupported by KaTeX */ -export function reportMacrosUnsupportedByKatex(ast: Ast.Ast): string[] { - const unsupported: string[] = []; +export function reportMacrosUnsupportedByKatex(ast: Ast.Ast): { + messages: VFileMessage[]; +} { + const unsupported: { messages: VFileMessage[] } = { messages: [] }; // match a macro supported by Katex const isSupported = match.createMacroMatcher(KATEX_SUPPORT.macros); @@ -18,7 +22,16 @@ export function reportMacrosUnsupportedByKatex(ast: Ast.Ast): string[] { if (anyMacro(node) && info.context.hasMathModeAncestor) { // check if not supported by katex if (!isSupported(node)) { - unsupported.push((node as Ast.Macro).content); + // add a warning message + unsupported.messages.push( + makeWarningMessage( + node, + `Warning: \"${ + (node as Ast.Macro).content + }\" is unsupported by Katex.`, + "report-unsupported-macro-katex" + ) + ); } } }); diff --git a/packages/unified-latex-to-pretext/libs/pre-conversion-subs/utils.ts b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/utils.ts new file mode 100644 index 00000000..5b958af5 --- /dev/null +++ b/packages/unified-latex-to-pretext/libs/pre-conversion-subs/utils.ts @@ -0,0 +1,46 @@ +import * as Ast from "@unified-latex/unified-latex-types"; +import { VisitInfo } from "@unified-latex/unified-latex-util-visit"; +import { VFile } from "unified-lint-rule/lib"; +import { s } from "@unified-latex/unified-latex-builder"; +import { VFileMessage } from "vfile-message"; + +/** + * Create a warning message about node from the given source file. + */ +export function makeWarningMessage( + node: Ast.Node, + message: string, + warningType: string +): VFileMessage { + const newMessage = new VFileMessage(message, node); + + newMessage.source = `unified-latex-to-pretext:${warningType}`; + + return newMessage; +} + +/** + * Create an empty Ast.String node, adding a warning message from + * the source file into the VFile. + */ +export function emptyStringWithWarningFactory( + warningMessage: string +): (node: Ast.Node, info: VisitInfo, file?: VFile) => Ast.String { + return (node, info, file) => { + // add a warning message + if (file) { + const message = makeWarningMessage( + node, + warningMessage, + "macro-subs" + ); + file.message( + message, + message.position, + `unified-latex-to-pretext:macro-subs` + ); + } + + return s(""); + }; +} diff --git a/packages/unified-latex-to-pretext/libs/pretext-subs/to-pretext.ts b/packages/unified-latex-to-pretext/libs/pretext-subs/to-pretext.ts index c8b8fb25..a76f553b 100644 --- a/packages/unified-latex-to-pretext/libs/pretext-subs/to-pretext.ts +++ b/packages/unified-latex-to-pretext/libs/pretext-subs/to-pretext.ts @@ -6,6 +6,11 @@ import { } from "@unified-latex/unified-latex-util-html-like"; import * as Ast from "@unified-latex/unified-latex-types"; import { printRaw } from "@unified-latex/unified-latex-util-print-raw"; +import { + divisions, + isMappedEnviron, +} from "../pre-conversion-subs/break-on-boundaries"; +import { getArgsContent } from "@unified-latex/unified-latex-util-arguments"; function formatNodeForError(node: Ast.Node | any): string { try { @@ -23,7 +28,7 @@ export function toPretextWithLoggerFactory( logger: (message: string, node: any) => void ) { /** - * Convert Ast.Node to Hast nodes. + * Convert Ast.Node to Xast nodes. */ return function toPretext( node: Ast.Node | Ast.Argument @@ -59,48 +64,105 @@ export function toPretextWithLoggerFactory( return x("m", printRaw(node.content)); case "mathenv": case "displaymath": - return x("m", printRaw(node.content)); + return x("me", printRaw(node.content)); case "verb": case "verbatim": - return x("pre", { className: node.env }, node.content); + return x("pre", node.content); case "whitespace": return { type: "text", value: " ", position: node.position }; case "parbreak": - return x("br"); + // warn first + logger( + `There is no equivalent for parbreak, so it was replaced with an empty string.`, + node + ); + + // return an empty string + return { + type: "text", + value: "", + position: node.position, + }; case "group": // Groups are just ignored. return node.content.flatMap(toPretext); case "environment": + // check if it's a new environment made to replace a division node + if (isMappedEnviron(node)) { + // get the division macro associated with this node + let divisionName = divisions.find( + (x) => x.mappedEnviron === node.env + )?.division; + + // for subparagraph, give a warning since pretext has no equivalent tag + if (divisionName === "subparagraph") { + logger( + `Warning: There is no equivalent tag for "subparagraph", "paragraphs" was used as a replacement.`, + node + ); + } + + // paragraph and subparagraph become paragraphs + if ( + divisionName === "paragraph" || + divisionName === "subparagraph" + ) { + divisionName = "paragraphs"; + } + + // create a title tag containing the division macro's title arg + const title = getArgsContent(node)[0]; + + if (!title) { + logger( + `Warning: No title was given, so an empty title tag was used.`, + node + ); + } + + const titleTag = x("title", title?.flatMap(toPretext)); + + if (divisionName) { + return x(divisionName, [ + titleTag, + ...node.content.flatMap(toPretext), + ]); + } + } + logger( - `Unknown environment when converting to HTML \`${formatNodeForError( + `Unknown environment when converting to XML \`${formatNodeForError( node.env )}\``, node ); - return x("div", node.content.flatMap(toPretext)); + return node.content.flatMap(toPretext); // just remove the environment case "macro": logger( - `Unknown macro when converting to HTML \`${formatNodeForError( + `Unknown macro when converting to XML \`${formatNodeForError( node )}\``, node ); - return x("span", (node.args || []).map(toPretext).flat()); + return (node.args || []).map(toPretext).flat(); case "argument": - return x( - "span", - { - "data-open-mark": node.openMark, - "data-close-mark": node.closeMark, - }, - printRaw(node.content) + logger( + `Unknown argument when converting to XML \`${formatNodeForError( + node + )}\``, + node ); + return { + type: "text", + value: printRaw(node.content), + position: node.position, + }; case "root": return node.content.flatMap(toPretext); default: { const _exhaustiveCheck: never = node; throw new Error( - `Unknown node type; cannot convert to HAST ${JSON.stringify( + `Unknown node type; cannot convert to XAST ${JSON.stringify( node )}` ); @@ -110,6 +172,6 @@ export function toPretextWithLoggerFactory( } /** - * Convert Ast.Node to Hast nodes. + * Convert Ast.Node to Xast nodes. */ export const toPretext = toPretextWithLoggerFactory(console.warn); diff --git a/packages/unified-latex-to-pretext/libs/split-for-pars.ts b/packages/unified-latex-to-pretext/libs/split-for-pars.ts index 2d62ec5a..b0d449a2 100644 --- a/packages/unified-latex-to-pretext/libs/split-for-pars.ts +++ b/packages/unified-latex-to-pretext/libs/split-for-pars.ts @@ -49,12 +49,6 @@ export function splitForPars( ret.push({ content: [node], wrapInPar: false }); continue; } - // Display-math should always break pars - if (node.type === "displaymath") { - pushBody(); - ret.push({ content: [node], wrapInPar: false }); - continue; - } if (match.parbreak(node) || match.macro(node, "par")) { pushBody(); continue; diff --git a/packages/unified-latex-to-pretext/libs/unified-latex-plugin-to-xml-like.ts b/packages/unified-latex-to-pretext/libs/unified-latex-plugin-to-pretext-like.ts similarity index 51% rename from packages/unified-latex-to-pretext/libs/unified-latex-plugin-to-xml-like.ts rename to packages/unified-latex-to-pretext/libs/unified-latex-plugin-to-pretext-like.ts index d8aeff80..56d51e85 100644 --- a/packages/unified-latex-to-pretext/libs/unified-latex-plugin-to-xml-like.ts +++ b/packages/unified-latex-to-pretext/libs/unified-latex-plugin-to-pretext-like.ts @@ -3,13 +3,17 @@ import { Plugin, unified } from "unified"; import { unifiedLatexLintNoTexFontShapingCommands } from "@unified-latex/unified-latex-lint/rules/unified-latex-lint-no-tex-font-shaping-commands"; import * as Ast from "@unified-latex/unified-latex-types"; import { deleteComments } from "@unified-latex/unified-latex-util-comments"; -import { match } from "@unified-latex/unified-latex-util-match"; +import { + anyEnvironment, + anyMacro, + match, +} from "@unified-latex/unified-latex-util-match"; import { printRaw } from "@unified-latex/unified-latex-util-print-raw"; import { replaceNode, unifiedLatexReplaceStreamingCommands, } from "@unified-latex/unified-latex-util-replace"; -import { EXIT, visit } from "@unified-latex/unified-latex-util-visit"; +import { EXIT, SKIP, visit } from "@unified-latex/unified-latex-util-visit"; import { environmentReplacements as _environmentReplacements } from "./pre-conversion-subs/environment-subs"; import { attachNeededRenderInfo, @@ -19,6 +23,14 @@ import { import { macroReplacements as _macroReplacements } from "./pre-conversion-subs/macro-subs"; import { streamingMacroReplacements } from "./pre-conversion-subs/streaming-command-subs"; import { unifiedLatexWrapPars } from "./unified-latex-wrap-pars"; +import { + breakOnBoundaries, + isMappedEnviron, +} from "./pre-conversion-subs/break-on-boundaries"; +import { reportMacrosUnsupportedByKatex } from "./pre-conversion-subs/report-unsupported-macro-katex"; +import { htmlLike } from "@unified-latex/unified-latex-util-html-like"; +import { getArgsContent } from "@unified-latex/unified-latex-util-arguments"; +import { s } from "@unified-latex/unified-latex-builder"; type EnvironmentReplacements = typeof _environmentReplacements; type MacroReplacements = typeof _macroReplacements; @@ -34,6 +46,12 @@ export type PluginOptions = { * You probably want to use the function `htmlLike(...)` to return a node that gets converted to specific HTML. */ macroReplacements?: MacroReplacements; + + /** + * A boolean where if it's true then the output won't be wrapped in the
... etc. tags. + * If it's false (default), a valid and complete PreTeXt document is returned. + */ + producePretextFragment?: boolean; }; /** @@ -43,7 +61,7 @@ export type PluginOptions = { * * Note: this plugin only wraps paragraphs in `p` tags if there are multiple paragraphs. Otherwise it omits the

tags. */ -export const unifiedLatexToXmlLike: Plugin< +export const unifiedLatexToPretextLike: Plugin< PluginOptions[], Ast.Root, Hast.Root @@ -58,6 +76,10 @@ export const unifiedLatexToXmlLike: Plugin< _environmentReplacements, options?.environmentReplacements || {} ); + const producePretextFragment = options?.producePretextFragment + ? options?.producePretextFragment + : false; + const isReplaceableMacro = match.createMacroMatcher(macroReplacements); const isReplaceableEnvironment = match.createEnvironmentMatcher( environmentReplacements @@ -69,7 +91,7 @@ export const unifiedLatexToXmlLike: Plugin< katexSpecificEnvironmentReplacements ); - return (tree) => { + return (tree, file) => { const originalTree = tree; // NOTE: These operations need to be done in a particular order. @@ -83,36 +105,69 @@ export const unifiedLatexToXmlLike: Plugin< replacers: streamingMacroReplacements, }); + // convert division macros into environments + const warningMessages = breakOnBoundaries(tree); + + // add warning messages into the file one at a time + for (const warningMessage of warningMessages.messages) { + file.message( + warningMessage, + warningMessage.position, + "unified-latex-to-pretext:break-on-boundaries" + ); + } + // Must be done *after* streaming commands are replaced. // We only wrap PARs if we *need* to. That is, if the content contains multiple paragraphs if (shouldBeWrappedInPars(tree)) { processor = processor.use(unifiedLatexWrapPars); } - tree = processor.runSync(tree); + tree = processor.runSync(tree, file); // Replace text-mode environments and then macros. Environments *must* be processed first, since // environments like tabular use `\\` as a newline indicator, but a `\\` macro gets replaced with - // a `
` during macro replacement. + // an empty Ast.String during macro replacement. replaceNode(tree, (node, info) => { // Children of math-mode are rendered by KaTeX/MathJax and so we shouldn't touch them! if (info.context.hasMathModeAncestor) { return; } if (isReplaceableEnvironment(node)) { - return environmentReplacements[printRaw(node.env)](node, info); + return environmentReplacements[printRaw(node.env)]( + node, + info, + file + ); } }); + replaceNode(tree, (node, info) => { // Children of math-mode are rendered by KaTeX/MathJax and so we shouldn't touch them! if (info.context.hasMathModeAncestor) { return; } if (isReplaceableMacro(node)) { - const replacement = macroReplacements[node.content](node, info); + const replacement = macroReplacements[node.content]( + node, + info, + file + ); return replacement; } }); + // before replacing math-mode macros, report any macros that can't be replaced + const unsupportedByKatex = reportMacrosUnsupportedByKatex(tree); + + // add these warning messages into the file one at a time + for (const warningMessage of unsupportedByKatex.messages) { + file.message( + warningMessage, + warningMessage.position, + "unified-latex-to-pretext:report-unsupported-macro-katex" + ); + } + // Replace math-mode macros for appropriate KaTeX rendering attachNeededRenderInfo(tree); replaceNode(tree, (node) => { @@ -126,8 +181,20 @@ export const unifiedLatexToXmlLike: Plugin< } }); + // Wrap in enough tags to ensure a valid pretext document + if (!producePretextFragment) { + // choose a book or article tag + createValidPretextDoc(tree); + + // wrap around with pretext tag + tree.content = [ + htmlLike({ tag: "pretext", content: tree.content }), + ]; + } + // Make sure we are actually mutating the current tree. originalTree.content = tree.content; + console.log(file.messages); }; }; @@ -147,7 +214,104 @@ function shouldBeWrappedInPars(tree: Ast.Root): boolean { { test: (node) => match.environment(node, "document") } ); - return content.some( - (node) => match.parbreak(node) || match.macro(node, "par") - ); + return containsPar(content); +} + +function containsPar(content: Ast.Node[]): boolean { + return content.some((node) => { + if (isMappedEnviron(node)) { + return containsPar(node.content); + } + + return match.parbreak(node) || match.macro(node, "par"); + }); +} + +/** + * Wrap the tree content in a book or article tag. + */ +function createValidPretextDoc(tree: Ast.Root): void { + // this will be incomplete since the author info isn't pushed yet, which obtains documentclass, title, etc. + let isBook: boolean = false; + + // look for a \documentclass (this will need to change, as this info will be gotten earlier) + const docClass = findMacro(tree, "documentclass"); + + // check if there was a documentclass + if (docClass) { + const docClassArg = getArgsContent(docClass)[0]; + + // get the actual class + if (docClassArg) { + const docClassTitle = docClassArg[0] as Ast.String; + + // memoirs will be books too + if ( + docClassTitle.content == "book" || + docClassTitle.content == "memoir" + ) { + isBook = true; + } + } + } + + // if we still don't know if it's a book, look for _chapters environments (since breakonboundaries was called before) + if (!isBook) { + visit(tree, (node) => { + if (anyEnvironment(node) && node.env == "_chapter") { + isBook = true; + return EXIT; + } + }); + } + + // a book and article tag must have a title tag right after it + // extract the title first + const title = findMacro(tree, "title"); + + if (title) { + const titleArg = getArgsContent(title)[1]; + + // get the actual title + if (titleArg) { + const titleString = titleArg[0] as Ast.String; + tree.content.unshift( + htmlLike({ tag: "title", content: titleString }) + ); + } + // if no title name was given, make an empty tag + else { + tree.content.unshift(htmlLike({ tag: "title", content: s("") })); + } + } + // if there is no title, add an empty title tag + else { + tree.content.unshift(htmlLike({ tag: "title", content: s("") })); + } + + // now create a book or article tag + if (isBook) { + tree.content = [htmlLike({ tag: "book", content: tree.content })]; + } else { + tree.content = [htmlLike({ tag: "article", content: tree.content })]; + } +} + +// this will likely be removed +function findMacro(tree: Ast.Root, content: string): Ast.Macro | null { + let macro: Ast.Macro | null = null; + + // look for the macro + visit(tree, (node) => { + // skip visiting the children of environments + if (anyEnvironment(node)) { + return SKIP; + } + if (anyMacro(node) && node.content === content) { + macro = node; + return EXIT; + } + }); + + return macro; } diff --git a/packages/unified-latex-to-pretext/libs/unified-latex-plugin-to-pretext.ts b/packages/unified-latex-to-pretext/libs/unified-latex-plugin-to-pretext.ts index 88eabd1a..63cba56f 100644 --- a/packages/unified-latex-to-pretext/libs/unified-latex-plugin-to-pretext.ts +++ b/packages/unified-latex-to-pretext/libs/unified-latex-plugin-to-pretext.ts @@ -8,11 +8,18 @@ import { match } from "@unified-latex/unified-latex-util-match"; import { EXIT, visit } from "@unified-latex/unified-latex-util-visit"; import { toPretextWithLoggerFactory } from "./pretext-subs/to-pretext"; import { - unifiedLatexToXmlLike, + unifiedLatexToPretextLike, PluginOptions as HtmlLikePluginOptions, -} from "./unified-latex-plugin-to-xml-like"; +} from "./unified-latex-plugin-to-pretext-like"; +import { expandUserDefinedMacros } from "./pre-conversion-subs/expand-user-defined-macros"; -export type PluginOptions = HtmlLikePluginOptions & {}; +export type PluginOptions = HtmlLikePluginOptions & { + /** + * A boolean where if it's true then the output won't be wrapped in the

... etc. tags. + * If it's false (default), a valid and complete PreTeXt document is returned. + */ + producePretextFragment?: boolean; +}; /** * Unified plugin to convert a `unified-latex` AST into a `xast` AST representation of PreTeXt source. @@ -23,11 +30,12 @@ export const unifiedLatexToPretext: Plugin< Xast.Root > = function unifiedLatexAttachMacroArguments(options) { return (tree, file) => { - unified().use(unifiedLatexToXmlLike, options).run(tree); + const producePretextFragment = options?.producePretextFragment + ? options?.producePretextFragment + : false; - // This should happen right before converting to HTML because macros like `\&` should - // be expanded via html rules first (and not turned into their corresponding ligature directly) - expandUnicodeLigatures(tree); + // expand user defined macros + expandUserDefinedMacros(tree); // If there is a \begin{document}...\end{document}, that's the only // content we want to convert. @@ -47,14 +55,35 @@ export const unifiedLatexToPretext: Plugin< } ); - const toHast = toPretextWithLoggerFactory(file.message.bind(file)); - let converted = toHast({ type: "root", content }); + // since we don't want to wrap content outside of \begin{document}...\end{document} with ... + tree.content = content; + + unified().use(unifiedLatexToPretextLike, options).run(tree, file); + + // This should happen right before converting to PreTeXt because macros like `\&` should + // be expanded via html rules first (and not turned into their corresponding ligature directly) + expandUnicodeLigatures(tree); + + // update content + content = tree.content; + + const toXast = toPretextWithLoggerFactory(file.message.bind(file)); + let converted = toXast({ type: "root", content }); if (!Array.isArray(converted)) { converted = [converted]; } - // Wrap everything in a Hast.Root node + // Wrap everything in a Xast.Root node let ret = x(); ret.children = converted; + + // add boilerplate + if (!producePretextFragment) { + ret.children.unshift({ + type: "instruction", + name: "xml", + value: "version='1.0' encoding='utf-8'", + }); + } return ret; }; }; diff --git a/packages/unified-latex-to-pretext/libs/unified-latex-wrap-pars.ts b/packages/unified-latex-to-pretext/libs/unified-latex-wrap-pars.ts index 49603ab1..a6f193ca 100644 --- a/packages/unified-latex-to-pretext/libs/unified-latex-wrap-pars.ts +++ b/packages/unified-latex-to-pretext/libs/unified-latex-wrap-pars.ts @@ -1,9 +1,9 @@ import { Plugin } from "unified"; import * as Ast from "@unified-latex/unified-latex-types"; import { match } from "@unified-latex/unified-latex-util-match"; -import { EXIT, visit } from "@unified-latex/unified-latex-util-visit"; +import { visit } from "@unified-latex/unified-latex-util-visit"; import { wrapPars } from "./wrap-pars"; - +import { isMappedEnviron } from "./pre-conversion-subs/break-on-boundaries"; type PluginOptions = { macrosThatBreakPars?: string[]; environmentsThatDontBreakPars?: string[]; @@ -20,20 +20,24 @@ export const unifiedLatexWrapPars: Plugin = options || {}; return (tree) => { // If \begin{document}...\end{document} is present, we only wrap pars inside of it. + let hasDocumentEnv = false; visit( tree, (env) => { - if (match.environment(env, "document")) { - hasDocumentEnv = true; + if ( + match.environment(env, "document") || + isMappedEnviron(env) + ) { + if (match.environment(env, "document")) { + hasDocumentEnv = true; + } // While we're here, we might as well wrap the pars! env.content = wrapPars(env.content, { macrosThatBreakPars, environmentsThatDontBreakPars, }); - - return EXIT; } }, { test: match.anyEnvironment } diff --git a/packages/unified-latex-to-pretext/tests/convert-to-pretext.test.ts b/packages/unified-latex-to-pretext/tests/convert-to-pretext.test.ts index bba5f4e5..12703dda 100644 --- a/packages/unified-latex-to-pretext/tests/convert-to-pretext.test.ts +++ b/packages/unified-latex-to-pretext/tests/convert-to-pretext.test.ts @@ -61,6 +61,7 @@ describe("unified-latex-to-pretext:convert-to-pretext", () => { yyy: (node) => htmlLike({ tag: "yyy", content: node.content }), }, + producePretextFragment: true, }); let ast; @@ -87,6 +88,7 @@ describe("unified-latex-to-pretext:convert-to-pretext", () => { }); }, }, + producePretextFragment: true, }) .use(xmlCompilePlugin) .processSync(value).value as string; diff --git a/packages/unified-latex-to-pretext/tests/report-unsupported-macro-katex.test.ts b/packages/unified-latex-to-pretext/tests/report-unsupported-macro-katex.test.ts index be40a3d8..f0a2f4f6 100644 --- a/packages/unified-latex-to-pretext/tests/report-unsupported-macro-katex.test.ts +++ b/packages/unified-latex-to-pretext/tests/report-unsupported-macro-katex.test.ts @@ -18,7 +18,7 @@ describe("unified-latex-to-pretext:report-unsupported-macro-katex", () => { const parser = getParser(); const ast = parser.parse(value); - expect(reportMacrosUnsupportedByKatex(ast)).toEqual(["fakemacro"]); + expect(reportMacrosUnsupportedByKatex(ast).messages.length).toEqual(1); }); it("can report no unsupported macros in mathmode", () => { @@ -27,7 +27,7 @@ describe("unified-latex-to-pretext:report-unsupported-macro-katex", () => { const parser = getParser(); const ast = parser.parse(value); - expect(reportMacrosUnsupportedByKatex(ast)).toEqual([]); + expect(reportMacrosUnsupportedByKatex(ast).messages.length).toEqual(0); }); it("doesn't report unsupported macros outside of math mode", () => { @@ -36,7 +36,7 @@ describe("unified-latex-to-pretext:report-unsupported-macro-katex", () => { const parser = getParser(); const ast = parser.parse(value); - expect(reportMacrosUnsupportedByKatex(ast)).toEqual([]); + expect(reportMacrosUnsupportedByKatex(ast).messages.length).toEqual(0); }); it("reports unsupported macros in text mode with a math anscestor", () => { @@ -45,7 +45,7 @@ describe("unified-latex-to-pretext:report-unsupported-macro-katex", () => { const parser = getParser(); const ast = parser.parse(value); - expect(reportMacrosUnsupportedByKatex(ast)).toEqual(["unsupported"]); + expect(reportMacrosUnsupportedByKatex(ast).messages.length).toEqual(1); }); it("can report unsupported macros in display mathmode", () => { @@ -54,7 +54,7 @@ describe("unified-latex-to-pretext:report-unsupported-macro-katex", () => { const parser = getParser(); const ast = parser.parse(value); - expect(reportMacrosUnsupportedByKatex(ast)).toEqual(["fake", "baz"]); + expect(reportMacrosUnsupportedByKatex(ast).messages.length).toEqual(2); }); it("can report unsupported macros in equation environment", () => { @@ -63,9 +63,6 @@ describe("unified-latex-to-pretext:report-unsupported-macro-katex", () => { const parser = getParser(); const ast = parser.parse(value); - expect(reportMacrosUnsupportedByKatex(ast)).toEqual([ - "unsupported", - "baz", - ]); + expect(reportMacrosUnsupportedByKatex(ast).messages.length).toEqual(2); }); }); diff --git a/packages/unified-latex-to-pretext/tests/unified-latex-to-pretext.test.ts b/packages/unified-latex-to-pretext/tests/unified-latex-to-pretext.test.ts index d1e8f59d..f3e3f026 100644 --- a/packages/unified-latex-to-pretext/tests/unified-latex-to-pretext.test.ts +++ b/packages/unified-latex-to-pretext/tests/unified-latex-to-pretext.test.ts @@ -29,39 +29,35 @@ describe("unified-latex-to-pretext:unified-latex-to-pretext", () => { const process = (value: string) => processLatexViaUnified({ macros: { xxx: { signature: "m m" } } }) - .use(unifiedLatexToPretext) + .use(unifiedLatexToPretext, { producePretextFragment: true }) .use(xmlCompilePlugin) .processSync({ value }).value as string; - it.skip("wrap pars and streaming commands", () => { + it("wrap pars and streaming commands", () => { html = process("a\n\nb"); expect(html).toEqual("

a

b

"); html = process("\\bfseries a\n\nb"); - expect(html).toEqual( - '

a

b

' - ); + expect(html).toEqual("

a

b

"); html = process("\\bf a\n\nb"); - expect(html).toEqual( - '

a

b

' - ); + expect(html).toEqual("

a

b

"); }); - it.skip("Can replace text-style macros", () => { + it("Can replace text-style macros", () => { html = process(String.raw`a \textbf{different} word`); expect(normalizeHtml(html)).toEqual( - normalizeHtml(`a different word`) + normalizeHtml(`a different word`) ); html = process(String.raw`a \textsf{different} word`); expect(normalizeHtml(html)).toEqual( - normalizeHtml(`a different word`) + normalizeHtml(`a different word`) ); html = process(String.raw`a \textrm{different} word`); expect(normalizeHtml(html)).toEqual( - normalizeHtml(`a different word`) + normalizeHtml(`a different word`) ); html = process(String.raw`a \emph{different} word`); @@ -69,20 +65,21 @@ describe("unified-latex-to-pretext:unified-latex-to-pretext", () => { normalizeHtml(`a different word`) ); }); - it.skip("Can replace headings", () => { + + it("Can replace headings", () => { html = process(String.raw`\chapter{My Chapter}`); expect(normalizeHtml(html)).toEqual( - normalizeHtml(`

My Chapter

`) + normalizeHtml(`My Chapter`) ); html = process(String.raw`\section{My Section}`); expect(normalizeHtml(html)).toEqual( - normalizeHtml(`My Section`) + normalizeHtml(`
My Section
`) ); html = process(String.raw`\section*{My Section}`); expect(normalizeHtml(html)).toEqual( - normalizeHtml(`My Section`) + normalizeHtml(`
My Section
`) ); }); @@ -100,24 +97,22 @@ describe("unified-latex-to-pretext:unified-latex-to-pretext", () => { expect(normalizeHtml(html)).toEqual(normalizeHtml(`

a

b

`)); }); - it.skip("Wraps URLs", () => { + it("Wraps URLs", () => { html = process(`a\\url{foo.com}b`); expect(normalizeHtml(html)).toEqual( - normalizeHtml(`afoo.comb`) + normalizeHtml(`afoo.comb`) ); html = process(`a\\href{foo.com}{FOO}b`); expect(normalizeHtml(html)).toEqual( - normalizeHtml(`aFOOb`) + normalizeHtml(`aFOOb`) ); }); - it.skip("Converts enumerate environments", () => { + it("Converts enumerate environments", () => { html = process(`\\begin{enumerate}\\item a\\item b\\end{enumerate}`); expect(normalizeHtml(html)).toEqual( - normalizeHtml( - `
  1. a

  2. b

` - ) + normalizeHtml(`
  1. a

  2. b

`) ); // Any content before an \item is ignored @@ -125,28 +120,28 @@ describe("unified-latex-to-pretext:unified-latex-to-pretext", () => { `\\begin{enumerate}before content\\item a\\item b\\end{enumerate}` ); expect(normalizeHtml(html)).toEqual( - normalizeHtml( - `
  1. a

  2. b

` - ) + normalizeHtml(`
  1. a

  2. b

`) ); // Custom labels are handled html = process( `\\begin{enumerate}before content\\item[x)] a\\item[] b\\end{enumerate}` ); + expect(normalizeHtml(html)).toEqual( - normalizeHtml(`
    -
  1. a

  2. -
  3. b

  4. -
`) + normalizeHtml( + `
+
  • x)

    a

  • +
  • b

  • +
    ` + ) ); }); - it.skip("Converts itemize environments", () => { + + it("Converts itemize environments", () => { html = process(`\\begin{itemize}\\item a\\item b\\end{itemize}`); expect(normalizeHtml(html)).toEqual( - normalizeHtml( - `
    • a

    • b

    ` - ) + normalizeHtml(`
    • a

    • b

    `) ); // Any content before an \item is ignored @@ -154,39 +149,40 @@ describe("unified-latex-to-pretext:unified-latex-to-pretext", () => { `\\begin{itemize}before content\\item a\\item b\\end{itemize}` ); expect(normalizeHtml(html)).toEqual( - normalizeHtml( - `
    • a

    • b

    ` - ) + normalizeHtml(`
    • a

    • b

    `) ); // Custom labels are handled html = process( `\\begin{itemize}before content\\item[x)] a\\item[] b\\end{itemize}` ); + expect(normalizeHtml(html)).toEqual( - normalizeHtml(`
      -
    • a

    • -
    • b

    • -
    `) + normalizeHtml( + `
    +
  • x)

    a

  • +
  • b

  • +
    ` + ) ); }); - it.skip("Converts tabular environment", () => { + it("Converts tabular environment", () => { html = process(`\\begin{tabular}{l l}a & b\\\\c & d\\end{tabular}`); + + expect(normalizeHtml(html)).toEqual( + normalizeHtml( + `abcd` + ) + ); + }); + + it("Converts tabular environment with different column alignments and borders", () => { + html = process(`\\begin{tabular}{|r||l|}a & b\\\\c & d\\end{tabular}`); expect(normalizeHtml(html)).toEqual( normalizeHtml( - ` - - - - - - - - - - -
    ab
    cd
    ` + `` + + `abcd` ) ); }); @@ -204,12 +200,14 @@ describe("unified-latex-to-pretext:unified-latex-to-pretext", () => { ); html = process(`a\\section{foo} b\n\nc`); expect(normalizeHtml(html)).toEqual( - normalizeHtml(`

    a

    foo

    b

    c

    `) + normalizeHtml( + `

    a

    foo

    b

    c

    ` + ) ); html = process(`a\\section{foo} b\\section{bar}\n\nc`); expect(normalizeHtml(html)).toEqual( normalizeHtml( - `

    a

    foo

    b

    bar

    c

    ` + `

    a

    foo

    b

    bar

    c

    ` ) ); html = process(`a\n \\emph{b}\n\nc`); @@ -218,18 +216,16 @@ describe("unified-latex-to-pretext:unified-latex-to-pretext", () => { ); html = process(`a\n b\\begin{foo}x\\end{foo}c\n\nd`); expect(normalizeHtml(html)).toEqual( - normalizeHtml(`

    a b

    x

    c

    d

    `) + normalizeHtml(`

    a b

    x

    c

    d

    `) ); }); - it.skip("Macros aren't replaced with html code in math mode", () => { + it("Macros aren't replaced with html code in math mode", () => { let ast; // Custom labels are handled ast = process(`\\[a\\\\b\\]`); - expect(normalizeHtml(ast)).toEqual( - normalizeHtml(`
    a\\\\b
    `) - ); + expect(normalizeHtml(ast)).toEqual(normalizeHtml(`a\\\\b`)); }); it("Ligatures that are nested inside of math mode are not replaced", () => { @@ -242,34 +238,33 @@ describe("unified-latex-to-pretext:unified-latex-to-pretext", () => { ); }); - it.skip("Pars are broken at display math", () => { + it("Pars are broken at display math", () => { let ast; ast = process(`x\n\ny\\[a\\\\b\\]z`); expect(normalizeHtml(ast)).toEqual( - normalizeHtml( - `

    x

    y

    a\\\\b

    z

    ` - ) + normalizeHtml(`

    x

    ya\\\\bz

    `) ); }); - it.skip("replaces command inside argument", () => { + it("replaces command inside argument", () => { let ast; ast = process(`\\emph{\\bfseries b}`); expect(normalizeHtml(ast)).toEqual( - normalizeHtml('b') + normalizeHtml("b") ); }); - it.skip("replaces command inside enumerate", () => { + + it("replaces command inside enumerate", () => { let ast; ast = process(`\\begin{enumerate}\\item\\bfseries b\\end{enumerate}`); expect(normalizeHtml(ast)).toEqual( - normalizeHtml(`
      -
    1. -

      b

      -
    2. -
    `) + normalizeHtml(`
      +
    1. +

      b

      +
    2. +
    `) ); }); it("replaces paragraphs", () => { @@ -277,9 +272,9 @@ describe("unified-latex-to-pretext:unified-latex-to-pretext", () => { ast = process(`\\paragraph{Important.} Paragraph`); expect(normalizeHtml(ast)).toEqual( - normalizeHtml(` - Important. Paragraph - `) + normalizeHtml( + `Important. Paragraph` + ) ); }); it("custom replacers work", () => { @@ -307,6 +302,7 @@ describe("unified-latex-to-pretext:unified-latex-to-pretext", () => { yyy: (node) => htmlLike({ tag: "yyy", content: node.content }), }, + producePretextFragment: true, }) .use(xmlCompilePlugin) .processSync({ value }).value as string; @@ -348,6 +344,7 @@ describe("unified-latex-to-pretext:unified-latex-to-pretext", () => { }); }, }, + producePretextFragment: true, }) .use(xmlCompilePlugin) .processSync({ value }).value as string; diff --git a/packages/unified-latex-to-pretext/tests/unified-latex-to-xml-like.test.ts b/packages/unified-latex-to-pretext/tests/unified-latex-to-xml-like.test.ts index 2f613e84..4fbb6545 100644 --- a/packages/unified-latex-to-pretext/tests/unified-latex-to-xml-like.test.ts +++ b/packages/unified-latex-to-pretext/tests/unified-latex-to-xml-like.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from "vitest"; import { processLatexViaUnified } from "@unified-latex/unified-latex"; -import { VFile } from "unified-lint-rule/lib"; +import { VFile } from "vfile"; import util from "util"; -import { unifiedLatexToXmlLike } from "../libs/unified-latex-plugin-to-xml-like"; +import { unifiedLatexToPretextLike } from "../libs/unified-latex-plugin-to-pretext-like"; import { htmlLike } from "@unified-latex/unified-latex-util-html-like"; import { printRaw } from "@unified-latex/unified-latex-util-print-raw"; @@ -18,7 +18,7 @@ describe("unified-latex-to-pretext:unified-latex-to-xml-like", () => { let file: VFile; const process = (value: string) => processLatexViaUnified() - .use(unifiedLatexToXmlLike) + .use(unifiedLatexToPretextLike, { producePretextFragment: true }) .processSync({ value }); it("wrap pars and streaming commands", () => { @@ -27,26 +27,26 @@ describe("unified-latex-to-pretext:unified-latex-to-xml-like", () => { file = process("\\bfseries a\n\nb"); expect(file.value).toEqual( - '\\html-tag:p{\\html-tag:b{\\html-attr:className{"textbf"}a}}\\html-tag:p{\\html-tag:b{\\html-attr:className{"textbf"}b}}' + "\\html-tag:p{\\html-tag:alert{a}}\\html-tag:p{\\html-tag:alert{b}}" ); file = process("\\bf a\n\nb"); expect(file.value).toEqual( - '\\html-tag:p{\\html-tag:b{\\html-attr:className{"textbf"}a}}\\html-tag:p{\\html-tag:b{\\html-attr:className{"textbf"}b}}' + "\\html-tag:p{\\html-tag:alert{a}}\\html-tag:p{\\html-tag:alert{b}}" ); file = process( "\\begin{enumerate}\\item foo\\item bar\\end{enumerate}" ); expect(file.value).toEqual( - '\\html-tag:ol{\\html-attr:className{"enumerate"}\\html-tag:li{\\html-tag:p{foo}}\\html-tag:li{\\html-tag:p{bar}}}' + "\\html-tag:ol{\\html-tag:li{\\html-tag:p{foo}}\\html-tag:li{\\html-tag:p{bar}}}" ); }); it("can accept custom replacers", () => { const process = (value: string) => processLatexViaUnified({ macros: { xxx: { signature: "m m" } } }) - .use(unifiedLatexToXmlLike, { + .use(unifiedLatexToPretextLike, { macroReplacements: { xxx: (node) => htmlLike({ @@ -63,6 +63,7 @@ describe("unified-latex-to-pretext:unified-latex-to-xml-like", () => { yyy: (node) => htmlLike({ tag: "yyy", content: node.content }), }, + producePretextFragment: true, }) .processSync({ value });