diff --git a/.gitignore b/.gitignore index e57e4114246..426c62110bf 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ win_configuration.bat src/resources/preview/quarto-preview.js .prettierrc.json _manuscript + +# generated by coverage tools +tests/docs/luacov/luacov.report.html +tests/docs/luacov/report.html diff --git a/dev-docs/lua-coverage-report.md b/dev-docs/lua-coverage-report.md index dc3e9cc8181..b436ea81d56 100644 --- a/dev-docs/lua-coverage-report.md +++ b/dev-docs/lua-coverage-report.md @@ -5,10 +5,7 @@ run the following commands: ```bash cd quarto-cli/tests -export QUARTO_LUACOV=`pwd`/luacov.stats.out -rm -f luacov.stats.out # to get a fresh report; otherwise it appends -./run-tests.sh -quarto render docs/luacov/report.qmd +./run-tests-with-luacov.sh ``` The report is an HTML file, and will be under `docs/luacov/luacov.report.html`. diff --git a/news/changelog-1.4.md b/news/changelog-1.4.md index bcd3cf9d060..de7c770953c 100644 --- a/news/changelog-1.4.md +++ b/news/changelog-1.4.md @@ -166,6 +166,10 @@ - ([#6766](https://github.com/quarto-dev/quarto-cli/issues/6766)): Add `id` as valid CSL property when specifying a documents citation metadata. +## Crossrefs + +- ([#6620](https://github.com/quarto-dev/quarto-cli/issues/6620)): Introduce `FloatRefTarget` AST nodes that generalize crossref targets to include figures, tables, and custom floating elements. + ## Other Fixes and Improvements - ([#2214](https://github.com/quarto-dev/quarto-cli/issues/2214), reopened): don't report a non-existing version of Google Chrome in macOS. @@ -186,6 +190,7 @@ - ([#6487](https://github.com/quarto-dev/quarto-cli/discussions/6487)): Fix `serviceworkers` check in `htmlDependency` to look at the correct key. - ([#6178](https://github.com/quarto-dev/quarto-cli/pull/6178)): When `QUARTO_LOG_LEVEL=DEBUG`, information about search for a R binary will be shown. - ([#5755](https://github.com/quarto-dev/quarto-cli/pull/5755)): Allow document metadata to control conditional content. +- ([#6620](https://github.com/quarto-dev/quarto-cli/pull/6620)): Rewrite Crossreferenceable figure support. See the [prerelease documentation](https://quarto.org/docs/prerelease/1.4/) for more information. - ([#6697](https://github.com/quarto-dev/quarto-cli/pull/6697)): Fix issue with outputing to stdout (`quarto render -o -`) on Windows. - ([#6705](https://github.com/quarto-dev/quarto-cli/pull/6705)): Fix issue with gfm output being removed when rendered with other formats. - ([#6746](https://github.com/quarto-dev/quarto-cli/issues/6746)): Let stdout and stderr finish independently to avoid deadlock. diff --git a/src/command/render/filters.ts b/src/command/render/filters.ts index 3ed9766a343..a13f830b6d0 100644 --- a/src/command/render/filters.ts +++ b/src/command/render/filters.ts @@ -97,6 +97,8 @@ const kQuartoVersion = "quarto-version"; const kQuartoSource = "quarto-source"; +const kQuartoCustomFormat = "quarto-custom-format"; + export async function filterParamsJson( args: string[], options: PandocOptions, @@ -127,6 +129,10 @@ export async function filterParamsJson( defaults, ); + const customFormatParams = extractCustomFormatParams( + options.format.metadata, + ); + const params: Metadata = { ...includes, ...initFilterParams(dependenciesFile), @@ -141,6 +147,7 @@ export async function filterParamsJson( ...jatsFilterParams(options), ...notebookContextFilterParams(options), ...filterParams, + ...customFormatParams, [kResultsFile]: pandocMetadataPath(resultsFile), [kTimingFile]: pandocMetadataPath(timingFile), [kQuartoFilters]: filterSpec, @@ -161,6 +168,21 @@ export function quartoMainFilter() { return resourcePath("filters/main.lua"); } +function extractCustomFormatParams( + metadata: Metadata, +) { + // pull out custom format spec if provided + const customFormatParams = metadata[kQuartoCustomFormat]; + if (customFormatParams) { + delete metadata[kQuartoCustomFormat]; + return { + [kQuartoCustomFormat]: customFormatParams, + }; + } else { + return {}; + } +} + function extractFilterSpecParams( metadata: Metadata, ) { diff --git a/src/core/handlers/base.ts b/src/core/handlers/base.ts index fc71135d233..cab703dcd4e 100644 --- a/src/core/handlers/base.ts +++ b/src/core/handlers/base.ts @@ -1,9 +1,8 @@ /* -* base.ts -* -* Copyright (C) 2022 Posit Software, PBC -* -*/ + * base.ts + * + * Copyright (C) 2022 Posit Software, PBC + */ import { HandlerContextResults, @@ -717,7 +716,7 @@ export function getDivAttributes( } } if (options?.[kCellLstCap]) { - attrs.push(`caption="${options?.[kCellLstCap]}"`); + attrs.push(`lst-cap="${options?.[kCellLstCap]}"`); } const classStr = (options?.classes as (string | undefined)) || ""; diff --git a/src/core/jupyter/jupyter.ts b/src/core/jupyter/jupyter.ts index b95abd1fe8c..d55639dbb7f 100644 --- a/src/core/jupyter/jupyter.ts +++ b/src/core/jupyter/jupyter.ts @@ -1379,7 +1379,7 @@ async function mdFromCodeCell( } if (typeof cell.options[kCellLstCap] === "string") { - md.push(` caption=\"${cell.options[kCellLstCap]}\"`); + md.push(` lst-cap=\"${cell.options[kCellLstCap]}\"`); } if (typeof cell.options[kCodeFold] !== "undefined") { md.push(` code-fold=\"${cell.options[kCodeFold]}\"`); diff --git a/src/dev_import_map.json b/src/dev_import_map.json index d69cfc61fac..27cef6379ac 100644 --- a/src/dev_import_map.json +++ b/src/dev_import_map.json @@ -45,6 +45,8 @@ "diff": "./vendor/cdn.skypack.dev/diff@5.0.0.js", "observablehq/parser": "./vendor/cdn.skypack.dev/@observablehq/parser@4.5.0.js", "js-yaml": "./vendor/cdn.skypack.dev/js-yaml@4.1.0.js", + "slimdom": "./vendor/cdn.skypack.dev/slimdom@4.2.0.js", + "fontoxpath": "./vendor/cdn.skypack.dev/fontoxpath@3.29.1.js", "https://deno.land/std@0.161.0/": "./vendor/deno.land/std@0.185.0/", "https://deno.land/std@0.101.0/": "./vendor/deno.land/std@0.185.0/", "https://deno.land/std@0.105.0/": "./vendor/deno.land/std@0.185.0/", @@ -280,6 +282,9 @@ "/-/acorn-walk@v7.2.0-HE7wS37ePcNncqJvsD8k/dist=es2019,mode=imports/optimized/acorn-walk.js": "./vendor/cdn.skypack.dev/-/acorn-walk@v7.2.0-HE7wS37ePcNncqJvsD8k/dist=es2019,mode=imports/optimized/acorn-walk.js", "/-/acorn-private-class-elements@v1.0.0-74UyKouPfmJKyVmXndKD/dist=es2019,mode=imports/optimized/acorn-private-class-elements.js": "./vendor/cdn.skypack.dev/-/acorn-private-class-elements@v1.0.0-74UyKouPfmJKyVmXndKD/dist=es2019,mode=imports/optimized/acorn-private-class-elements.js", "/-/acorn@v8.4.0-TUBEehokUmfefnUMjao9/dist=es2019,mode=imports/optimized/acorn.js": "./vendor/cdn.skypack.dev/-/acorn@v8.4.0-TUBEehokUmfefnUMjao9/dist=es2019,mode=imports/optimized/acorn.js", + "/-/xspattern@v3.1.0-ChOssaTvtX8cZQgPaNnM/dist=es2019,mode=imports/optimized/xspattern.js": "./vendor/cdn.skypack.dev/-/xspattern@v3.1.0-ChOssaTvtX8cZQgPaNnM/dist=es2019,mode=imports/optimized/xspattern.js", + "/-/prsc@v4.0.0-yiYip3qo0YwPataeg654/dist=es2019,mode=imports/optimized/prsc.js": "./vendor/cdn.skypack.dev/-/prsc@v4.0.0-yiYip3qo0YwPataeg654/dist=es2019,mode=imports/optimized/prsc.js", + "/-/whynot@v5.0.0-TIWeI93neceQKiPCfmA6/dist=es2019,mode=imports/optimized/whynot.js": "./vendor/cdn.skypack.dev/-/whynot@v5.0.0-TIWeI93neceQKiPCfmA6/dist=es2019,mode=imports/optimized/whynot.js", "/-/@observablehq/parser@v4.5.0-rWZiNfab8flhVomtfVvr/dist=es2019,mode=imports/optimized/@observablehq/parser.js": "./vendor/cdn.skypack.dev/-/@observablehq/parser@v4.5.0-rWZiNfab8flhVomtfVvr/dist=es2019,mode=imports/optimized/@observablehq/parser.js", "/-/acorn-class-fields@v1.0.0-VEggkLxq9gMrdwRuKkzZ/dist=es2019,mode=imports/optimized/acorn-class-fields.js": "./vendor/cdn.skypack.dev/-/acorn-class-fields@v1.0.0-VEggkLxq9gMrdwRuKkzZ/dist=es2019,mode=imports/optimized/acorn-class-fields.js", "/-/acorn-walk@v8.2.0-X811aiix0R2fkBGq305v/dist=es2019,mode=imports/optimized/acorn-walk.js": "./vendor/cdn.skypack.dev/-/acorn-walk@v8.2.0-X811aiix0R2fkBGq305v/dist=es2019,mode=imports/optimized/acorn-walk.js", @@ -287,6 +292,7 @@ "/-/binary-search-bounds@v2.0.5-c8IgO4OqUhed8ANHQXKv/dist=es2019,mode=imports/optimized/binary-search-bounds.js": "./vendor/cdn.skypack.dev/-/binary-search-bounds@v2.0.5-c8IgO4OqUhed8ANHQXKv/dist=es2019,mode=imports/optimized/binary-search-bounds.js", "/-/blueimp-md5@v2.19.0-FsBtHB6ITwdC3L5Giq4Q/dist=es2019,mode=imports/optimized/blueimp-md5.js": "./vendor/cdn.skypack.dev/-/blueimp-md5@v2.19.0-FsBtHB6ITwdC3L5Giq4Q/dist=es2019,mode=imports/optimized/blueimp-md5.js", "/-/dayjs@v1.8.21-6syVEc6qGP8frQXKlmJD/dist=es2019,mode=imports/optimized/dayjs.js": "./vendor/cdn.skypack.dev/-/dayjs@v1.8.21-6syVEc6qGP8frQXKlmJD/dist=es2019,mode=imports/optimized/dayjs.js", + "/-/fontoxpath@v3.29.1-a0ohYsVP957eLX7RfgAa/dist=es2019,mode=imports/optimized/fontoxpath.js": "./vendor/cdn.skypack.dev/-/fontoxpath@v3.29.1-a0ohYsVP957eLX7RfgAa/dist=es2019,mode=imports/optimized/fontoxpath.js", "/-/lodash@v4.17.21-K6GEbP02mWFnLA45zAmi/dist=es2019,mode=imports/unoptimized/cloneDeep.js": "./vendor/cdn.skypack.dev/-/lodash@v4.17.21-K6GEbP02mWFnLA45zAmi/dist=es2019,mode=imports/unoptimized/cloneDeep.js", "/-/lodash@v4.17.21-K6GEbP02mWFnLA45zAmi/dist=es2019,mode=imports/unoptimized/debounce.js": "./vendor/cdn.skypack.dev/-/lodash@v4.17.21-K6GEbP02mWFnLA45zAmi/dist=es2019,mode=imports/unoptimized/debounce.js", "/-/lodash@v4.17.21-K6GEbP02mWFnLA45zAmi/dist=es2019,mode=imports/unoptimized/difference.js": "./vendor/cdn.skypack.dev/-/lodash@v4.17.21-K6GEbP02mWFnLA45zAmi/dist=es2019,mode=imports/unoptimized/difference.js", @@ -303,7 +309,8 @@ "/-/lodash@v4.17.21-K6GEbP02mWFnLA45zAmi/dist=es2019,mode=imports/unoptimized/toString.js": "./vendor/cdn.skypack.dev/-/lodash@v4.17.21-K6GEbP02mWFnLA45zAmi/dist=es2019,mode=imports/unoptimized/toString.js", "/-/lodash@v4.17.21-K6GEbP02mWFnLA45zAmi/dist=es2019,mode=imports/unoptimized/uniq.js": "./vendor/cdn.skypack.dev/-/lodash@v4.17.21-K6GEbP02mWFnLA45zAmi/dist=es2019,mode=imports/unoptimized/uniq.js", "/-/lodash@v4.17.21-K6GEbP02mWFnLA45zAmi/dist=es2019,mode=imports/unoptimized/uniqBy.js": "./vendor/cdn.skypack.dev/-/lodash@v4.17.21-K6GEbP02mWFnLA45zAmi/dist=es2019,mode=imports/unoptimized/uniqBy.js", - "/-/moment-guess@v1.2.4-bDXl7KQy0hLGNuGhyGb4/dist=es2019,mode=imports/optimized/moment-guess.js": "./vendor/cdn.skypack.dev/-/moment-guess@v1.2.4-bDXl7KQy0hLGNuGhyGb4/dist=es2019,mode=imports/optimized/moment-guess.js" + "/-/moment-guess@v1.2.4-bDXl7KQy0hLGNuGhyGb4/dist=es2019,mode=imports/optimized/moment-guess.js": "./vendor/cdn.skypack.dev/-/moment-guess@v1.2.4-bDXl7KQy0hLGNuGhyGb4/dist=es2019,mode=imports/optimized/moment-guess.js", + "/-/slimdom@v4.2.0-QzuHPU3P67qdOzczKt6u/dist=es2019,mode=imports/optimized/slimdom.js": "./vendor/cdn.skypack.dev/-/slimdom@v4.2.0-QzuHPU3P67qdOzczKt6u/dist=es2019,mode=imports/optimized/slimdom.js" } } } \ No newline at end of file diff --git a/src/execute/ojs/compile.ts b/src/execute/ojs/compile.ts index b98015e81b8..9c80ba579f6 100644 --- a/src/execute/ojs/compile.ts +++ b/src/execute/ojs/compile.ts @@ -424,9 +424,6 @@ export async function ojsCompile( if (outputVal === "all") { attrs.push(`output="all"`); } - if (cell.options?.[kCellLstCap]) { - attrs.push(`caption="${cell.options?.[kCellLstCap]}"`); - } const { classes, attrs: otherAttrs, diff --git a/src/format/html/format-html-bootstrap.ts b/src/format/html/format-html-bootstrap.ts index 8f552f2802b..ea11b4c8a0a 100644 --- a/src/format/html/format-html-bootstrap.ts +++ b/src/format/html/format-html-bootstrap.ts @@ -277,10 +277,10 @@ function bootstrapHtmlPostprocessor( for (let j = 0; j < images.length; j++) { (images[j] as Element).classList.add("figure-img"); } - const captions = figure.querySelectorAll("figcaption"); - for (let j = 0; j < captions.length; j++) { - (captions[j] as Element).classList.add("figure-caption"); - } + // const captions = figure.querySelectorAll("figcaption"); + // for (let j = 0; j < captions.length; j++) { + // (captions[j] as Element).classList.add("figure-caption"); + // } } // move the toc if there is a sidebar diff --git a/src/format/pdf/format-pdf.ts b/src/format/pdf/format-pdf.ts index b56fe854df6..42c6b76163b 100644 --- a/src/format/pdf/format-pdf.ts +++ b/src/format/pdf/format-pdf.ts @@ -374,14 +374,19 @@ function pdfLatexPostProcessor( lineProcessors.push(codeListAnnotationPostProcessor()); } - lineProcessors.push(longTableSidenoteProcessor()); + lineProcessors.push(tableSidenoteProcessor()); + // This is pass 1 await processLines(output, lineProcessors, temp); + + // This is pass 2; we need these to happen after the first pass + const pass2Processors: LineProcessor[] = [ + longTableSidenoteProcessor(), + ]; if (Object.keys(renderedCites).length > 0) { - await processLines(output, [ - placePandocBibliographyEntries(renderedCites), - ], temp); + pass2Processors.push(placePandocBibliographyEntries(renderedCites)); } + await processLines(output, pass2Processors, temp); }; } @@ -610,7 +615,7 @@ const captionFootnoteLineProcessor = () => { } case "capturing": capturedLines.push(line); - if (line.match(/^\\end{figure}$/)) { + if (line.match(/^\\end{figure}%*$/)) { state = "scanning"; // read the whole figure and clear any capture state @@ -626,96 +631,118 @@ const captionFootnoteLineProcessor = () => { }; }; -const processLongTableSidenotes = (latexLongTable: string) => { - const sideNoteMarker = "\\sidenote{\\footnotesize "; - let strProcessing = latexLongTable; - const strOutput: string[] = []; - const sidenotes: string[] = []; +const processSideNotes = (endMarker: string) => { + return (latexLongTable: string) => { + const sideNoteMarker = "\\sidenote{\\footnotesize "; + let strProcessing = latexLongTable; + const strOutput: string[] = []; + const sidenotes: string[] = []; - let sidenotePos = strProcessing.indexOf(sideNoteMarker); - while (sidenotePos > -1) { - strOutput.push(strProcessing.substring(0, sidenotePos)); + let sidenotePos = strProcessing.indexOf(sideNoteMarker); + while (sidenotePos > -1) { + strOutput.push(strProcessing.substring(0, sidenotePos)); - const remainingStr = strProcessing.substring( - sidenotePos + sideNoteMarker.length, - ); - let escaped = false; - let sideNoteEnd = -1; - for (let i = 0; i < remainingStr.length; i++) { - const ch = remainingStr[i]; - if (ch === "\\") { - escaped = true; - } else { - if (!escaped && ch === "}") { - sideNoteEnd = i; - break; + const remainingStr = strProcessing.substring( + sidenotePos + sideNoteMarker.length, + ); + let escaped = false; + let sideNoteEnd = -1; + for (let i = 0; i < remainingStr.length; i++) { + const ch = remainingStr[i]; + if (ch === "\\") { + escaped = true; } else { - escaped = false; + if (!escaped && ch === "}") { + sideNoteEnd = i; + break; + } else { + escaped = false; + } } } - } - if (sideNoteEnd > -1) { - strOutput.push("\\sidenotemark{}"); - const contents = remainingStr.substring(0, sideNoteEnd); - sidenotes.push(contents); - strProcessing = remainingStr.substring(sideNoteEnd + 1); - sidenotePos = strProcessing.indexOf(sideNoteMarker); - } else { - strOutput.push(remainingStr); + if (sideNoteEnd > -1) { + strOutput.push("\\sidenotemark{}"); + const contents = remainingStr.substring(0, sideNoteEnd); + sidenotes.push(contents); + strProcessing = remainingStr.substring(sideNoteEnd + 1); + sidenotePos = strProcessing.indexOf(sideNoteMarker); + } else { + strOutput.push(remainingStr); + } } - } - // Ensure that we inject sidenotes after the longtable - const endTable = "\\end{longtable}"; - const endPos = strProcessing.indexOf(endTable); - const prefix = strProcessing.substring(0, endPos + endTable.length); - const suffix = strProcessing.substring( - endPos + endTable.length, - strProcessing.length, - ); + // Ensure that we inject sidenotes after the longtable + const endTable = endMarker; + const endPos = strProcessing.indexOf(endTable); + const prefix = strProcessing.substring(0, endPos + endTable.length); + const suffix = strProcessing.substring( + endPos + endTable.length, + strProcessing.length, + ); - strOutput.push(prefix); - for (const note of sidenotes) { - strOutput.push(`\\sidenotetext{${note}}\n`); - } - if (suffix) { - strOutput.push(suffix); - } + strOutput.push(prefix); + for (const note of sidenotes) { + strOutput.push(`\\sidenotetext{${note}}\n`); + } + if (suffix) { + strOutput.push(suffix); + } - return strOutput.join(""); + return strOutput.join(""); + }; }; -const longTableSidenoteProcessor = () => { - let state: "scanning" | "capturing" = "scanning"; - let capturedLines: string[] = []; - return (line: string): string | undefined => { - switch (state) { - case "scanning": - if (line.match(/^\\begin{longtable}.*$/)) { - state = "capturing"; - capturedLines = [line]; - return undefined; - } else { - return line; - } - case "capturing": - capturedLines.push(line); - if (line.match(/\\end{longtable}/)) { - state = "scanning"; - - // read the whole figure and clear any capture state - const lines = capturedLines.join("\n"); - capturedLines = []; +const processLongTableSidenotes = processSideNotes("\\end{longtable}"); +const processTableSidenotes = processSideNotes("\\end{table}"); - // Process the captions and relocate footnotes - return processLongTableSidenotes(lines); - } else { - return undefined; - } - } +const sideNoteProcessor = ( + beginRegex: RegExp, + endRegex: RegExp, + callback: (str: string) => string, +) => { + return () => { + let state: "scanning" | "capturing" = "scanning"; + let capturedLines: string[] = []; + return (line: string): string | undefined => { + switch (state) { + case "scanning": + if (line.match(beginRegex)) { + state = "capturing"; + capturedLines = [line]; + return undefined; + } else { + return line; + } + case "capturing": + capturedLines.push(line); + if (line.match(endRegex)) { + state = "scanning"; + + // read the whole figure and clear any capture state + const lines = capturedLines.join("\n"); + capturedLines = []; + + // Process the captions and relocate footnotes + return callback(lines); + } else { + return undefined; + } + } + }; }; }; +const longTableSidenoteProcessor = sideNoteProcessor( + /^\\begin{longtable}.*$/, + /^\\end{longtable}%*$/, + processLongTableSidenotes, +); + +const tableSidenoteProcessor = sideNoteProcessor( + /^\\begin{table}.*$/, + /^\\end{table}%*$/, + processTableSidenotes, +); const calloutFloatHoldLineProcessor = () => { let state: "scanning" | "replacing" = "scanning"; @@ -846,7 +873,10 @@ const longtableBottomCaptionProcessor = () => { capturing = !line.match(/\\tabularnewline$/); return undefined; } else { - if (line.match(/^\\caption.*?\\tabularnewline$/)) { + if ( + line.match(/^\\caption.*?\\tabularnewline$/) || + line.match(/^\\caption{.*}\\\\$/) + ) { caption = line; return undefined; } else if (line.match(/^\\caption.*?/)) { diff --git a/src/format/reveal/format-reveal.ts b/src/format/reveal/format-reveal.ts index 54e33502fae..2e8441e1ca1 100644 --- a/src/format/reveal/format-reveal.ts +++ b/src/format/reveal/format-reveal.ts @@ -799,7 +799,7 @@ function applyStretch(doc: Document, autoStretch: boolean) { imageEl.classList.add("r-stretch"); } - // If is not a direct child of
, move it + // If is not a direct child of
, move it if ( hasStretchClass(imageEl) && imageEl.parentNode?.nodeName !== "SECTION" diff --git a/src/import_map.json b/src/import_map.json index 31213499076..edad2c6f071 100644 --- a/src/import_map.json +++ b/src/import_map.json @@ -46,6 +46,9 @@ "observablehq/parser": "https://cdn.skypack.dev/@observablehq/parser@4.5.0", "js-yaml": "https://cdn.skypack.dev/js-yaml@4.1.0", + "slimdom": "https://cdn.skypack.dev/slimdom@4.2.0", + "fontoxpath": "https://cdn.skypack.dev/fontoxpath@3.29.1", + "https://deno.land/std@0.161.0/": "https://deno.land/std@0.185.0/", "https://deno.land/std@0.101.0/": "https://deno.land/std@0.185.0/", "https://deno.land/std@0.105.0/": "https://deno.land/std@0.185.0/", diff --git a/src/resources/editor/tools/vs-code.mjs b/src/resources/editor/tools/vs-code.mjs index e4255752649..5d66419b3b2 100644 --- a/src/resources/editor/tools/vs-code.mjs +++ b/src/resources/editor/tools/vs-code.mjs @@ -12066,6 +12066,47 @@ var require_yaml_intelligence_resources = __commonJS({ object: { closed: true, properties: { + custom: { + arrayOf: { + object: { + description: "A custom cross reference type.", + closed: true, + properties: { + kind: { + enum: [ + "float" + ], + description: 'The kind of cross reference (currently only "float" is supported).' + }, + prefix: { + string: { + description: "The prefix used in rendered citations when referencing this type." + } + }, + name: { + string: { + description: "The prefix used in captions when referencing this type." + } + }, + "ref-type": { + string: { + description: 'The prefix string used in references ("dia-", etc.) when referencing this type.' + } + }, + "latex-env": { + string: { + description: "The name of the custom LaTeX environment that quarto will use to create this type of crossreferenceable object in LaTeX output." + } + }, + "latex-list-of-name": { + string: { + description: 'The name of the custom LaTeX "list of" command that quarto will use to create this type of crossreferenceable object in LaTeX output.' + } + } + } + } + } + }, chapters: { boolean: { description: "Use top level sections (H1) in this document as chapters.", @@ -20176,6 +20217,13 @@ var require_yaml_intelligence_resources = __commonJS({ }, "Configuration for document commenting.", "Configuration for crossref labels and prefixes.", + "A custom cross reference type.", + "The kind of cross reference (currently only \u201CFloat\u201D is\nsupported).", + "The prefix used in rendered citations when referencing this type.", + "The prefix used in captions when referencing this type.", + "The prefix string used in references (\u201Cdia-\u201D, etc.) when referencing\nthis type.", + "The name of the custom LaTeX environment that quarto will use to\ncreate this type of crossreferenceable object in LaTeX output.", + "The name of the custom LaTeX \u201Clist of\u201D command that quarto will use\nto create this type of crossreferenceable object in LaTeX output.", "Use top level sections (H1) in this document as chapters.", "The delimiter used between the prefix and the caption.", "The title prefix used for figure captions.", @@ -22020,12 +22068,12 @@ var require_yaml_intelligence_resources = __commonJS({ mermaid: "%%" }, "handlers/mermaid/schema.yml": { - _internalId: 164461, + _internalId: 165772, type: "object", description: "be an object", properties: { "mermaid-format": { - _internalId: 164453, + _internalId: 165764, type: "enum", enum: [ "png", @@ -22041,7 +22089,7 @@ var require_yaml_intelligence_resources = __commonJS({ exhaustiveCompletions: true }, theme: { - _internalId: 164460, + _internalId: 165771, type: "anyOf", anyOf: [ { diff --git a/src/resources/editor/tools/yaml/web-worker.js b/src/resources/editor/tools/yaml/web-worker.js index 195095b1b05..9d2bb72a56a 100644 --- a/src/resources/editor/tools/yaml/web-worker.js +++ b/src/resources/editor/tools/yaml/web-worker.js @@ -12067,6 +12067,47 @@ try { object: { closed: true, properties: { + custom: { + arrayOf: { + object: { + description: "A custom cross reference type.", + closed: true, + properties: { + kind: { + enum: [ + "float" + ], + description: 'The kind of cross reference (currently only "float" is supported).' + }, + prefix: { + string: { + description: "The prefix used in rendered citations when referencing this type." + } + }, + name: { + string: { + description: "The prefix used in captions when referencing this type." + } + }, + "ref-type": { + string: { + description: 'The prefix string used in references ("dia-", etc.) when referencing this type.' + } + }, + "latex-env": { + string: { + description: "The name of the custom LaTeX environment that quarto will use to create this type of crossreferenceable object in LaTeX output." + } + }, + "latex-list-of-name": { + string: { + description: 'The name of the custom LaTeX "list of" command that quarto will use to create this type of crossreferenceable object in LaTeX output.' + } + } + } + } + } + }, chapters: { boolean: { description: "Use top level sections (H1) in this document as chapters.", @@ -20177,6 +20218,13 @@ try { }, "Configuration for document commenting.", "Configuration for crossref labels and prefixes.", + "A custom cross reference type.", + "The kind of cross reference (currently only \u201CFloat\u201D is\nsupported).", + "The prefix used in rendered citations when referencing this type.", + "The prefix used in captions when referencing this type.", + "The prefix string used in references (\u201Cdia-\u201D, etc.) when referencing\nthis type.", + "The name of the custom LaTeX environment that quarto will use to\ncreate this type of crossreferenceable object in LaTeX output.", + "The name of the custom LaTeX \u201Clist of\u201D command that quarto will use\nto create this type of crossreferenceable object in LaTeX output.", "Use top level sections (H1) in this document as chapters.", "The delimiter used between the prefix and the caption.", "The title prefix used for figure captions.", @@ -22021,12 +22069,12 @@ try { mermaid: "%%" }, "handlers/mermaid/schema.yml": { - _internalId: 164461, + _internalId: 165772, type: "object", description: "be an object", properties: { "mermaid-format": { - _internalId: 164453, + _internalId: 165764, type: "enum", enum: [ "png", @@ -22042,7 +22090,7 @@ try { exhaustiveCompletions: true }, theme: { - _internalId: 164460, + _internalId: 165771, type: "anyOf", anyOf: [ { diff --git a/src/resources/editor/tools/yaml/yaml-intelligence-resources.json b/src/resources/editor/tools/yaml/yaml-intelligence-resources.json index 9b0595e3219..3951aa6c43b 100644 --- a/src/resources/editor/tools/yaml/yaml-intelligence-resources.json +++ b/src/resources/editor/tools/yaml/yaml-intelligence-resources.json @@ -5038,6 +5038,47 @@ "object": { "closed": true, "properties": { + "custom": { + "arrayOf": { + "object": { + "description": "A custom cross reference type.", + "closed": true, + "properties": { + "kind": { + "enum": [ + "float" + ], + "description": "The kind of cross reference (currently only \"float\" is supported)." + }, + "prefix": { + "string": { + "description": "The prefix used in rendered citations when referencing this type." + } + }, + "name": { + "string": { + "description": "The prefix used in captions when referencing this type." + } + }, + "ref-type": { + "string": { + "description": "The prefix string used in references (\"dia-\", etc.) when referencing this type." + } + }, + "latex-env": { + "string": { + "description": "The name of the custom LaTeX environment that quarto will use to create this type of crossreferenceable object in LaTeX output." + } + }, + "latex-list-of-name": { + "string": { + "description": "The name of the custom LaTeX \"list of\" command that quarto will use to create this type of crossreferenceable object in LaTeX output." + } + } + } + } + } + }, "chapters": { "boolean": { "description": "Use top level sections (H1) in this document as chapters.", @@ -13148,6 +13189,13 @@ }, "Configuration for document commenting.", "Configuration for crossref labels and prefixes.", + "A custom cross reference type.", + "The kind of cross reference (currently only “Float” is\nsupported).", + "The prefix used in rendered citations when referencing this type.", + "The prefix used in captions when referencing this type.", + "The prefix string used in references (“dia-”, etc.) when referencing\nthis type.", + "The name of the custom LaTeX environment that quarto will use to\ncreate this type of crossreferenceable object in LaTeX output.", + "The name of the custom LaTeX “list of” command that quarto will use\nto create this type of crossreferenceable object in LaTeX output.", "Use top level sections (H1) in this document as chapters.", "The delimiter used between the prefix and the caption.", "The title prefix used for figure captions.", @@ -14992,12 +15040,12 @@ "mermaid": "%%" }, "handlers/mermaid/schema.yml": { - "_internalId": 164461, + "_internalId": 165772, "type": "object", "description": "be an object", "properties": { "mermaid-format": { - "_internalId": 164453, + "_internalId": 165764, "type": "enum", "enum": [ "png", @@ -15013,7 +15061,7 @@ "exhaustiveCompletions": true }, "theme": { - "_internalId": 164460, + "_internalId": 165771, "type": "anyOf", "anyOf": [ { diff --git a/src/resources/extensions/quarto/confluence/_extension.yml b/src/resources/extensions/quarto/confluence/_extension.yml index 10c0c0cfaed..a91c53195c9 100644 --- a/src/resources/extensions/quarto/confluence/_extension.yml +++ b/src/resources/extensions/quarto/confluence/_extension.yml @@ -16,6 +16,7 @@ contributes: publish: writer: publish.lua output-ext: xml + quarto-custom-format: confluence html: theme: [default, theme.scss] code-line-numbers: true diff --git a/src/resources/extensions/quarto/docusaurus/docusaurus_renderers.lua b/src/resources/extensions/quarto/docusaurus/docusaurus_renderers.lua index c5f8adf2636..aa5260331e7 100644 --- a/src/resources/extensions/quarto/docusaurus/docusaurus_renderers.lua +++ b/src/resources/extensions/quarto/docusaurus/docusaurus_renderers.lua @@ -19,4 +19,6 @@ local codeBlock = require('docusaurus_utils').codeBlock -- return admonition -- end) -return {} -- return an empty table as a hack to pretend we're a shortcode handler for now \ No newline at end of file +-- luacov: disable +return {} -- return an empty table as a hack to pretend we're a shortcode handler for now +-- luacov: enable \ No newline at end of file diff --git a/src/resources/extensions/quarto/kbd/kbd.lua b/src/resources/extensions/quarto/kbd/kbd.lua index 74d0d0a4072..76fea0e88f3 100644 --- a/src/resources/extensions/quarto/kbd/kbd.lua +++ b/src/resources/extensions/quarto/kbd/kbd.lua @@ -64,7 +64,9 @@ return { else -- all kwargs if n_kwargs == 0 then + -- luacov: disable error("kbd requires at least one argument") + -- luacov: enable else for k, v in pairs(kwargs) do table.insert(result, pandoc.Code(pandoc.utils.stringify(v))) diff --git a/src/resources/extensions/quarto/video/video.lua b/src/resources/extensions/quarto/video/video.lua index 1c18b7681ba..04915205e61 100644 --- a/src/resources/extensions/quarto/video/video.lua +++ b/src/resources/extensions/quarto/video/video.lua @@ -328,7 +328,9 @@ return { if #raw_args > 0 then srcValue = pandoc.utils.stringify(raw_args[1]) else + -- luacov: disable fail("No video source specified for video shortcode") + -- luacov: enable end end diff --git a/src/resources/filters/ast/customnodes.lua b/src/resources/filters/ast/customnodes.lua index afda06e3f90..729a10b1445 100644 --- a/src/resources/filters/ast/customnodes.lua +++ b/src/resources/filters/ast/customnodes.lua @@ -1,7 +1,7 @@ -- customnodes.lua -- support for custom nodes in quarto's emulated ast -- --- Copyright (C) 2022 by RStudio, PBC +-- Copyright (C) 2023 Posit Software, PBC local handlers = {} @@ -9,6 +9,17 @@ local custom_node_data = pandoc.List({}) local n_custom_nodes = 0 local profiler = require('profiler') +function scaffold(node) + local pt = pandoc.utils.type(node) + if pt == "Blocks" then + return pandoc.Div(node, {"", {"quarto-scaffold"}}) + elseif pt == "Inlines" then + return pandoc.Span(node, {"", {"quarto-scaffold"}}) + else + return node + end +end + function is_custom_node(node) if node.attributes and node.attributes.__quarto_custom == "true" then return node @@ -37,6 +48,19 @@ function run_emulated_filter(doc, filter) end end + local function checked_walk(node, filter_param) + if node.walk == nil then + if #node == 0 then -- empty node + return node + else + -- luacov: disable + internal_error() + -- luacov: enable + end + end + return node:walk(filter_param) + end + -- performance: if filter is empty, do nothing if sz == 0 then return doc @@ -65,11 +89,28 @@ function run_emulated_filter(doc, filter) return result end + ::regular:: + -- if user passed a table corresponding to the custom node instead + -- of the custom node, then first we will get the actual node + if doc.__quarto_custom_node ~= nil then + doc = doc.__quarto_custom_node + needs_custom = true + end + local is_custom = is_custom_node(doc) if not needs_custom or (not is_custom and filter._is_wrapped) then - local result, recurse = doc:walk(filter) + if doc.walk == nil then + if #doc == 0 then -- empty doc + return doc + else + -- luacov: disable + internal_error() + -- luacov: enable + end + end + local result, recurse = checked_walk(doc, filter) if in_filter then profiler.category = "" end @@ -94,30 +135,17 @@ function run_emulated_filter(doc, filter) Inline = "CustomInline" } local filter_fn = filter[t] or filter[node_type[kind]] or filter.Custom - if filter_fn ~= nil then - local result = filter_fn(custom_data, custom_node) + local result, recurse = filter_fn(custom_data, custom_node) if result == nil then - return nil + return nil, recurse end -- do the user a kindness and unwrap the result if it's a custom node if type(result) == "table" and result.__quarto_custom_node ~= nil then - return result.__quarto_custom_node + return result.__quarto_custom_node, recurse end - return result - end - end - - if is_custom then - local custom_data, t, kind = _quarto.ast.resolve_custom_data(doc) - local result, recurse = process_custom_preamble(custom_data, t, kind, doc) - if in_filter then - profiler.category = "" + return result, recurse end - if result == nil then - result = doc - end - return result, recurse end function wrapped_filter.Div(node) @@ -125,7 +153,10 @@ function run_emulated_filter(doc, filter) local custom_data, t, kind = _quarto.ast.resolve_custom_data(node) -- here, if the node is actually an inline, -- it's ok, because Pandoc will wrap it in a Plain - return process_custom_preamble(custom_data, t, kind, custom) + return process_custom_preamble(custom_data, t, kind, node) + end + if node.attributes.__quarto_custom_scaffold == "true" then + return nil end if filter.Div ~= nil then return filter.Div(node) @@ -138,10 +169,15 @@ function run_emulated_filter(doc, filter) local custom_data, t, kind = _quarto.ast.resolve_custom_data(node) -- only follow through if node matches the expected kind if kind == "Inline" then - return process_custom_preamble(custom_data, t, kind, custom) + return process_custom_preamble(custom_data, t, kind, node) end + -- luacov: disable fatal("Custom node of type " .. t .. " is not an inline, but found in an inline context") return nil + -- luacov: enable + end + if node.attributes.__quarto_custom_scaffold == "true" then + return nil end if filter.Span ~= nil then return filter.Span(node) @@ -149,7 +185,20 @@ function run_emulated_filter(doc, filter) return nil end - return doc:walk(wrapped_filter) + if is_custom then + local custom_data, t, kind = _quarto.ast.resolve_custom_data(doc) + local result, recurse = process_custom_preamble(custom_data, t, kind, doc) + if in_filter then + profiler.category = "" + end + if result ~= nil then + doc = result + end + if recurse == false then + return doc, recurse + end + end + return checked_walk(doc, wrapped_filter) end function create_custom_node_scaffold(t, context) @@ -159,7 +208,9 @@ function create_custom_node_scaffold(t, context) elseif context == "Inline" then result = pandoc.Span({}) else + -- luacov: disable fatal("Invalid context for custom node: " .. context) + -- luacov: enable end n_custom_nodes = n_custom_nodes + 1 local id = tostring(n_custom_nodes) @@ -184,19 +235,13 @@ _quarto.ast = { custom_node_data = custom_node_data, create_custom_node_scaffold = create_custom_node_scaffold, - -- FIXME WE NEED TO REDO THIS WITH PROXY OBJECTS - -- - -- -- this is used in non-lua filters to handle custom nodes - -- reset_custom_tbl = function(tbl) - -- custom_node_data = tbl - -- n_custom_nodes = #tbl - -- end, - grow_scaffold = function(node, size) local n = #node.content local ctor = pandoc[node.t or pandoc.utils.type(node)] for _ = n + 1, size do - node.content:insert(ctor({})) + local scaffold = ctor({}) + scaffold.attributes.__quarto_custom_scaffold = "true" + node.content:insert(scaffold) end end, @@ -245,6 +290,7 @@ _quarto.ast = { end local node = node_accessor(table) local t = pandoc.utils.type(value) + -- FIXME this is broken; that can only be "Block", "Inline", etc if t == "Div" or t == "Span" then local custom_data, t, kind = _quarto.ast.resolve_custom_data(value) if custom_data ~= nil then @@ -254,7 +300,12 @@ _quarto.ast = { if index > #node.content then _quarto.ast.grow_scaffold(node, index) end - node.content[index].content = value + local pt = pandoc.utils.type(value) + if pt == "Block" or pt == "Inline" then + node.content[index].content = {value} + else + node.content[index].content = value + end end } end, @@ -288,9 +339,11 @@ _quarto.ast = { local n = div_or_span.attributes.__quarto_custom_id local kind = div_or_span.attributes.__quarto_custom_context local handler = _quarto.ast.resolve_handler(t) + -- luacov: disable if handler == nil then fatal("Internal Error: handler not found for custom node " .. t) end + -- luacov: enable local custom_data = _quarto.ast.custom_node_data[n] custom_data["__quarto_custom_node"] = div_or_span @@ -300,11 +353,15 @@ _quarto.ast = { add_handler = function(handler) local state = quarto_global_state.extended_ast_handlers if type(handler.constructor) == "nil" then + -- luacov: disable quarto.utils.dump(handler) fatal("Internal Error: extended ast handler must have a constructor") + -- luacov: enable elseif type(handler.class_name) == "nil" then + -- luacov: disable quarto.utils.dump(handler) fatal("handler must define class_name") + -- luacov: enable elseif type(handler.class_name) == "string" then state.namedHandlers[handler.class_name] = handler elseif type(handler.class_name) == "table" then @@ -312,8 +369,10 @@ _quarto.ast = { state.namedHandlers[name] = handler end else + -- luacov: disable quarto.utils.dump(handler) fatal("ERROR: class_name must be a string or an array of strings") + -- luacov: enable end local forwarder = { } @@ -329,7 +388,7 @@ _quarto.ast = { local tbl, need_emulation = handler.constructor(params) if need_emulation ~= false then - return create_emulated_node(handler.ast_name, tbl, handler.kind, forwarder), tbl + return create_emulated_node(handler.ast_name, tbl, handler.kind, forwarder) else tbl.t = handler.ast_name -- set t always to custom ast type custom_node_data[tbl.__quarto_custom_node.attributes.__quarto_custom_id] = tbl @@ -344,7 +403,9 @@ _quarto.ast = { add_renderer = function(name, condition, renderer) local handler = _quarto.ast.resolve_handler(name) if handler == nil then + -- luacov: disable fatal("Internal Error in add_renderer: handler not found for custom node " .. name) + -- luacov: enable end if handler.renderers == nil then handler.renderers = { } @@ -359,7 +420,11 @@ _quarto.ast = { if state.namedHandlers ~= nil then return state.namedHandlers[name] end + -- TODO: should we just fail here? We seem to be failing downstream of every nil + -- result anyway. + -- luacov: disable return nil + -- luacov: enable end, walk = run_emulated_filter, @@ -369,7 +434,14 @@ _quarto.ast = { local function custom_walk(node) local handler = quarto._quarto.ast.resolve_handler(node.t) if handler == nil then + -- luacov: disable fatal("Internal Error: handler not found for custom node " .. node.t) + -- luacov: enable + end + if handler.render == nil then + -- luacov: disable + fatal("Internal Error: handler for custom node " .. node.t .. " does not have a render function") + -- luacov: enable end return handler.render(node) end @@ -395,9 +467,13 @@ function construct_extended_ast_handler_state() quarto_global_state.extended_ast_handlers = state end + -- we currently don't have any handlers at startup, + -- so we disable coverage for this block + -- luacov: disable for _, handler in ipairs(handlers) do _quarto.ast.add_handler(handler) end + -- luacov: enable end construct_extended_ast_handler_state() \ No newline at end of file diff --git a/src/resources/filters/ast/render.lua b/src/resources/filters/ast/render.lua index a8170f9e625..6d68bc761d7 100644 --- a/src/resources/filters/ast/render.lua +++ b/src/resources/filters/ast/render.lua @@ -3,42 +3,62 @@ -- -- Copyright (C) 2022 by RStudio, PBC -function render_raw(raw) - local parts = split(raw.text) - local t = parts[1] - local n = tonumber(parts[2]) - local handler = _quarto.ast.resolve_handler(t) - if handler == nil then - fatal("Internal Error: handler not found for custom node " .. t) - end - local customNode = _quarto.ast.custom_node_data[n] - return handler.render(customNode) -end - function render_extended_nodes() - if string.find(FORMAT, ".lua$") then - return {} -- don't render in custom writers, so we can handle them in the custom writer code. + local function has_custom_nodes(node) + local has_custom_nodes = false + _quarto.ast.walk(node, { + Custom = function() + has_custom_nodes = true + end + }) + return has_custom_nodes end - return { - Custom = function(node) - local handler = _quarto.ast.resolve_handler(node.t) - if handler == nil then - fatal("Internal Error: handler not found for custom node " .. node.t) + local filter + + local function render_custom(node) + local function postprocess_render(render_result) + -- we need to recurse in case custom nodes render to other custom nodes + if is_custom_node(render_result) then + -- recurse directly + return render_custom(render_result) + elseif has_custom_nodes(render_result) then + -- recurse via the filter + return _quarto.ast.walk(render_result, filter) + else + return render_result end - if handler.renderers then - for _, renderer in ipairs(handler.renderers) do - if renderer.condition(node) then - return renderer.render(node) - end + end + if type(node) == "userdata" then + node = _quarto.ast.resolve_custom_data(node) + end + + local handler = _quarto.ast.resolve_handler(node.t) + if handler == nil then + -- luacov: disable + fatal("Internal Error: handler not found for custom node " .. node.t) + -- luacov: enable + end + if handler.renderers then + for _, renderer in ipairs(handler.renderers) do + if renderer.condition(node) then + return scaffold(postprocess_render(scaffold(renderer.render(node)))) end - quarto.utils.dump(node) - fatal("Internal Error: renderers table was exhausted without a match for custom node " .. node.t) - elseif handler.render ~= nil then - return handler.render(node) - else - fatal("Internal Error: handler for custom node " .. node.t .. " does not have a render function or renderers table") end + -- luacov: disable + fatal("Internal Error: renderers table was exhausted without a match for custom node " .. node.t) + -- luacov: enable + elseif handler.render ~= nil then + return scaffold(postprocess_render(scaffold(handler.render(node)))) + else + -- luacov: disable + fatal("Internal Error: handler for custom node " .. node.t .. " does not have a render function or renderers table") + -- luacov: enable end + end + + filter = { + Custom = render_custom } + return filter end \ No newline at end of file diff --git a/src/resources/filters/ast/runemulation.lua b/src/resources/filters/ast/runemulation.lua index a20b6d17599..0574369f385 100644 --- a/src/resources/filters/ast/runemulation.lua +++ b/src/resources/filters/ast/runemulation.lua @@ -7,12 +7,13 @@ local profiler = require('profiler') local function run_emulated_filter_chain(doc, filters, afterFilterPass, profiling) init_trace(doc) - -- print(os.clock(), " - starting") for i, v in ipairs(filters) do local function callback() if v.flags then if type(v.flags) ~= "table" then + -- luacov: disable fatal("filter " .. v.name .. " has invalid flags") + -- luacov: enable end local can_skip = true for _, index in ipairs(v.flags) do @@ -21,23 +22,26 @@ local function run_emulated_filter_chain(doc, filters, afterFilterPass, profilin end end if can_skip then - -- print(" - Skipping", v.name) return end end - -- print(os.clock(), " - running", v.name) + -- We don't seem to need coverage for profiling + -- luacov: disable if profiling then profiler.category = v.name end + -- luacov: enable doc = run_emulated_filter(doc, v.filter) add_trace(doc, v.name) + -- luacov: disable if profiling then profiler.category = "" end + -- luacov: enable end if v.filter.scriptFile then _quarto.withScriptFile(v.filter.scriptFile, callback) @@ -56,6 +60,7 @@ local function emulate_pandoc_filter(filters, afterFilterPass) local cached_paths local profiler + -- luacov: disable local function get_paths(tmpdir) if cached_paths then return cached_paths @@ -69,6 +74,7 @@ local function emulate_pandoc_filter(filters, afterFilterPass) paths_file:close() return cached_paths end + -- luacov: enable return { traverse = 'topdown', @@ -77,6 +83,7 @@ local function emulate_pandoc_filter(filters, afterFilterPass) if not profiling then return run_emulated_filter_chain(doc, filters, afterFilterPass), false end + -- luacov: disable if profiler == nil then profiler = require('profiler') end @@ -90,6 +97,7 @@ local function emulate_pandoc_filter(filters, afterFilterPass) return nil end) return doc, false + -- luacov: enable end } end @@ -112,7 +120,9 @@ function run_as_extended_ast(specTable) }) end else - print("Warning: filter " .. v.name .. " didn't declare filter or filters.") + -- luacov: disable + warn("filter " .. v.name .. " didn't declare filter or filters.") + -- luacov: enable end end diff --git a/src/resources/filters/ast/traceexecution.lua b/src/resources/filters/ast/traceexecution.lua index 49dec6e65fd..8f68c3e282e 100644 --- a/src/resources/filters/ast/traceexecution.lua +++ b/src/resources/filters/ast/traceexecution.lua @@ -5,6 +5,10 @@ local data = {} +-- don't test coverage for filter tracing +-- TODO but maybe we should? +-- +-- luacov: disable if os.getenv("QUARTO_TRACE_FILTERS") then function init_trace(doc) table.insert(data, { @@ -66,4 +70,6 @@ else end function end_trace() end -end \ No newline at end of file +end + +-- luacov: enable diff --git a/src/resources/filters/ast/wrappedwriter.lua b/src/resources/filters/ast/wrappedwriter.lua index e41cd4d4dd8..69cbb2afba7 100644 --- a/src/resources/filters/ast/wrappedwriter.lua +++ b/src/resources/filters/ast/wrappedwriter.lua @@ -128,7 +128,9 @@ function wrapped_writer() if tbl ~= nil then local astHandler = _quarto.ast.resolve_handler(t) if astHandler == nil then + -- luacov: disable fatal("Internal error: no handler for " .. t) + -- luacov: enable end local nodeHandler = astHandler and handler[astHandler.ast_name] and handler[astHandler.ast_name].handle if nodeHandler == nil then diff --git a/src/resources/filters/common/collate.lua b/src/resources/filters/common/collate.lua new file mode 100644 index 00000000000..e040063df1a --- /dev/null +++ b/src/resources/filters/common/collate.lua @@ -0,0 +1,26 @@ +-- collate.lua +-- Copyright (C) 2023 Posit Software, PBC + +-- improved formatting for dumping tables +function collate(lst, predicate) + local result = pandoc.List({}) + local current_block = pandoc.List({}) + for _, block in ipairs(lst) do + if #current_block == 0 then + current_block = pandoc.List({ block }) + else + if predicate(block, current_block[#current_block]) then + current_block:insert(block) + else + if #current_block > 0 then + result:insert(current_block) + end + current_block = pandoc.List({ block }) + end + end + end + if #current_block > 0 then + result:insert(current_block) + end + return result +end diff --git a/src/resources/filters/common/crossref.lua b/src/resources/filters/common/crossref.lua new file mode 100644 index 00000000000..b1285328914 --- /dev/null +++ b/src/resources/filters/common/crossref.lua @@ -0,0 +1,5 @@ +-- crossref.lua +-- Copyright (C) 2023 Posit Software, PBC +-- +-- common crossref functions/data + diff --git a/src/resources/filters/common/error.lua b/src/resources/filters/common/error.lua index d2b504c48a4..930af18b78d 100644 --- a/src/resources/filters/common/error.lua +++ b/src/resources/filters/common/error.lua @@ -1,15 +1,24 @@ -- debug.lua -- Copyright (C) 2020-2022 Posit Software, PBC -function fail(message) +-- luacov: disable +function fail_and_ask_for_bug_report(message) + fail(message .. "\nThis is a quarto bug. Please consider filing a bug report at https://github.com/quarto-dev/quarto-cli/issues", 5) +end + +function fail(message, level) local file = currentFile() if file then - fatal("An error occurred while processing '" .. file .. "':\n" .. message, 4) + fatal("An error occurred while processing '" .. file .. "':\n" .. message, level or 4) else - fatal("An error occurred:\n" .. message, 4) + fatal("An error occurred:\n" .. message, level or 4) end end +function internal_error() + fail("This is an internal error. Please file a bug report at https://github.com/quarto-dev/quarto-cli/", 5) +end + function currentFile() -- if we're in a multifile contatenated render, return which file we're rendering local fileState = currentFileMetadataState() @@ -23,3 +32,4 @@ function currentFile() return nil end end +-- luacov: enable diff --git a/src/resources/filters/common/figures.lua b/src/resources/filters/common/figures.lua index 98b1f41e10a..662164db091 100644 --- a/src/resources/filters/common/figures.lua +++ b/src/resources/filters/common/figures.lua @@ -27,15 +27,10 @@ function figAlignAttribute(el) return validatedAlign(align) end --- is this an image containing a figure -function isFigureImage(el) - return hasFigureRef(el) and #el.caption > 0 -end - -- is this a Div containing a figure function isFigureDiv(el) if el.t == "Div" and hasFigureRef(el) then - return refCaptionFromDiv(el) ~= nil + return el.attributes[kFigCap] ~= nil or refCaptionFromDiv(el) ~= nil else return discoverLinkedFigureDiv(el) ~= nil end @@ -78,58 +73,6 @@ function discoverLinkedFigure(el, captionRequired) return nil end -function createFigureDiv(paraEl, fig) - flags.has_figure_divs = true - - -- create figure div - local figureDiv = pandoc.Div({}) - - -- transfer identifier - figureDiv.attr.identifier = fig.attr.identifier - fig.attr.identifier = "" - - -- provide anonymous identifier if necessary - if figureDiv.attr.identifier == "" then - figureDiv.attr.identifier = anonymousFigId() - end - - -- transfer classes - figureDiv.attr.classes = fig.attr.classes:clone() - tclear(fig.attr.classes) - - -- transfer fig. attributes - for k,v in pairs(fig.attr.attributes) do - if isFigAttribute(k) then - figureDiv.attr.attributes[k] = v - end - end - local attribs = tkeys(fig.attr.attributes) - for _,k in ipairs(attribs) do - if isFigAttribute(k) then - fig.attr.attributes[k] = v - end - end - - -- collect caption - local caption = fig.caption:clone() - fig.caption = {} - - -- if the image is a .tex file we need to tex \input - if latexIsTikzImage(fig) then - paraEl = pandoc.walk_block(paraEl, { - Image = latexFigureInline - }) - end - - -- insert the paragraph and a caption paragraph - figureDiv.content:insert(paraEl) - figureDiv.content:insert(pandoc.Para(caption)) - - -- return the div - return figureDiv - -end - function discoverLinkedFigureDiv(el, captionRequired) if el.t == "Div" and hasFigureRef(el) and @@ -156,8 +99,6 @@ function isReferenceableFig(figEl) not isAnonymousFigId(figEl.attr.identifier) end - - function latexIsTikzImage(image) return _quarto.format.isLatexOutput() and string.find(image.src, "%.tex$") end diff --git a/src/resources/filters/common/floats.lua b/src/resources/filters/common/floats.lua new file mode 100644 index 00000000000..0a9396272ce --- /dev/null +++ b/src/resources/filters/common/floats.lua @@ -0,0 +1,25 @@ +-- floats.lua +-- Copyright (C) 2023 Posit Software, PBC + +-- constants for float attributes +local kFloatAlignSuffix = "-align" +-- local kEnvSuffix = "-env" +-- local kAltSuffix = "-alt" +-- local kPosSuffix = "-pos" +-- local kCapSuffix = "-cap" +-- local kScapSuffix = "-scap" +-- local kResizeWidth = "resize.width" +-- local kResizeHeight = "resize.height" + +function align_attribute(float) + local prefix = refType(float.identifier) + local attr_key = prefix .. kFloatAlignSuffix + local default = pandoc.utils.stringify( + param(attr_key, pandoc.Str("default")) + ) + local align = attribute(float, attr_key, default) + if align == "default" then + align = default + end + return validatedAlign(align) +end \ No newline at end of file diff --git a/src/resources/filters/common/layout.lua b/src/resources/filters/common/layout.lua index 75f2713081d..c7b6d0ac718 100644 --- a/src/resources/filters/common/layout.lua +++ b/src/resources/filters/common/layout.lua @@ -7,22 +7,28 @@ kLayoutNcol = "layout-ncol" kLayoutNrow = "layout-nrow" kLayout = "layout" - -function layoutAlignAttribute(el, default) - return validatedAlign(attribute(el, kLayoutAlign, default)) +function layout_align_attribute(el_with_attr, default) + return validatedAlign(el_with_attr.attributes[kLayoutAlign] or default) end -function layoutVAlignAttribute(el, default) - return validatedVAlign(attribute(el, kLayoutVAlign, default)) +-- now unused. Remove? +-- luacov: disable +function layout_valign_attribute(el_with_attr, default) + return validatedVAlign(el_with_attr.attributes[kLayoutVAlign] or default) end +-- luacov: enable -function hasLayoutAttributes(el) - local attribs = tkeys(el.attr.attributes) +function attr_has_layout_attributes(attr) + local attribs = tkeys(attr.attributes) return attribs:includes(kLayoutNrow) or attribs:includes(kLayoutNcol) or attribs:includes(kLayout) end +function hasLayoutAttributes(el) + return attr_has_layout_attributes(el.attr) +end + function isLayoutAttribute(key) return key == kLayoutNrow or key == kLayoutNcol or @@ -49,11 +55,13 @@ end -- we often wrap a table in a div, unwrap it function tableFromLayoutCell(cell) - if #cell.content == 1 and cell.content[1].t == "Table" then - return cell.content[1] - else - return nil - end + local tbl + cell:walk({ + Table = function(t) + tbl = t + end + }) + return tbl end -- resolve alignment for layout cell (default to center or left depending @@ -72,19 +80,6 @@ function layoutCellAlignment(cell, align) end end --- does the layout cell have a ref parent -function layoutCellHasRefParent(cell) - if hasRefParent(cell) then - return true - else - local image = figureImageFromLayoutCell(cell) - if image then - return hasRefParent(image) - end - end - return false -end - function sizeToPercent(size) if size then local percent = string.match(size, "^([%d%.]+)%%$") diff --git a/src/resources/filters/common/log.lua b/src/resources/filters/common/log.lua index d8212fb9213..584358be1fe 100644 --- a/src/resources/filters/common/log.lua +++ b/src/resources/filters/common/log.lua @@ -5,6 +5,7 @@ -- could write to named filed (e.g. .filter.log) and client could read warnings and delete (also delete before run) -- always append b/c multiple filters +-- luacov: disable local function caller_info(offset) offset = offset or 3 local caller = debug.getinfo(offset, "lS") @@ -28,4 +29,4 @@ function fatal(message, offset) -- TODO write stack trace into log, and then exit. crash_with_stack_trace() end - +-- luacov: enable \ No newline at end of file diff --git a/src/resources/filters/common/pandoc.lua b/src/resources/filters/common/pandoc.lua index 378bcc4c3a3..85f281ad262 100644 --- a/src/resources/filters/common/pandoc.lua +++ b/src/resources/filters/common/pandoc.lua @@ -1,21 +1,16 @@ -- pandoc.lua -- Copyright (C) 2020-2022 Posit Software, PBC +local readqmd = require("readqmd") + function hasBootstrap() local hasBootstrap = param("has-bootstrap", false) return hasBootstrap end - -- read attribute w/ default function attribute(el, name, default) - -- FIXME: Doesn't attributes respond to __index? - for k,v in pairs(el.attributes) do - if k == name then - return v - end - end - return default + return el.attributes[name] or default end function removeClass(classes, remove) @@ -71,18 +66,26 @@ function combineFilters(filters) end function inlinesToString(inlines) + local pt = pandoc.utils.type(inlines) + if pt ~= "Inlines" then + -- luacov: disable + fail("inlinesToString: expected Inlines, got " .. pt) + return "" + -- luacov: enable + end return pandoc.utils.stringify(pandoc.Span(inlines)) end -- lua string to pandoc inlines function stringToInlines(str) if str then - return pandoc.List({pandoc.Str(str)}) + return pandoc.Inlines({pandoc.Str(str)}) else - return pandoc.List({}) + return pandoc.Inlines({}) end end +-- FIXME we should no longer be using this. -- lua string with markdown to pandoc inlines function markdownToInlines(str) if str then @@ -170,20 +173,48 @@ function compileTemplate(template, meta) return renderedDoc.blocks else + -- luacov: disable fail('Error compiling template: ' .. template) + -- luacov: enable end end -local md_shortcode = require("lpegshortcode") --- FIXME pick a better name for this. -function string_to_quarto_ast_blocks(text) - local after_shortcodes = md_shortcode.md_shortcode:match(text) or "" - local after_reading = pandoc.read(after_shortcodes, "markdown") +function merge_attrs(attr, ...) + local result = pandoc.Attr(attr.identifier, attr.classes, attr.attributes) + for _, a in ipairs({...}) do + if a ~= nil then + result.identifier = result.identifier or a.identifier + result.classes:extend(a.classes) + for k, v in pairs(a.attributes) do + result.attributes[k] = v + end + end + end + return result +end + +-- used to convert metatable, attributetable, etc +-- to plain tables that can be serialized to JSON +function as_plain_table(value) + local result = {} + for k, v in pairs(value) do + result[k] = v + end + return result +end + +function string_to_quarto_ast_blocks(text, opts) + local doc = readqmd.readqmd(text, opts or quarto_global_state.reader_options) - -- FIXME we should run the whole normalization pipeline here - local after_parsing = after_reading:walk(parse_extended_nodes()):walk(compute_flags()) - return after_parsing.blocks + -- run the whole normalization pipeline here to get extended AST nodes, etc. + for _, filter in ipairs(quarto_ast_pipeline()) do + doc = doc:walk(filter.filter) + end + + -- compute flags so we don't skip filters that depend on them + doc:walk(compute_flags()) + return doc.blocks end function string_to_quarto_ast_inlines(text, sep) diff --git a/src/resources/filters/common/refs.lua b/src/resources/filters/common/refs.lua index 9c08e042a32..10e62fb1645 100644 --- a/src/resources/filters/common/refs.lua +++ b/src/resources/filters/common/refs.lua @@ -6,16 +6,21 @@ kRefParent = "ref-parent" -- does this element have a figure label? function hasFigureRef(el) - return isFigureRef(el.attr.identifier) + return isFigureRef(el.identifier) end function isFigureRef(identifier) - return (identifier ~= nil) and string.find(identifier, "^fig%-") + if identifier == nil then + return nil + end + + local ref = refType(identifier) + return crossref.categories.by_ref_type[ref] ~= nil end -- does this element have a table label? function hasTableRef(el) - return isTableRef(el.attr.identifier) + return isTableRef(el.identifier) end function isTableRef(identifier) @@ -24,18 +29,11 @@ end -- does this element support sub-references function hasFigureOrTableRef(el) - return el.attr and (hasFigureRef(el) or hasTableRef(el)) -end - - -function isRefParent(el) - return el.t == "Div" and - (hasFigureRef(el) or hasTableRef(el)) and - refCaptionFromDiv(el) ~= nil + return hasFigureRef(el) or hasTableRef(el) end function hasRefParent(el) - return el.attr.attributes[kRefParent] ~= nil + return el.attributes[kRefParent] ~= nil end function refType(id) @@ -62,31 +60,4 @@ end function emptyCaption() return pandoc.Str("") -end - -function hasSubRefs(divEl, type) - if hasFigureOrTableRef(divEl) and not hasRefParent(divEl) then - -- children w/ parent id - local found = false - local function checkForParent(el) - if not found then - if hasRefParent(el) then - if not type or (refType(el.attr.identifier) == type) then - found = true - end - end - - end - end - _quarto.ast.walk(divEl, { - Div = checkForParent, - Image = checkForParent - }) - return found - else - return false - end -end - - - +end \ No newline at end of file diff --git a/src/resources/filters/common/string.lua b/src/resources/filters/common/string.lua index b657f5490a3..b380c3f22ee 100644 --- a/src/resources/filters/common/string.lua +++ b/src/resources/filters/common/string.lua @@ -43,8 +43,28 @@ function patternEscape(str) return str:gsub("([^%w])", "%%%1") end +function html_escape(s, in_attribute) + return s:gsub("[<>&\"']", + function(x) + if x == '<' then + return '<' + elseif x == '>' then + return '>' + elseif x == '&' then + return '&' + elseif in_attribute and x == '"' then + return '"' + elseif in_attribute and x == "'" then + return ''' + else + return x + end + end) +end + -- Escape '%' in string by replacing by '%%' -- This is especially useful in Lua patterns to escape a '%' function percentEscape(str) return str:gsub("%%", "%%%%") -end \ No newline at end of file +end + diff --git a/src/resources/filters/common/tables.lua b/src/resources/filters/common/tables.lua index dd14a80a43a..8e23c76f20b 100644 --- a/src/resources/filters/common/tables.lua +++ b/src/resources/filters/common/tables.lua @@ -32,7 +32,7 @@ function parseTableCaption(caption) end end if beginIndex ~= nil then - local attrText = trim(inlinesToString(tslice(caption, beginIndex, #caption))) + local attrText = trim(inlinesToString(pandoc.Inlines(tslice(caption, beginIndex, #caption)))) attrText = attrText:gsub("“", "'"):gsub("”", "'") local elWithAttr = pandoc.read("## " .. attrText).blocks[1] if elWithAttr.attr ~= nil then diff --git a/src/resources/filters/common/validate.lua b/src/resources/filters/common/validate.lua index b6a1cac0ec5..52097c1ce53 100644 --- a/src/resources/filters/common/validate.lua +++ b/src/resources/filters/common/validate.lua @@ -16,8 +16,10 @@ function validateInList(value, list, attribute, default) if value == "default" then return default elseif value and not list:includes(value) then + -- luacov: disable warn("Invalid " .. attribute .. " attribute value: " .. value) return default + -- luacov: enable elseif value then return value else diff --git a/src/resources/filters/common/wrapped-filter.lua b/src/resources/filters/common/wrapped-filter.lua index 4226d767e41..7bb7e1f189d 100644 --- a/src/resources/filters/common/wrapped-filter.lua +++ b/src/resources/filters/common/wrapped-filter.lua @@ -98,6 +98,7 @@ function makeWrappedJsonFilter(scriptFile, filterHandler) local custom_node_map = {} local has_custom_nodes = false doc = doc:walk({ + -- FIXME: This is broken with new AST. Needs to go through Custom node instead. RawInline = function(raw) local custom_node, t, kind = _quarto.ast.resolve_custom_data(raw) if custom_node ~= nil then diff --git a/src/resources/filters/crossref/#crossref.lua# b/src/resources/filters/crossref/#crossref.lua# new file mode 100644 index 00000000000..cdc681af1ca --- /dev/null +++ b/src/resources/filters/crossref/#crossref.lua# @@ -0,0 +1,82 @@ + -- crossref.lua +-- Copyright (C) 2020-2023 Posit Software, PBC + +-- this is the standalone version of our crossref filters, used in the IDEs for auto-completion + +-- required version +PANDOC_VERSION:must_be_at_least '2.13' + +-- [import] +function import(script) + local path = PANDOC_SCRIPT_FILE:match("(.*[/\\])") + dofile(path .. script) +end + +import("../mainstateinit.lua") + +import("../ast/customnodes.lua") +import("../ast/emulatedfilter.lua") +import("../ast/parse.lua") +import("../ast/render.lua") +import("../ast/runemulation.lua") +import("../ast/traceexecution.lua") +import("../ast/wrappedwriter.lua") + + +import("index.lua") +import("preprocess.lua") +import("sections.lua") +import("figures.lua") +import("tables.lua") +import("equations.lua") +import("listings.lua") +import("theorems.lua") +import("qmd.lua") +import("refs.lua") +import("meta.lua") +import("format.lua") +import("options.lua") +import("../normalize/flags.lua") +import("../normalize/pandoc3.lua") +import("../common/lunacolors.lua") +import("../common/log.lua") +import("../common/pandoc.lua") +import("../common/format.lua") +import("../common/base64.lua") +import("../common/options.lua") +import("../common/refs.lua") +import("../common/filemetadata.lua") +import("../common/figures.lua") +import("../common/tables.lua") +import("../common/theorems.lua") +import("../common/meta.lua") +import("../common/table.lua") +import("../common/string.lua") +import("../common/debug.lua") +import("../common/layout.lua") + +-- [/import] + +initCrossrefIndex() + +-- chain of filters +return { + init_crossref_options(), + compute_flags(), + parse_pandoc3_figures(), + crossref_preprocess(), + crossref_preprocess_theorems(), + combineFilters({ + file_metadata(), + qmd(), + sections(), + crossref_figures(), + crossref_tables(), + equations(), + listings(), + crossref_theorems(), + }), + resolveRefs(), + crossrefMetaInject(), + writeIndex() +} \ No newline at end of file diff --git a/src/resources/filters/crossref/crossref-standalone.lua b/src/resources/filters/crossref/crossref-standalone.lua index d510670a7de..47762e0438d 100644 --- a/src/resources/filters/crossref/crossref-standalone.lua +++ b/src/resources/filters/crossref/crossref-standalone.lua @@ -10,6 +10,9 @@ function import(script) local path = PANDOC_SCRIPT_FILE:match("(.*[/\\])") dofile(path .. script) end + +-- FIXME: needs updating for float-reftargets branch. + import("../mainstateinit.lua") import("index.lua") import("preprocess.lua") @@ -17,7 +20,6 @@ import("sections.lua") import("figures.lua") import("tables.lua") import("equations.lua") -import("listings.lua") import("theorems.lua") import("qmd.lua") import("refs.lua") @@ -46,7 +48,7 @@ initCrossrefIndex() -- chain of filters return { init_crossref_options(), - crossref_preprocess(), + crossref_mark_subfloats(), crossref_preprocess_theorems(), combineFilters({ file_metadata(), @@ -55,7 +57,6 @@ return { crossref_figures(), crossref_tables(), equations(), - listings(), crossref_theorems(), }), resolveRefs(), diff --git a/src/resources/filters/crossref/crossref.lua b/src/resources/filters/crossref/crossref.lua index bb93d8c9b04..2037fdb4220 100644 --- a/src/resources/filters/crossref/crossref.lua +++ b/src/resources/filters/crossref/crossref.lua @@ -22,61 +22,208 @@ import("../ast/runemulation.lua") import("../ast/traceexecution.lua") import("../ast/wrappedwriter.lua") - -import("index.lua") -import("preprocess.lua") -import("sections.lua") -import("figures.lua") -import("tables.lua") -import("equations.lua") -import("listings.lua") -import("theorems.lua") -import("qmd.lua") -import("refs.lua") -import("meta.lua") -import("format.lua") -import("options.lua") -import("../normalize/flags.lua") -import("../normalize/pandoc3.lua") -import("../common/lunacolors.lua") -import("../common/log.lua") -import("../common/pandoc.lua") -import("../common/format.lua") import("../common/base64.lua") +import("../common/citations.lua") +import("../common/colors.lua") +import("../common/collate.lua") +import("../common/debug.lua") +import("../common/error.lua") +import("../common/figures.lua") +import("../common/filemetadata.lua") +import("../common/floats.lua") +import("../common/format.lua") +import("../common/latex.lua") +import("../common/layout.lua") +import("../common/list.lua") +import("../common/log.lua") +import("../common/lunacolors.lua") +import("../common/maporcall.lua") +import("../common/meta.lua") import("../common/options.lua") +import("../common/pandoc.lua") +import("../common/paths.lua") import("../common/refs.lua") -import("../common/filemetadata.lua") -import("../common/figures.lua") +import("../common/string.lua") +import("../common/table.lua") import("../common/tables.lua") import("../common/theorems.lua") -import("../common/meta.lua") -import("../common/table.lua") -import("../common/string.lua") -import("../common/debug.lua") -import("../common/layout.lua") +import("../common/url.lua") +import("../common/validate.lua") +import("../common/wrapped-filter.lua") + +import("../quarto-init/configurefilters.lua") +import("../quarto-init/includes.lua") +import("../quarto-init/resourcerefs.lua") + +import("../normalize/flags.lua") +import("../normalize/normalize.lua") +import("../normalize/parsehtml.lua") +import("../normalize/extractquartodom.lua") + +import("../crossref/custom.lua") +import("../crossref/index.lua") +import("../crossref/preprocess.lua") +import("../crossref/sections.lua") +import("../crossref/figures.lua") +import("../crossref/tables.lua") +import("../crossref/equations.lua") +import("../crossref/theorems.lua") +import("../crossref/qmd.lua") +import("../crossref/refs.lua") +import("../crossref/meta.lua") +import("../crossref/format.lua") +import("../crossref/options.lua") + +import("../quarto-pre/bibliography-formats.lua") +import("../quarto-pre/book-links.lua") +import("../quarto-pre/book-numbering.lua") +import("../quarto-pre/code-annotation.lua") +import("../quarto-pre/code-filename.lua") +import("../quarto-pre/engine-escape.lua") +import("../quarto-pre/figures.lua") +import("../quarto-pre/hidden.lua") +import("../quarto-pre/include-paths.lua") +import("../quarto-pre/input-traits.lua") +import("../quarto-pre/line-numbers.lua") +import("../quarto-pre/meta.lua") +import("../quarto-pre/options.lua") +import("../quarto-pre/output-location.lua") +import("../quarto-pre/outputs.lua") +import("../quarto-pre/panel-input.lua") +import("../quarto-pre/panel-layout.lua") +import("../quarto-pre/panel-sidebar.lua") +import("../quarto-pre/parsefiguredivs.lua") +import("../quarto-pre/project-paths.lua") +import("../quarto-pre/resourcefiles.lua") +import("../quarto-pre/results.lua") +import("../quarto-pre/shortcodes-handlers.lua") +import("../quarto-pre/table-classes.lua") +import("../quarto-pre/table-captions.lua") +import("../quarto-pre/table-colwidth.lua") +import("../quarto-pre/table-rawhtml.lua") +import("../quarto-pre/theorems.lua") + +import("../customnodes/latexenv.lua") +import("../customnodes/latexcmd.lua") +import("../customnodes/htmltag.lua") +import("../customnodes/shortcodes.lua") +import("../customnodes/content-hidden.lua") +import("../customnodes/decoratedcodeblock.lua") +import("../customnodes/callout.lua") +import("../customnodes/panel-tabset.lua") +import("../customnodes/floatreftarget.lua") + +import("../quarto-init/metainit.lua") -- [/import] initCrossrefIndex() --- chain of filters -return { - init_crossref_options(), - compute_flags(), - parse_pandoc3_figures(), - crossref_preprocess(), - crossref_preprocess_theorems(), - combineFilters({ +initShortcodeHandlers() + +local quarto_init_filters = { + { name = "init-quarto-meta-init", filter = quarto_meta_init() }, + { name = "init-quarto-custom-meta-init", filter = { + Meta = function(meta) + content_hidden_meta(meta) + end + }}, +} + +-- v1.4 change: quartoNormalize is responsible for producing a +-- "normalized" document that is ready for quarto-pre, etc. +-- notably, user filters will run on the normalized document and +-- see a "Quarto AST". For example, Figure nodes are no longer +-- going to be present, and will instead be represented by +-- our custom AST infrastructure (FloatRefTarget specifically). + +local quarto_normalize_filters = { + { name = "normalize", filter = filterIf(function() + if quarto_global_state.active_filters == nil then + return false + end + return quarto_global_state.active_filters.normalization + end, normalize_filter()) }, + + { name = "pre-table-merge-raw-html", + filter = table_merge_raw_html() + }, + + -- { name = "pre-content-hidden-meta", + -- filter = content_hidden_meta() }, + + -- 2023-04-11: We want to combine combine-1 and combine-2, but parse_md_in_html_rawblocks + -- can't be combined with parse_html_tables. combineFilters + -- doesn't inspect the contents of the results in the inner loop in case + -- the result is "spread" into a Blocks or Inlines. + + { name = "normalize-combined-1", filter = combineFilters({ + parse_html_tables(), + parse_extended_nodes(), + code_filename(), + }) + }, + { + name = "normalize-combine-2", + filter = combineFilters({ + parse_md_in_html_rawblocks(), + parse_floats(), + }), + }, +} + +local quarto_pre_filters = { + -- quarto-pre + + { name = "flags", filter = compute_flags() }, + + { name = "pre-shortcodes-filter", + filter = shortcodes_filter(), + flags = { "has_shortcodes" } }, +} + +local quarto_crossref_filters = { + + { name = "crossref-preprocess-floats", filter = crossref_mark_subfloats(), + }, + + { name = "crossref-preprocessTheorems", + filter = crossref_preprocess_theorems(), + flags = { "has_theorem_refs" } }, + + { name = "crossref-combineFilters", filter = combineFilters({ file_metadata(), qmd(), sections(), crossref_figures(), - crossref_tables(), equations(), - listings(), crossref_theorems(), - }), - resolveRefs(), - crossrefMetaInject(), - writeIndex() -} \ No newline at end of file + })}, + + { name = "crossref-resolveRefs", filter = resolveRefs(), + flags = { "has_cites" } }, + + { name = "crossref-crossrefMetaInject", filter = crossrefMetaInject() }, + { name = "crossref-writeIndex", filter = writeIndex() }, +} + +local filterList = {} + +tappend(filterList, quarto_init_filters) +tappend(filterList, quarto_normalize_filters) +tappend(filterList, quarto_pre_filters) +tappend(filterList, quarto_crossref_filters) + +local result = run_as_extended_ast({ + pre = { + init_options() + }, + afterFilterPass = function() + -- After filter pass is called after each pass through a filter group + -- allowing state or other items to be handled + resetFileMetadata() + end, + filters = filterList, +}) + +return result \ No newline at end of file diff --git a/src/resources/filters/crossref/custom.lua b/src/resources/filters/crossref/custom.lua new file mode 100644 index 00000000000..2a207cd309c --- /dev/null +++ b/src/resources/filters/crossref/custom.lua @@ -0,0 +1,82 @@ +-- custom.lua +-- Copyright (C) 2023 Posit Software, PBC +-- +-- custom crossref categories + +function initialize_custom_crossref_categories(meta) + local cr = meta["crossref"] + if pandoc.utils.type(cr) ~= "table" then + return nil + end + local custom = cr["custom"] + if custom == nil then + return nil + end + if type(custom) ~= "table" then + -- luacov: disable + fail_and_ask_for_bug_report("crossref.custom entry must be a table") + return nil + -- luacov: enable + end + local keys = { + "default-caption-location", + "kind", + "name", + "prefix", + "ref-type", + "latex-env", + "latex-list-of-name" + } + local obj_mapping = { + ["default-caption-location"] = "default_caption_location", + ["latex-env"] = "latex_env", + ["latex-list-of-name"] = "latex_list_of_name", + ["ref-type"] = "ref_type" + } + for _, v in ipairs(custom) do + local entry = {} + for _, key in ipairs(keys) do + if v[key] ~= nil then + entry[key] = pandoc.utils.stringify(v[key]) + end + end + if entry["default-caption-location"] == nil then + entry["default-caption-location"] = "bottom" + end + -- slightly inefficient because we recompute the indices at + -- every call, but should be totally ok for the number of categories + -- we expect to see in documents + local obj_entry = {} + for k, v in pairs(entry) do + if obj_mapping[k] ~= nil then + obj_entry[obj_mapping[k]] = v + else + obj_entry[k] = v + end + end + add_crossref_category(obj_entry) + + if quarto.doc.isFormat("pdf") then + metaInjectLatex(meta, function(inject) + local env_name = entry["latex-env"] + local name = entry["name"] + local env_prefix = entry["prefix"] + local ref_type = entry["ref-type"] + local list_of_name = entry["latex-list-of-name"] + + -- FIXME do we need different "lop" extensions for each category? + -- we should be able to test this by creating a document with listings and diagrams + + inject( + usePackage("float") .. "\n" .. + "\\floatstyle{plain}\n" .. + "\\@ifundefined{c@chapter}{\\newfloat{" .. env_name .. "}{h}{lop}}{\\newfloat{" .. env_name .. "}{h}{lop}[chapter]}\n" .. + "\\floatname{".. env_name .. "}{" .. titleString(ref_type, env_prefix) .. "}\n" + ) + inject( + "\\newcommand*\\listof" .. env_name .. "s{\\listof{" .. env_name .. "}{" .. listOfTitle(list_of_name, "List of " .. name .. "s") .. "}}\n" + ) + end) + end + end +end \ No newline at end of file diff --git a/src/resources/filters/crossref/figures.lua b/src/resources/filters/crossref/figures.lua index 5c488aece2d..72973569a83 100644 --- a/src/resources/filters/crossref/figures.lua +++ b/src/resources/filters/crossref/figures.lua @@ -4,58 +4,42 @@ -- process all figures function crossref_figures() return { - Div = function(el) - if isFigureDiv(el) and isReferenceableFig(el) then - local caption = refCaptionFromDiv(el) - if caption then - process_figure(el, caption.content) - end + -- process a float + -- adding it to the global index of floats (figures, tables, etc) + -- + -- in 1.4, we won't re-write its caption here, but instead we'll + -- do it at the render filter. + + FloatRefTarget = function(float) + -- if figure is unlabeled, do not process + if is_unlabeled_float(float) then + return nil end - return el - end, - Para = function(el) - local image = discoverFigure(el) - if image and isFigureImage(image) then - process_figure(image, image.caption) + -- get label and base caption + -- local label = el.attr.identifier + local kind = refType(float.identifier) + if kind == nil then + -- luacov: disable + warn("Could not determine float type for " .. float.identifier) + return nil + -- luacov: enable end - return el + + -- determine order, parent, and displayed caption + local order + local parent = float.parent_id + if (parent) then + order = nextSubrefOrder() + else + order = indexNextOrder(kind) + end + + float.order = order + -- update the index + indexAddEntry(float.identifier, parent, order, {float.caption_long}) + return float end } end - --- process a figure, re-writing its caption as necessary and --- adding it to the global index of figures -function process_figure(el, captionContent) - -- get label and base caption - local label = el.attr.identifier - local caption = captionContent:clone() - - -- determine order, parent, and displayed caption - local order - local parent = el.attr.attributes[kRefParent] - if (parent) then - order = nextSubrefOrder() - prependSubrefNumber(captionContent, order) - else - order = indexNextOrder("fig") - if _quarto.format.isLatexOutput() then - tprepend(captionContent, { - pandoc.RawInline('latex', '\\label{' .. label .. '}') - }) - elseif _quarto.format.isAsciiDocOutput() or _quarto.format.isTypstOutput() then - el.attr.identifier = label - else - tprepend(captionContent, figureTitlePrefix(order)) - end - end - - -- update the index - indexAddEntry(label, parent, order, caption) -end - - -function figureTitlePrefix(order) - return titlePrefix("fig", param("crossref-fig-title", "Figure"), order) -end diff --git a/src/resources/filters/crossref/format.lua b/src/resources/filters/crossref/format.lua index 75019b69084..d854644b371 100644 --- a/src/resources/filters/crossref/format.lua +++ b/src/resources/filters/crossref/format.lua @@ -56,7 +56,14 @@ end function refPrefix(type, upper) local opt = type .. "-prefix" - local default = stringToInlines(param("crossref-" .. type .. "-prefix", type .. ".")) + local default = param("crossref-" .. type .. "-prefix") + if default == nil then + default = crossref.categories.by_ref_type[type].prefix + end + if default == nil then + default = type .. "." + end + default = stringToInlines(default) local prefix = crossrefOption(opt, default) if upper then local el = pandoc.Plain(prefix) @@ -141,7 +148,7 @@ function formatNumberOption(type, order, default) end num = sectionIndex .. "." .. num end - return { pandoc.Str(num) } + return pandoc.Inlines({ pandoc.Str(num) }) end -- Compute option name and default value @@ -191,7 +198,7 @@ function formatNumberOption(type, order, default) if section then tprepend(option, { pandoc.Str(tostring(section[1]) .. ".") }) end - return { option } + return pandoc.Inlines({ option }) end end diff --git a/src/resources/filters/crossref/index.lua b/src/resources/filters/crossref/index.lua index 98a862136a8..0eda3037465 100644 --- a/src/resources/filters/crossref/index.lua +++ b/src/resources/filters/crossref/index.lua @@ -115,7 +115,11 @@ function writeKeysIndex(indexFile) } -- add caption if we have one if v.caption ~= nil then - entry.caption = inlinesToString(v.caption) + if v.caption[1].t == "Str" then + entry.caption = v.caption[1].text + else + entry.caption = inlinesToString(pandoc.Inlines(v.caption[1].content)) + end else entry.caption = "" end @@ -192,7 +196,11 @@ function writeFullIndex(indexFile) } -- add caption if we have one if v.caption ~= nil then - entry.caption = inlinesToString(v.caption) + if pandoc.utils.type(v.caption[1]) == "Inline" then + entry.caption = inlinesToString(pandoc.Inlines({v.caption[1]})) + else + entry.caption = inlinesToString(pandoc.Inlines(v.caption[1].content)) + end else entry.caption = "" end diff --git a/src/resources/filters/crossref/listings.lua b/src/resources/filters/crossref/listings.lua deleted file mode 100644 index e649de2fb38..00000000000 --- a/src/resources/filters/crossref/listings.lua +++ /dev/null @@ -1,103 +0,0 @@ --- listings.lua --- Copyright (C) 2020-2022 Posit Software, PBC - -local constants = require("modules/constants") - -function isListingRef(identifier) - return string.match(identifier, "^lst%-[^ ]+$") -end - --- process all listings -function listings() - - return { - DecoratedCodeBlock = function(node) - local el = node.code_block - local label = string.match(el.attr.identifier, "^lst%-[^ ]+$") - local caption = el.attr.attributes[constants.kLstCap] - if label and caption then - -- the listing number - local order = indexNextOrder("lst") - - -- generate content from markdown caption - local captionContent = markdownToInlines(caption) - - -- add the listing to the index - indexAddEntry(label, nil, order, captionContent) - - node.caption = captionContent - node.order = order - return node - end - return nil - end, - - CodeBlock = function(el) - local label = string.match(el.attr.identifier, "^lst%-[^ ]+$") - local caption = el.attr.attributes[constants.kLstCap] - if label and caption then - - -- the listing number - local order = indexNextOrder("lst") - - -- generate content from markdown caption - local captionContent = markdownToInlines(caption) - - -- add the listing to the index - indexAddEntry(label, nil, order, captionContent) - - if _quarto.format.isLatexOutput() then - - -- add listing class to the code block - el.attr.classes:insert("listing") - - -- if we are use the listings package we don't need to do anything - -- further, otherwise generate the listing div and return it - if not latexListings() then - local listingDiv = pandoc.Div({}) - local env = "\\begin{codelisting}" - if el.classes:includes('code-annotation-code') then - env = env .. "[H]" - end - listingDiv.content:insert(pandoc.RawBlock("latex", env)) - local listingCaption = pandoc.Plain({pandoc.RawInline("latex", "\\caption{")}) - listingCaption.content:extend(captionContent) - listingCaption.content:insert(pandoc.RawInline("latex", "}")) - listingDiv.content:insert(listingCaption) - listingDiv.content:insert(el) - listingDiv.content:insert(pandoc.RawBlock("latex", "\\end{codelisting}")) - return listingDiv - end - - else - - -- Prepend the title - tprepend(captionContent, listingTitlePrefix(order)) - - -- return a div with the listing - return pandoc.Div( - { - pandoc.Para(captionContent), - el - }, - pandoc.Attr(label, {"listing"}) - ) - end - - end - - -- if we get this far then just reflect back the el - return el - end - } - -end - -function listingTitlePrefix(order) - return titlePrefix("lst", "Listing", order) -end - - -function latexListings() - return param("listings", false) -end \ No newline at end of file diff --git a/src/resources/filters/crossref/meta.lua b/src/resources/filters/crossref/meta.lua index d2997fd63e0..37fc665a245 100644 --- a/src/resources/filters/crossref/meta.lua +++ b/src/resources/filters/crossref/meta.lua @@ -19,7 +19,7 @@ function crossrefMetaInject() "}\n" ) - if latexListings() then + if param("listings", false) then inject( "\\newcommand*\\listoflistings\\lstlistoflistings\n" .. "\\AtBeginDocument{%\n" .. diff --git a/src/resources/filters/crossref/options.lua b/src/resources/filters/crossref/options.lua index d021fefc07b..f7a184e4876 100644 --- a/src/resources/filters/crossref/options.lua +++ b/src/resources/filters/crossref/options.lua @@ -2,20 +2,15 @@ -- Copyright (C) 2020-2022 Posit Software, PBC -- initialize options from 'crossref' metadata value -function init_crossref_options() - return { - Meta = function(meta) - crossref.options = readFilterOptions(meta, "crossref") +function init_crossref_options(meta) + crossref.options = readFilterOptions(meta, "crossref") - -- automatically set maxHeading to 1 if we are in chapters mode, otherwise set to max (7) - if crossrefOption("chapters", false) then - crossref.maxHeading = 1 - else - crossref.maxHeading = 7 - end - - end - } + -- automatically set maxHeading to 1 if we are in chapters mode, otherwise set to max (7) + if crossrefOption("chapters", false) then + crossref.maxHeading = 1 + else + crossref.maxHeading = 7 + end end -- get option value diff --git a/src/resources/filters/crossref/preprocess.lua b/src/resources/filters/crossref/preprocess.lua index ec0e51a25de..0dd7c2b7aff 100644 --- a/src/resources/filters/crossref/preprocess.lua +++ b/src/resources/filters/crossref/preprocess.lua @@ -3,101 +3,26 @@ -- figures and tables support sub-references. mark them up before -- we proceed with crawling for cross-refs -function crossref_preprocess() - +function crossref_mark_subfloats() return { - Pandoc = function(doc) - local walkRefs - walkRefs = function(parentId) - return { - Div = function(el) - if hasFigureOrTableRef(el) then - - -- provide error caption if there is none - if not refCaptionFromDiv(el) then - local err = pandoc.Para(noCaption()) - el.content:insert(err) - end - - if parentId ~= nil then - if refType(el.attr.identifier) == refType(parentId) then - el.attr.attributes[kRefParent] = parentId - end - else - -- mark as table parent if required - if isTableRef(el.attr.identifier) then - el.attr.classes:insert("tbl-parent") - flags.has_tbl_parent = true -- to be able to early-out later on - end - el = _quarto.ast.walk(el, walkRefs(el.attr.identifier)) - end + traverse = "topdown", + FloatRefTarget = function(float) + float.content = _quarto.ast.walk(float.content, { + FloatRefTarget = function(subfloat) + crossref.subfloats[subfloat.identifier] = { + parent_id = float.identifier + } + subfloat.parent_id = float.identifier + subfloat.content = _quarto.ast.walk(subfloat.content, { + Image = function(image) + image.attributes[kRefParent] = float.identifier + return image end - return el - end, - - Table = function(el) - return preprocessTable(el, parentId) - end, - - RawBlock = function(el) - return preprocessRawTableBlock(el, parentId) - end, - - Para = function(el) - - -- provide error caption if there is none - local fig = discoverFigure(el, false) - if fig and hasFigureRef(fig) and #fig.caption == 0 then - if isFigureRef(parentId) then - fig.caption:insert(emptyCaption()) - fig.title = "fig:" .. fig.title - else - fig.caption:insert(noCaption()) - end - end - - -- if we have a parent fig: then mark it's sub-refs - if parentId and isFigureRef(parentId) then - local image = discoverFigure(el) - if image and isFigureImage(image) then - image.attr.attributes[kRefParent] = parentId - end - end - - return el - end - } - end - - -- walk all blocks in the document - for i,el in pairs(doc.blocks) do - -- always wrap referenced tables in a div - if el.t == "Table" then - doc.blocks[i] = preprocessTable(el, nil) - elseif el.t == "RawBlock" then - doc.blocks[i] = preprocessRawTableBlock(el, nil) - elseif el.t ~= "Header" then - local parentId = nil - if hasFigureOrTableRef(el) and el.content ~= nil then - parentId = el.attr.identifier - - -- mark as parent - if isTableRef(el.attr.identifier) then - el.attr.classes:insert("tbl-parent") - end - - -- provide error caption if there is none - if not refCaptionFromDiv(el) then - local err = pandoc.Para(noCaption()) - el.content:insert(err) - end - end - doc.blocks[i] = _quarto.ast.walk(el, walkRefs(parentId)) + }) + return subfloat end - end - - return doc - + }) + return float, false end } end diff --git a/src/resources/filters/crossref/refs.lua b/src/resources/filters/crossref/refs.lua index 041d08e3e2f..02b08bc5b68 100644 --- a/src/resources/filters/crossref/refs.lua +++ b/src/resources/filters/crossref/refs.lua @@ -1,6 +1,8 @@ -- refs.lua -- Copyright (C) 2020-2022 Posit Software, PBC +-- FIXME this resolveRefs filter should be in post-processing +-- since it emits format-specific AST elements -- resolve references function resolveRefs() @@ -70,7 +72,16 @@ function resolveRefs() elseif _quarto.format.isAsciiDocOutput() then ref = pandoc.List({pandoc.RawInline('asciidoc', '<<' .. label .. '>>')}) elseif _quarto.format.isTypstOutput() then - ref = pandoc.List({pandoc.RawInline('typst', '@' .. label)}) + -- if we're referencing a subfloat, + -- we need to package the parent_id information in the + -- supplement as well, so that we can provide + -- better numbering in the typst renderer + local subfloat_info = crossref.subfloats[label] + if subfloat_info == nil then + ref = pandoc.List({pandoc.RawInline('typst', '@' .. label)}) + else + ref = pandoc.List({pandoc.RawInline('typst', '@' .. label .. '[45127368-afa1-446a-820f-fc64c546b2c5%' .. subfloat_info.parent_id .. ']')}) + end else if not resolve then local refClasses = pandoc.List({"quarto-unresolved-ref"}) @@ -126,7 +137,25 @@ function resolveRefs() } end -function autoRefLabel(parentId) + +-- we're removing the dashes from this uuid because +-- it makes it easier to handling it in lua patterns + +local quarto_auto_label_safe_latex_uuid = "539a35d47e664c97a50115a146a7f1bd" +function autoRefLabel(refType) + local index = 1 + while true do + local label = refType .. "-" .. quarto_auto_label_safe_latex_uuid .. "-" ..tostring(index) + if not crossref.autolabels:includes(label) then + crossref.autolabels:insert(label) + return label + else + index = index + 1 + end + end +end + +function autoSubrefLabel(parentId) local index = 1 while true do local label = parentId .. "-" .. tostring(index) @@ -161,10 +190,16 @@ end function validRefTypes() local types = tkeys(theoremTypes) - table.insert(types, "fig") - table.insert(types, "tbl") + for k, _ in pairs(crossref.categories.by_ref_type) do + table.insert(types, k) + -- if v.type ~= nil and not tcontains(types, v.type) then + -- table.insert(types, v.type) + -- end + end + -- table.insert(types, "fig") + -- table.insert(types, "tbl") + -- table.insert(types, "lst") table.insert(types, "eq") - table.insert(types, "lst") table.insert(types, "sec") return types end diff --git a/src/resources/filters/crossref/tables.lua b/src/resources/filters/crossref/tables.lua index ec54cede13c..b3d18de3d49 100644 --- a/src/resources/filters/crossref/tables.lua +++ b/src/resources/filters/crossref/tables.lua @@ -7,43 +7,6 @@ local patterns = require("modules/patterns") -function crossref_tables() - return { - Div = function(el) - if isTableDiv(el) and isReferenceableTbl(el) then - - -- are we a parent of subrefs? If so then process the caption - -- at the bottom of the div - if hasSubRefs(el, "tbl") then - - local caption = refCaptionFromDiv(el) - if not caption then - caption = pandoc.Para(noCaption()) - el.content:insert(caption) - end - local captionClone = caption:clone().content - local label = el.attr.identifier - local order = indexNextOrder("tbl") - prependTitlePrefix(caption, label, order) - indexAddEntry(label, nil, order, captionClone) - - else - -- look for various ways of expressing tables in a div - local processors = { processMarkdownTable, processRawTable } - for _, process in ipairs(processors) do - local tblDiv = process(el) - if tblDiv then - return tblDiv - end - end - end - end - -- default to just reflecting the div back - return el - end - } -end - function preprocessRawTableBlock(rawEl, parentId) local function divWrap(el, label, caption) @@ -67,7 +30,7 @@ function preprocessRawTableBlock(rawEl, parentId) -- remove label from caption rawEl.text = rawEl.text:gsub(captionPattern, "%1" .. caption:gsub("%%", "%%%%") .. "%3", 1) elseif parentId then - label = autoRefLabel(parentId) + label = autoSubrefLabel(parentId) end if label then @@ -87,7 +50,7 @@ function preprocessRawTableBlock(rawEl, parentId) -- remove label from caption rawEl.text = rawEl.text:gsub(captionPattern, "%1%2%4", 1) elseif parentId then - label = autoRefLabel(parentId) + label = autoSubrefLabel(parentId) end if label then @@ -126,7 +89,7 @@ function preprocessTable(el, parentId) -- if there is a parent then auto-assign a label if there is none elseif parentId then - label = autoRefLabel(parentId) + label = autoSubrefLabel(parentId) end if label then @@ -148,28 +111,37 @@ function preprocessTable(el, parentId) end -function processMarkdownTable(divEl) - for i,el in pairs(divEl.content) do +function process(float) + local changed = false + local content = float.content + if pandoc.utils.type(content) ~= "Blocks" then + content = pandoc.List({content}) + end + for _,el in ipairs(content) do if el.t == "Table" then if el.caption.long ~= nil and #el.caption.long > 0 then local label = divEl.attr.identifier local caption = el.caption.long[#el.caption.long] - processMarkdownTableEntry(divEl, el, label, caption) - return divEl + processMarkdownTableEntry(float) + changed = true + return float end end end + if changed then + return float + end return nil end -function processMarkdownTableEntry(divEl, el, label, caption) +function processMarkdownTableEntry(float) -- clone the caption so we can add a clean copy to our index local captionClone = caption.content:clone() -- determine order / insert prefix local order - local parent = divEl.attr.attributes[kRefParent] + local parent = float.parent_id if (parent) then order = nextSubrefOrder() prependSubrefNumber(caption.content, order) @@ -250,6 +222,16 @@ function isTableDiv(el) end +function float_title_prefix(float) + local category = crossref.categories.by_name[float.type] + if category == nil then + fail("unknown float type '" .. float.type .. "'") + return + end + + return titlePrefix(category.ref_type, category.name, float.order) +end + function tableTitlePrefix(order) return titlePrefix("tbl", "Table", order) end diff --git a/src/resources/filters/customnodes/content-hidden.lua b/src/resources/filters/customnodes/content-hidden.lua index c0e7bf721ea..a94ba91153e 100644 --- a/src/resources/filters/customnodes/content-hidden.lua +++ b/src/resources/filters/customnodes/content-hidden.lua @@ -18,8 +18,10 @@ function is_visible(node) elseif node.behavior == constants.kContentHidden then return not match else + -- luacov: disable fatal("Internal Error: invalid behavior for conditional block: " .. node.behavior) return false + -- luacov: enable end end @@ -73,7 +75,9 @@ _quarto.ast.add_handler({ }; for i, v in ipairs(tbl.condition or {}) do if kConditions:find(v[1]) == nil then + -- luacov: disable error("Ignoring invalid condition in conditional block: " .. v[1]) + -- luacov: enable else result.condition[v[1]] = v[2] end @@ -93,12 +97,14 @@ _quarto.ast.add_handler({ local _content_hidden_meta = nil -function content_hidden_meta() - return { - Meta = function(meta) - _content_hidden_meta = meta - end - } +-- we capture a copy of meta here for convenience; +-- +function content_hidden_meta(meta) + -- return { + -- Meta = function(meta) + _content_hidden_meta = meta + -- end + -- } end local function get_meta(key) diff --git a/src/resources/filters/customnodes/decoratedcodeblock.lua b/src/resources/filters/customnodes/decoratedcodeblock.lua index 0b4cfb9f2e3..cc0262197fe 100644 --- a/src/resources/filters/customnodes/decoratedcodeblock.lua +++ b/src/resources/filters/customnodes/decoratedcodeblock.lua @@ -19,21 +19,12 @@ _quarto.ast.add_handler({ -- and returns the custom node parse = function(div) -- luacov: disable - fatal("internal error, DecoratedCodeBlock has no native parser") + internal_error() -- luacov: enable end, constructor = function(tbl) - local caption = tbl.caption - if tbl.code_block.attributes["lst-cap"] ~= nil then - caption = pandoc.read(tbl.code_block.attributes["lst-cap"], "markdown").blocks[1].content - end - return { - filename = tbl.filename, - order = tbl.order, - caption = caption, - code_block = tbl.code_block - } + return tbl end }) @@ -72,7 +63,7 @@ _quarto.ast.add_renderer("DecoratedCodeBlock", end end) --- latex renderer + -- latex renderer _quarto.ast.add_renderer("DecoratedCodeBlock", function(_) return _quarto.format.isLatexOutput() @@ -84,7 +75,7 @@ _quarto.ast.add_renderer("DecoratedCodeBlock", -- if we are use the listings package we don't need to do anything -- further, otherwise generate the listing div and return it - if not latexListings() then + if not param("listings", false) then local listingDiv = pandoc.Div({}) local position = "" if _quarto.format.isBeamerOutput() then @@ -146,20 +137,6 @@ _quarto.ast.add_renderer("DecoratedCodeBlock", classes:insert("code-with-filename") fancy_output = true end - if node.caption ~= nil then - local order = node.order - if order == nil then - warn("Node with caption " .. pandoc.utils.stringify(node.caption) .. " is missing a listing id (lst-*).") - warn("This usage is unsupported in HTML formats.") - return el - end - local captionContent = node.caption - tprepend(captionContent, listingTitlePrefix(order)) - caption = pandoc.Para(captionContent) - classes:insert("listing") - fancy_output = true - end - if not fancy_output then return el end diff --git a/src/resources/filters/customnodes/floatreftarget.lua b/src/resources/filters/customnodes/floatreftarget.lua new file mode 100644 index 00000000000..e2b3217a6bc --- /dev/null +++ b/src/resources/filters/customnodes/floatreftarget.lua @@ -0,0 +1,616 @@ +-- floatreftarget.lua +-- Copyright (C) 2023 Posit Software, PBC + +_quarto.ast.add_handler({ + + -- empty table so this handler is only called programmatically + class_name = {}, + + -- the name of the ast node, used as a key in extended ast filter tables + ast_name = "FloatRefTarget", + + -- generic names this custom AST node responds to + -- this is still unimplemented + interfaces = {"Crossref"}, + + -- float reftargets are always blocks + kind = "Block", + + parse = function(div) + -- luacov: disable + internal_error() + -- luacov: enable + end, + + slots = { "content", "caption_long", "caption_short" }, + + constructor = function(tbl) + if tbl.attr then + tbl.identifier = tbl.attr.identifier + tbl.classes = tbl.attr.classes + tbl.attributes = as_plain_table(tbl.attr.attributes) + tbl.attr = nil + end + + tbl.attributes = pandoc.List(tbl.attributes) + tbl.classes = pandoc.List(tbl.classes) + + table_colwidth_cell(tbl) -- table colwidth forwarding + return tbl + end +}) + +function cap_location(float) + local ref = refType(float.identifier) + local qualified_key = ref .. '-cap-location' + local result = ( + float.attributes[qualified_key] or + float.attributes['cap-location'] or + option_as_string(qualified_key) or + option_as_string('cap-location') or + capLocation(ref) or + crossref.categories.by_ref_type[ref].default_caption_location) + + if result ~= "margin" and result ~= "top" and result ~= "bottom" then + -- luacov: disable + error("Invalid caption location for float: " .. float.identifier .. + " requested " .. result .. + ".\nOnly 'top', 'bottom', and 'margin' are supported. Assuming 'bottom'.") + result = "bottom" + -- luacov: enable + end + + return result +end + +local function get_node_from_float_and_type(float, type) + -- this explicit check appears necessary for the case where + -- float.content is directly the node we want, and not a container that + -- contains the node. + if float.content.t == type then + return float.content + else + local found_node = nil + float.content:walk({ + traverse = "topdown", + [type] = function(node) + found_node = node + return nil, false -- don't recurse + end + }) + return found_node + end +end + +-- default renderer first +_quarto.ast.add_renderer("FloatRefTarget", function(_) + return true +end, function(float) + warn("\nEmitting a placeholder FloatRefTarget\nOutput format " .. FORMAT .. " does not currently support FloatRefTarget nodes.") + return scaffold(float.content) +end) + +local function ensure_custom(node) + if pandoc.utils.type(node) == "Block" or pandoc.utils.type(node) == "Inline" then + return _quarto.ast.resolve_custom_data(node) + end + return node +end + +function is_unlabeled_float(float) + -- from src/resources/filters/common/refs.lua + return float.identifier:match("^%a+%-539a35d47e664c97a50115a146a7f1bd%-") +end + +function decorate_caption_with_crossref(float) + if not param("enable-crossref", true) then + -- don't decorate captions with crossrefs information if crossrefs are disabled + return float + end + float = ensure_custom(float) + -- nil should never happen here, but the Lua analyzer doesn't know it + if float == nil then + -- luacov: disable + internal_error() + -- luacov: enable + end + local caption_content = float.caption_long.content or float.caption_long + + if float.parent_id then + if float.order == nil then + warn("Subfloat without crossref information") + else + prependSubrefNumber(caption_content, float.order) + end + else + -- in HTML, unlabeled floats do not get a title prefix + if (not is_unlabeled_float(float)) and (caption_content ~= nil) and (#caption_content > 0) then + local title_prefix = float_title_prefix(float) + tprepend(caption_content, title_prefix) + end + end + return float +end + +-- capture relevant figure attributes then strip them +local function get_figure_attributes(el) + local align = figAlignAttribute(el) + local keys = tkeys(el.attr.attributes) + for _,k in pairs(keys) do + if isFigAttribute(k) then + el.attr.attributes[k] = nil + end + end + local figureAttr = {} + local style = el.attr.attributes["style"] + if style then + figureAttr["style"] = style + el.attributes["style"] = nil + end + return { + align = align, + figureAttr = figureAttr + } +end + +_quarto.ast.add_renderer("FloatRefTarget", function(_) + return _quarto.format.isLatexOutput() +end, function(float) + local figEnv = latexFigureEnv(float) + local figPos = latexFigurePosition(float, figEnv) + local float_type = refType(float.identifier) + local crossref_category = crossref.categories.by_ref_type[float_type] or crossref.categories.by_name.Figure + + local capLoc = capLocation(float_type, crossref_category.default_caption_location) + local caption_cmd_name = latexCaptionEnv(float) + + if float.parent_id then + if caption_cmd_name == kSideCaptionEnv then + fail_and_ask_for_bugreport("Subcaptions for side captions are unimplemented.") + return {} + end + caption_cmd_name = "subcaption" + elseif float.content.t == "Table" and float_type == "tbl" then -- float.parent_id is nil here + -- special-case the situation where the figure is Table and the content is Table + -- + -- just return the table itself with the caption inside the table + + -- FIXME how about tables in margin figures? + + caption_cmd_name = "caption" + float.content.caption.long = float.caption_long + float.content.attr = pandoc.Attr(float.identifier, float.classes or {}, float.attributes or {}) + return float.content + end + + local fig_scap = attribute(float, kFigScap, nil) + if fig_scap then + fig_scap = pandoc.Span(markdownToInlines(fig_scap)) + end + + local latex_caption + + if float.caption_long and #float.caption_long.content > 0 then + local label_cmd = quarto.LatexInlineCommand({ + name = "label", + arg = pandoc.Str(float.identifier) + }) + float.caption_long.content:insert(1, label_cmd) + latex_caption = quarto.LatexInlineCommand({ + name = caption_cmd_name, + opt_arg = fig_scap, + arg = pandoc.Span(quarto.utils.as_inlines(float.caption_long) or {}) -- unnecessary to do the "or {}" bit but the Lua analyzer doesn't know that + }) + end + + if float.parent_id then + -- need to fixup subtables because nested longtables appear to give latex fits + local vAlign = validatedVAlign(float.attributes[kLayoutVAlign]) + local function handle_table(tbl) + return latexTabular(tbl, vAlign) + end + if float.content.t == "Table" then + float.content = handle_table(float.content) + else + float.content = _quarto.ast.walk(float.content, { + Table = handle_table + }) or pandoc.Div({}) -- unnecessary to do the "or {}" bit but the Lua analyzer doesn't know that + end + end + + local figure_content + local pt = pandoc.utils.type(float.content) + if pt == "Block" then + figure_content = pandoc.Blocks({ float.content }) + elseif pt == "Blocks" then + figure_content = float.content + else + -- luacov: disable + fail_and_ask_for_bug_report("Unexpected type for float content: " .. pt) + return {} + -- luacov: enable + end + + -- align the figure + local align = figAlignAttribute(float) + if align == "center" then + figure_content = pandoc.Blocks({ + quarto.LatexBlockCommand({ + name = "centering", + inside = true, + arg = scaffold(figure_content) + }) + }) + elseif align == "right" then + figure_content:insert(1, quarto.LatexInlineCommand({ + name = "hfill", + })) + end -- otherwise, do nothing + -- figure_content:insert(1, pandoc.RawInline("latex", latexBeginAlign(align))) + -- figure_content:insert(pandoc.RawInline("latex", latexEndAlign(align))) + + if latex_caption then + if caption_cmd_name == kSideCaptionEnv then + if #figure_content > 1 then + figure_content:insert(2, latex_caption) -- FIXME why is this 2 and not 1? + else + figure_content:insert(latex_caption) + end + elseif capLoc == "top" then + figure_content:insert(1, latex_caption) + else + figure_content:insert(latex_caption) + end + end + + if float.parent_id then + local width = float.width or "0.50" + return quarto.LatexEnvironment({ + name = "minipage", + pos = "[t]{" .. width .. "\\linewidth}", + content = figure_content + }) + else + return quarto.LatexEnvironment({ + name = figEnv, + pos = figPos, + content = figure_content + }) + end +end) + +_quarto.ast.add_renderer("FloatRefTarget", function(_) + return _quarto.format.isHtmlOutput() +end, function(float) + decorate_caption_with_crossref(float) + + ------------------------------------------------------------------------------------ + -- Special handling for listings + local found_listing = get_node_from_float_and_type(float, "CodeBlock") + if found_listing then + found_listing.attr = merge_attrs(found_listing.attr, pandoc.Attr("", float.classes or {}, float.attributes or {})) + -- FIXME this seems to be necessary for our postprocessor to kick in + -- check this out later + found_listing.identifier = float.identifier + end + + ------------------------------------------------------------------------------------ + + return float_reftarget_render_html_figure(float) +end) + +_quarto.ast.add_renderer("FloatRefTarget", function(_) + return _quarto.format.isDocxOutput() or _quarto.format.isOdtOutput() +end, function(float) + -- docx format requires us to annotate the caption prefix explicitly + decorate_caption_with_crossref(float) + + -- options + local options = { + pageWidth = wpPageWidth(), + } + + -- determine divCaption handler (always left-align) + local divCaption = nil + if _quarto.format.isDocxOutput() then + divCaption = docxDivCaption + elseif _quarto.format.isOdtOutput() then + divCaption = odtDivCaption + else + -- luacov: disable + internal_error() + return + -- luacov: enable + end + + options.divCaption = function(el, align) return divCaption(el, "left") end + + -- get alignment + local align = align_attribute(float) + + -- create the row/cell for the figure + local row = pandoc.List() + local cell = pandoc.Div({}) + cell.attr = pandoc.Attr(float.identifier, float.classes or {}, float.attributes or {}) + local c = float.content.content or float.content + if pandoc.utils.type(c) == "Block" then + cell.content:insert(c) + elseif pandoc.utils.type(c) == "Blocks" then + cell.content = c + elseif pandoc.utils.type(c) == "Inlines" then + cell.content:insert(pandoc.Plain(c)) + end + transfer_float_image_width_to_cell(float, cell) + row:insert(cell) + + -- handle caption + local new_caption = options.divCaption(float.caption_long, align) + local caption_location = cap_location(float) + if caption_location == 'top' then + cell.content:insert(1, new_caption) + else + cell.content:insert(new_caption) + end + + -- content fixups for docx, based on old docx.lua code + cell = docx_content_fixups(cell, align) + + -- make the table + local figureTable = pandoc.SimpleTable( + pandoc.List(), -- caption + { layoutTableAlign(align) }, + { 1 }, -- full width + pandoc.List(), -- no headers + { row } -- figure + ) + + -- return it + return pandoc.utils.from_simple_table(figureTable) +end) + +local figcaption_uuid = "0ceaefa1-69ba-4598-a22c-09a6ac19f8ca" + +local function create_figcaption(float) + if float.caption_long == nil or pandoc.utils.stringify(float.caption_long) == "" then + return nil + end + local ref_type = refType(float.identifier) + -- use a uuid to ensure that the figcaption ids won't conflict with real + -- ids in the document + local caption_id = float.identifier .. "-caption-" .. figcaption_uuid + local classes = { float.type:lower() } + if float.parent_id then + table.insert(classes, "quarto-subfloat-caption") + table.insert(classes, "quarto-subfloat-" .. ref_type) + else + table.insert(classes, "quarto-float-caption") + table.insert(classes, "quarto-float-" .. ref_type) + end + return quarto.HtmlTag({ + name = "figcaption", + attr = pandoc.Attr(caption_id, classes, {}), + content = float.caption_long, + }), caption_id +end + +function float_reftarget_render_html_figure(float) + float = ensure_custom(float) + if float == nil then + -- luacov: disable + internal_error() + return pandoc.Div({}) + -- luacov: enable + end + + local caption_content, caption_id = create_figcaption(float) + local caption_location = cap_location(float) + + local float_content = pandoc.Div(_quarto.ast.walk(float.content, { + -- strip image captions + Image = function(image) + image.caption = {} + return image + end + }) or pandoc.Div({})) -- this should never happen but the lua analyzer doesn't know it + if caption_id ~= nil then + float_content.attributes["aria-describedby"] = caption_id + end + + -- otherwise, we render the float as a div with the caption + local div = pandoc.Div({}) + + local found_image = get_node_from_float_and_type(float, "Image") or pandoc.Div({}) + local figure_attrs = get_figure_attributes(found_image) + + div.attr = merge_attrs( + pandoc.Attr(float.identifier, float.classes or {}, float.attributes or {}), + pandoc.Attr("", {}, figure_attrs.figureAttr)) + if float.type == "Listing" then + div.attr.classes:insert("listing") + elseif float.type == "Figure" then + -- apply standalone figure css + div.attr.classes:insert("quarto-figure") + div.attr.classes:insert("quarto-figure-" .. figure_attrs.align) + end + + -- also forward any column or caption classes + local currentClasses = found_image.attr.classes + for _,k in pairs(currentClasses) do + if isCaptionClass(k) or isColumnClass(k) then + div.attr.classes:insert(k) + end + end + + local ref = refType(float.identifier) + local figure_class + if float.parent_id then + figure_class = "quarto-subfloat-" .. ref + else + figure_class = "quarto-float-" .. ref + end + + -- Notice that we need to insert the figure_div value + -- into the div, but we need to use figure_tbl + -- to manipulate the contents of the custom node. + -- + -- This is because the figure_div is a pandoc.Div (required to + -- be inserted into pandoc divs), but figure_tbl is + -- the lua table with the metatable required to marshal + -- the inner contents of the custom node. + -- + -- This is relatively ugly, and another instance + -- of the impedance mismatch we have in the custom AST + -- API. + -- + -- it's possible that the better API is for custom constructors + -- to always return a Lua object and then have a separate + -- function to convert that to a pandoc AST node. + local figure_div, figure_tbl = quarto.HtmlTag({ + name = "figure", + attr = pandoc.Attr("", {"quarto-float", figure_class}, {}), + }) + + figure_tbl.content.content:insert(float_content) + if caption_content ~= nil then + if caption_location == 'top' then + figure_tbl.content.content:insert(1, caption_content) + else + figure_tbl.content.content:insert(caption_content) + end + end + div.content:insert(figure_div) + return div +end + +_quarto.ast.add_renderer("FloatRefTarget", function(_) + return _quarto.format.isAsciiDocOutput() +end, function(float) + if float.content.t == "Plain" and #float.content.content == 1 and float.content.content[1].t == "Image" then + return pandoc.Figure( + {float.content}, + {float.caption_long}, + float.identifier) + end + + float.caption_long.content:insert(1, pandoc.RawInline("asciidoc", ". ")) + float.caption_long.content:insert(pandoc.RawInline("asciidoc", "\n[[" .. float.identifier .. "]]\n====")) + return pandoc.Div({ + float.caption_long, + -- pandoc.RawBlock("asciidoc", "[[" .. float.identifier .. "]]\n====\n"), + float.content, + pandoc.RawBlock("asciidoc", "====\n\n") + }) + +end) + +_quarto.ast.add_renderer("FloatRefTarget", function(_) + return _quarto.format.isJatsOutput() +end, function(float) + -- don't emit unlabeled floats in JATS + if is_unlabeled_float(float) then + float.identifier = "" + end + decorate_caption_with_crossref(float) + return pandoc.Figure( + {float.content}, + {float.caption_long}, + float.identifier + ) +end) + +_quarto.ast.add_renderer("FloatRefTarget", function(_) + return _quarto.format.isIpynbOutput() and param("enable-crossref", true) +end, function(float) + decorate_caption_with_crossref(float) + if float.content.t == "Plain" and #float.content.content == 1 and float.content.content[1].t == "Image" then + return pandoc.Figure( + {float.content}, + {float.caption_long}, + float.identifier) + end + + return pandoc.Div({ + float.content, + pandoc.Para(quarto.utils.as_inlines(float.caption_long) or {}), + }); +end) + +-- this should really be "_quarto.format.isEmbedIpynb()" or something like that.. +_quarto.ast.add_renderer("FloatRefTarget", function(_) + return _quarto.format.isIpynbOutput() and not param("enable-crossref", true) +end, function(float) + if float.content.t == "Plain" and #float.content.content == 1 and float.content.content[1].t == "Image" then + -- replicate pre-reftarget behavior because it'll be used in embedding + -- and needs precisely the same AST output + float.content.content[1].caption = quarto.utils.as_inlines(float.caption_long) + return pandoc.Div({ + pandoc.Para({ + float.content.content[1] + }) + }) + elseif (float.content.t == "Plain" and #float.content.content == 2 + and float.content.content[1].t == "Image" + and float.content.content[2].t == "RawInline" + and float.content.content[2].format == "markdown") then + -- we assume this is the ipynb-specific which we need to manage here. + + -- replicate pre-reftarget behavior because it'll be used in embedding + -- and needs precisely the same AST output + float.content.content[1].caption = quarto.utils.as_inlines(float.caption_long) + return pandoc.Div({ + pandoc.Para({ + float.content.content[1], + float.content.content[2] + }) + }) + else + -- we're not sure how to handle this directly, so we'll just embed the caption + -- as a paragraph after the content + local result = pandoc.Div({}) + result.content:insert(float.content) + if float.caption_long then + result.content:insert(pandoc.Para(quarto.utils.as_inlines(float.caption_long) or {})) + end + return result + end +end) + +_quarto.ast.add_renderer("FloatRefTarget", function(_) + return _quarto.format.isTypstOutput() +end, function(float) + local ref = refType(float.identifier) + local info = crossref.categories.by_ref_type[ref] + if info == nil then + -- luacov: disable + warning("Unknown float type: " .. ref .. "\n Will emit without crossref information.") + return float.content + -- luacov: enable + end + local kind + local supplement = "" + local numbering = "" + + if float.parent_id then + kind = "quarto-subfloat-" .. ref + numbering = "(a)" + else + kind = "quarto-float-" .. ref + numbering = "1" + supplement = info.name + end + + local caption_location = cap_location(float) + + -- FIXME: Listings shouldn't emit centered blocks. I don't know how to disable that right now. + -- FIXME: custom numbering doesn't work yet + + return make_typst_figure { + content = float.content, + caption_location = caption_location, + caption = float.caption_long, + kind = kind, + supplement = supplement, + numbering = numbering, + identifier = float.identifier + } +end) \ No newline at end of file diff --git a/src/resources/filters/customnodes/htmltag.lua b/src/resources/filters/customnodes/htmltag.lua new file mode 100644 index 00000000000..f9cd8098959 --- /dev/null +++ b/src/resources/filters/customnodes/htmltag.lua @@ -0,0 +1,60 @@ +-- htmltag.lua +-- Copyright (C) 2023 Posit Software, PBC + +_quarto.ast.add_handler({ + + -- empty table so this handler is only called programmatically + class_name = {}, + + -- the name of the ast node, used as a key in extended ast filter tables + ast_name = "HtmlTag", + + -- float crossrefs are always blocks + kind = "Block", + + parse = function(div) + -- luacov: disable + internal_error() + -- luacov: enable + end, + + slots = { "content" }, + + constructor = function(tbl) + if tbl.attr then + tbl.identifier = tbl.attr.identifier + tbl.classes = tbl.attr.classes + tbl.attributes = as_plain_table(tbl.attr.attributes) + tbl.attr = nil + end + tbl.classes = tbl.classes or {} + tbl.attributes = tbl.attributes or {} + tbl.identifier = tbl.identifier or "" + tbl.content = pandoc.Div(tbl.content or {}) + return tbl + end +}) + +_quarto.ast.add_renderer("HtmlTag", function(_) return true end, +function(tag) + local div = pandoc.Blocks({}) + local result = div + local result_attrs = { + class = table.concat(tag.classes, " "), + } + if tag.identifier ~= nil and tag.identifier ~= "" then + result_attrs.id = tag.identifier + end + for k, v in pairs(tag.attributes) do + result_attrs[k] = v + end + local attr_string = {} + for k, v in spairs(result_attrs) do + table.insert(attr_string, k .. "=\"" .. html_escape(v, true) .. "\"") + end + result:insert(pandoc.RawBlock("html", "<" .. tag.name .. " " .. table.concat(attr_string, " ") .. ">")) + result:extend(tag.content.content) + result:insert(pandoc.RawBlock("html", "")) + + return div +end) \ No newline at end of file diff --git a/src/resources/filters/customnodes/latexcmd.lua b/src/resources/filters/customnodes/latexcmd.lua new file mode 100644 index 00000000000..c33db8c3985 --- /dev/null +++ b/src/resources/filters/customnodes/latexcmd.lua @@ -0,0 +1,92 @@ +-- latexcmd.lua +-- Copyright (C) 2023 Posit Software, PBC + +_quarto.ast.add_handler({ + class_name = {}, + ast_name = "LatexInlineCommand", + kind = "Inline", + -- luacov: disable + parse = function() internal_error() end, + -- luacov: enable + slots = { "arg", "opt_arg" }, + constructor = function(tbl) return tbl end +}) + +_quarto.ast.add_handler({ + class_name = {}, + ast_name = "LatexBlockCommand", + kind = "Block", + -- luacov: disable + parse = function() internal_error() end, + -- luacov: enable + slots = { "arg", "opt_arg" }, + constructor = function(tbl) return tbl end +}) + +_quarto.ast.add_renderer("LatexInlineCommand", function(_) return true end, +function(cmd) + local result = pandoc.Inlines({}) + result:insert(pandoc.RawInline("latex", "\\" .. cmd.name)) + local opt_arg = cmd.opt_arg + if opt_arg then + result:insert(pandoc.RawInline("latex", "[")) + if opt_arg.content then + result:extend(opt_arg.content) + else + result:insert(opt_arg) + end + result:insert(pandoc.RawInline("latex", "]")) + end + local arg = cmd.arg + if arg then + result:insert(pandoc.RawInline("latex", "{")) + if arg.content then + result:extend(arg.content) + else + result:insert(arg) + end + result:insert(pandoc.RawInline("latex", "}")) + end + return result +end) + +_quarto.ast.add_renderer("LatexBlockCommand", function(_) return true end, +function(cmd) + local result = pandoc.Blocks({}) + local preamble = pandoc.Inlines({}) + local postamble = pandoc.Inlines({}) + preamble:insert(pandoc.RawInline("latex", "\\" .. cmd.name)) + local opt_arg = cmd.opt_arg + if opt_arg then + preamble:insert(pandoc.RawInline("latex", "[")) + if opt_arg.content then + preamble:extend(opt_arg.content) + else + preamble:insert(opt_arg) + end + preamble:insert(pandoc.RawInline("latex", "]")) + end + preamble:insert(pandoc.RawInline("latex", "{")) + result:insert(pandoc.Plain(preamble)) + local arg = cmd.arg + if arg then + local pt = pandoc.utils.type(arg) + if pt == "Blocks" then + result:extend(arg) + elseif pt == "Block" then + if arg.content then + result:extend(arg.content) + else + result:insert(arg) + end + else + -- luacov: disable + fail_and_ask_for_bug_report("Unexpected type for LatexBlockCommand arg: " .. pt) + return nil + -- luacov: enable + end + end + postamble:insert(pandoc.RawInline("latex", "}")) + result:insert(pandoc.Plain(postamble)) + return result +end) \ No newline at end of file diff --git a/src/resources/filters/customnodes/latexenv.lua b/src/resources/filters/customnodes/latexenv.lua new file mode 100644 index 00000000000..d7f80e635af --- /dev/null +++ b/src/resources/filters/customnodes/latexenv.lua @@ -0,0 +1,35 @@ +-- latexenv.lua +-- Copyright (C) 2023 Posit Software, PBC + +_quarto.ast.add_handler({ + + -- empty table so this handler is only called programmatically + class_name = {}, + + -- the name of the ast node, used as a key in extended ast filter tables + ast_name = "LatexEnvironment", + + kind = "Block", + + parse = function(div) + -- luacov: disable + internal_error() + -- luacov: enable + end, + + slots = { "content" }, + + constructor = function(tbl) + tbl.content = pandoc.Div(tbl.content or {}) + return tbl + end +}) + +_quarto.ast.add_renderer("LatexEnvironment", function(_) return true end, +function(env) + local result = pandoc.Blocks({}) + result:insert(latexBeginEnv(env.name, env.pos)) + result:extend(env.content.content or env.content) + result:insert(pandoc.RawBlock("latex-merge", "\\end{" .. env.name .. "}%")) + return result +end) \ No newline at end of file diff --git a/src/resources/filters/customnodes/shortcodes.lua b/src/resources/filters/customnodes/shortcodes.lua index 026216282ca..235e741fc16 100644 --- a/src/resources/filters/customnodes/shortcodes.lua +++ b/src/resources/filters/customnodes/shortcodes.lua @@ -18,8 +18,10 @@ _quarto.ast.add_handler({ end) local shortcode_content = span.content:map(function(el) if not el.classes:includes("quarto-shortcode__-param") then + -- luacov: disable quarto.log.output(el) fatal("Unexpected span in a shortcode parse") + -- luacov: enable end -- is it a recursive shortcode? @@ -64,8 +66,10 @@ _quarto.ast.add_handler({ } end else + -- luacov: disable quarto.log.output(el) fatal("Unexpected span in a shortcode parse") + -- luacov: enable end end) local name = shortcode_content:remove(1) @@ -87,7 +91,9 @@ _quarto.ast.add_handler({ end, render = function(node) - fatal("Should not need to render a shortcode.") + -- luacov: disable + internal_error() + -- luacov: enable end, constructor = function(tbl) @@ -109,8 +115,10 @@ local function handle_shortcode(shortcode_tbl, node) -- we need to handle this explicitly if type(shortcode_tbl.name) ~= "number" then + -- luacov: disable quarto.log.output(shortcode_tbl.name) fatal("Unexpected shortcode name type " .. type(shortcode_tbl.name)) + -- luacov: enable end local shortcode_node = node.content[shortcode_tbl.name] @@ -119,8 +127,10 @@ local function handle_shortcode(shortcode_tbl, node) local custom_data, t, kind = _quarto.ast.resolve_custom_data(v) if custom_data ~= nil then if t ~= "Shortcode" then + -- luacov: disable quarto.log.output(t) fatal("Unexpected shortcode content type " .. tostring(t)) + -- luacov: enable end -- we are not resolved, so resolve shortcode_node.content[i] = handle_shortcode(custom_data, v) @@ -151,9 +161,11 @@ local function handle_shortcode(shortcode_tbl, node) if custom_data == nil then result = pandoc.utils.stringify(shortcode_node) elseif t ~= "Shortcode" then + -- luacov: disable quarto.log.output(custom_data) quarto.log.output(t) fatal("Unexpected shortcode content type " .. tostring(t)) + -- luacov: enable else local result = handle_shortcode(custom_data, shortcode_node) result = pandoc.utils.stringify(result) @@ -164,8 +176,10 @@ local function handle_shortcode(shortcode_tbl, node) table.insert(args, { value = v.value }) table.insert(raw_args, v.value) else + -- luacov: disable quarto.log.output(v) fatal("Unexpected shortcode param type " .. tostring(v.type)) + -- luacov: enable end end @@ -342,9 +356,11 @@ function shortcodeResultAsInlines(result, name) elseif isBlockEl(result) then return pandoc.utils.blocks_to_inlines( { result }, { pandoc.Space() }) else + -- luacov: disable error("Unexpected result from shortcode " .. name .. "") quarto.log.output(result) fatal("This is a bug in the shortcode. If this is a quarto shortcode, please report it at https://github.com/quarto-dev/quarto-cli") + -- luacov: enable end end @@ -374,8 +390,10 @@ function shortcodeResultAsBlocks(result, name) elseif isInlineEl(result) then return pandoc.Blocks( {pandoc.Para( {result} ) }) -- why not a plain? else + -- luacov: disable error("Unexpected result from shortcode " .. name .. "") quarto.log.output(result) fatal("This is a bug in the shortcode. If this is a quarto shortcode, please report it at https://github.com/quarto-dev/quarto-cli") + -- luacov: enable end end diff --git a/src/resources/filters/layout/asciidoc.lua b/src/resources/filters/layout/asciidoc.lua index 6e6c0df2f22..da509a67ff5 100644 --- a/src/resources/filters/layout/asciidoc.lua +++ b/src/resources/filters/layout/asciidoc.lua @@ -31,7 +31,7 @@ function asciidocFigure(image) -- the figure itself figure:extend({"image::" .. image.src .. "[\"" .. altText .. "\"]"}) - return pandoc.RawBlock("asciidoc", table.concat(figure, "")) + return pandoc.RawBlock("asciidoc", table.concat(figure, "") .. "\n\n") end function asciidocDivFigure(el) @@ -56,3 +56,69 @@ function asciidocDivFigure(el) tappend(figure, contents) return figure end + +_quarto.ast.add_renderer("PanelLayout", function(layout) + return _quarto.format.isAsciiDocOutput() +end, function(layout) + + if layout.float == nil then + fail_and_ask_for_bug_report("asciidoc format doesn't currently support layouts without floats.") + return pandoc.Div({}) + end + + -- empty options by default + if not options then + options = {} + end + -- outer panel to contain css and figure panel + local attr = pandoc.Attr(layout.identifier or "", layout.classes or {}, layout.attributes or {}) + local panel_content = pandoc.Blocks({}) + -- layout + for i, row in ipairs(layout.layout) do + + local aligns = row:map(function(cell) + -- get the align + local align = cell.attributes[kLayoutAlign] + return layoutTableAlign(align) + end) + local widths = row:map(function(cell) + -- propagage percents if they are provided + local layoutPercent = horizontalLayoutPercent(cell) + if layoutPercent then + return layoutPercent / 100 + else + return 0 + end + end) + + local cells = pandoc.List() + for _, cell in ipairs(row) do + cells:insert(cell) + end + + -- make the table + local panelTable = pandoc.SimpleTable( + pandoc.List(), -- caption + aligns, + widths, + pandoc.List(), -- headers + { cells } + ) + + -- add it to the panel + panel_content:insert(pandoc.utils.from_simple_table(panelTable)) + end + + -- this is exceedingly hacky, but it works. + local caption_str = pandoc.write(pandoc.Pandoc({layout.float.caption_long}), "asciidoc") + + -- we need to recurse into render_extended_nodes here, sigh + local content_str = pandoc.write(_quarto.ast.walk(pandoc.Pandoc(panel_content), render_extended_nodes()) or {}, "asciidoc") + local figure_str = ". " .. caption_str .. "[#" .. layout.identifier .. "]\n" .. content_str + + if layout.preamble then + return pandoc.Blocks({ layout.preamble, pandoc.RawBlock("asciidoc", figure_str) }) + else + return pandoc.RawBlock("asciidoc", figure_str) + end +end) \ No newline at end of file diff --git a/src/resources/filters/layout/cites.lua b/src/resources/filters/layout/cites.lua index ed51d749533..8f919053266 100644 --- a/src/resources/filters/layout/cites.lua +++ b/src/resources/filters/layout/cites.lua @@ -3,8 +3,7 @@ function cites_preprocess() - -- FIXME do we need parentheses here? - if not _quarto.format.isLatexOutput() and marginCitations() then + if (not _quarto.format.isLatexOutput()) and marginCitations() then return { } end @@ -20,56 +19,62 @@ function cites_preprocess() end end, - Para = function(para) - local figure = discoverFigure(para, true) - if figure and _quarto.format.isLatexOutput() and hasFigureRef(figure) then - if hasMarginColumn(figure) or hasMarginCaption(figure) then - -- This is a figure in the margin itself, we need to append citations at the end of the caption - -- without any floating - para.content[1] = _quarto.ast.walk(figure, { - Inlines = walkUnresolvedCitations(function(citation, appendInline, appendAtEnd) - appendAtEnd(citePlaceholderInlineWithProtection(citation)) - end) - }) - return para - elseif marginCitations() then - -- This is a figure is in the body, but the citation should be in the margin. Use - -- protection to shift any citations over - para.content[1] = _quarto.ast.walk(figure, { - Inlines = walkUnresolvedCitations(function(citation, appendInline, appendAtEnd) - appendInline(marginCitePlaceholderWithProtection(citation)) - end) + FloatRefTarget = function(float) + local inlines_filter + local has_margin_column = hasMarginColumn(float) + + -- general figure caption cites fixups + if (_quarto.format.isLatexOutput() and has_margin_column) or hasMarginCaption(float) then + -- This is a figure in the margin itself, we need to append citations at the end of the caption + -- without any floating + inlines_filter = walkUnresolvedCitations(function(citation, appendInline, appendAtEnd) + appendAtEnd(citePlaceholderInlineWithProtection(citation)) + end) + elseif marginCitations() then + -- This is a figure is in the body, but the citation should be in the margin. Use + -- protection to shift any citations over + inlines_filter = walkUnresolvedCitations(function(citation, appendInline, appendAtEnd) + appendInline(marginCitePlaceholderInlineWithProtection(citation)) + end) + end + if inlines_filter then + float.caption_long = _quarto.ast.walk(float.caption_long, { + Inlines = inlines_filter + }) + end + + -- table caption cites fixups + if (refType(float.identifier) == 'tbl' and _quarto.format.isLatexOutput() and hasMarginColumn(float)) or marginCitations() then + local ref_table + _quarto.ast.walk(float.content, { + Table = function(t) + ref_table = t + end + }) + if ref_table ~= nil then + -- we don't want to update this inside the float.content walk above + -- because the caption_long is part of the content and that + -- will cause weirdness + float.caption_long = _quarto.ast.walk(float.caption_long, { + Inlines = function(inlines) + return resolveCaptionCitations(inlines, has_margin_column) + end }) - return para - end + end end + + return float end, Div = function(div) - if _quarto.format.isLatexOutput() and hasMarginColumn(div) or marginCitations() then - if hasTableRef(div) then - -- inspect the table caption for refs and just mark them as resolved - local table = discoverTable(div) - if table ~= nil and table.caption ~= nil and table.caption.long ~= nil then - local cites = false - -- go through any captions and resolve citations into latex - for i, caption in ipairs(table.caption.long) do - cites = resolveCaptionCitations(caption.content, hasMarginColumn(div)) or cites - end - if cites then - return div + if (_quarto.format.isLatexOutput() and hasMarginColumn(div)) or marginCitations() then + return _quarto.ast.walk(div, { + Inlines = walkUnresolvedCitations(function(citation, appendInline, appendAtEnd) + if hasMarginColumn(div) then + appendAtEnd(citePlaceholderInline(citation)) end - end - else - return _quarto.ast.walk(div, { - Inlines = walkUnresolvedCitations(function(citation, appendInline, appendAtEnd) - if hasMarginColumn(div) then - appendAtEnd(citePlaceholderInline(citation)) - end - end) - }) - end - + end) + }) end end @@ -83,9 +88,11 @@ function cites() return { -- go through inlines and resolve any unresolved citations - Inlines = walkUnresolvedCitations(function(citation, appendInline) - appendInline(marginCitePlaceholderInline(citation)) - end) + Inlines = function(inlines) + return (walkUnresolvedCitations(function(citation, appendInline) + appendInline(marginCitePlaceholderInline(citation)) + end)(inlines)) + end } end diff --git a/src/resources/filters/layout/columns-preprocess.lua b/src/resources/filters/layout/columns-preprocess.lua index dee24b01b8c..5b006aa2594 100644 --- a/src/resources/filters/layout/columns-preprocess.lua +++ b/src/resources/filters/layout/columns-preprocess.lua @@ -3,15 +3,23 @@ function columns_preprocess() return { - Div = function(el) - - if el.attr.classes:includes('cell') then + FloatRefTarget = function(float) + local location = cap_location(float) + if location == 'margin' then + float.classes:insert('margin-caption') + noteHasColumns() + return float + end + end, + + Div = function(el) + if el.classes:includes('cell') then -- for code chunks that aren't layout panels, forward the column classes to the output -- figures or tables (otherwise, the column class should be used to layout the whole panel) resolveColumnClassesForCodeCell(el) else resolveColumnClassesForEl(el) - end + end return el end, @@ -26,9 +34,8 @@ function columns_preprocess() end -- resolves column classes for an element -function resolveColumnClassesForEl(el) - -- ignore sub figures and sub tables - if not hasRefParent(el) then +function resolveColumnClassesForEl(el) + if not hasRefParent(el) then if hasFigureRef(el) then resolveElementForScopedColumns(el, 'fig') elseif hasTableRef(el) then @@ -38,15 +45,26 @@ function resolveColumnClassesForEl(el) end -- forward column classes from code chunks onto their display / outputs -function resolveColumnClassesForCodeCell(el) +function resolveColumnClassesForCodeCell(el) + + local float_classes = {} + local float_caption_classes = {} + local found = false + + for k, v in ipairs(crossref.categories.all) do + local ref_type = v.ref_type + float_classes[ref_type] = computeClassesForScopedColumns(el, ref_type) + float_caption_classes[ref_type] = computeClassesForScopedCaption(el, ref_type) + found = #float_classes[ref_type] > 0 or #float_caption_classes[ref_type] > 0 + end -- read the classes that should be forwarded - local figClasses = computeClassesForScopedColumns(el, 'fig') - local tblClasses = computeClassesForScopedColumns(el, 'tbl') - local figCaptionClasses = computeClassesForScopedCaption(el, 'fig') - local tblCaptionClasses = computeClassesForScopedCaption(el, 'tbl') + local figClasses = float_classes.fig + local tblClasses = float_classes.tbl + local figCaptionClasses = float_caption_classes.fig + local tblCaptionClasses = float_caption_classes.tbl - if #tblClasses > 0 or #figClasses > 0 or #figCaptionClasses > 0 or #tblCaptionClasses > 0 then + if found then noteHasColumns() if hasLayoutAttributes(el) then @@ -56,28 +74,35 @@ function resolveColumnClassesForCodeCell(el) else -- Forward the column classes inside code blocks for i, childEl in ipairs(el.content) do - if childEl.attr ~= nil and childEl.attr.classes:includes('cell-output-display') then - + if childEl.classes ~= nil and childEl.classes:includes('cell-output-display') then -- look through the children for any figures or tables local forwarded = false for j, figOrTableEl in ipairs(childEl.content) do - local figure = discoverFigure(figOrTableEl, false) - if figure ~= nil then - -- forward to figures - applyClasses(figClasses, figCaptionClasses, el, childEl, figure, 'fig') - forwarded = true - elseif figOrTableEl.attr ~= nil and hasFigureRef(figOrTableEl) then - -- forward to figure divs - applyClasses(figClasses, figCaptionClasses, el, childEl, figOrTableEl, 'fig') - forwarded = true - elseif (figOrTableEl.t == 'Div' and hasTableRef(figOrTableEl)) then - -- for a table div, apply the classes to the figOrTableEl itself - applyClasses(tblClasses, tblCaptionClasses, el, childEl, figOrTableEl, 'tbl') - forwarded = true - elseif figOrTableEl.t == 'Table' then - -- the figOrTableEl is a table, just apply the classes to the div around it - applyClasses(tblClasses, tblCaptionClasses, el, childEl, childEl, 'tbl') - forwarded = true + local custom = _quarto.ast.resolve_custom_data(figOrTableEl) + if custom ~= nil then + local ref_type = crossref.categories.by_name[custom.type].ref_type + local custom_classes = float_classes[ref_type] + local custom_caption_classes = float_caption_classes[ref_type] + applyClasses(custom_classes, custom_caption_classes, el, childEl, custom, ref_type) + else + local figure = discoverFigure(figOrTableEl, false) + if figure ~= nil then + -- forward to figures + applyClasses(figClasses, figCaptionClasses, el, childEl, figure, 'fig') + forwarded = true + elseif hasFigureRef(figOrTableEl) then + -- forward to figure divs + applyClasses(figClasses, figCaptionClasses, el, childEl, figOrTableEl, 'fig') + forwarded = true + elseif (figOrTableEl.t == 'Div' and hasTableRef(figOrTableEl)) then + -- for a table div, apply the classes to the figOrTableEl itself + applyClasses(tblClasses, tblCaptionClasses, el, childEl, figOrTableEl, 'tbl') + forwarded = true + elseif figOrTableEl.t == 'Table' then + -- the figOrTableEl is a table, just apply the classes to the div around it + applyClasses(tblClasses, tblCaptionClasses, el, childEl, childEl, 'tbl') + forwarded = true + end end end @@ -152,7 +177,7 @@ function applyCaptionClasses(el, classes, scope) end -- write the resolve scopes - tappend(el.attr.classes, classes) + tappend(el.classes, classes) end function applyColumnClasses(el, classes, scope) @@ -166,7 +191,7 @@ function applyColumnClasses(el, classes, scope) end -- write the resolve scopes - tappend(el.attr.classes, classes) + tappend(el.classes, classes) end function computeClassesForScopedCaption(el, scope) @@ -241,7 +266,7 @@ function mergedScopedColumnClasses(el, scope) end function resolveScopedColumnClasses(el, scope) - local filtered = el.attr.classes:filter(function(clz) + local filtered = el.classes:filter(function(clz) return clz:match('^' .. scope .. '%-column%-') end) @@ -251,7 +276,7 @@ function resolveScopedColumnClasses(el, scope) end function resolveScopedCaptionClasses(el, scope) - local filtered = el.attr.classes:filter(function(clz) + local filtered = el.classes:filter(function(clz) return clz:match('^' .. scope .. '%-cap%-location%-') end) @@ -266,18 +291,30 @@ function resolveScopedCaptionClasses(el, scope) end end +function is_scoped_column_class(scope) + return function(clz) + return clz:match('^' .. scope .. '%-column%-') + end +end + +function is_scoped_caption_class(scope) + return function(clz) + return clz:match('^' .. scope .. '%-cap%-location%-') + end +end + function removeScopedColumnClasses(el, scope) - for i, clz in ipairs(el.attr.classes) do + for i, clz in ipairs(el.classes) do if clz:match('^' .. scope .. '%-column%-') then - el.attr.classes:remove(i) + el.classes:remove(i) end end end function removeScopedCaptionClasses(el, scope) - for i, clz in ipairs(el.attr.classes) do + for i, clz in ipairs(el.classes) do if clz:match('^' .. scope .. '%-cap%-location%-') then - el.attr.classes:remove(i) + el.classes:remove(i) end end end diff --git a/src/resources/filters/layout/columns.lua b/src/resources/filters/layout/columns.lua index d6ab0cce016..bdd700c4f51 100644 --- a/src/resources/filters/layout/columns.lua +++ b/src/resources/filters/layout/columns.lua @@ -23,12 +23,12 @@ function columns() return el else -- convert the aside class to a column-margin class - if el.attr.classes and tcontains(el.attr.classes, 'aside') then + if el.classes and tcontains(el.classes, 'aside') then noteHasColumns() - el.attr.classes = el.attr.classes:filter(function(attr) + el.classes = el.classes:filter(function(attr) return attr ~= "aside" end) - tappend(el.attr.classes, {'column-margin'}) + tappend(el.classes, {'column-margin'}) return el end end @@ -61,12 +61,12 @@ function renderDivColumn(el) -- be marked a column-margin element (so that it is processed -- by post processors). -- For example: https://github.com/quarto-dev/quarto-cli/issues/2701 - if el.attr.classes and tcontains(el.attr.classes, 'aside') then + if el.classes and tcontains(el.classes, 'aside') then noteHasColumns() - el.attr.classes = el.attr.classes:filter(function(attr) + el.classes = el.classes:filter(function(attr) return attr ~= "aside" end) - tappend(el.attr.classes, {'column-margin'}) + tappend(el.classes, {'column-margin'}) return el end @@ -92,7 +92,7 @@ function renderDivColumn(el) el.content[2] = captionContainer else -- move to container - el.attr.classes:insert(clz) + el.classes:insert(clz) end end end @@ -105,7 +105,7 @@ function renderDivColumn(el) if #columnClasses > 0 then noteHasColumns() - if el.attr.classes:includes('cell-output-display') and #el.content > 0 then + if el.classes:includes('cell-output-display') and #el.content > 0 then -- this could be a code-display-cell local figOrTable = false for j=1,#el.content do @@ -197,20 +197,20 @@ end function hasColumnClasses(el) - return tcontains(el.attr.classes, isColumnClass) or hasMarginColumn(el) + return tcontains(el.classes, isColumnClass) or hasMarginColumn(el) end function hasMarginColumn(el) - if el.attr ~= nil and el.attr.classes ~= nil then - return tcontains(el.attr.classes, 'column-margin') or tcontains(el.attr.classes, 'aside') + if el.classes ~= nil then + return tcontains(el.classes, 'column-margin') or tcontains(el.classes, 'aside') else return false end end function hasMarginCaption(el) - if el.attr ~= nil and el.attr.classes ~= nil then - return tcontains(el.attr.classes, 'margin-caption') + if el.classes ~= nil then + return tcontains(el.classes, 'margin-caption') else return false end @@ -225,7 +225,7 @@ function notColumnClass(clz) end function resolveColumnClasses(el) - return el.attr.classes:filter(isColumnClass) + return el.classes:filter(isColumnClass) end function columnToClass(column) @@ -237,10 +237,10 @@ function columnToClass(column) end function removeColumnClasses(el) - if el.attr and el.attr.classes then - for i, clz in ipairs(el.attr.classes) do + if el.classes then + for i, clz in ipairs(el.classes) do if isColumnClass(clz) then - el.attr.classes:remove(i) + el.classes:remove(i) end end end @@ -250,26 +250,26 @@ function addColumnClasses(classes, toEl) removeColumnClasses(toEl) for i, clz in ipairs(classes) do if isColumnClass(clz) then - toEl.attr.classes:insert(clz) + toEl.classes:insert(clz) end end end function removeCaptionClasses(el) - for i, clz in ipairs(el.attr.classes) do + for i, clz in ipairs(el.classes) do if isCaptionClass(clz) then - el.attr.classes:remove(i) + el.classes:remove(i) end end end function resolveCaptionClasses(el) - local filtered = el.attr.classes:filter(isCaptionClass) + local filtered = el.classes:filter(isCaptionClass) if #filtered > 0 then return {'margin-caption'} else -- try looking for attributes - if el.attr.attributes['cap-location'] == "margin" then + if el.attributes ~= nil and el.attributes['cap-location'] == "margin" then return {'margin-caption'} else return {} diff --git a/src/resources/filters/layout/confluence.lua b/src/resources/filters/layout/confluence.lua new file mode 100644 index 00000000000..c698829725c --- /dev/null +++ b/src/resources/filters/layout/confluence.lua @@ -0,0 +1,129 @@ +-- confluence.lua +-- Copyright (C) 2023 Posit Software, PBC + +-- FIXME this is repeated from overrides.lua but we need to +-- sort out our require() situation first. +local function interpolate(str, vars) + -- Allow replace_vars{str, vars} syntax as well as replace_vars(str, {vars}) + if not vars then + vars = str + str = vars[1] + end + return (string.gsub(str, "({([^}]+)})", + function(whole, i) + return vars[i] or whole + end)) +end + +local function HTMLAnchorConfluence(id) + if (not id or #id == 0) then + return pandoc.RawInline("confluence", "") + end + + local SNIPPET = [[{id}]] + + return pandoc.RawInline("confluence", interpolate { + SNIPPET, + id = id or '' + }) +end + +_quarto.ast.add_renderer("FloatRefTarget", function(_) + return _quarto.format.isConfluenceOutput() +end, function(float) + decorate_caption_with_crossref(float) + + local attr = pandoc.Attr(float.identifier or "", float.classes or {}, float.attributes or {}) + if float.content.t == "Plain" and #float.content.content == 1 and float.content.content[1].t == "Image" then + local result = float.content.content[1] + result.caption = quarto.utils.as_inlines(float.caption_long) + result.attr = merge_attrs(result.attr, attr) + return pandoc.Blocks({ HTMLAnchorConfluence(float.identifier), result }) + end + + local div_content = pandoc.Div({}, attr) + div_content.content:insert(float.content) + + if float.caption_long then + div_content.content:insert(float.caption_long) + end + + return div_content + + -- local content = pandoc.Blocks({}) + -- return pandoc.Div(content, pandoc.Attr(float.identifier or "", float.classes or {}, float.attributes or {})) +end) + +_quarto.ast.add_renderer("PanelLayout", function(_) + return _quarto.format.isConfluenceOutput() +end, function(layout) + decorate_caption_with_crossref(layout.float) + + if layout.float == nil then + fail_and_ask_for_bug_report("Confluence format can't render layouts without floats") + return nil + end + + -- empty options by default + if not options then + options = {} + end + -- outer panel to contain css and figure panel + local attr = pandoc.Attr(layout.identifier or "", layout.classes or {}, layout.attributes or {}) + local panel_content = pandoc.Blocks({}) + + -- layout + for i, row in ipairs(layout.layout) do + + local aligns = row:map(function(cell) + -- get the align + local align = cell.attributes[kLayoutAlign] + return layoutTableAlign(align) + end) + local widths = row:map(function(cell) + -- propagage percents if they are provided + local layoutPercent = horizontalLayoutPercent(cell) + if layoutPercent then + return layoutPercent / 100 + else + return 0 + end + end) + + local cells = pandoc.List() + for _, cell in ipairs(row) do + local align = cell.attributes[kLayoutAlign] + cells:insert(cell) + end + + -- make the table + local panelTable = pandoc.SimpleTable( + pandoc.List(), -- caption + aligns, + widths, + pandoc.List(), -- headers + { cells } + ) + + -- add it to the panel + panel_content:insert(pandoc.utils.from_simple_table(panelTable)) + end + if layout.float.caption_long then + panel_content:insert(pandoc.Para(quarto.utils.as_inlines(layout.float.caption_long) or {})) + end + + local result = pandoc.Div(panel_content, attr) + + if layout.preamble then + local pt = pandoc.utils.type(layout.preamble) + if pt == "Blocks" then + layout.preamble:insert(result) + return result + elseif pt == "Block" then + return pandoc.Blocks({ layout.preamble, result }) + end + else + return result + end +end) + diff --git a/src/resources/filters/layout/docx.lua b/src/resources/filters/layout/docx.lua index eb04b82144d..abf82af20cf 100644 --- a/src/resources/filters/layout/docx.lua +++ b/src/resources/filters/layout/docx.lua @@ -1,6 +1,64 @@ -- docx.lua -- Copyright (C) 2020-2022 Posit Software, PBC +function docx_content_fixups(el, align, layoutPercent) + local width = wpPageWidth() + return _quarto.ast.walk(el, { + traverse = "topdown", + Div = function(div) + if div.classes:includes("quarto-layout-cell-subref") then + layoutPercent = horizontalLayoutPercent(div) + return docx_content_fixups(div, align, layoutPercent), false + end + end, + Image = function(image) + if width then + if layoutPercent then + local inches = (layoutPercent/100) * width + image.attr.attributes["width"] = string.format("%2.2f", inches) .. "in" + return image + end + end + end, + Table = function(tbl) + if align == "center" then + -- force widths to occupy 100% + layoutEnsureFullTableWidth(tbl) + return tbl + end + end + }) or pandoc.Div({}) -- not necessary but the lua analyzer doesn't know that +end + +_quarto.ast.add_renderer("PanelLayout", function(_) + return _quarto.format.isDocxOutput() or _quarto.format.isOdtOutput() +end, function(layout) + decorate_caption_with_crossref(layout.float) + + local div = pandoc.Div({}) + + local layout_attr = pandoc.Attr(layout.identifier or "", layout.classes or {}, layout.attributes or {}) + local float_attr = pandoc.Attr(layout.float.identifier or "", layout.float.classes or {}, layout.float.attributes or {}) + div.attr = merge_attrs(float_attr, layout_attr) + + local rows = layout.rows.content:map(function(div) return div.content end) + local rendered_panel = tableDocxPanel(div, rows, layout.float.caption_long) + local align = align_attribute(layout.float) + rendered_panel = docx_content_fixups(rendered_panel, align) + + local preamble = layout.preamble + if preamble == nil then + return rendered_panel + end + + local result = pandoc.Blocks({}) + panel_insert_preamble(result, preamble) + result:insert(rendered_panel) + + return result +end) + + function tableDocxPanel(divEl, layout, caption) return tablePanel(divEl, layout, caption, { diff --git a/src/resources/filters/layout/figures.lua b/src/resources/filters/layout/figures.lua index 8fc161dedf3..434122ed2ba 100644 --- a/src/resources/filters/layout/figures.lua +++ b/src/resources/filters/layout/figures.lua @@ -3,43 +3,6 @@ local constants = require("modules/constants") --- extended figure features including fig-align, fig-env, etc. -function extended_figures() - return { - - Para = function(el) - local image = discoverFigure(el, false) - if image and shouldHandleExtendedImage(image) then - if _quarto.format.isHtmlOutput() then - return htmlImageFigure(image) - elseif _quarto.format.isLatexOutput() then - return latexImageFigure(image) - elseif _quarto.format.isDocxOutput() then - return wpDivFigure(createFigureDiv(el, image)) - elseif _quarto.format.isAsciiDocOutput() then - return asciidocFigure(image) - end - end - end, - - Div = function(el) - if isFigureDiv(el) and shouldHandleExtended(el) then - if _quarto.format.isLatexOutput() then - return latexDivFigure(el) - elseif _quarto.format.isHtmlOutput() then - return htmlDivFigure(el) - elseif _quarto.format.isDocxOutput() then - return wpDivFigure(el) - elseif _quarto.format.isJatsOutput() then - return jatsDivFigure(el) - elseif _quarto.format.isAsciiDocOutput() then - return asciidocDivFigure(el) - end - end - end - } -end - function preventExtendedFigure(el) el.attr.attributes[constants.kFigExtended] = "false" end diff --git a/src/resources/filters/layout/html.lua b/src/resources/filters/layout/html.lua index 8aeaa866e8d..3ddd4ab5d2e 100644 --- a/src/resources/filters/layout/html.lua +++ b/src/resources/filters/layout/html.lua @@ -1,119 +1,216 @@ -- html.lua -- Copyright (C) 2020-2022 Posit Software, PBC -function htmlPanel(divEl, layout, caption) - - -- outer panel to contain css and figure panel - local divId = divEl.attr.identifier - if divId == nil then - divId = '' - end - - local panel = pandoc.Div({}, pandoc.Attr(divId, divEl.attr.classes)) - panel.attr.classes:insert("quarto-layout-panel") - - -- enclose in figure if it's a figureRef - if hasFigureRef(divEl) then - panel.content:insert(pandoc.RawBlock("html", "
")) - end +_quarto.ast.add_renderer("PanelLayout", function(_) + return _quarto.format.isHtmlOutput() +end, function(panel_layout) + local panel = pandoc.Div({}) - -- compute vertical alignment and remove attribute - local vAlign = validatedVAlign(divEl.attr.attributes[kLayoutVAlign]) - local vAlignClass = vAlignClass(vAlign); - divEl.attr.attributes[kLayoutVAlign] = nil - -- layout - for i, row in ipairs(layout) do - - local rowDiv = pandoc.Div({}, pandoc.Attr("", {"quarto-layout-row"})) - - -- add the vertical align element to this row - if vAlignClass then - rowDiv.attr.classes:insert(vAlignClass); + for i, row in ipairs(panel_layout.rows.content) do + local row_div = row + row_div.attr.classes:insert("quarto-layout-row") + if panel_layout.valign_class then + row_div.attr.classes:insert(panel_layout.valign_class) end - - for i, cellDiv in ipairs(row) do + for j, cell_div in ipairs(row.content) do -- add cell class - cellDiv.attr.classes:insert("quarto-layout-cell") - - -- if it has a ref parent then give it another class - -- (used to provide subcaption styling) - if layoutCellHasRefParent(cellDiv) then - cellDiv.attr.classes:insert("quarto-layout-cell-subref") - end - + cell_div.attr.classes:insert("quarto-layout-cell") + -- create css style for width - local cellDivStyle = "" - local width = cellDiv.attr.attributes["width"] - local align = cellDiv.attr.attributes[kLayoutAlign] - cellDiv.attr.attributes[kLayoutAlign] = nil - cellDivStyle = cellDivStyle .. "flex-basis: " .. width .. ";" - cellDiv.attr.attributes["width"] = nil + local cell_div_style = "" + local width = cell_div.attr.attributes["width"] + local align = cell_div.attr.attributes[kLayoutAlign] + cell_div.attr.attributes[kLayoutAlign] = nil + cell_div_style = cell_div_style .. "flex-basis: " .. width .. ";" + cell_div.attr.attributes["width"] = nil local justify = flexAlign(align) - cellDivStyle = cellDivStyle .. "justify-content: " .. justify .. ";" - cellDiv.attr.attributes["style"] = cellDivStyle + cell_div_style = cell_div_style .. "justify-content: " .. justify .. ";" + cell_div.attr.attributes["style"] = cell_div_style + local has_table = false + local parent_id -- if it's a table then our table-inline style will cause table headers -- (th) to be centered. set them to left is they are default - local tbl = tableFromLayoutCell(cellDiv) - if tbl then - tbl.colspecs = tbl.colspecs:map(function(spec) - if spec[1] == pandoc.AlignDefault then - spec[1] = pandoc.AlignLeft + -- print(cell_div) + cell_div = _quarto.ast.walk(cell_div, { + FloatRefTarget = function(float) + parent_id = float.parent_id + return nil + end, + Table = function(table) + has_table = true + local changed = false + table.colspecs = table.colspecs:map(function(spec) + if spec[1] == pandoc.AlignDefault then + spec[1] = pandoc.AlignLeft + changed = true + end + return spec + end) + if changed then + return table end - return spec - end) + end + }) or {} -- this isn't needed by the Lua analyzer doesn't know it + + if has_table and parent_id ~= nil then + cell_div.attr.attributes[kRefParent] = parent_id end - - -- add div to row - rowDiv.content:insert(cellDiv) + row_div.content[j] = cell_div end -- add row to the panel - panel.content:insert(rowDiv) + panel.content:insert(row_div) + end + + local rendered_panel + + if panel_layout.is_float_reftarget then + rendered_panel = float_reftarget_render_html_figure( + decorate_caption_with_crossref(quarto.FloatRefTarget({ + identifier = panel_layout.identifier, + classes = panel_layout.classes, + attributes = panel_layout.attributes, + order = panel_layout.order, + type = panel_layout.type, + content = panel.content, + caption_long = pandoc.List({panel_layout.caption_long}), + }))) + rendered_panel.attr = pandoc.Attr(panel_layout.identifier, {"quarto-layout-panel"}) + else + rendered_panel = panel + rendered_panel.attr = pandoc.Attr( + panel_layout.identifier or "", + panel_layout.classes, + panel_layout.attributes) + rendered_panel.attr.classes:insert("quarto-layout-panel") + end + local preamble = panel_layout.preamble + if preamble == nil then + return rendered_panel end - -- determine alignment - local align = layoutAlignAttribute(divEl) - divEl.attr.attributes[kLayoutAlign] = nil + local result = pandoc.Blocks({}) + panel_insert_preamble(result, preamble) + result:insert(rendered_panel) + return result +end) + +-- function htmlPanel(divEl, layout, caption) - -- insert caption and
- if caption then - if hasFigureRef(divEl) then - local captionPara = pandoc.Para({}) - -- apply alignment if we have it - local figcaption = "
" - captionPara.content:insert(pandoc.RawInline("html", figcaption)) - tappend(captionPara.content, caption.content) - captionPara.content:insert(pandoc.RawInline("html", "
")) - if capLocation('fig', 'bottom') == 'bottom' then - panel.content:insert(captionPara) - else - tprepend(panel.content, { captionPara }) - end - else - local panelCaption = pandoc.Div(caption, pandoc.Attr("", { "panel-caption" })) - if hasTableRef(divEl) then - panelCaption.attr.classes:insert("table-caption") - if capLocation('tbl', 'top') == 'bottom' then - panel.content:insert(panelCaption) - else - tprepend(panel.content, { panelCaption }) - end - else - panel.content:insert(panelCaption) - end - end - end +-- -- outer panel to contain css and figure panel +-- local divId = divEl.attr.identifier +-- if divId == nil then +-- divId = '' +-- end + +-- local panel = pandoc.Div({}, pandoc.Attr(divId, divEl.attr.classes)) +-- panel.attr.classes:insert("quarto-layout-panel") - if hasFigureRef(divEl) then - panel.content:insert(pandoc.RawBlock("html", "")) - end +-- -- enclose in figure if it's a figureRef +-- if hasFigureRef(divEl) then +-- panel.content:insert(pandoc.RawBlock("html", "
")) +-- end + +-- -- compute vertical alignment and remove attribute +-- local vAlign = validatedVAlign(divEl.attr.attributes[kLayoutVAlign]) +-- local vAlignClass = vAlignClass(vAlign); +-- divEl.attr.attributes[kLayoutVAlign] = nil - -- return panel - return panel -end +-- -- layout +-- for i, row in ipairs(layout) do + +-- local rowDiv = pandoc.Div({}, pandoc.Attr("", {"quarto-layout-row"})) + +-- -- add the vertical align element to this row +-- if vAlignClass then +-- rowDiv.attr.classes:insert(vAlignClass); +-- end + +-- for i, cellDiv in ipairs(row) do + +-- -- add cell class +-- cellDiv.attr.classes:insert("quarto-layout-cell") + +-- -- if it has a ref parent then give it another class +-- -- (used to provide subcaption styling) +-- if layoutCellHasRefParent(cellDiv) then +-- cellDiv.attr.classes:insert("quarto-layout-cell-subref") +-- end + +-- -- create css style for width +-- local cellDivStyle = "" +-- local width = cellDiv.attr.attributes["width"] +-- local align = cellDiv.attr.attributes[kLayoutAlign] +-- cellDiv.attr.attributes[kLayoutAlign] = nil +-- cellDivStyle = cellDivStyle .. "flex-basis: " .. width .. ";" +-- cellDiv.attr.attributes["width"] = nil +-- local justify = flexAlign(align) +-- cellDivStyle = cellDivStyle .. "justify-content: " .. justify .. ";" +-- cellDiv.attr.attributes["style"] = cellDivStyle + +-- -- if it's a table then our table-inline style will cause table headers +-- -- (th) to be centered. set them to left is they are default +-- local tbl = tableFromLayoutCell(cellDiv) +-- if tbl then +-- tbl.colspecs = tbl.colspecs:map(function(spec) +-- if spec[1] == pandoc.AlignDefault then +-- spec[1] = pandoc.AlignLeft +-- end +-- return spec +-- end) +-- end + +-- -- add div to row +-- rowDiv.content:insert(cellDiv) +-- end + +-- -- add row to the panel +-- panel.content:insert(rowDiv) +-- end + +-- -- determine alignment +-- local align = layoutAlignAttribute(divEl) +-- divEl.attr.attributes[kLayoutAlign] = nil + +-- -- insert caption and
+-- if caption then +-- if hasFigureRef(divEl) then +-- local captionPara = pandoc.Para({}) +-- -- apply alignment if we have it +-- local figcaption = "
" +-- captionPara.content:insert(pandoc.RawInline("html", figcaption)) +-- tappend(captionPara.content, caption.content) +-- captionPara.content:insert(pandoc.RawInline("html", "
")) +-- if capLocation('fig', 'bottom') == 'bottom' then +-- panel.content:insert(captionPara) +-- else +-- tprepend(panel.content, { captionPara }) +-- end +-- else +-- local panelCaption = pandoc.Div(caption, pandoc.Attr("", { "panel-caption" })) +-- if hasTableRef(divEl) then +-- panelCaption.attr.classes:insert("table-caption") +-- if capLocation('tbl', 'top') == 'bottom' then +-- panel.content:insert(panelCaption) +-- else +-- tprepend(panel.content, { panelCaption }) +-- end +-- else +-- panel.content:insert(panelCaption) +-- end +-- end +-- end + +-- if hasFigureRef(divEl) then +-- panel.content:insert(pandoc.RawBlock("html", "")) +-- end + +-- -- return panel +-- return panel +-- end function htmlDivFigure(el) diff --git a/src/resources/filters/layout/ipynb.lua b/src/resources/filters/layout/ipynb.lua new file mode 100644 index 00000000000..60b27a99ebb --- /dev/null +++ b/src/resources/filters/layout/ipynb.lua @@ -0,0 +1,75 @@ +-- ipynb.lua +-- Copyright (C) 2020-2023 Posit Software, PBC + +_quarto.ast.add_renderer("PanelLayout", function(_) + return _quarto.format.isIpynbOutput() and param("enable-crossref", true) +end, function(layout) + if layout.float == nil then + fail_and_ask_for_bug_report("Ipynb format can't render layouts without floats") + return pandoc.Div({}) + end + decorate_caption_with_crossref(layout.float) + + -- empty options by default + if not options then + options = {} + end + -- outer panel to contain css and figure panel + local attr = pandoc.Attr(layout.identifier or "", layout.classes or {}, layout.attributes or {}) + local panel_content = pandoc.Blocks({}) + -- layout + for i, row in ipairs(layout.layout) do + + local aligns = row:map(function(cell) + -- get the align + local align = cell.attributes[kLayoutAlign] + return layoutTableAlign(align) + end) + local widths = row:map(function(cell) + -- propagage percents if they are provided + local layoutPercent = horizontalLayoutPercent(cell) + if layoutPercent then + return layoutPercent / 100 + else + return 0 + end + end) + + local cells = pandoc.List() + for _, cell in ipairs(row) do + cells:insert(cell) + end + + -- make the table + local panelTable = pandoc.SimpleTable( + pandoc.List(), -- caption + aligns, + widths, + pandoc.List(), -- headers + { cells } + ) + + -- add it to the panel + panel_content:insert(pandoc.utils.from_simple_table(panelTable)) + end + + local result = pandoc.Div({}) + + if layout.float.caption_long then + result.content:insert(pandoc.Para(quarto.utils.as_inlines(layout.float.caption_long) or {})) + end + + if layout.preamble then + return pandoc.Blocks({ layout.preamble, result }) + else + return result + end +end) + +-- this should really be "_quarto.format.isEmbedIpynb()" or something like that.. +_quarto.ast.add_renderer("PanelLayout", function(_) + return _quarto.format.isIpynbOutput() and not param("enable-crossref", true) +end, function(float) + internal_error() + return pandoc.Div({}) +end) \ No newline at end of file diff --git a/src/resources/filters/layout/jats.lua b/src/resources/filters/layout/jats.lua index 16b78fd2bb9..8c5684e960e 100644 --- a/src/resources/filters/layout/jats.lua +++ b/src/resources/filters/layout/jats.lua @@ -47,3 +47,65 @@ function jatsPosition(el) return "float" end end + +_quarto.ast.add_renderer("PanelLayout", function(layout) + return _quarto.format.isJatsOutput() +end, function(layout) + + if layout.float == nil then + fail_and_ask_for_bug_report("JATS format can't render layouts without floats") + return nil + end + + -- empty options by default + if not options then + options = {} + end + -- outer panel to contain css and figure panel + local attr = pandoc.Attr(layout.identifier or "", layout.classes or {}, layout.attributes or {}) + local panel_content = pandoc.Blocks({}) + -- layout + for i, row in ipairs(layout.layout) do + + local aligns = row:map(function(cell) + -- get the align + local align = cell.attributes[kLayoutAlign] + return layoutTableAlign(align) + end) + local widths = row:map(function(cell) + -- propagage percents if they are provided + local layoutPercent = horizontalLayoutPercent(cell) + if layoutPercent then + return layoutPercent / 100 + else + return 0 + end + end) + + local cells = pandoc.List() + for _, cell in ipairs(row) do + local align = cell.attributes[kLayoutAlign] + cells:insert(cell) + end + + -- make the table + local panelTable = pandoc.SimpleTable( + pandoc.List(), -- caption + aligns, + widths, + pandoc.List(), -- headers + { cells } + ) + + -- add it to the panel + panel_content:insert(pandoc.utils.from_simple_table(panelTable)) + end + decorate_caption_with_crossref(layout.float) + local result = pandoc.Figure(panel_content, {layout.float.caption_long}, attr) + + if layout.preamble then + return pandoc.Blocks({ layout.preamble, result }) + else + return result + end +end) \ No newline at end of file diff --git a/src/resources/filters/layout/latex.lua b/src/resources/filters/layout/latex.lua index 28c42f8140f..b57956e7b3e 100644 --- a/src/resources/filters/layout/latex.lua +++ b/src/resources/filters/layout/latex.lua @@ -2,33 +2,42 @@ -- Copyright (C) 2020-2022 Posit Software, PBC kSideCaptionEnv = 'sidecaption' -function latexPanel(divEl, layout, caption) +_quarto.ast.add_renderer("PanelLayout", function(_) + return _quarto.format.isLatexOutput() +end, function(layout) + local rendered_panel = latexPanel(layout) + local preamble = layout.preamble + if preamble == nil then + return rendered_panel + end + + local result = pandoc.Blocks({}) + panel_insert_preamble(result, preamble) + result:insert(rendered_panel) + + return result +end) + +-- function latexPanel(divEl, layout, caption) +function latexPanel(layout) - -- create container - local panel = pandoc.Div({}) - -- begin container - local env, pos = latexPanelEnv(divEl, layout) - panel.content:insert(latexBeginEnv(env, pos)); + local env, pos = latexPanelEnv(layout) + local panel_node, panel = quarto.LatexEnvironment({ + name = env, + pos = pos + }) - local capType = "fig" - local locDefault = "bottom" - if hasTableRef(divEl) then - capType = "tbl" - locDefault = "top" - end - local capLoc = capLocation(capType, locDefault) - if (caption and capLoc == "top") then - insertLatexCaption(divEl, panel.content, caption.content) - end + local capLoc = cap_location(layout.float) + local caption = create_latex_caption(layout) -- read vertical alignment and strip attribute - local vAlign = validatedVAlign(divEl.attr.attributes[kLayoutVAlign]) - divEl.attr.attributes[kLayoutVAlign] = nil + local vAlign = validatedVAlign(layout.attributes[kLayoutVAlign]) + layout.attributes[kLayoutVAlign] = nil - for i, row in ipairs(layout) do + for i, row in ipairs(layout.rows.content) do - for j, cell in ipairs(row) do + for j, cell in ipairs(row.content) do -- there should never be \begin{table} inside a panel (as that would -- create a nested float). this can happen if knitr injected it as a @@ -36,64 +45,47 @@ function latexPanel(divEl, layout, caption) cell = latexRemoveTableDelims(cell) -- process cell (enclose content w/ alignment) - local endOfTable = i == #layout - local endOfRow = j == #row + local endOfTable = i == #layout.rows.content + local endOfRow = j == #row.content local prefix, content, suffix = latexCell(cell, vAlign, endOfRow, endOfTable) - panel.content:insert(prefix) - local align = cell.attr.attributes[kLayoutAlign] - if align == "center" then - panel.content:insert(pandoc.RawBlock("latex", latexBeginAlign(align))) - end - tappend(panel.content, content) - if align == "center" then - panel.content:insert(pandoc.RawBlock("latex", latexEndAlign(align))) - end - panel.content:insert(suffix) + -- local align = cell.attributes[kLayoutAlign] + -- if align == "center" then + -- panel.content.content:insert(pandoc.RawBlock("latex", latexBeginAlign(align))) + -- end + tappend(panel.content.content, content) + -- if align == "center" then + -- panel.content.content:insert(pandoc.RawBlock("latex", latexEndAlign(align))) + -- end + -- panel.content.content:insert(suffix) end end -- surround caption w/ appropriate latex (and end the panel) - if caption and capLoc == "bottom" then - insertLatexCaption(divEl, panel.content, caption.content) + if caption then + if capLoc == "top" then + panel.content.content:insert(1, caption) + elseif capLoc == "bottom" then + panel.content.content:insert(caption) + else + warn("unknown caption location '" .. capLoc .. "'. Skipping caption.") + end end - - -- end latex env - panel.content:insert(latexEndEnv(env)); - - -- conjoin paragarphs - panel.content = latexJoinParas(panel.content) - + -- conjoin paragraphs + panel.content.content = latexJoinParas(panel.content.content) + -- return panel - return panel - + return panel_node end -- determine the environment (and pos) to use for a latex panel -function latexPanelEnv(divEl, layout) +function latexPanelEnv(layout) -- defaults - local env = latexFigureEnv(divEl) - local pos = nil + local env = latexFigureEnv(layout) + local pos = attribute(layout.float or { attributes = {} }, kFigPos) - -- explicit figure panel - if hasFigureRef(divEl) then - pos = attribute(divEl, kFigPos, pos) - -- explicit table panel - elseif hasTableRef(divEl) then - env = latexTableEnv(divEl) - -- if there are embedded tables then we need to use table - else - local haveTables = layout:find_if(function(row) - return row:find_if(hasTableRef) - end) - if haveTables then - env = latexTableEnv(divEl) - end - end - return env, pos - end -- conjoin paragraphs (allows % to work correctly between minipages or subfloats) @@ -109,117 +101,35 @@ function latexJoinParas(content) return blocks end -function latexImageFigure(image) - - return renderLatexFigure(image, function(figure) - - -- make a copy of the caption and clear it - local caption = image.caption:clone() - tclear(image.caption) - - -- get align - local align = figAlignAttribute(image) - - -- insert the figure without the caption - local figureContent = { pandoc.Para({ - pandoc.RawInline("latex", latexBeginAlign(align)), - image, - pandoc.RawInline("latex", latexEndAlign(align)), - pandoc.RawInline("latex", "\n") - }) } - - -- return the figure and caption - return figureContent, caption - - end) -end - -function latexDivFigure(divEl) - - return renderLatexFigure(divEl, function(figure) - - -- get align - local align = figAlignAttribute(divEl) - - -- append everything before the caption - local blocks = tslice(divEl.content, 1, #divEl.content - 1) - local figureContent = pandoc.List() - if align == "center" then - figureContent:insert(pandoc.RawBlock("latex", latexBeginAlign(align))) - end - tappend(figureContent, blocks) - if align == "center" then - figureContent:insert(pandoc.RawBlock("latex", latexEndAlign(align))) - end - - -- return the figure and caption - local caption = refCaptionFromDiv(divEl) - if not caption then - caption = pandoc.Inlines() - end - return figureContent, caption.content - - end) - -end - -function renderLatexFigure(el, render) - - -- create container - local figure = pandoc.Div({}) - - -- begin the figure - local figEnv = latexFigureEnv(el) - local figPos = latexFigurePosition(el, figEnv) - - figure.content:insert(latexBeginEnv(figEnv, figPos)) - - -- get the figure content and caption inlines - local figureContent, captionInlines = render(figure) - - local capLoc = capLocation("fig", "bottom") - - -- surround caption w/ appropriate latex (and end the figure) - if captionInlines and inlinesToString(captionInlines) ~= "" then - if capLoc == "top" then - insertLatexCaption(el, figure.content, captionInlines) - tappend(figure.content, figureContent) - else - tappend(figure.content, figureContent) - insertLatexCaption(el, figure.content, captionInlines) - end - else - tappend(figure.content, figureContent) - end - - -- end figure - figure.content:insert(latexEndEnv(figEnv)) - - -- return the figure - return figure - -end - function latexCaptionEnv(el) - if el.attr.classes:includes(kSideCaptionClass) then + if el.classes:includes(kSideCaptionClass) then return kSideCaptionEnv else return 'caption' end end -function insertLatexCaption(divEl, content, captionInlines) - local captionEnv = latexCaptionEnv(divEl) - markupLatexCaption(divEl, captionInlines, captionEnv) - if captionEnv == kSideCaptionEnv then - if #content > 1 then - content:insert(2, pandoc.Para(captionInlines)) - else - content:insert(#content, pandoc.Para(captionInlines)) - end - else - content:insert(pandoc.Para(captionInlines)) +function create_latex_caption(layout) + local caption_env = latexCaptionEnv(layout.float) + if ((layout.caption_long == nil or #layout.caption_long.content == 0) and + (layout.caption_short == nil or #layout.caption_short.content == 0)) then + return nil + end + local cap_inlines = quarto.utils.as_inlines(layout.caption_long) or pandoc.Inlines({}) -- unneeded but the Lua analyzer doesn't know that + if layout.identifier then + -- local label_node = quarto.LatexInlineCommand({ name = "label", arg = layout.identifier }) + local label_node = pandoc.RawInline("latex", "\\label{" .. layout.identifier .. "}") + + cap_inlines:insert(1, label_node) + end + local caption_node, caption = quarto.LatexInlineCommand({ + name = caption_env, + arg = scaffold(cap_inlines), + }) + if layout.caption_short ~= nil then + caption.opt_arg = quarto.utils.as_inlines(layout.caption_short) end + return caption_node end function latexWrapSignalPostProcessor(el, token) @@ -275,7 +185,7 @@ local kEndSideNote = '\\end{footnotesize}}' function latexEndSidenote(el, block) local offset = '' if el.attr ~= nil then - local offsetValue = el.attr.attributes['offset'] + local offsetValue = el.attributes['offset'] if offsetValue ~= nil then offset = '[' .. offsetValue .. ']' end @@ -323,7 +233,7 @@ function latexBeginEnv(env, pos, inline) if inline then return pandoc.RawInline("latex", beginEnv) else - return pandoc.RawBlock("latex", beginEnv) + return pandoc.RawBlock("latex-merge", beginEnv) end end @@ -331,17 +241,17 @@ function latexEndEnv(env, inline) if inline then return pandoc.RawInline("latex", "\\end{" .. env .. "}") else - return pandoc.RawBlock("latex", "\\end{" .. env .. "}") + return pandoc.RawBlock("latex-merge", "\\end{" .. env .. "}%") end end function latexCell(cell, vAlign, endOfRow, endOfTable) -- figure out what we are dealing with - local label = cell.attr.identifier + local label = cell.identifier local image = figureImageFromLayoutCell(cell) if (label == "") and image then - label = image.attr.identifier + label = image.identifier end local isFigure = isFigureRef(label) local isTable = isTableRef(label) @@ -349,7 +259,7 @@ function latexCell(cell, vAlign, endOfRow, endOfTable) local tbl = tableFromLayoutCell(cell) -- determine width - local width = cell.attr.attributes["width"] + local width = cell.attributes["width"] -- derive prefix, content, and suffix local prefix = pandoc.List() @@ -596,10 +506,35 @@ function latexFigureEnv(el) -- the user specified figure environment return figEnv else + local crossref_cat + if pandoc.utils.type(el) == "Block" then + local ref_type = refType(el.identifier) + if ref_type ~= nil then + crossref_cat = crossref.categories.by_ref_type[ref_type] + else + crossref_cat = crossref.categories.by_name.Figure + end + elseif pandoc.utils.type(el) == "table" then + crossref_cat = crossref.categories.by_name[el.type] + if crossref_cat == nil then + crossref_cat = crossref.categories.by_name.Figure + end + elseif pandoc.utils.type(el) == "Inline" then + local ref_type = refType(el.identifier) + if ref_type ~= nil then + crossref_cat = crossref.categories.by_ref_type[ref_type] + else + crossref_cat = crossref.categories.by_name.Figure + end + else + fail("Don't know how to handle " .. pandoc.utils.type(el) .. " in latexFigureEnv") + end + local env_name = crossref_cat.latex_env -- if not user specified, look for other classes which might determine environment local classes = el.classes for i,class in ipairs(classes) do + -- FIXME how to deal with margin custom floats? -- a margin figure or aside if isMarginEnv(class) then noteHasColumns() @@ -609,12 +544,12 @@ function latexFigureEnv(el) -- any column that resolves to full width if isStarEnv(class) then noteHasColumns() - return "figure*" + return env_name .. "*" end end -- the default figure environment - return "figure" + return env_name end end @@ -656,6 +591,90 @@ function latexTableEnv(el) return "table" end +-- this is still used by stray Figure nodes from Pandoc 3's AST +function latexImageFigure(image) + + return renderLatexFigure(image, function(figure) + + -- make a copy of the caption and clear it + local caption = image.caption:clone() + tclear(image.caption) + + -- get align + local align = figAlignAttribute(image) + + -- insert the figure without the caption + local figureContent = { pandoc.Para({ + pandoc.RawInline("latex", latexBeginAlign(align)), + image, + pandoc.RawInline("latex", latexEndAlign(align)), + pandoc.RawInline("latex", "\n") + }) } + + -- return the figure and caption + return figureContent, caption + + end) +end + +function renderLatexFigure(el, render) + + -- create container + local figure = pandoc.Div({}) + + -- begin the figure + local figEnv = latexFigureEnv(el) + local figPos = latexFigurePosition(el, figEnv) + + figure.content:insert(latexBeginEnv(figEnv, figPos)) + + -- get the figure content and caption inlines + local figureContent, captionInlines = render(figure) + + local capLoc = capLocation("fig", "bottom") + + -- surround caption w/ appropriate latex (and end the figure) + if captionInlines and inlinesToString(captionInlines) ~= "" then + if capLoc == "top" then + insertLatexCaption(el, figure.content, captionInlines) + tappend(figure.content, figureContent) + else + tappend(figure.content, figureContent) + insertLatexCaption(el, figure.content, captionInlines) + end + else + tappend(figure.content, figureContent) + end + + -- end figure + figure.content:insert(latexEndEnv(figEnv)) + + -- return the figure + return figure + +end + +function latexCaptionEnv(el) + if el.classes:includes(kSideCaptionClass) then + return kSideCaptionEnv + else + return 'caption' + end +end + +function insertLatexCaption(divEl, content, captionInlines) + local captionEnv = latexCaptionEnv(divEl) + markupLatexCaption(divEl, captionInlines, captionEnv) + if captionEnv == kSideCaptionEnv then + if #content > 1 then + content:insert(2, pandoc.Para(captionInlines)) + else + content:insert(#content, pandoc.Para(captionInlines)) + end + else + content:insert(pandoc.Para(captionInlines)) + end +end function isStarEnv(clz) return (clz:match('^column%-screen') or clz:match('^column%-page')) and not clz:match('%-left$') diff --git a/src/resources/filters/layout/layout.lua b/src/resources/filters/layout/layout.lua index 519cd47e9ef..8d12f6a163f 100644 --- a/src/resources/filters/layout/layout.lua +++ b/src/resources/filters/layout/layout.lua @@ -12,90 +12,50 @@ layoutState = { function layout_panels() return { - Div = function(el) - if requiresPanelLayout(el) then - - -- partition - local preamble, cells, caption = partitionCells(el) - - -- derive layout - local layout = layoutCells(el, cells) - - -- call the panel layout functions - local panel - if _quarto.format.isLatexOutput() then - panel = latexPanel(el, layout, caption) - elseif _quarto.format.isHtmlOutput() then - panel = htmlPanel(el, layout, caption) - elseif _quarto.format.isDocxOutput() then - panel = tableDocxPanel(el, layout, caption) - elseif _quarto.format.isOdtOutput() then - panel = tableOdtPanel(el, layout, caption) - elseif _quarto.format.isWordProcessorOutput() then - panel = tableWpPanel(el, layout, caption) - elseif _quarto.format.isPowerPointOutput() then - panel = pptxPanel(el, layout) - else - panel = tablePanel(el, layout, caption) - end - - -- transfer attributes from el to panel - local keys = tkeys(el.attr.attributes) - for _,k in pairs(keys) do - if not isLayoutAttribute(k) then - panel.attr.attributes[k] = el.attr.attributes[k] - end - end - - if #preamble > 0 then - local div = pandoc.Div({}) - if #preamble > 0 then - tappend(div.content, preamble) - end - div.content:insert(panel) - return div - - -- otherwise just return the panel - else - return panel - end - + Div = function(div) + if not attr_requires_panel_layout(div.attr) then + return nil end - end + local preamble, cells = partition_cells(div) + local layout = layout_cells(div, cells) + return quarto.PanelLayout({ + attr = div.attr, + preamble = preamble, + layout = layout, + }) + end, + FloatRefTarget = function(float) + local attr = pandoc.Attr(float.identifier, float.classes, float.attributes) + if not attr_requires_panel_layout(attr) then + return nil + end + + local preamble, cells = partition_cells(float) + local layout = layout_cells(float, cells) + + return quarto.PanelLayout({ + float = float, + preamble = preamble, + layout = layout, + }) + end, } end - -function requiresPanelLayout(divEl) - - if hasLayoutAttributes(divEl) then +function attr_requires_panel_layout(attr) + if attr_has_layout_attributes(attr) then return true - -- latex and html require special layout markup for subcaptions - elseif (_quarto.format.isLatexOutput() or _quarto.format.isHtmlOutput()) and - divEl.attr.classes:includes("tbl-parent") then - return true - else - return false end - + return (_quarto.format.isLatexOutput() or _quarto.format.isHtmlOutput()) and + attr.classes:includes("tbl-parent") end - -function partitionCells(divEl) - +function partition_cells(float) local preamble = pandoc.List() local cells = pandoc.List() - local caption = nil - - -- extract caption if it's a table or figure div - if hasFigureOrTableRef(divEl) then - caption = refCaptionFromDiv(divEl) - divEl.content = tslice(divEl.content, 1, #divEl.content-1) - end - + local heading = nil - for _,block in ipairs(divEl.content) do - + for _, block in ipairs(float.content) do if isPreambleBlock(block) then if block.t == "CodeBlock" and #preamble > 0 and preamble[#preamble].t == "CodeBlock" then preamble[#preamble].text = preamble[#preamble].text .. "\n" .. block.text @@ -108,28 +68,41 @@ function partitionCells(divEl) else heading = block end - else - -- ensure we are dealing with a div + else local cellDiv = nil - if block.t == "Div" then - -- if this has a single figure div then unwrap it - if #block.content == 1 and - block.content[#block.content].t == "Div" and - hasFigureOrTableRef(block.content[#block.content]) then - cellDiv = block.content[#block.content] - else - cellDiv = block - end - + local subfloat = _quarto.ast.resolve_custom_data(block) + + -- if we were given a scaffolding div like cell-output-display, etc, + -- we use it. + if subfloat == nil and block.t == "Div" then + cellDiv = block else cellDiv = pandoc.Div(block) end + + -- -- ensure we are dealing with a div + -- local cellDiv = nil + -- if block.t == "Div" then + -- -- if this has a single figure div then unwrap it + -- if #block.content == 1 and + -- block.content[#block.content].t == "Div" and + -- hasFigureOrTableRef(block.content[#block.content]) then + -- cellDiv = block.content[#block.content] + -- else + -- cellDiv = block + -- end + -- else + -- cellDiv = pandoc.Div(block) + -- end - -- special behavior for cells with figures (including ones w/o captions) - local fig = figureImageFromLayoutCell(cellDiv) - if fig then - -- transfer width to cell - transferImageWidthToCell(fig, cellDiv) + -- -- special behavior for cells with figures (including ones w/o captions) + -- local fig = figureImageFromLayoutCell(cellDiv) + -- if fig then + -- -- transfer width to cell + -- transferImageWidthToCell(fig, cellDiv) + -- end + if subfloat ~= nil and subfloat.t == "FloatRefTarget" then + transfer_float_image_width_to_cell(subfloat, cellDiv) end -- if we have a heading then insert it @@ -138,11 +111,17 @@ function partitionCells(divEl) heading = nil end + -- if this is .cell-output-display that isn't a figure or table -- then unroll multiple blocks - if cellDiv.attr.classes:find("cell-output-display") and - #cellDiv.content > 1 and - not hasFigureOrTableRef(cellDiv) then + local is_subfloat + _quarto.ast.walk(cellDiv, { + FloatRefTarget = function(float) + is_subfloat = true + return nil + end + }) + if cellDiv.attr.classes:find("cell-output-display") and is_subfloat == nil then for _,outputBlock in ipairs(cellDiv.content) do if outputBlock.t == "Div" then cells:insert(outputBlock) @@ -154,25 +133,39 @@ function partitionCells(divEl) -- add the div cells:insert(cellDiv) end + + -- -- if this is .cell-output-display that isn't a figure or table + -- -- then unroll multiple blocks + -- if cellDiv.attr.classes:find("cell-output-display") and + -- #cellDiv.content > 1 and + -- not hasFigureOrTableRef(cellDiv) then + -- for _,outputBlock in ipairs(cellDiv.content) do + -- if outputBlock.t == "Div" then + -- cells:insert(outputBlock) + -- else + -- cells:insert(pandoc.Div(outputBlock)) + -- end + -- end + -- else + -- -- add the div + -- cells:insert(cellDiv) + -- end end - end - return preamble, cells, caption - + return preamble, cells end - -function layoutCells(divEl, cells) +function layout_cells(float_or_div, cells) -- layout to return (list of rows) local rows = pandoc.List() -- note any figure layout attributes - local layoutRows = tonumber(attribute(divEl, kLayoutNrow, nil)) - local layoutCols = tonumber(attribute(divEl, kLayoutNcol, nil)) - local layout = attribute(divEl, kLayout, nil) + local layoutRows = tonumber(float_or_div.attributes[kLayoutNrow]) + local layoutCols = tonumber(float_or_div.attributes[kLayoutNcol]) + local layout = float_or_div.attributes[kLayout] -- default to 1 column if nothing is specified if not layoutCols and not layoutRows and not layout then @@ -240,7 +233,7 @@ function layoutCells(divEl, cells) end -- determine alignment - local align = layoutAlignAttribute(divEl) + local align = layout_align_attribute(float_or_div) -- some width and alignment handling rows = rows:map(function(row) @@ -267,6 +260,21 @@ function layoutCells(divEl, cells) end + +function requiresPanelLayout(divEl) + + if hasLayoutAttributes(divEl) then + return true + -- latex and html require special layout markup for subcaptions + elseif (_quarto.format.isLatexOutput() or _quarto.format.isHtmlOutput()) and + divEl.attr.classes:includes("tbl-parent") then + return true + else + return false + end + +end + function isPreambleBlock(el) return (el.t == "CodeBlock" and el.attr.classes:includes("cell-code")) or (el.t == "Div" and el.attr.classes:includes("cell-output-stderr")) diff --git a/src/resources/filters/layout/manuscript.lua b/src/resources/filters/layout/manuscript.lua index 0dcb7dbbc4a..8235b0ca048 100644 --- a/src/resources/filters/layout/manuscript.lua +++ b/src/resources/filters/layout/manuscript.lua @@ -143,49 +143,23 @@ function manuscript() end local labelInlines = pandoc.List({ pandoc.Str(notebookPrefix), pandoc.Str(':'), pandoc.Space(), pandoc.Link(nbTitle, notebookUrl)}) + local did_resolve = false -- Attempt to forward the link into element captions, when possible local resolvedEl = _quarto.ast.walk(divEl, { - Div = function(el) - - -- Forward to figure div - if isFigureDiv(el) then - local last = el.content[#el.content] - if last and last.t == "Para" and #el.content > 1 then - labelInlines:insert(1, pandoc.Space()) - tappend(last.content, labelInlines) - else - return nil - end - return el - end - end, - - -- Forward to figure image - Para = function(el) - local image = discoverFigure(el) - if image and isFigureImage(image) then + FloatRefTarget = function(float) + if float.caption then + did_resolve = true labelInlines:insert(1, pandoc.Space()) - tappend(image.caption, labelInlines) - return el + tappend(float.caption, labelInlines) + return float end end, - - -- Forward to tables - Table = function(el) - if el.caption then - labelInlines:insert(1, pandoc.Space()) - tappend(el.caption, labelInlines) - return el - end - end }) - if resolvedEl then + if did_resolve then return resolvedEl - else - -- FIXME This is unreachable code, walk always returns a new element - + else -- We couldn't forward to caption, just place inline divEl.content:insert(pandoc.Subscript(labelInlines)) return divEl diff --git a/src/resources/filters/layout/pandoc3_figure.lua b/src/resources/filters/layout/pandoc3_figure.lua new file mode 100644 index 00000000000..be44061aec0 --- /dev/null +++ b/src/resources/filters/layout/pandoc3_figure.lua @@ -0,0 +1,52 @@ +-- pandoc3_figure.lua +-- Copyright (C) 2023 Posit Software, PBC + +-- Figure nodes (from Pandoc3) can exist in our AST. They're +-- never cross-referenceable but they need to be rendered as +-- if they were. + +function render_pandoc3_figure() + if _quarto.format.isHtmlOutput() then + return { + traverse = "topdown", + Figure = function(figure) + local image + _quarto.ast.walk(figure, { + Image = function(img) + image = img + end + }) + if image == nil then + return figure + end + if figure.caption.long ~= nil then + image.caption = quarto.utils.as_inlines(figure.caption.long) + end + return htmlImageFigure(image) + end + } + else + return { + traverse = "topdown", + Figure = function(figure) + local image + _quarto.ast.walk(figure, { + Image = function(img) + image = img + end + }) + if image == nil then + return figure + end + if figure.caption.long ~= nil then + image.caption = quarto.utils.as_inlines(figure.caption.long) + end + for k, v in pairs(figure.attributes) do + image.attributes[k] = v + end + image.classes:extend(figure.classes) + return latexImageFigure(image) + end + } + end +end diff --git a/src/resources/filters/layout/table.lua b/src/resources/filters/layout/table.lua index fd2339a82be..271f3307d43 100644 --- a/src/resources/filters/layout/table.lua +++ b/src/resources/filters/layout/table.lua @@ -8,7 +8,7 @@ function tablePanel(divEl, layout, caption, options) options = {} end -- outer panel to contain css and figure panel - local divId = divEl.attr.identifier + local divId = divEl.identifier if divId == nil then divId = '' end diff --git a/src/resources/filters/layout/typst.lua b/src/resources/filters/layout/typst.lua new file mode 100644 index 00000000000..e1ebba0e074 --- /dev/null +++ b/src/resources/filters/layout/typst.lua @@ -0,0 +1,97 @@ +-- typst.lua +-- Copyright (C) 2023 Posit Software, PBC + +function make_typst_figure(tbl) + local content = tbl.content + local caption_location = tbl.caption_location + local caption = tbl.caption + local kind = tbl.kind + local supplement = tbl.supplement + local numbering = tbl.numbering + local identifier = tbl.identifier + + quarto.utils.dump { tbl = tbl } + return pandoc.Blocks({ + pandoc.RawInline("typst", "#figure(["), + content, + pandoc.RawInline("typst", "], caption: figure.caption("), + pandoc.RawInline("typst", "position: " .. caption_location .. ", "), + pandoc.RawInline("typst", "["), + caption, + -- apparently typst doesn't allow separate prefix and name + pandoc.RawInline("typst", "]), "), + pandoc.RawInline("typst", "kind: \"" .. kind .. "\", "), + pandoc.RawInline("typst", supplement and ("supplement: \"" .. supplement .. "\", ") or ""), + pandoc.RawInline("typst", numbering and ("numbering: \"" .. numbering .. "\", ") or ""), + pandoc.RawInline("typst", ")"), + pandoc.RawInline("typst", identifier and ("<" .. identifier .. ">") or ""), + pandoc.RawInline("typst", "\n\n") + }) +end + +_quarto.ast.add_renderer("PanelLayout", function(_) + return _quarto.format.isTypstOutput() +end, function(layout) + if layout.float == nil then + fail_and_ask_for_bug_report("PanelLayout renderer requires a float in typst output") + return pandoc.Div({}) + end + + local ref = refType(layout.float.identifier) + local kind = "quarto-float-" .. ref + local info = crossref.categories.by_ref_type[ref] + if info == nil then + -- luacov: disable + warning("Unknown float type: " .. ref .. "\n Will emit without crossref information.") + return float.content + -- luacov: enable + end + + -- typst output currently only supports a single grid + -- as output, so no rows of varying columns, etc. + local n_cols = layout.attributes[kLayoutNcol] or "1" + + local typst_figure_content = pandoc.Div({}) + typst_figure_content.content:insert(pandoc.RawInline("typst", "#grid(columns: " .. n_cols .. ", gutter: 2em,\n")) + local is_first = true + _quarto.ast.walk(layout.float.content, { + FloatRefTarget = function(_, float_obj) + if is_first then + is_first = false + else + typst_figure_content.content:insert(pandoc.RawInline("typst", ",\n")) + end + typst_figure_content.content:insert(pandoc.RawInline("typst", " [")) + typst_figure_content.content:insert(float_obj) + typst_figure_content.content:insert(pandoc.RawInline("typst", "]")) + return nil + end + }) + typst_figure_content.content:insert(pandoc.RawInline("typst", ")\n")) + local result = pandoc.Blocks({}) + if layout.preamble then + result:insert(layout.preamble) + end + local caption_location = cap_location(layout.float) + + return make_typst_figure { + content = typst_figure_content, + caption_location = caption_location, + caption = layout.float.caption_long, + kind = kind, + supplement = info.prefix, + numbering = info.numbering, + identifier = layout.float.identifier + } + -- result:extend({ + -- pandoc.RawInline("typst", "\n\n#figure(["), + -- typst_figure_content, + -- pandoc.RawInline("typst", "], caption: ["), + -- layout.float.caption_long, + -- -- apparently typst doesn't allow separate prefix and name + -- pandoc.RawInline("typst", "], kind: \"" .. kind .. "\", supplement: \"" .. info.prefix .. "\""), + -- pandoc.RawInline("typst", ", caption-pos: " .. caption_location), + -- pandoc.RawInline("typst", ")<" .. layout.float.identifier .. ">\n\n") + -- }) + -- return result +end) diff --git a/src/resources/filters/layout/width.lua b/src/resources/filters/layout/width.lua index dce108e7b97..f83bc9d9d53 100644 --- a/src/resources/filters/layout/width.lua +++ b/src/resources/filters/layout/width.lua @@ -134,4 +134,13 @@ function transferImageWidthToCell(img, divEl) img.attributes["height"] = nil end +function transfer_float_image_width_to_cell(float, div_el) + local width_attr = float.attributes["width"] + div_el.attr.attributes["width"] = width_attr + if sizeToPercent(width_attr) then + float.attributes["width"] = nil + end + float.attributes["height"] = nil +end + diff --git a/src/resources/filters/main.lua b/src/resources/filters/main.lua index 7e4e17b6022..2b330ce75d4 100644 --- a/src/resources/filters/main.lua +++ b/src/resources/filters/main.lua @@ -23,10 +23,12 @@ import("./ast/wrappedwriter.lua") import("./common/base64.lua") import("./common/citations.lua") import("./common/colors.lua") +import("./common/collate.lua") import("./common/debug.lua") import("./common/error.lua") import("./common/figures.lua") import("./common/filemetadata.lua") +import("./common/floats.lua") import("./common/format.lua") import("./common/latex.lua") import("./common/layout.lua") @@ -71,26 +73,26 @@ import("./quarto-post/pdf-images.lua") import("./quarto-post/cellcleanup.lua") import("./quarto-post/bibliography.lua") import("./quarto-post/code.lua") +import("./quarto-post/html.lua") import("./quarto-finalize/dependencies.lua") import("./quarto-finalize/book-cleanup.lua") import("./quarto-finalize/mediabag.lua") import("./quarto-finalize/meta-cleanup.lua") +import("./quarto-finalize/coalesceraw.lua") +import("./quarto-finalize/descaffold.lua") +import("./quarto-finalize/typst.lua") import("./normalize/flags.lua") import("./normalize/normalize.lua") import("./normalize/parsehtml.lua") -import("./normalize/pandoc3.lua") import("./normalize/extractquartodom.lua") +import("./normalize/astpipeline.lua") +import("./normalize/capturereaderstate.lua") -import("./layout/asciidoc.lua") import("./layout/meta.lua") import("./layout/width.lua") -import("./layout/latex.lua") -import("./layout/html.lua") import("./layout/wp.lua") -import("./layout/docx.lua") -import("./layout/jats.lua") import("./layout/odt.lua") import("./layout/pptx.lua") import("./layout/table.lua") @@ -98,16 +100,17 @@ import("./layout/figures.lua") import("./layout/cites.lua") import("./layout/columns.lua") import("./layout/manuscript.lua") +import("./layout/pandoc3_figure.lua") import("./layout/columns-preprocess.lua") import("./layout/layout.lua") +import("./crossref/custom.lua") import("./crossref/index.lua") import("./crossref/preprocess.lua") import("./crossref/sections.lua") import("./crossref/figures.lua") import("./crossref/tables.lua") import("./crossref/equations.lua") -import("./crossref/listings.lua") import("./crossref/theorems.lua") import("./crossref/qmd.lua") import("./crossref/refs.lua") @@ -133,6 +136,7 @@ import("./quarto-pre/outputs.lua") import("./quarto-pre/panel-input.lua") import("./quarto-pre/panel-layout.lua") import("./quarto-pre/panel-sidebar.lua") +import("./quarto-pre/parsefiguredivs.lua") import("./quarto-pre/project-paths.lua") import("./quarto-pre/resourcefiles.lua") import("./quarto-pre/results.lua") @@ -143,11 +147,27 @@ import("./quarto-pre/table-colwidth.lua") import("./quarto-pre/table-rawhtml.lua") import("./quarto-pre/theorems.lua") +import("./layout/html.lua") +import("./layout/latex.lua") +import("./layout/docx.lua") +import("./layout/jats.lua") +import("./layout/asciidoc.lua") + +import("./customnodes/latexenv.lua") +import("./customnodes/latexcmd.lua") +import("./customnodes/htmltag.lua") import("./customnodes/shortcodes.lua") import("./customnodes/content-hidden.lua") import("./customnodes/decoratedcodeblock.lua") import("./customnodes/callout.lua") import("./customnodes/panel-tabset.lua") +import("./customnodes/floatreftarget.lua") + +import("./layout/confluence.lua") +import("./layout/ipynb.lua") +import("./layout/typst.lua") + +import("./quarto-init/metainit.lua") -- [/import] @@ -158,50 +178,47 @@ initShortcodeHandlers() -- see whether the cross ref filter is enabled local enableCrossRef = param("enable-crossref", true) -local quartoInit = { - { name = "init-configure-filters", filter = configure_filters() }, - { name = "init-read-includes", filter = read_includes() }, +local quarto_init_filters = { + { name = "init-quarto-meta-init", filter = quarto_meta_init() }, + { name = "init-quarto-custom-meta-init", filter = { + Meta = function(meta) + content_hidden_meta(meta) + end + }}, + -- FIXME this could probably be moved into the next combineFilters below, + -- in quartoNormalize { name = "init-metadata-resource-refs", filter = combineFilters({ file_metadata(), resourceRefs() })}, } -local quartoNormalize = { +-- v1.4 change: quartoNormalize is responsible for producing a +-- "normalized" document that is ready for quarto-pre, etc. +-- notably, user filters will run on the normalized document and +-- see a "Quarto AST". For example, Figure nodes are no longer +-- going to be present, and will instead be represented by +-- our custom AST infrastructure (FloatRefTarget specifically). + +local quarto_normalize_filters = { { name = "normalize", filter = filterIf(function() + if quarto_global_state.active_filters == nil then + return false + end return quarto_global_state.active_filters.normalization end, normalize_filter()) }, - { name = "pre-table-merge-raw-html", - filter = table_merge_raw_html() - }, - - { name = "pre-content-hidden-meta", - filter = content_hidden_meta() }, - - -- 2023-04-11: We want to combine these filters but parse_md_in_html_rawblocks - -- can't be combined with parse_html_tables because combineFilters - -- doesn't inspect the contents of the results in the inner loop. - { name = "normalize-combined", filter = combineFilters({ - parse_html_tables(), - parse_extended_nodes(), - }) - }, - { - name = "normalize-extractQuartoDom", - filter = parse_md_in_html_rawblocks(), - }, + { name = "normalize-capture-reader-state", filter = normalize_capture_reader_state() } } -local quartoPre = { +tappend(quarto_normalize_filters, quarto_ast_pipeline()) + +local quarto_pre_filters = { -- quarto-pre -- TODO we need to compute flags on the results of the user filters { name = "pre-run-user-filters", filters = make_wrapped_user_filters("beforeQuartoFilters") }, - -- do this early so we can compute maxHeading while in the big traversal - { name = "crossref-init-crossref-options", filter = init_crossref_options() }, - { name = "flags", filter = compute_flags() }, -- https://github.com/quarto-dev/quarto-cli/issues/5031 @@ -210,21 +227,12 @@ local quartoPre = { -- when they mutate options { name = "pre-read-options-again", filter = init_options() }, - { name = "pre-parse-pandoc3-figures", - filter = parse_pandoc3_figures(), - flags = { "has_pandoc3_figure" } - }, - { name = "pre-bibliography-formats", filter = bibliography_formats() }, { name = "pre-shortcodes-filter", filter = shortcodes_filter(), flags = { "has_shortcodes" } }, - { name = "pre-table-colwidth-cell", - filter = table_colwidth_cell(), - flags = { "has_tbl_colwidths" } }, - { name = "pre-hidden", filter = hidden(), flags = { "has_hidden" } }, @@ -264,7 +272,6 @@ local quartoPre = { quarto_pre_figures(), quarto_pre_theorems(), docx_callout_and_table_fixup(), - code_filename(), engine_escape(), line_numbers(), bootstrap_panel_input(), @@ -282,7 +289,7 @@ local quartoPre = { { name = "pre-write-results", filter = write_results() }, } -local quartoPost = { +local quarto_post_filters = { -- quarto-post { name = "post-cell-cleanup", filter = cell_cleanup(), @@ -290,7 +297,6 @@ local quartoPost = { { name = "post-cites", filter = indexCites() }, { name = "post-foldCode", filter = foldCode() }, { name = "post-bibliography", filter = bibliography() }, - { name = "post-ipynb", filter = ipynbCode()}, { name = "post-ipynb", filter = ipynb()}, { name = "post-figureCleanupCombined", filter = combineFilters({ latexDiv(), @@ -325,11 +331,13 @@ local quartoPost = { -- extensible rendering { name = "post-render_extended_nodes", filter = render_extended_nodes() }, - { name = "post-render-pandoc-3-figures", filter = render_pandoc3_figures() }, + -- format fixups post rendering + { name = "post-render-html-fixups", filter = render_html_fixups() }, + { name = "post-userAfterQuartoFilters", filters = make_wrapped_user_filters("afterQuartoFilters") }, } -local quartoFinalize = { +local quarto_finalize_filters = { -- quarto-finalize { name = "finalize-fileMetadataAndMediabag", filter = combineFilters({ @@ -341,32 +349,29 @@ local quartoFinalize = { { name = "finalize-cites", filter = writeCites() }, { name = "finalize-metaCleanup", filter = metaCleanup() }, { name = "finalize-dependencies", filter = dependencies() }, - { name = "finalize-wrapped-writer", filter = wrapped_writer() } + { name = "finalize-coalesce-raw", filters = coalesce_raw() }, + { name = "finalize-descaffold", filter = descaffold() }, + { name = "finalize-wrapped-writer", filter = wrapped_writer() }, + { name = "finalize-typst-state", filter = setup_typst_state() } } -local quartoLayout = { +local quarto_layout_filters = { { name = "manuscript filtering", filter = manuscript() }, { name = "manuscript filtering", filter = manuscriptUnroll() }, + { name = "layout-render-pandoc3-figure", filter = render_pandoc3_figure(), + flags = { "has_pandoc3_figure" } }, { name = "layout-columns-preprocess", filter = columns_preprocess() }, { name = "layout-columns", filter = columns() }, { name = "layout-cites-preprocess", filter = cites_preprocess() }, { name = "layout-cites", filter = cites() }, - { name = "layout-panels", filter = layout_panels(), flags = - { "has_layout_attributes", "has_tbl_parent" } }, - { name = "layout-extended-figures", filter = extended_figures(), flags = - { "has_discoverable_figures", "has_figure_divs"} }, + { name = "layout-panels", filter = layout_panels() }, { name = "layout-meta-inject-latex-packages", filter = layout_meta_inject_latex_packages() } } -local quartoCrossref = { +local quarto_crossref_filters = { - { name = "crossref-preprocess", filter = crossref_preprocess(), - flags = { - "has_figure_or_table_ref", - "has_discoverable_figures", - "has_table_with_long_captions", - "has_latex_table_captions" - } }, + { name = "crossref-preprocess-floats", filter = crossref_mark_subfloats(), + }, { name = "crossref-preprocessTheorems", filter = crossref_preprocess_theorems(), @@ -377,9 +382,7 @@ local quartoCrossref = { qmd(), sections(), crossref_figures(), - crossref_tables(), equations(), - listings(), crossref_theorems(), })}, @@ -392,15 +395,15 @@ local quartoCrossref = { local filterList = {} -tappend(filterList, quartoInit) -tappend(filterList, quartoNormalize) -tappend(filterList, quartoPre) +tappend(filterList, quarto_init_filters) +tappend(filterList, quarto_normalize_filters) +tappend(filterList, quarto_pre_filters) if enableCrossRef then - tappend(filterList, quartoCrossref) + tappend(filterList, quarto_crossref_filters) end -tappend(filterList, quartoLayout) -tappend(filterList, quartoPost) -tappend(filterList, quartoFinalize) +tappend(filterList, quarto_layout_filters) +tappend(filterList, quarto_post_filters) +tappend(filterList, quarto_finalize_filters) local result = run_as_extended_ast({ pre = { diff --git a/src/resources/filters/mainstateinit.lua b/src/resources/filters/mainstateinit.lua index 244796ea24c..8c0bf1aec57 100644 --- a/src/resources/filters/mainstateinit.lua +++ b/src/resources/filters/mainstateinit.lua @@ -14,7 +14,8 @@ quarto_global_state = { file = nil, appendix = false, fileSectionIds = {}, - emulatedNodeHandlers = {} + emulatedNodeHandlers = {}, + reader_options = {} } crossref = { @@ -22,5 +23,65 @@ crossref = { startAppendix = nil, -- initialize autolabels table - autolabels = pandoc.List() + autolabels = pandoc.List(), + + -- store a subfloat index to be able to lookup by id later. + subfloats = {}, + + -- kinds are "float", "block", "inline", "anchor" + categories = { + all = { + { + default_caption_location = "bottom", + kind = "float", + name = "Figure", + prefix = "Figure", + latex_env = "figure", + ref_type = "fig", + }, + { + default_caption_location = "top", + kind = "float", + name = "Table", + prefix = "Table", + latex_env = "table", + ref_type = "tbl", + }, + { + default_caption_location = "top", + kind = "float", + name = "Listing", + prefix = "Listing", + latex_env = "codelisting", + ref_type = "lst", + } + } + + -- eventually we'll have block kinds here + -- with callouts + theorem envs + + -- eventually we'll have inline kinds here + -- with equation refs + + -- eventually we'll have anchor kinds here + -- with section/chapter/slide refs, etc + } } + + +-- set up crossref category indices +function setup_crossref_category_indices() + crossref.categories.by_ref_type = {} + crossref.categories.by_name = {} + for _, category in ipairs(crossref.categories.all) do + crossref.categories.by_ref_type[category.ref_type] = category + crossref.categories.by_name[category.name] = category + end +end + +function add_crossref_category(category) + table.insert(crossref.categories.all, category) + setup_crossref_category_indices() +end + +setup_crossref_category_indices() \ No newline at end of file diff --git a/src/resources/filters/modules/patterns.lua b/src/resources/filters/modules/patterns.lua index 0b922716dab..a8818ad73a7 100644 --- a/src/resources/filters/modules/patterns.lua +++ b/src/resources/filters/modules/patterns.lua @@ -15,6 +15,13 @@ local html_paged_table = "