diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000000..2bc450ec98d --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,8 @@ +# This file lists revisions of large-scale formatting/style changes so that +# they can be excluded from git blame results. +# +# To set this file as the default ignore file for git blame, run: +# $ git config blame.ignoreRevsFile .git-blame-ignore-revs + +# Run air formatter on project +809462301b7e19d95dcf160ae9a85e1fe2c49b0d \ No newline at end of file diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index e35e8f271b4..db427db6a95 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -37,6 +37,7 @@ jobs: fetch-depth: 0 - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun # we _also_ need npm, specifically for webui/preview @@ -104,6 +105,7 @@ jobs: ref: ${{ needs.configure.outputs.version_commit }} - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - name: Make Tarball @@ -125,6 +127,7 @@ jobs: ref: ${{ needs.configure.outputs.version_commit }} - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - name: Configure @@ -161,6 +164,7 @@ jobs: ref: ${{ needs.configure.outputs.version_commit }} - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - name: Configure @@ -197,6 +201,7 @@ jobs: ref: ${{ needs.configure.outputs.version_commit }} - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - name: Configure @@ -244,6 +249,7 @@ jobs: ref: ${{ needs.configure.outputs.version_commit }} - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - name: Configure @@ -277,6 +283,7 @@ jobs: ref: ${{ needs.configure.outputs.version_commit }} - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - name: Configure @@ -312,6 +319,7 @@ jobs: .github - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - uses: actions/download-artifact@v4 @@ -321,6 +329,17 @@ jobs: - run: | tar -zxf quarto-${{needs.configure.outputs.version}}-linux-amd64.tar.gz echo "$GITHUB_WORKSPACE/quarto-${{needs.configure.outputs.version}}/bin" >> $GITHUB_PATH + + # Check for share/preview/quarto-preview.js + - name: Ensure share/preview/quarto-preview.js exists + shell: bash + run: | + if [ ! -f "share/preview/quarto-preview.js" ]; then + echo "::error file=share/preview/quarto-preview.js::Required file share/preview/quarto-preview.js not found. Check the linux build Prepare Distribution step." + exit 1 + fi + working-directory: ${{ github.workspace }}/quarto-${{needs.configure.outputs.version}} + - run: | tar -tzvf quarto-${{needs.configure.outputs.version}}-linux-amd64.tar.gz | head ls -lR @@ -339,6 +358,7 @@ jobs: ref: ${{ needs.configure.outputs.version_commit }} - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - name: Configure Rust Tools @@ -423,16 +443,39 @@ jobs: .github - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - - uses: actions/download-artifact@v4 + - name: "Download Windows Zip" + uses: actions/download-artifact@v4 with: name: Windows Zip path: . - - run: | + + - name: "Extract Windows Zip" + run: | tar -xf quarto-${{needs.configure.outputs.version}}-win.zip - - run: Add-Content $env:GITHUB_PATH "$env:GITHUB_WORKSPACE\bin" - - run: | + + # Check for share/preview/quarto-preview.js + - name: Ensure share/preview/quarto-preview.js exists + shell: bash + run: | + if [ ! -f "share/preview/quarto-preview.js" ]; then + echo "::error file=share/preview/quarto-preview.js::Required file share/preview/quarto-preview.js not found. Check the windows build Prepare Distribution step." + exit 1 + fi + + - name: "Add bin to PATH" + run: Add-Content $env:GITHUB_PATH "$env:GITHUB_WORKSPACE\bin" + + - name: "Show Directory Listing and PATH" + run: | + ls -lR + echo $PATH + shell: bash + + - name: "Run Quarto Commands" + run: | quarto check quarto --paths quarto --version @@ -446,6 +489,7 @@ jobs: ref: ${{ needs.configure.outputs.version_commit }} - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - name: Configure @@ -516,6 +560,7 @@ jobs: .github - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - uses: actions/download-artifact@v4 @@ -525,6 +570,16 @@ jobs: - run: | tar -zxf quarto-${{needs.configure.outputs.version}}-macos.tar.gz echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH + + # Check for share/preview/quarto-preview.js + - name: Ensure share/preview/quarto-preview.js exists + shell: bash + run: | + if [ ! -f "share/preview/quarto-preview.js" ]; then + echo "::error file=share/preview/quarto-preview.js::Required file share/preview/quarto-preview.js not found. Check the windows build configure step." + exit 1 + fi + - run: tar -tzvf quarto-${{needs.configure.outputs.version}}-macos.tar.gz | head - run: ls -lR - run: echo $PATH @@ -561,6 +616,7 @@ jobs: .github - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./quarto-cli/.github/workflows/actions/prevent-rerun - name: Download Artifacts @@ -666,6 +722,7 @@ jobs: - uses: actions/checkout@v4 - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - name: Revert commit of version.txt @@ -696,6 +753,7 @@ jobs: .github - name: Prevent Re-run + if: ${{ inputs.publish-release }} uses: ./.github/workflows/actions/prevent-rerun - name: Download Artifacts diff --git a/dev-docs/checklist-make-a-new-stable-quarto-release.md b/dev-docs/checklist-make-a-new-stable-quarto-release.md index 4030de1bd29..f587418d569 100644 --- a/dev-docs/checklist-make-a-new-stable-quarto-release.md +++ b/dev-docs/checklist-make-a-new-stable-quarto-release.md @@ -23,7 +23,7 @@ - **Publishing Test**: You may elect to publish to test.pypi first by _unchecking_ the `Production Release` option - Once complete, test using ```bash - python3 -m pip install --index-url https://test.pypi.org/ --extra-index-url https://pypi.org/ quarto-cli + python3 -m pip install -i https://test.pypi.org/simple --extra-index-url https://pypi.org/simple quarto-cli ``` - You may have to run this command twice as the first time may report the package not found and cause cache invalidation. The next try should succeed. - Published to: diff --git a/news/changelog-1.6.md b/news/changelog-1.6.md index 2f06dfc868e..d4b432cf16e 100644 --- a/news/changelog-1.6.md +++ b/news/changelog-1.6.md @@ -2,7 +2,7 @@ All changes included in 1.6: ## Breaking changes -- The syntax for standard library imports in `quarto run` TypeScript files (`*.ts`) changed. Please see for how to make the necessary changes. +- The syntax for standard library imports in `quarto run` TypeScript files (`*.ts`) changed. Please see for how to make the necessary changes. ## Shortcodes diff --git a/news/changelog-1.8.md b/news/changelog-1.8.md index d2d2f80b7fd..390412fdc73 100644 --- a/news/changelog-1.8.md +++ b/news/changelog-1.8.md @@ -22,8 +22,10 @@ All changes included in 1.8: - ([#5879](https://github.com/quarto-dev/quarto-cli/issues/5879)): Improve font rendering of `kbd` shortcode on macOS. `kbd` will now also be stricter in converting keyboard shortcuts to macOS icons. - ([#8568](https://github.com/quarto-dev/quarto-cli/issues/8568)) Default inline code background color to the code block background color if not specified; foreground color is `$pre-color` in dark mode and (remains) purple in light mode. - ([#10983](https://github.com/quarto-dev/quarto-cli/issues/10983)): Fix spacing inconsistency between paras and first section headings. +- ([#11982](https://github.com/quarto-dev/quarto-cli/issues/11982)): Ensure brand.yml logos are resolved correctly when document is in a subdirectory. - ([#12259](https://github.com/quarto-dev/quarto-cli/issues/12259)): Fix conflict between `html-math-method: katex` and crossref popups (author: @benkeks). - ([#12341](https://github.com/quarto-dev/quarto-cli/issues/12341)): Enable light and dark logos for html formats (sidebar, navbar, dashboard). +- ([#12501](https://github.com/quarto-dev/quarto-cli/issues/12501)): Enable `source: file` for `brand.yml` fonts in HTML. - ([#12643](https://github.com/quarto-dev/quarto-cli/issues/12643)): Ensure brand.yml logos using urls are rendered correctly by passing them through when resolving brand `processedData`, and not processing them as paths. - ([#12734](https://github.com/quarto-dev/quarto-cli/issues/12734)): `highlight-style` now correctly supports setting a different `light` and `dark`. - ([#12747](https://github.com/quarto-dev/quarto-cli/issues/12747)): Ensure `th` elements are properly restored when Quarto's HTML table processing is happening. @@ -54,11 +56,16 @@ All changes included in 1.8: - ([#12815](https://github.com/quarto-dev/quarto-cli/issues/12815)): Do not crash when floats have no content. - ([#13119](https://github.com/quarto-dev/quarto-cli/pull/13119)): Expose `brand.logo` metadata as Typst dictionaries. - ([#13133](https://github.com/quarto-dev/quarto-cli/pull/13133)): Allow customization of light and dark logos at document level, consistent with other formats. +- ([#13297](https://github.com/quarto-dev/quarto-cli/pull/13297)): Ensure brand.yml logos are resolved correctly when Typst document is in a subdirectory. ### `beamer` - ([#12775](https://github.com/quarto-dev/quarto-cli/issues/12775)): Convert Quarto-native layouts (divs with `layout` syntax) to Beamer columns, equivalent to using the Pandoc-native syntax of div with `columns` and `column` classes. +### `pdf` + +- ([#12732](https://github.com/quarto-dev/quarto-cli/issues/12732)): Correctly detect missing definition files in multiline babel error for search package to auto-install. + ### `hugo-md` - ([#12676](https://github.com/quarto-dev/quarto-cli/issues/12676)): Add support for rendering layout panels that are not floats. @@ -67,6 +74,7 @@ All changes included in 1.8: ### `website` +- ([#10284](https://github.com/quarto-dev/quarto-cli/issues/10284)): a11y - Fix keyboard navigation for tabset panels when using an HTML theme. Tabs now properly receive keyboard focus. - ([#12551](https://github.com/quarto-dev/quarto-cli/pull/12551)): Improve warning issued when `aliases` would overwrite an existing document. - ([#12616](https://github.com/quarto-dev/quarto-cli/issues/12616)): find SVG images in image discovery for listings. - ([#12693](https://github.com/quarto-dev/quarto-cli/issues/12693)): Prevent resource exhaustion on large websites by serializing `NotebookContext` information to file instead of the environment. @@ -135,9 +143,10 @@ All changes included in 1.8: - ([#13031](https://github.com/quarto-dev/quarto-cli/pull/13031)): Add `.quarto_ipynb` files to `.gitignore` by default. - ([#13085](https://github.com/quarto-dev/quarto-cli/pull/13085)): Avoid `kbd` shortcode crashes on unknown OS keys. - ([#13164](https://github.com/quarto-dev/quarto-cli/pull/13164)): add `julia` to execute schema to allow autocomplete suggestions. (@mcanouil) - +- ([#13121](https://github.com/quarto-dev/quarto-cli/issues/13121)): Allow `contents` shortcode to find inline elements. +- ([#13216](https://github.com/quarto-dev/quarto-cli/issues/13216)): Properly disable `downlit` (`code-link`) and enable `code-annotations` when non-R code blocks are present. ## Quarto Internals - ([#13155](https://github.com/quarto-dev/quarto-cli/pull/13155)): Process `pandoc-reader-FORMAT` raw blocks through `pandoc.read(FORMAT)`. -- ([#13255](https://github.com/quarto-dev/quarto-cli/pull/13255)): Move some Lua code to use Pandoc's Lua API. \ No newline at end of file +- ([#13255](https://github.com/quarto-dev/quarto-cli/pull/13255)): Move some Lua code to use Pandoc's Lua API. diff --git a/package/src/common/prepare-dist.ts b/package/src/common/prepare-dist.ts index 2dfbd908bde..c10c3a88add 100755 --- a/package/src/common/prepare-dist.ts +++ b/package/src/common/prepare-dist.ts @@ -30,9 +30,6 @@ export async function prepareDist( // copy from resources dir to the 'share' dir (which is resources) // config.directoryInfo.share - // FIXME holding off on prepareDist building assets until we fix - // this issue: https://github.com/quarto-dev/quarto-cli/runs/4229822735?check_suite_focus=true - // Moving appropriate binaries into place // Get each dependency extracted into the 'bin' folder diff --git a/src/command/dev-call/cmd.ts b/src/command/dev-call/cmd.ts index 7fc67453ce7..30f3346a75d 100644 --- a/src/command/dev-call/cmd.ts +++ b/src/command/dev-call/cmd.ts @@ -4,6 +4,7 @@ import { commands } from "../command.ts"; import { buildJsCommand } from "./build-artifacts/cmd.ts"; import { validateYamlCommand } from "./validate-yaml/cmd.ts"; import { showAstTraceCommand } from "./show-ast-trace/cmd.ts"; +import { makeAstDiagramCommand } from "./make-ast-diagram/cmd.ts"; type CommandOptionInfo = { name: string; @@ -73,4 +74,5 @@ export const devCallCommand = new Command() .command("cli-info", generateCliInfoCommand) .command("validate-yaml", validateYamlCommand) .command("build-artifacts", buildJsCommand) - .command("show-ast-trace", showAstTraceCommand); + .command("show-ast-trace", showAstTraceCommand) + .command("make-ast-diagram", makeAstDiagramCommand); diff --git a/src/command/dev-call/make-ast-diagram/cmd.ts b/src/command/dev-call/make-ast-diagram/cmd.ts new file mode 100644 index 00000000000..85a6eec2b1f --- /dev/null +++ b/src/command/dev-call/make-ast-diagram/cmd.ts @@ -0,0 +1,36 @@ +/* + * cmd.ts + * + * Copyright (C) 2025 Posit Software, PBC + */ + +import { Command } from "cliffy/command/mod.ts"; +import { join } from "../../../deno_ral/path.ts"; +import { resourcePath } from "../../../core/resources.ts"; +import { execProcess } from "../../../core/process.ts"; + +export const makeAstDiagramCommand = new Command() + .name("make-ast-diagram") + .hidden() + .option("-m, --mode ", "Diagram mode (default: full)") + .arguments("") + .description( + "Creates a diagram of the Pandoc AST.\n\n", + ) + //deno-lint-ignore no-explicit-any + .action(async (options: any, ...args: string[]) => { + const renderOpts = { + cmd: Deno.execPath(), + args: [ + "run", + "--allow-read", + "--allow-write", + "--allow-run", + resourcePath(join("tools", "ast-diagram", "main.ts")), + ...args, + "--mode", + options.mode || "full", + ], + }; + await execProcess(renderOpts); + }); diff --git a/src/command/render/latexmk/parse-error.ts b/src/command/render/latexmk/parse-error.ts index d58e3ef785c..ca577e1f5ca 100644 --- a/src/command/render/latexmk/parse-error.ts +++ b/src/command/render/latexmk/parse-error.ts @@ -270,7 +270,7 @@ const packageMatchers = [ { regex: /.* Loading '([^']+)' aborted!.*/g }, { regex: /.*! LaTeX Error: File `([^']+)' not found.*/g }, { regex: /.* file ['`]?([^' ]+)'? not found.*/g }, - { regex: /.*the language definition file ([^ ]+) .*/g }, + { regex: /.*the language definition file ([^\s]*).*/g }, { regex: /.* \\(file ([^)]+)\\): cannot open .*/g }, { regex: /.*file `([^']+)' .*is missing.*/g }, { regex: /.*! CTeX fontset `([^']+)' is unavailable.*/g }, diff --git a/src/core/brand/brand.ts b/src/core/brand/brand.ts index 375d2ef8846..336f3767a7d 100644 --- a/src/core/brand/brand.ts +++ b/src/core/brand/brand.ts @@ -29,7 +29,7 @@ import { } from "../../resources/types/zod/schema-types.ts"; import { InternalError } from "../lib/error.ts"; -import { join, relative } from "../../deno_ral/path.ts"; +import { dirname, join, relative, resolve } from "../../deno_ral/path.ts"; import { warnOnce } from "../log.ts"; import { isCssColorName } from "../css/color-names.ts"; import { @@ -38,6 +38,7 @@ import { LogoSpecifier, LogoSpecifierPathOptional, } from "../../resources/types/schema-types.ts"; +import { ensureLeadingSlash } from "../path.ts"; type ProcessedBrandData = { color: Record; @@ -390,6 +391,35 @@ export function resolveLogo( }; } +const ensureLeadingSlashIfNotExternal = (path: string) => + isExternalPath(path) ? path : ensureLeadingSlash(path); + +export function logoAddLeadingSlashes( + spec: NormalizedLogoLightDarkSpecifier | undefined, + brand: LightDarkBrand | undefined, + input: string | undefined, +): NormalizedLogoLightDarkSpecifier | undefined { + if (!spec) { + return spec; + } + if (input) { + const inputDir = dirname(resolve(input)); + if (!brand || inputDir === brand.light?.projectDir) { + return spec; + } + } + return { + light: spec.light && { + ...spec.light, + path: ensureLeadingSlashIfNotExternal(spec.light.path), + }, + dark: spec.dark && { + ...spec.dark, + path: ensureLeadingSlashIfNotExternal(spec.dark.path), + }, + }; +} + // this a typst workaround but might as well write it as a proper function export function fillLogoPaths( brand: LightDarkBrand | undefined, diff --git a/src/core/path.ts b/src/core/path.ts index 65da687e6e1..23e606ff66a 100644 --- a/src/core/path.ts +++ b/src/core/path.ts @@ -200,6 +200,14 @@ export function removeTrailingSlash(path: string) { } } +export function ensureLeadingSlash(path: string) { + if (path && !path.startsWith("/")) { + return "/" + path; + } else { + return path; + } +} + export function resolveGlobs( root: string, globs: string[], diff --git a/src/core/sass/brand.ts b/src/core/sass/brand.ts index 6ed3bd6369b..2d1f1c112ef 100644 --- a/src/core/sass/brand.ts +++ b/src/core/sass/brand.ts @@ -18,6 +18,7 @@ import { BrandFont, // BrandFontBunny, BrandFontCommon, + BrandFontFile, BrandFontGoogle, BrandFontWeight, Zod, @@ -25,6 +26,7 @@ import { import { Brand } from "../brand/brand.ts"; import { darkModeDefault } from "../../format/html/format-html-info.ts"; import { kBrandMode } from "../../config/constants.ts"; +import { join, relative } from "../../deno_ral/path.ts"; const defaultColorNameMap: Record = { "link-color": "link", @@ -149,6 +151,28 @@ const googleFontImportString = (description: BrandFontGoogle) => { }:${styleString}wght@${weights}&display=${display}');`; }; +const fileFontImportString = (brand: Brand, description: BrandFontFile) => { + const pathPrefix = relative(brand.projectDir, brand.brandDir); + const parts = []; + for (const file of description.files) { + let path, weight, style; + if (typeof file === "string") { + path = file; + } else { + path = file.path; + weight = file.weight; + style = file.style; + } + parts.push(`@font-face { + font-family: '${description.family}'; + src: url('${join(pathPrefix, path).replace(/\\/g, '/')}'); + font-weight: ${weight || "normal"}; + font-style: ${style || "normal"}; +}\n`); + } + return parts.join("\n"); +}; + const brandColorLayer = ( brand: Brand, nameMap: Record, @@ -352,7 +376,7 @@ const brandTypographyLayer = ( const resolveBunnyFontFamily = ( font: BrandFont[], ): string | undefined => { - let googleFamily = ""; + let bunnyFamily = ""; for (const _resolvedFont of font) { const safeResolvedFont = Zod.BrandFontBunny.safeParse(_resolvedFont); if (!safeResolvedFont.success) { @@ -367,19 +391,49 @@ const brandTypographyLayer = ( if (!thisFamily) { continue; } - if (googleFamily === "") { - googleFamily = thisFamily; - } else if (googleFamily !== thisFamily) { + if (bunnyFamily === "") { + bunnyFamily = thisFamily; + } else if (bunnyFamily !== thisFamily) { throw new Error( - `Inconsistent Google font families found: ${googleFamily} and ${thisFamily}`, + `Inconsistent Bunny font families found: ${bunnyFamily} and ${thisFamily}`, ); } typographyImports.add(bunnyFontImportString(resolvedFont)); } - if (googleFamily === "") { + if (bunnyFamily === "") { return undefined; } - return googleFamily; + return bunnyFamily; + }; + + const resolveFileFontFamily = ( + brand: Brand, + font: BrandFont[], + ): string | undefined => { + let fileFamily = ""; + for (const _resolvedFont of font) { + const safeResolvedFont = Zod.BrandFontFile.safeParse(_resolvedFont); + if (!safeResolvedFont.success) { + return undefined; + } + const resolvedFont = safeResolvedFont.data; + const thisFamily = resolvedFont.family; + if (!thisFamily) { + continue; + } + if (fileFamily === "") { + fileFamily = thisFamily; + } else if (fileFamily !== thisFamily) { + throw new Error( + `Inconsistent Files font families found: ${fileFamily} and ${thisFamily}`, + ); + } + typographyImports.add(fileFontImportString(brand, resolvedFont)); + } + if (fileFamily === "") { + return undefined; + } + return fileFamily; }; type HTMLFontInformation = { [key: string]: unknown }; @@ -410,7 +464,7 @@ const brandTypographyLayer = ( const font = getFontFamilies(family); result.family = resolveGoogleFontFamily(font) ?? resolveBunnyFontFamily(font) ?? - // resolveFilesFontFamily(font) ?? + resolveFileFontFamily(brand, font) ?? family; for ( const entry of [ diff --git a/src/format/dashboard/format-dashboard.ts b/src/format/dashboard/format-dashboard.ts index a3da7e7b798..87776f5f39f 100644 --- a/src/format/dashboard/format-dashboard.ts +++ b/src/format/dashboard/format-dashboard.ts @@ -66,7 +66,7 @@ import { processToolbars } from "./format-dashboard-toolbar.ts"; import { processDatatables } from "./format-dashboard-tables.ts"; import { assert } from "testing/asserts"; import { brandBootstrapSassBundles } from "../../core/sass/brand.ts"; -import { resolveLogo } from "../../core/brand/brand.ts"; +import { logoAddLeadingSlashes, resolveLogo } from "../../core/brand/brand.ts"; const kDashboardClz = "quarto-dashboard"; @@ -130,12 +130,14 @@ export function dashboardFormat() { alt: format.metadata[kLogoAlt] as string, }; } - format.metadata[kLogo] = resolveLogo(brand, logoSpec, [ + let logo = resolveLogo(brand, logoSpec, [ "small", "medium", "large", ]); + logo = logoAddLeadingSlashes(logo, brand, input); + format.metadata[kLogo] = logo; const extras: FormatExtras = await baseHtmlFormat.formatExtras( input, markdown, diff --git a/src/format/reveal/format-reveal.ts b/src/format/reveal/format-reveal.ts index 69d9416abaa..9bf7b0e69ef 100644 --- a/src/format/reveal/format-reveal.ts +++ b/src/format/reveal/format-reveal.ts @@ -79,7 +79,7 @@ import { ProjectContext } from "../../project/types.ts"; import { titleSlidePartial } from "./format-reveal-title.ts"; import { registerWriterFormatHandler } from "../format-handlers.ts"; import { pandocNativeStr } from "../../core/pandoc/codegen.ts"; -import { resolveLogo } from "../../core/brand/brand.ts"; +import { logoAddLeadingSlashes, resolveLogo } from "../../core/brand/brand.ts"; export function revealResolveFormat(format: Format) { format.metadata = revealMetadataFilter(format.metadata); @@ -299,7 +299,7 @@ export function revealjsFormat() { theme["text-highlighting-mode"], ), ], - [kMarkdownAfterBody]: [revealMarkdownAfterBody(format)], + [kMarkdownAfterBody]: [revealMarkdownAfterBody(format, input)], }, }, ); @@ -378,7 +378,7 @@ export function revealjsFormat() { ); } -function revealMarkdownAfterBody(format: Format) { +function revealMarkdownAfterBody(format: Format, input: string) { let brandMode: "light" | "dark" = "light"; if (format.metadata[kBrandMode] === "dark") { brandMode = "dark"; @@ -387,13 +387,14 @@ function revealMarkdownAfterBody(format: Format) { lines.push("::: {.quarto-auto-generated-content style='display: none;'}\n"); const revealLogo = format .metadata[kSlideLogo] as (string | { path: string } | undefined); - const logo = resolveLogo(format.render.brand, revealLogo, [ + let logo = resolveLogo(format.render.brand, revealLogo, [ "small", "medium", "large", ]); if (logo && logo[brandMode]) { - const modeLogo = logo[brandMode]!; + logo = logoAddLeadingSlashes(logo, format.render.brand, input); + const modeLogo = logo![brandMode]!; const altText = modeLogo.alt ? `alt="${modeLogo.alt}" ` : ""; lines.push( ``, diff --git a/src/project/types/website/website-navigation.ts b/src/project/types/website/website-navigation.ts index ed6b92b6bdc..31c8805fa70 100644 --- a/src/project/types/website/website-navigation.ts +++ b/src/project/types/website/website-navigation.ts @@ -1015,16 +1015,6 @@ async function sidebarEjsData(project: ProjectContext, sidebar: Sidebar) { // ensure title and search are present sidebar.title = await sidebarTitle(sidebar, project) as string | undefined; - if (sidebar.logo) { - // sidebar logo has been normalized - const sidebarLogo = sidebar.logo as NormalizedLogoLightDarkSpecifier; - if (sidebarLogo.light) { - sidebarLogo.light.path = resolveLogo(sidebarLogo.light.path)!; - } - if (sidebarLogo.dark) { - sidebarLogo.dark.path = resolveLogo(sidebarLogo.dark.path)!; - } - } const searchOpts = await searchOptions(project); sidebar.search = sidebar.search !== undefined ? sidebar.search @@ -1258,16 +1248,6 @@ async function navbarEjsData( : ("-" + (navbar[kCollapseBelow] || "lg")) as LayoutBreak, pinned: navbar.pinned !== undefined ? !!navbar.pinned : false, }; - if (data.logo) { - // navbar logo has been normalized - const navbarLogo = data.logo as NormalizedLogoLightDarkSpecifier; - if (navbarLogo.light) { - navbarLogo.light.path = resolveLogo(navbarLogo.light.path)!; - } - if (navbarLogo.dark) { - navbarLogo.dark.path = resolveLogo(navbarLogo.dark.path)!; - } - } // if there is no navbar title and it hasn't been set to 'false' // then use the site title if (!data.title && data.title !== false) { @@ -1515,14 +1495,6 @@ async function sidebarTitle(sidebar: Sidebar, project: ProjectContext) { } } -function resolveLogo(logo?: string) { - if (logo && !isExternalPath(logo) && !logo.startsWith("/")) { - return "/" + logo; - } else { - return logo; - } -} - async function websiteHeadroom(project: ProjectContext) { const { navbar, sidebars } = await websiteNavigationConfig(project); if (navbar || sidebars?.length) { diff --git a/src/project/types/website/website-shared.ts b/src/project/types/website/website-shared.ts index 8b6992407e2..463c99c3e5e 100644 --- a/src/project/types/website/website-shared.ts +++ b/src/project/types/website/website-shared.ts @@ -47,7 +47,10 @@ import { Format, FormatExtras } from "../../../config/types.ts"; import { kPageTitle, kTitle, kTitlePrefix } from "../../../config/constants.ts"; import { md5HashAsync } from "../../../core/hash.ts"; export { type NavigationFooter } from "../../types.ts"; -import { resolveLogo } from "../../../core/brand/brand.ts"; +import { + logoAddLeadingSlashes, + resolveLogo, +} from "../../../core/brand/brand.ts"; export interface Navigation { navbar?: Navbar; @@ -137,11 +140,13 @@ export async function websiteNavigationConfig(project: ProjectContext) { navLogo = { path: navLogo, alt: navbar[kLogoAlt] }; } } - navbar.logo = resolveLogo(projectBrand, navLogo, [ + let logo = resolveLogo(projectBrand, navLogo, [ "small", "medium", "large", ]); + logo = logoAddLeadingSlashes(logo, projectBrand, undefined); + navbar.logo = logo; } // read sidebar const sidebar = websiteConfig(kSiteSidebar, project.config); @@ -194,11 +199,13 @@ export async function websiteNavigationConfig(project: ProjectContext) { // } } } - sidebars[0].logo = resolveLogo(projectBrand, sideLogo, [ + let logo = resolveLogo(projectBrand, sideLogo, [ "medium", "small", "large", ]); + logo = logoAddLeadingSlashes(logo, projectBrand, undefined); + sidebars[0].logo = logo; // convert contents: auto into items for (const sb of sidebars) { diff --git a/src/resources/filters/customnodes/panel-tabset.lua b/src/resources/filters/customnodes/panel-tabset.lua index 03b208fb88a..75972b75050 100644 --- a/src/resources/filters/customnodes/panel-tabset.lua +++ b/src/resources/filters/customnodes/panel-tabset.lua @@ -286,7 +286,7 @@ function bootstrapTabs() active = " active" selected = "true" end - return 'class="nav-link' .. active .. '" id="' .. tablinkid .. '" data-bs-toggle="tab" data-bs-target="#' .. tabid .. '" role="tab" aria-controls="' .. tabid .. '" aria-selected="' .. selected .. '"' + return 'class="nav-link' .. active .. '" id="' .. tablinkid .. '" data-bs-toggle="tab" data-bs-target="#' .. tabid .. '" role="tab" aria-controls="' .. tabid .. '" aria-selected="' .. selected .. '" href=""' end, paneAttribs = function(tabid, isActive, headingAttribs) local tablinkid = tabid .. "-tab" diff --git a/src/resources/filters/quarto-post/typst-brand-yaml.lua b/src/resources/filters/quarto-post/typst-brand-yaml.lua index ef071921721..5c2cb3e00e6 100644 --- a/src/resources/filters/quarto-post/typst-brand-yaml.lua +++ b/src/resources/filters/quarto-post/typst-brand-yaml.lua @@ -318,6 +318,10 @@ function render_typst_brand_yaml() imageFilename = imageFilename and imageFilename:gsub('\\_', '_') else -- backslashes need to be doubled for Windows + if imageFilename[1] ~= "/" and _quarto.projectOffset() ~= "." then + local offset = _quarto.projectOffset() + imageFilename = pandoc.path.join({offset, imageFilename}) + end imageFilename = string.gsub(imageFilename, '\\', '\\\\') end logoOptions.path = pandoc.RawInline('typst', imageFilename) diff --git a/src/resources/filters/quarto-pre/contentsshortcode.lua b/src/resources/filters/quarto-pre/contentsshortcode.lua index 6e21e30bb08..174d9ddfc69 100644 --- a/src/resources/filters/quarto-pre/contentsshortcode.lua +++ b/src/resources/filters/quarto-pre/contentsshortcode.lua @@ -6,6 +6,27 @@ function contents_shortcode_filter() local divs = {} local spans = {} + local function handle_inline_with_attr(el) + if ids_used[el.attr.identifier] then + spans[el.attr.identifier] = el + return {} + end + + -- remove 'cell-' from identifier, try again + local truncated_id = el.attr.identifier:match("^cell%-(.+)$") + if ids_used[truncated_id] then + spans[truncated_id] = el + -- FIXME: this is a workaround for the fact that we don't have a way to + -- distinguish between divs that appear as the output of code cells + -- (which have a different id creation mechanism) + -- and "regular" divs. + -- We need to fix https://github.com/quarto-dev/quarto-cli/issues/7062 first. + return {} + else + return nil + end + end + return { Pandoc = function(doc) _quarto.ast.walk(doc.blocks, { @@ -43,13 +64,10 @@ function contents_shortcode_filter() return nil end end, - Span = function(el) - if not ids_used[el.attr.identifier] then - return nil - end - spans[el.attr.identifier] = el - return {} - end + Code = handle_inline_with_attr, + Image = handle_inline_with_attr, + Span = handle_inline_with_attr, + Link = handle_inline_with_attr }) local handle_block = function(el) @@ -75,14 +93,22 @@ function contents_shortcode_filter() return {} end local div = divs[data] - if div == nil then - warn( - "[Malformed document] Found `contents` shortcode without a corresponding div with id: " .. tostring(data) .. ".\n" .. - "This might happen because the shortcode is used in div context, while the id corresponds to a span.\n" .. - "Removing from document.") - return {} + if div ~= nil then + -- if we have a div, return it + return div + end + -- if we don't have a div, try to find a span + -- and wrap it in a div + local span = spans[data] + if span ~= nil then + -- if we have a span, return it wrapped in a div + return pandoc.Div(pandoc.Plain({span})) end - return div + quarto.log.warning( + "[Malformed document] Found `contents` shortcode without a corresponding div with id: " .. tostring(data) .. ".\n" .. + "This might happen because the shortcode is used in div context, while the id corresponds to a span.\n" .. + "Removing from document.") + return {} end -- replace div-context entries doc.blocks = _quarto.ast.walk(doc.blocks, { diff --git a/src/resources/filters/quarto-pre/shortcodes-handlers.lua b/src/resources/filters/quarto-pre/shortcodes-handlers.lua index ce75e5b1a96..5f241cc8861 100644 --- a/src/resources/filters/quarto-pre/shortcodes-handlers.lua +++ b/src/resources/filters/quarto-pre/shortcodes-handlers.lua @@ -112,6 +112,13 @@ function initShortcodeHandlers() return quarto.shortcode.error_output("brand", args, context) end + local add_leading_slash = function(path) + if path:match '^https?:' or path[1] == "/" then + return path + end + return "/" .. path + end + if brandCommand == "color" then local brandMode = 'light' if #args > 2 then @@ -167,11 +174,11 @@ function initShortcodeHandlers() end local images = {} if lightLogo then - table.insert(images, pandoc.Image(pandoc.Inlines {}, lightLogo.path, "", + table.insert(images, pandoc.Image(pandoc.Inlines {}, add_leading_slash(lightLogo.path), "", pandoc.Attr("", {"light-content"}, {alt = lightLogo.alt}))) end if darkLogo then - table.insert(images, pandoc.Image(pandoc.Inlines {}, darkLogo.path, "", + table.insert(images, pandoc.Image(pandoc.Inlines {}, add_leading_slash(darkLogo.path), "", pandoc.Attr("", {"dark-content"}, {alt = darkLogo.alt}))) end if context == "block" then diff --git a/src/resources/rmd/execute.R b/src/resources/rmd/execute.R index 51b61f8257e..c99c40538d1 100644 --- a/src/resources/rmd/execute.R +++ b/src/resources/rmd/execute.R @@ -19,8 +19,11 @@ execute <- function( markdown ) { # calculate knit_root_dir (before we setwd below) - knit_root_dir <- if (!is.null(cwd)) tools::file_path_as_absolute(cwd) else + knit_root_dir <- if (!is.null(cwd)) { + tools::file_path_as_absolute(cwd) + } else { NULL + } # change to input dir and make input relative (matches # behavior/expectations of rmarkdown::render code) @@ -210,8 +213,11 @@ execute <- function( # include supporting files supporting <- if ( !is.null(intermediates_dir) && file_test("-d", intermediates_dir) - ) - rmarkdown:::abs_path(intermediates_dir) else character() + ) { + rmarkdown:::abs_path(intermediates_dir) + } else { + character() + } # ammend knit_meta with paged table if df_print == "paged" if (df_print == "paged") { @@ -287,7 +293,9 @@ knitr_options <- function(format, resourceDir, handledLanguages) { # opt_knit for compatibility w/ rmarkdown::render to <- format$pandoc$to - if (identical(to, "pdf")) to <- "latex" + if (identical(to, "pdf")) { + to <- "latex" + } opts_knit <- list( quarto.version = 1, rmarkdown.pandoc.from = format$pandoc$from, @@ -397,7 +405,9 @@ knitr_options_with_cache <- function(input, format, opts) { knitr_cache_dir <- function(input, format) { pandoc_to <- format$pandoc$to base_pandoc_to <- gsub('[-+].*', '', pandoc_to) - if (base_pandoc_to == 'html4') base_pandoc_to <- 'html' + if (base_pandoc_to == 'html4') { + base_pandoc_to <- 'html' + } cache_dir <- rmarkdown:::knitr_cache_dir(input, base_pandoc_to) cache_dir <- gsub("/$", "", cache_dir) cache_dir @@ -669,8 +679,9 @@ is_dashboard_output <- function(format) { # apply patches to output as required apply_patches <- function(format, includes) { - if (format$pandoc$to %in% c("slidy", "revealjs")) + if (format$pandoc$to %in% c("slidy", "revealjs")) { includes <- apply_slides_patch(includes) + } includes } @@ -771,12 +782,15 @@ has_crop_tools <- function(warn = TRUE) { ghostscript = unname(tools::find_gs_cmd()) ) missing <- tools[tools == ""] - if (length(missing) == 0) return(TRUE) + if (length(missing) == 0) { + return(TRUE) + } x <- paste0(names(missing), collapse = ", ") - if (warn) + if (warn) { warning( sprintf("\nTool(s) not installed or not in PATH: %s", x), "\n-> As a result, figure cropping will be disabled." ) + } FALSE } diff --git a/src/resources/rmd/hooks.R b/src/resources/rmd/hooks.R index f696c5c5736..7364f36749a 100644 --- a/src/resources/rmd/hooks.R +++ b/src/resources/rmd/hooks.R @@ -159,7 +159,9 @@ knitr_hooks <- function(format, resourceDir, handledLanguages) { opts_hooks[[option]] <<- function(options) { if (identical(options[[option]], FALSE)) { options[[option]] <- TRUE - for (hide in hidden) options[[paste0(hide, ".hidden")]] <- TRUE + for (hide in hidden) { + options[[paste0(hide, ".hidden")]] <- TRUE + } } options } @@ -191,8 +193,9 @@ knitr_hooks <- function(format, resourceDir, handledLanguages) { # prefix for classes classes <- c("cell-output", paste0("cell-output-", classes)) # add .hidden class if keep-hidden hook injected an option - if (isTRUE(options[[paste0(type, ".hidden")]])) + if (isTRUE(options[[paste0(type, ".hidden")]])) { classes <- c(classes, "hidden") + } output_div(x, NULL, classes) } }) @@ -390,26 +393,32 @@ knitr_hooks <- function(format, resourceDir, handledLanguages) { forwardAttr, sprintf("%s='%s'", unknown_opts, unknown_values) ) - if (length(forwardAttr) > 0) - forwardAttr <- paste0(" ", paste(forwardAttr, collapse = " ")) else + if (length(forwardAttr) > 0) { + forwardAttr <- paste0(" ", paste(forwardAttr, collapse = " ")) + } else { forwardAttr <- "" + } # handle classes classes <- c("cell", options[["classes"]]) - if (is.character(options[["panel"]])) + if (is.character(options[["panel"]])) { classes <- c(classes, paste0("panel-", options[["panel"]])) - if (is.character(options[["column"]])) + } + if (is.character(options[["column"]])) { classes <- c(classes, paste0("column-", options[["column"]])) + } if (is.character(options[["fig-column"]])) { classes <- c(classes, paste0("fig-column-", options[["fig-column"]])) } else if (is.character(options[["fig.column"]])) { # knitr < 1.44 compatibility where fig- -> fig. classes <- c(classes, paste0("fig-column-", options[["fig.column"]])) } - if (is.character(options[["tbl-column"]])) + if (is.character(options[["tbl-column"]])) { classes <- c(classes, paste0("tbl-column-", options[["tbl-column"]])) - if (is.character(options[["cap-location"]])) + } + if (is.character(options[["cap-location"]])) { classes <- c(classes, paste0("caption-", options[["cap-location"]])) + } if (is.character(options[["fig-cap-location"]])) { classes <- c( classes, @@ -422,11 +431,12 @@ knitr_hooks <- function(format, resourceDir, handledLanguages) { paste0("fig-cap-location-", options[["fig.cap-location"]]) ) } - if (is.character(options[["tbl-cap-location"]])) + if (is.character(options[["tbl-cap-location"]])) { classes <- c( classes, paste0("tbl-cap-location-", options[["tbl-cap-location"]]) ) + } if (isTRUE(options[["include.hidden"]])) { classes <- c(classes, "hidden") @@ -498,11 +508,11 @@ knitr_hooks <- function(format, resourceDir, handledLanguages) { attr <- paste(attr, paste0('lst-cap="', options[["lst-cap"]], '"')) } } - if (identical(options[["code-overflow"]], "wrap")) - class <- paste(class, "code-overflow-wrap") else if ( - identical(options[["code-overflow"]], "scroll") - ) + if (identical(options[["code-overflow"]], "wrap")) { + class <- paste(class, "code-overflow-wrap") + } else if (identical(options[["code-overflow"]], "scroll")) { class <- paste(class, "code-overflow-scroll") + } fold <- options[["code-fold"]] if (!is.null(fold)) { attr <- paste( @@ -615,7 +625,9 @@ knitr_plot_hook <- function(format) { # classes classes <- paste0("cell-output-display") - if (isTRUE(options[["plot.hidden"]])) classes <- c(classes, "hidden") + if (isTRUE(options[["plot.hidden"]])) { + classes <- c(classes, "hidden") + } # label placeholder <- output_label_placeholder(options) @@ -746,7 +758,9 @@ knitr_plot_hook <- function(format) { } # result = "asis" specific - if (identical(options[["results"]], "asis")) return(md) + if (identical(options[["results"]], "asis")) { + return(md) + } # enclose in output div output_div(md, NULL, classes) @@ -1063,8 +1077,13 @@ figure_cap <- function(options) { if (is.null(output_label) || is_figure_label(output_label)) { fig.cap <- options[["fig.cap"]] fig.subcap <- options[["fig.subcap"]] - if (length(fig.subcap) != 0) fig.subcap else if (length(fig.cap) != 0) - fig.cap else "" + if (length(fig.subcap) != 0) { + fig.subcap + } else if (length(fig.cap) != 0) { + fig.cap + } else { + "" + } } else { "" } @@ -1151,8 +1170,12 @@ latex_animation <- function(x, options) { ow = options$out.width # maxwidth does not work with animations - if (identical(ow, '\\maxwidth')) ow = NULL - if (is.numeric(ow)) ow = paste0(ow, 'px') + if (identical(ow, '\\maxwidth')) { + ow = NULL + } + if (is.numeric(ow)) { + ow = paste0(ow, 'px') + } size = paste( c( sprintf('width=%s', ow), @@ -1165,7 +1188,9 @@ latex_animation <- function(x, options) { aniopts = options$aniopts aniopts = if (is.na(aniopts)) NULL else gsub(';', ',', aniopts) size = paste(c(size, sprintf('%s', aniopts)), collapse = ',') - if (nzchar(size)) size = sprintf('[%s]', size) + if (nzchar(size)) { + size = sprintf('[%s]', size) + } sprintf( '\\animategraphics%s{%s}{%s}{%s}{%s}', size, diff --git a/src/resources/rmd/patch.R b/src/resources/rmd/patch.R index 1794293ec23..891b78eefcf 100644 --- a/src/resources/rmd/patch.R +++ b/src/resources/rmd/patch.R @@ -111,7 +111,9 @@ wrap_asis_output <- function(options, x) { } classes <- paste0("cell-output-display") attrs <- NULL - if (isTRUE(options[["output.hidden"]])) classes <- paste0(classes, " .hidden") + if (isTRUE(options[["output.hidden"]])) { + classes <- paste0(classes, " .hidden") + } if (identical(options[["html-table-processing"]], "none")) { attrs <- paste(attrs, "html-table-processing=none") @@ -130,7 +132,9 @@ wrap_asis_output <- function(options, x) { } # If asis output, don't include the output div - if (identical(options[["results"]], "asis")) return(x) + if (identical(options[["results"]], "asis")) { + return(x) + } output_div(x, output_label_placeholder(options), classes, attrs) } diff --git a/src/resources/rmd/rmd.R b/src/resources/rmd/rmd.R index 751a90f0e43..c3f45b289b8 100755 --- a/src/resources/rmd/rmd.R +++ b/src/resources/rmd/rmd.R @@ -34,8 +34,11 @@ knit_meta <- lapply(data, jsonlite::unserializeJSON) # determine files_dir - files_dir <- if (!is.null(libDir)) libDir else + files_dir <- if (!is.null(libDir)) { + libDir + } else { rmarkdown:::knitr_files_dir(output) + } # yield pandoc format list( @@ -60,7 +63,9 @@ # bail if we don't have any perserved chunks and aren't doing code linking code_link <- isHTML && isTRUE(format$render$`code-link`) - if (length(preserved_chunks) == 0 && code_link == FALSE) return() + if (length(preserved_chunks) == 0 && code_link == FALSE) { + return() + } # change to input dir and make input relative oldwd <- setwd(dirname(rmarkdown:::abs_path(input))) @@ -97,7 +102,14 @@ # within a code chunk for (x in seq_along(chunkStarts)) { start <- chunkStarts[x] - end <- chunkEnds[x] + # Ensure end is greater than start + end <- start + for (e in chunkEnds) { + if (e > start) { + end <- e + break + } + } for (y in start:end) { if (y > start && y < end) { chunkMap[y] <- TRUE @@ -191,8 +203,12 @@ run <- function(input, port, host) { shiny_args <- list() - if (!is.null(port)) shiny_args$port <- port - if (!is.null(host)) shiny_args$host <- host + if (!is.null(port)) { + shiny_args$port <- port + } + if (!is.null(host)) { + shiny_args$host <- host + } # we already ran quarto render before the call to run Sys.setenv(RMARKDOWN_RUN_PRERENDER = "0") @@ -226,7 +242,9 @@ # print execute-debug message ("spin" and "run" don't pass format option) debug <- (!request$action %in% c("spin", "run")) && isTRUE(params$format$execute[["debug"]]) - if (debug) message("[knitr engine]: ", request$action) + if (debug) { + message("[knitr engine]: ", request$action) + } # dispatch request if (request$action == "spin") { @@ -267,7 +285,9 @@ } # write results - if (debug) message("[knitr engine]: writing results") + if (debug) { + message("[knitr engine]: writing results") + } resultJson <- jsonlite::toJSON(auto_unbox = TRUE, result) xfun:::write_utf8(paste(resultJson, collapse = "\n"), request[["results"]]) if (debug) message("[knitr engine]: exiting") diff --git a/src/resources/tools/ast-diagram/README.md b/src/resources/tools/ast-diagram/README.md new file mode 100644 index 00000000000..86877552e6b --- /dev/null +++ b/src/resources/tools/ast-diagram/README.md @@ -0,0 +1,3 @@ +This is currently copied over from https://github.com/cscheid/pandoc-ast-block-diagram + +Ideally that becomes a whole set of NPM packages, and we then import from that directly. \ No newline at end of file diff --git a/src/resources/tools/ast-diagram/ast-diagram.ts b/src/resources/tools/ast-diagram/ast-diagram.ts new file mode 100644 index 00000000000..366b1baab2a --- /dev/null +++ b/src/resources/tools/ast-diagram/ast-diagram.ts @@ -0,0 +1,1074 @@ +/* + * ast-diagram.ts + * + * (C) Posit, PBC 2025 + */ + +import { PandocAST, MetaValue, Inline } from "./types.ts"; + +/** + * Converts a Pandoc AST JSON to an HTML block diagram + * @param json The Pandoc AST JSON object + * @param renderMode The rendering mode: "block" (default), "inline" (detailed inline AST), or "full" (all nodes including Str/Space) + * @returns HTML string representing the block diagram + */ +export function convertToBlockDiagram(json: PandocAST, mode = "block"): string { + + // Start with a container + let html = '
\n'; + + // Process metadata if it exists + if (Object.keys(json.meta).length > 0) { + html += processMetadata(json.meta, mode); + } + + // Process the blocks + html += processBlocks(json.blocks, mode); + + // Close container + html += '
\n'; + + return html; +} + +export function renderPandocAstToBlockDiagram( + pandocAst: PandocAST, + cssContent: string, + mode = "block" +): string { + + // Convert to HTML block diagram + console.log("Converting to HTML block diagram..."); + const html = convertToBlockDiagram(pandocAst, mode); + + // Add HTML wrapper and CSS + const fullHtml = ` + + + + + + + + Pandoc AST Block Diagram + + + + +

Diagram

+ ${html} + +`; + return fullHtml; +} + + +/** + * Process document metadata + */ +function processMetadata(meta: Record, mode: string): string { + let html = `\n`; + + return html; +} + +/** + * Process a metadata value of any type + */ +function processMetaValue(value: MetaValue, mode: string): string { + switch (value.t) { + case "MetaMap": + return processMetaMap(value, mode); + case "MetaList": + return processMetaList(value, mode); + case "MetaBlocks": + return processMetaBlocks(value, mode); + case "MetaInlines": + return processMetaInlines(value, mode); + case "MetaBool": + return processMetaBool(value, mode); + case "MetaString": + return processMetaString(value, mode); + default: + // deno-lint-ignore no-explicit-any + return `
Unknown metadata type: ${(value as any).t}
`; + } +} + +/** + * Process a MetaMap metadata value + */ +function processMetaMap(value: Extract, mode: string): string { + const map = value.c; + + let html = `
+
Map
+
`; + + for (const [key, mapValue] of Object.entries(map)) { + html += `
+
${escapeHtml(key)}
+
${processMetaValue(mapValue, mode)}
+
`; + } + + html += `
+
`; + + return html; +} + +/** + * Process a MetaList metadata value + */ +function processMetaList(value: Extract, mode: string): string { + const list = value.c; + + let html = `
+
List
+
+
    `; + + for (const item of list) { + html += `
  • ${processMetaValue(item, mode)}
  • `; + } + + html += `
+
+
`; + + return html; +} + +/** + * Process a MetaBlocks metadata value + */ +function processMetaBlocks(value: Extract, mode: string): string { + const blocks = value.c; + + const html = `
+
Blocks
+
${processBlocks(blocks, mode)}
+
`; + + return html; +} + +/** + * Process a MetaInlines metadata value + */ +function processMetaInlines(value: Extract, mode: string): string { + const inlines = value.c; + + const html = `
+
${processInlines(inlines, mode)}
+
`; + + return html; +} + +/** + * Process a MetaBool metadata value + */ +function processMetaBool(value: Extract, _mode: string): string { + const bool = value.c; + + return `
+
${bool ? 'true' : 'false'}
+
`; +} + +/** + * Process a MetaString metadata value + */ +function processMetaString(value: Extract, _mode: string): string { + const str = value.c; + + return `
+
${escapeHtml(str)}
+
`; +} + +/** + * Process an array of block elements + */ +function processBlocks(blocks: PandocAST["blocks"], mode: string): string { + let html = ''; + + for (const block of blocks) { + html += processBlock(block, mode); + } + + return html; +} + +/** + * Process a block element with no content + */ +function processNoContentBlock(block: Extract, _mode: string): string { + return `
+
+ ${block.t} +
+
\n`; +} + +/** + * Process a single block element + */ +function processBlock(block: PandocAST["blocks"][0], mode: string): string { + switch (block.t) { + case "Header": + return processHeader(block, mode); + case "Para": + return processPara(block, mode); + case "Plain": + return processPlain(block, mode); + case "BulletList": + return processBulletList(block, mode); + case "Div": + return processDiv(block, mode); + case "CodeBlock": + return processCodeBlock(block, mode); + case "HorizontalRule": + return processNoContentBlock(block, mode); + case "DefinitionList": + return processDefinitionList(block, mode); + case "Figure": + return processFigure(block, mode); + case "OrderedList": + return processOrderedList(block, mode); + case "LineBlock": + return processLineBlock(block, mode); + case "RawBlock": + return processRawBlock(block, mode); + case "BlockQuote": + return processBlockQuote(block, mode); + // Add other block types as needed + default: + return `
+
+ ${block.t} +
+
Unknown block type
+
\n`; + } +} + +/** + * Process a header block + */ +function processHeader(block: Extract, mode: string): string { + const [level, [id, classes, attrs], content] = block.c; + + const classAttr = classes.length > 0 ? ` class="${classes.join(' ')}"` : ''; + const idAttr = id ? ` id="${id}"` : ''; + + const nodeAttrs = formatNodeAttributes(id, classes, attrs); + + return `
+
+ Header (${level})${nodeAttrs} + +
+
${processInlines(content, mode)}
+
\n`; +} + +/** + * Process a paragraph block + */ +function processPara(block: Extract, mode: string): string { + return `
+
+ Paragraph + +
+
${processInlines(block.c, mode)}
+
\n`; +} + +/** + * Process a plain block + */ +function processPlain(block: Extract, mode: string): string { + return `
+
+ Plain + +
+
${processInlines(block.c, mode)}
+
\n`; +} + +/** + * Process a bullet list block + */ +function processBulletList(block: Extract, mode: string): string { + const items = block.c; + + let html = `
+
+ Bullet List + +
+
`; + + for (const item of items) { + html += `
${processBlocks(item, mode)}
`; + } + + html += `
+
\n`; + + return html; +} + +/** + * Process a div block + */ +function processDiv(block: Extract, mode: string): string { + const [[id, classes, attrs], content] = block.c; + + const classAttr = classes.length > 0 ? ` class="${classes.join(' ')}"` : ''; + const idAttr = id ? ` id="${id}"` : ''; + + let attrsText = ''; + if (attrs.length > 0) { + attrsText = ` data-attrs="${attrs.map(([k, v]) => `${k}=${v}`).join(', ')}"`; + } + + const nodeAttrs = formatNodeAttributes(id, classes, attrs); + + return `
+
+ Div${nodeAttrs} + +
+
${processBlocks(content, mode)}
+
\n`; +} + +/** + * Process a code block + */ +function processCodeBlock(block: Extract, mode: string): string { + const [[id, classes, attrs], code] = block.c; + + const language = classes.length > 0 ? classes[0] : ''; + const classAttr = classes.length > 0 ? ` class="${classes.join(' ')}"` : ''; + const idAttr = id ? ` id="${id}"` : ''; + + const nodeAttrs = formatNodeAttributes(id, classes, attrs); + + return `
+
+ Code Block${language ? ` (${language})` : ''}${nodeAttrs} + +
+
${escapeHtml(code)}
+
\n`; +} + +/** + * Process a definition list block + */ +function processDefinitionList(block: Extract, mode: string): string { + const items = block.c; + + let html = `
+
+ Definition List + +
+
`; + + for (const [term, definitions] of items) { + html += `
+
${processInlines(term, mode)}
`; + + for (const definition of definitions) { + html += `
${processBlocks(definition, mode)}
`; + } + + html += `
`; + } + + html += `
+
\n`; + + return html; +} + +/** + * Process a figure block + */ +function processFigure(block: Extract, mode: string): string { + const [attr, [_, caption], content] = block.c; + const [id, classes, attrs] = attr; + + const classAttr = classes.length > 0 ? ` class="${classes.join(' ')}"` : ''; + const idAttr = id ? ` id="${id}"` : ''; + + const nodeAttrs = formatNodeAttributes(id, classes, attrs); + + let html = `
+
+ Figure${nodeAttrs} + +
+
`; + + // Add caption if present + if (caption && caption.length > 0) { + html += `
${processBlocks(caption, mode)}
`; + } + + // Add figure content + html += `
${processBlocks(content, mode)}
`; + + html += `
+
\n`; + + return html; +} + +/** + * Process an ordered list block + */ +function processOrderedList(block: Extract, mode: string): string { + const [[startNumber, style, delimiter], items] = block.c; + + // Extract style and delimiter values from their objects + const styleStr = style.t; + const delimiterStr = delimiter.t; + + let html = `
+
+ Ordered List (start: ${startNumber}, style: ${styleStr}, delimiter: ${delimiterStr}) + +
+
`; + + for (const item of items) { + html += `
${processBlocks(item, mode)}
`; + } + + html += `
+
\n`; + + return html; +} + +/** + * Process a line block + */ +function processLineBlock(block: Extract, mode: string): string { + const lines = block.c; + + let html = `
+
+ Line Block + +
+
`; + + for (const line of lines) { + html += `
${processInlines(line, mode)}
`; + } + + html += `
+
\n`; + + return html; +} + +/** + * Process a RawBlock element + */ +function processRawBlock(block: Extract, mode: string): string { + const [format, content] = block.c; + + return `
+
+ RawBlock (${format}) + +
+
+
${escapeHtml(content)}
+
+
\n`; +} + +/** + * Process a BlockQuote element + */ +function processBlockQuote(block: Extract, mode: string): string { + const content = block.c; + + return `
+
+ BlockQuote + +
+
${processBlocks(content, mode)}
+
\n`; +} + +/** + * Process a Code inline element in verbose mode + */ +function processCodeInline(inline: Extract, mode: string): string { + const [[id, classes, attrs], codeText] = inline.c; + + const classAttr = classes.length > 0 ? ` class="${classes.join(' ')}"` : ''; + const idAttr = id ? ` id="${id}"` : ''; + + const nodeAttrs = formatNodeAttributes(id, classes, attrs); + + return `
+
+ Code${nodeAttrs} + +
+
${escapeHtml(codeText)}
+
`; +} + +/** + * Process a Link inline element in verbose mode + */ +function processLinkInline(inline: Extract, mode: string): string { + const [[id, classes, attrs], linkText, [url, title]] = inline.c; + + const classAttr = classes.length > 0 ? ` class="${classes.join(' ')}"` : ''; + const idAttr = id ? ` id="${id}"` : ''; + + const nodeAttrs = formatNodeAttributes(id, classes, attrs); + + return ``; +} + +/** + * Process an Image inline element in verbose mode + */ +function processImageInline(inline: Extract, mode: string): string { + const [[id, classes, attrs], altText, [url, title]] = inline.c; + + const classAttr = classes.length > 0 ? ` class="${classes.join(' ')}"` : ''; + const idAttr = id ? ` id="${id}"` : ''; + + const nodeAttrs = formatNodeAttributes(id, classes, attrs); + + return `
+
+ Image${nodeAttrs} + +
+ + ${title ? `
${escapeHtml(title)}
` : ''} +
+
${processInlines(altText, mode)}
+
+
`; +} + +/** + * Process a Math inline element in verbose mode + */ +function processMathInline(inline: Extract, mode: string): string { + const [mathType, content] = inline.c; + + // The mathType object has a property 't' that is either 'InlineMath' or 'DisplayMath' + const type = mathType.t; + const isDisplay = type === 'DisplayMath'; + + return `
+
+ Math (${isDisplay ? 'Display' : 'Inline'}) + +
+
+
${escapeHtml(content)}
+
+
`; +} + +/** + * Process a Quoted inline element in verbose mode + */ +function processQuotedInline(inline: Extract, mode: string): string { + const [quoteType, content] = inline.c; + + // The quoteType object has a property 't' that is either 'SingleQuote' or 'DoubleQuote' + const type = quoteType.t; + const isSingle = type === 'SingleQuote'; + + return `
+
+ Quoted (${isSingle ? 'Single' : 'Double'}) + +
+
+
${processInlines(content, mode)}
+
+
`; +} + +/** + * Process a Note inline element in verbose mode + */ +function processNoteInline(inline: Extract, mode: string): string { + const content = inline.c; + + return `
+
+ Note + +
+
+
${processBlocks(content, mode)}
+
+
`; +} + +/** + * Process a Cite inline element in verbose mode + */ +function processCiteInline(inline: Extract, mode: string): string { + const [citations, text] = inline.c; + + let html = `
+
+ Cite + +
+
`; + + // Display text representation + html += `
${processInlines(text, mode)}
`; + + // Display each citation + html += `
`; + for (const citation of citations) { + const citationMode = citation.citationMode.t; + html += `
+
${escapeHtml(citation.citationId)}
+
${escapeHtml(citationMode)}
`; + + // Display prefix if present + if (citation.citationPrefix.length > 0) { + html += `
${processInlines(citation.citationPrefix, mode)}
`; + } + + // Display suffix if present + if (citation.citationSuffix.length > 0) { + html += `
${processInlines(citation.citationSuffix, mode)}
`; + } + + html += `
`; + } + html += `
`; + + html += `
+
`; + + return html; +} + +/** + * Process a RawInline element in verbose mode + */ +function processRawInlineInline(inline: Extract, mode: string): string { + const [format, content] = inline.c; + + return `
+
+ RawInline (${format}) + +
+
+ ${escapeHtml(content)} +
+
`; +} + +/** + * Process a Span inline element in verbose mode + */ +function processSpanInline(inline: Extract, mode: string): string { + const [[id, classes, attrs], spanContent] = inline.c; + + const classAttr = classes.length > 0 ? ` class="${classes.join(' ')}"` : ''; + const idAttr = id ? ` id="${id}"` : ''; + + const nodeAttrs = formatNodeAttributes(id, classes, attrs); + + return `
+
+ Span${nodeAttrs} + +
+
${processInlines(spanContent, mode)}
+
`; +} + +/** + * Process simple inline elements (Emph, Strong, SmallCaps, etc.) in verbose mode + */ +function processSimpleInline(inline: Extract, mode: string): string { + const nodeType = inline.t; // Get the type name (Emph, Strong, etc.) + const content = inline.c; // Get the content (array of Inline elements) + + return `
+
+ ${nodeType} + +
+
${processInlines(content, mode)}
+
`; +} + +/** + * Process a Str inline element in full mode + */ +function processStrInline(inline: Extract, mode: string): string { + const content = inline.c; // Get the string content + + return `
+
+ Str + +
+
${escapeHtml(content)}
+
`; +} + +const foldedOnlyString = (type: string) => { + switch (type) { + case "Space": return "⏘"; + default: return type; + } +} + +/** + * Process inline elements with no content + */ +function processNoContentInline(inline: Extract, mode: string): string { + return `
+
${inline.t}
+
${foldedOnlyString(inline.t)}
+
`; +} + +/** + * Process inline elements + */ +// deno-lint-ignore no-explicit-any +function processInlines(inlines: any[], mode: string): string { + let html = ''; + + for (const inline of inlines) { + switch (inline.t) { + case "Str": + if (mode === "full") { + html += processStrInline(inline, mode); + } else { + html += escapeHtml(inline.c); + } + break; + case "Space": + if (mode === "full") { + html += processNoContentInline(inline, mode); + } else { + html += ' '; + } + break; + case "SoftBreak": + if (mode === "full") { + html += processNoContentInline(inline, mode); + } else { + html += ' '; + } + break; + case "LineBreak": + if (mode === "full") { + html += processNoContentInline(inline, mode); + } else { + html += '
'; + } + break; + case "Code": + if (mode === "inline" || mode === "full") { + html += processCodeInline(inline, mode); + } else { + const [[, codeClasses], codeText] = inline.c; + html += `${escapeHtml(codeText)}`; + } + break; + case "RawInline": + if (mode === "inline" || mode === "full") { + html += processRawInlineInline(inline, mode); + } else { + const [format, content] = inline.c; + html += `${escapeHtml(content)}`; + } + break; + case "Link": + if (mode === "inline" || mode === "full") { + html += processLinkInline(inline, mode); + } else { + const [[, linkClasses], linkText, [url, title]] = inline.c; + html += `${processInlines(linkText, mode)}`; + } + break; + case "Image": + if (mode === "inline" || mode === "full") { + html += processImageInline(inline, mode); + } else { + const [[imgId, imgClasses, imgAttrs], altText, [url, title]] = inline.c; + // In block mode, represent the image as markdown-like syntax in a code tag + let imgMarkdown = `![${processInlines(altText, mode)}](${url}`; + if (title) { + imgMarkdown += ` "${title}"`; + } + imgMarkdown += ')'; + + // Add attributes if present + if (imgId || imgClasses.length > 0 || imgAttrs.length > 0) { + imgMarkdown += '{'; + if (imgId) { + imgMarkdown += `#${imgId}`; + } + for (const cls of imgClasses) { + imgMarkdown += ` .${cls}`; + } + for (const [k, v] of imgAttrs) { + imgMarkdown += ` ${k}=${v}`; + } + imgMarkdown += '}'; + } + + html += `${escapeHtml(imgMarkdown)}`; + } + break; + case "Math": + if (mode === "inline" || mode === "full") { + html += processMathInline(inline, mode); + } else { + const [mathType, content] = inline.c; + const type = mathType.t; + const isDisplay = type === 'DisplayMath'; + + // In block mode, represent the math as TeX/LaTeX in a code tag + const delimiter = isDisplay ? '$$' : '$'; + html += `${delimiter}${escapeHtml(content)}${delimiter}`; + } + break; + case "Quoted": + if (mode === "inline" || mode === "full") { + html += processQuotedInline(inline, mode); + } else { + const [quoteType, content] = inline.c; + const type = quoteType.t; + const isSingle = type === 'SingleQuote'; + + // In block mode, represent the quoted text with actual quote marks + const quote = isSingle ? "'" : '"'; + html += `${quote}${processInlines(content, mode)}${quote}`; + } + break; + case "Note": + // Note is a special inline element that contains block elements + // We always use processNoteInline regardless of mode to properly visualize its structure + html += processNoteInline(inline, mode); + break; + case "Cite": + if (mode === "inline" || mode === "full") { + html += processCiteInline(inline, mode); + } else { + // In block mode, just use the text representation + const [_, text] = inline.c; + html += processInlines(text, mode); + } + break; + case "Span": + if (mode === "inline" || mode === "full") { + html += processSpanInline(inline, mode); + } else { + const [[spanId, spanClasses], spanContent] = inline.c; + const spanClassAttr = spanClasses.length > 0 ? ` class="${spanClasses.join(' ')}"` : ''; + const spanIdAttr = spanId ? ` id="${spanId}"` : ''; + html += `${processInlines(spanContent, mode)}`; + } + break; + // Simple inline types processed with the generic function + case "Emph": + case "Strong": + case "SmallCaps": + case "Strikeout": + case "Subscript": + case "Superscript": + case "Underline": + if (mode === "inline" || mode === "full") { + html += processSimpleInline(inline, mode); + } else { + const tag = inline.t === "Emph" ? "em" : + inline.t === "Strong" ? "strong" : + inline.t === "SmallCaps" ? "span class=\"small-caps\"" : + inline.t === "Strikeout" ? "s" : + inline.t === "Subscript" ? "sub" : + inline.t === "Superscript" ? "sup" : + inline.t === "Underline" ? "u" : "span"; + html += `<${tag}>${processInlines(inline.c, mode)}`; + } + break; + // Add other inline types as needed + default: + html += `
+
+ + ${inline.t} +
+
Unknown inline type
+
`; + } + } + + return html; +} + + +/** + * Format node ID, classes, and attributes for display + */ +function formatNodeAttributes(id: string, classes: string[], attrs: [string, string][]): string { + let result = ''; + + // Add ID if present + if (id) { + result += ` #${id}`; + } + + // Add classes if present + if (classes.length > 0) { + result += ` ${classes.map(c => `.${c}`).join(' ')}`; + } + + // Add attributes if present + if (attrs.length > 0) { + result += ` ${attrs.map(([k, v]) => `${k}="${v}"`).join(' ')}`; + } + + return result; +} + +/** + * Simple HTML escape function + */ +function escapeHtml(unsafe: string): string { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} \ No newline at end of file diff --git a/src/resources/tools/ast-diagram/main.ts b/src/resources/tools/ast-diagram/main.ts new file mode 100755 index 00000000000..7e35855ff4e --- /dev/null +++ b/src/resources/tools/ast-diagram/main.ts @@ -0,0 +1,119 @@ +#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run + +import { join } from "https://deno.land/std/path/mod.ts"; +import { renderPandocAstToBlockDiagram } from "./ast-diagram.ts"; +import { PandocAST } from "./types.ts"; + +/** + * Convert a markdown file to an HTML block diagram + */ +async function renderMarkdownToBlockDiagram( + markdownFile: string, + outputFile: string, + mode = "block", +) { + console.log(`Processing ${markdownFile}...`); + + try { + // Read the markdown file content + console.log("Reading markdown source file..."); + // Read the CSS file + const scriptDir = new URL(".", import.meta.url).pathname; + const cssPath = join(scriptDir, "style.css"); + const cssContent = await Deno.readTextFile(cssPath); + + // Run pandoc to convert markdown to JSON + console.log("Running pandoc to convert to JSON..."); + const command = new Deno.Command("quarto", { + args: ["pandoc", "-t", "json", markdownFile], + stdout: "piped", + }); + + const { code, stdout } = await command.output(); + if (code !== 0) { + throw new Error(`Pandoc command failed with exit code ${code}`); + } + + const jsonOutput = new TextDecoder().decode(stdout); + + // Parse the JSON + console.log("Parsing Pandoc JSON..."); + const pandocAst = JSON.parse(jsonOutput) as PandocAST; + const fullHtml = renderPandocAstToBlockDiagram(pandocAst, cssContent, mode); + + // Write the result to the output file + console.log(`Writing output to ${outputFile}...`); + await Deno.writeTextFile(outputFile, fullHtml); + + console.log("Done!"); + return true; + } catch (err) { + // deno-lint-ignore no-explicit-any + console.error("Error:", (err as unknown as any).message); + return false; + } +} + +// Check if script is run directly +if (import.meta.main) { + // Get markdown file from command line arguments + const args = Deno.args; + + // Check for --mode flag and its value + let mode = "block"; + const modeIndex = args.indexOf("--mode"); + if (modeIndex !== -1 && modeIndex + 1 < args.length) { + const modeValue = args[modeIndex + 1]; + if ( + modeValue === "block" || modeValue === "inline" || modeValue === "full" + ) { + mode = modeValue; + } else { + console.error( + "Invalid mode value. Must be 'block', 'inline', or 'full'.", + ); + Deno.exit(1); + } + } + + // Backwards compatibility for --verbose flag + const verboseIndex = args.indexOf("--verbose"); + if (verboseIndex !== -1) { + mode = "inline"; + } + + // Remove the --mode flag and its value, or --verbose flag for the rest of argument processing + const cleanedArgs = [...args]; + if (modeIndex !== -1) { + cleanedArgs.splice(modeIndex, 2); // Remove both flag and value + } else if (verboseIndex !== -1) { + cleanedArgs.splice(verboseIndex, 1); // Remove just the flag + } + + if (cleanedArgs.length < 1) { + console.log( + "Usage: main.ts [--mode ] [output-html-file]", + ); + console.log("Options:"); + console.log( + " --mode block|inline|full Rendering mode: 'block' (default), 'inline' (detailed AST), or 'full' (all nodes)", + ); + console.log( + " --verbose (Legacy) Equivalent to --mode inline", + ); + console.log( + "\nIf output file is not specified, it will use the input filename with .html extension", + ); + Deno.exit(1); + } + + const markdownFile = cleanedArgs[0]; + let outputFile = cleanedArgs[1]; + + if (!outputFile) { + // Default output file is input file with .html extension + outputFile = markdownFile.replace(/\.[^\.]+$/, "") + ".html"; + } + + renderMarkdownToBlockDiagram(markdownFile, outputFile, mode); +} diff --git a/src/resources/tools/ast-diagram/style.css b/src/resources/tools/ast-diagram/style.css new file mode 100644 index 00000000000..497528e0499 --- /dev/null +++ b/src/resources/tools/ast-diagram/style.css @@ -0,0 +1,498 @@ +/* Pandoc AST Block Diagram Styles */ +* { + box-sizing: border-box; +} + +body { + font-family: "Nunito Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + line-height: 1.5; + color: #333; + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +.pandoc-block-diagram { + /* inherits styles from body */ + margin-left: 15px; +} + +/* Block elements */ +.block { + border: 1px solid #ddd; + border-radius: 4px; + margin: 2px 0; + padding: 5px 10px; + background-color: #f9f9f9; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + display: grid; + grid-template-columns: 120px 1fr; +} + +.block-type { + font-weight: 600; + color: #666; + font-size: 0.9em; + text-transform: uppercase; + display: flex; + align-items: center; + cursor: pointer; +} + +/* Toggle button styles */ +.toggle-button { + background: none; + border: none; + color: #666; + font-size: 0.9em; + cursor: pointer; + transition: transform 0.2s ease; + position: relative; + top: -0.1em; +} + +/* Folded state */ +.block.folded .block-content { + display: none; +} + +.block.folded .toggle-button { + transform: rotate(-90deg); +} + +.block-content { + padding-left: 15px; + border-left: 3px solid #eee; + line-height: 2em; +} + +.block.block-bullet-list .block-content { + padding-left: 0; + border-left: none; +} + +/* Specific Block Types */ +.block-header { + background-color: #f3f7fd; + border-color: #c5d7f2; +} + +.block-para { + background-color: #f9f9f9; +} + +.block-bullet-list { + background-color: #f5f9f5; + border-color: #d1e7d1; +} + +.block-div { + background-color: #fff8f5; + border-color: #f5d3bf; +} + +.block-code { + background-color: #f5f5f7; + border-color: #cfd2e0; +} + +.block-metadata { + background-color: #f2f9fd; + border-color: #bde0f3; +} + +/* Metadata styling */ +.metadata-entry { + margin: 8px 0; + padding: 8px; + border-left: 3px solid #ddf; + background-color: rgba(255, 255, 255, 0.5); + border-radius: 4px; +} + +.metadata-key { + font-weight: 600; + color: #446; + margin-bottom: 5px; + font-size: 0.95em; +} + +.meta-map, .meta-list, .meta-blocks, .meta-inlines, .meta-bool, .meta-string { + padding: 5px; + border-radius: 3px; +} + +.meta-map { + background-color: #f0f8ff; + border: 1px solid #e0f0ff; +} + +.meta-list { + background-color: #f8f8ff; + border: 1px solid #e8e8ff; +} + +.meta-blocks { + background-color: #fff8f8; + border: 1px solid #ffe8e8; +} + +.meta-inlines { + background-color: #f8fff8; + border: 1px solid #e8ffe8; +} + +.meta-bool { + background-color: #fffff8; + border: 1px solid #ffffe8; +} + +.meta-string { + background-color: #fff8ff; + border: 1px solid #ffe8ff; +} + +.meta-type { + font-size: 0.85em; + color: #777; + margin-bottom: 5px; + font-weight: 600; + text-transform: uppercase; +} + +.meta-map-entry { + margin: 6px 0; + padding: 5px; + border-left: 2px solid #ddf; +} + +.meta-map-key { + font-weight: 600; + color: #557; + font-size: 0.9em; + margin-bottom: 3px; +} + +.meta-list-items { + margin: 5px 0 5px 20px; + padding-left: 10px; +} + +.meta-list-item { + margin: 5px 0; +} + +.meta-content { + padding-left: 10px; +} + +/* Header Levels */ +.level-1 .block-type, +.level-2 .block-type, +.level-3 .block-type, +.level-4 .block-type { + color: #666; +} + +/* List Items */ +.list-item { + margin: 8px 0; + padding-left: 15px; + border-left: 3px solid #eee; +} + +/* Code Formatting */ +pre { + background-color: #f5f5f5; + padding: 10px; + border-radius: 3px; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.9em; + overflow-x: auto; +} + +code { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + background-color: #f5f5f5; + padding: 2px 4px; + border-radius: 3px; + font-size: 0.9em; +} + +/* Nested Elements */ +.block .block { + margin-left: 5px; +} + +/* Unknown Elements */ +.block-type-unknown, .inline-unknown { + background-color: #fcf8e3; + border-color: #faebcc; + color: #8a6d3b; +} + +/* Inline Elements */ +.inline { + border: 1px solid #ddd; + border-radius: 4px; + margin: 5px; + padding: 0 2px 0 2px; + background-color: #f9f9f9; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + display: inline; +} + +.inline-type { + font-weight: 600; + color: #666; + font-size: 0.6em; + text-transform: uppercase; + display: inline; + align-items: center; + cursor: pointer; + top: -0.15em; + position: relative; + margin-left: 5px; + margin-right: 2px; +} + +.inline-type .toggle-button { + padding: 0; +} + +/* Override for inline-type with no content */ +.inline-type.inline-type-no-content { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +div .inline-type.inline-type-no-content-folded-only { + display: none; +} + +div.folded .inline-type.inline-type-no-content { + display: none; +} +div.folded .inline-type.inline-type-no-content-folded-only { + display: inherit; +} + +/* Folded state for inline elements */ +.inline.folded .inline-content, +.inline.folded .inline-classes, +.inline.folded .inline-attrs, +.inline.folded .inline-url, +.inline.folded .inline-title { + display: none; +} + +.inline.folded .toggle-button { + transform: rotate(-90deg); +} + +.inline-content { + display: inline; +} + +/* Link styles with pseudo-elements */ +.inline-url, .inline-title { + padding-left: 15px; + border-left: 3px solid #eee; + margin-bottom: 6px; +} + +.inline-url::before { + content: "URL: "; + font-weight: 600; + color: #666; + margin-right: 4px; +} + +.inline-title::before { + content: "Title: "; + font-weight: 600; + color: #666; +} + +.inline-text-content::before { + content: "Text: "; + font-weight: 600; + color: #666; +} + +/* Special link styling */ +.inline-url a { + display: inline-block; + padding: 4px 8px; + margin-left: 4px; + background-color: #f0f7ff; + border: 1px solid #d0e0f7; + border-radius: 3px; + word-break: break-all; + max-width: calc(100% - 50px); +} + +/* Simple inline element styling */ +.inline-emph { + background-color: #f7f9f4; + border-color: #dbe7c9; +} + +.inline-strong { + background-color: #f9f4f4; + border-color: #e7c9c9; +} + +.inline-smallcaps { + background-color: #f4f9f9; + border-color: #c9e7e7; +} + +.inline-strikeout { + background-color: #f9f8f4; + border-color: #e7e2c9; +} + +.inline-subscript { + background-color: #f4f4f9; + border-color: #c9c9e7; +} + +.inline-superscript { + background-color: #f9f4f9; + border-color: #e7c9e7; +} + +.inline-underline { + background-color: #f4f9f4; + border-color: #c9e7c9; +} + +a { + color: #337ab7; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +h2#ast-diagram-heading { + display: flex; + justify-content: space-between; + align-items: center; /* Optional: vertically centers content */ +} + +/* Fold controls */ +.fold-controls { + margin: 15px 0 15px 15px; + display: inline-flex; + flex-wrap: wrap; + gap: 15px; +} + +.control-group { + display: flex; + align-items: center; + border: 1px solid #eee; + border-radius: 4px; + padding: 5px 10px; + background-color: #f9f9f9; +} + +.control-group span { + font-weight: 600; + margin-right: 10px; + color: #666; +} + +.fold-controls button { + background-color: #f0f0f0; + border: 1px solid #ddd; + border-radius: 3px; + padding: 4px 8px; + margin-right: 6px; + cursor: pointer; + font-size: 0.85em; +} + +.fold-controls button:hover { + background-color: #e0e0e0; +} + +.fold-controls button:active { + background-color: #d0d0d0; +} + +/* Page Layout and Headings */ +h1, h2 { + margin-left: 15px; + color: #555; +} + +h1 { + font-size: 1.6em; + border-bottom: 1px solid #eee; + padding-bottom: 10px; + margin-bottom: 10px; +} + +.source-path { + margin-left: 15px; + color: #777; + font-style: italic; + margin-bottom: 20px; +} + +h2 { + font-size: 1.3em; + margin-top: 30px; + margin-bottom: 15px; +} + +/* Markdown source code */ +.source-info { + margin: 20px 0 20px 15px; +} + +/* Source info heading styles removed as they're now redundant + and inheriting properly from the general h2 styling */ + +.markdown-source { + margin: 15px 0; +} + +/* Clean up: markdown header styles removed since folding is no longer needed */ + +.markdown-source pre { + max-height: 300px; + overflow-y: auto; + background-color: #f5f5f5; + padding: 15px; + border-radius: 4px; + border: 1px solid #ddd; + white-space: pre-wrap; +} + +.language-markdown { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.9em; +} + +/* Node attribute styling */ +.node-id, .node-classes, .node-attrs { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.85em; + border-radius: 3px; + display: inline-block; + margin-left: 5px; + margin-right: 5px; + padding: 0 4px; + text-transform: none; + color: #666; + background-color: #f7f7f7; +} \ No newline at end of file diff --git a/src/resources/tools/ast-diagram/types.ts b/src/resources/tools/ast-diagram/types.ts new file mode 100644 index 00000000000..4cf9905501d --- /dev/null +++ b/src/resources/tools/ast-diagram/types.ts @@ -0,0 +1,276 @@ +// TypeScript definitions for Pandoc AST + +export interface PandocAST { + "pandoc-api-version": number[]; + meta: Record; + blocks: Block[]; +} + +// Meta value types +export type MetaValue = + | MetaMap + | MetaList + | MetaBlocks + | MetaInlines + | MetaBool + | MetaString; + +export interface MetaMap { + t: "MetaMap"; + c: Record; +} + +export interface MetaList { + t: "MetaList"; + c: MetaValue[]; +} + +export interface MetaBlocks { + t: "MetaBlocks"; + c: Block[]; +} + +export interface MetaInlines { + t: "MetaInlines"; + c: Inline[]; +} + +export interface MetaBool { + t: "MetaBool"; + c: boolean; +} + +export interface MetaString { + t: "MetaString"; + c: string; +} + +// Block element types +export type Block = + | HeaderBlock + | ParaBlock + | BulletListBlock + | DivBlock + | CodeBlockBlock + | PlainBlock + | OrderedListBlock + | BlockQuoteBlock + | RawBlockBlock + | HorizontalRuleBlock + | TableBlock + | DefinitionListBlock + | FigureBlock + | LineBlockBlock; + +// Inline element types +export type Inline = + | StrInline + | SpaceInline + | CodeInline + | LinkInline + | SpanInline + | EmphInline + | StrongInline + | StrikeoutInline + | SubscriptInline + | SuperscriptInline + | SmallCapsInline + | UnderlineInline + | QuotedInline + | RawInlineInline + | MathInline + | ImageInline + | SoftBreakInline + | LineBreakInline + | NoteInline + | CiteInline; + +// Attributes type +export type Attr = [string, string[], [string, string][]]; + +// Block type definitions +export interface HeaderBlock { + t: "Header"; + c: [number, Attr, Inline[]]; +} + +export interface ParaBlock { + t: "Para"; + c: Inline[]; +} + +export interface BulletListBlock { + t: "BulletList"; + c: Block[][]; +} + +export interface DivBlock { + t: "Div"; + c: [Attr, Block[]]; +} + +export interface CodeBlockBlock { + t: "CodeBlock"; + c: [Attr, string]; +} + +export interface PlainBlock { + t: "Plain"; + c: Inline[]; +} + +export interface OrderedListBlock { + t: "OrderedList"; + c: [[number, { t: string }, { t: string }], Block[][]]; +} + +export interface BlockQuoteBlock { + t: "BlockQuote"; + c: Block[]; +} + +export interface RawBlockBlock { + t: "RawBlock"; + c: [string, string]; +} + +export interface HorizontalRuleBlock { + t: "HorizontalRule"; + c?: []; +} + +export interface TableBlock { + t: "Table"; + c: any[]; // Complex structure simplified +} + +export interface DefinitionListBlock { + t: "DefinitionList"; + c: [Inline[], Block[][]][]; +} + +export interface FigureBlock { + t: "Figure"; + c: [Attr, [null, Block[]], Block[]]; +} + +export interface LineBlockBlock { + t: "LineBlock"; + c: Inline[][]; +} + +// Inline type definitions +export interface StrInline { + t: "Str"; + c: string; +} + +export interface SpaceInline { + t: "Space"; + c?: []; +} + +export interface CodeInline { + t: "Code"; + c: [Attr, string]; +} + +export interface LinkInline { + t: "Link"; + c: [Attr, Inline[], [string, string]]; +} + +export interface SpanInline { + t: "Span"; + c: [Attr, Inline[]]; +} + +export interface EmphInline { + t: "Emph"; + c: Inline[]; +} + +export interface StrongInline { + t: "Strong"; + c: Inline[]; +} + +export interface StrikeoutInline { + t: "Strikeout"; + c: Inline[]; +} + +export interface SubscriptInline { + t: "Subscript"; + c: Inline[]; +} + +export interface SuperscriptInline { + t: "Superscript"; + c: Inline[]; +} + +export interface UnderlineInline { + t: "Underline"; + c: Inline[]; +} + +export interface QuotedInline { + t: "Quoted"; + c: [{ t: "SingleQuote" | "DoubleQuote" }, Inline[]]; +} + +export interface SmallCapsInline { + t: "SmallCaps"; + c: Inline[]; +} + +export interface RawInlineInline { + t: "RawInline"; + c: [string, string]; +} + +export interface MathInline { + t: "Math"; + c: [{ t: "InlineMath" | "DisplayMath" }, string]; // [MathType, Content] +} + +export interface ImageInline { + t: "Image"; + c: [Attr, Inline[], [string, string]]; // [Attr, Alt, [URL, Title]] +} + +export interface SoftBreakInline { + t: "SoftBreak"; + c?: []; +} + +export interface LineBreakInline { + t: "LineBreak"; + c?: []; +} + +export interface NoteInline { + t: "Note"; + c: Block[]; // Content of the footnote as block elements +} + +// Citation mode type +export interface CitationMode { + t: "AuthorInText" | "SuppressAuthor" | "NormalCitation"; +} + +// Citation type +export interface Citation { + citationId: string; + citationPrefix: Inline[]; + citationSuffix: Inline[]; + citationMode: CitationMode; + citationNoteNum: number; + citationHash: number; +} + +export interface CiteInline { + t: "Cite"; + c: [Citation[], Inline[]]; // [Citations array, Text representation] +} \ No newline at end of file diff --git a/tests/docs/smoke-all/2025/07/23/13121.qmd b/tests/docs/smoke-all/2025/07/23/13121.qmd new file mode 100644 index 00000000000..b9677dcea18 --- /dev/null +++ b/tests/docs/smoke-all/2025/07/23/13121.qmd @@ -0,0 +1,34 @@ +--- +format: typst +title: Another section +_quarto: + tests: + typst: + noErrorsOrWarnings: true +--- + +## A section + +Here we define a plot. + +::: {.cell execution_count=1} + +::: {.cell-output .cell-output-display} +`code`{#a-cell} +::: +::: + + + +Here we use the plot, inside a callout: + + +::: callout-note + +## Note the following plot + +{{< contents a-cell >}} + +::: + + diff --git a/tests/docs/smoke-all/2025/08/14/issue13216.qmd b/tests/docs/smoke-all/2025/08/14/issue13216.qmd new file mode 100644 index 00000000000..7e1ffbcf2eb --- /dev/null +++ b/tests/docs/smoke-all/2025/08/14/issue13216.qmd @@ -0,0 +1,24 @@ +--- +title: "Issue 13216" +format: html +code-annotations: below +code-link: true +_quarto: + tests: + html: + ensureHtmlElements: + - + - "dl.code-annotation-container-grid" +--- + +`downlit` (`code-link`) should be disable and `code-annotations` should be enabled. + +``` +hello +``` + +```{r} +2 + 2 # <1> +``` + +1. This is an annotation that should be properly displayed based on `code-annotations` setting. diff --git a/tests/docs/smoke-all/2025/08/20/issue-13233/.gitignore b/tests/docs/smoke-all/2025/08/20/issue-13233/.gitignore new file mode 100644 index 00000000000..075b2542afb --- /dev/null +++ b/tests/docs/smoke-all/2025/08/20/issue-13233/.gitignore @@ -0,0 +1 @@ +/.quarto/ diff --git a/tests/docs/smoke-all/2025/08/20/issue-13233/_quarto.yml b/tests/docs/smoke-all/2025/08/20/issue-13233/_quarto.yml new file mode 100644 index 00000000000..357b006b20b --- /dev/null +++ b/tests/docs/smoke-all/2025/08/20/issue-13233/_quarto.yml @@ -0,0 +1,21 @@ +project: + type: website + +website: + title: "issue-13233" + navbar: + left: + - href: index.qmd + text: Home + - about.qmd + +format: + html: + theme: + - cosmo + - brand + css: styles.css + toc: true + + + diff --git a/tests/docs/smoke-all/2025/08/20/issue-13233/about.qmd b/tests/docs/smoke-all/2025/08/20/issue-13233/about.qmd new file mode 100644 index 00000000000..07c5e7f9d13 --- /dev/null +++ b/tests/docs/smoke-all/2025/08/20/issue-13233/about.qmd @@ -0,0 +1,5 @@ +--- +title: "About" +--- + +About this site diff --git a/tests/docs/smoke-all/2025/08/20/issue-13233/index.qmd b/tests/docs/smoke-all/2025/08/20/issue-13233/index.qmd new file mode 100644 index 00000000000..4d5fcfda60b --- /dev/null +++ b/tests/docs/smoke-all/2025/08/20/issue-13233/index.qmd @@ -0,0 +1,21 @@ +--- +title: "mysite" +_quarto: + tests: + html: + ensureFileRegexMatches: + - ['href=""'] +--- + +::: {.panel-tabset} + +## First tab + +1. Press Tab. "First tab" should be focused. +2. Press Right Arrow. "Second tab" should now be active and focused. + +## Second tab + +Other text might go here + +::: diff --git a/tests/docs/smoke-all/2025/08/20/issue-13233/styles.css b/tests/docs/smoke-all/2025/08/20/issue-13233/styles.css new file mode 100644 index 00000000000..2ddf50c7b42 --- /dev/null +++ b/tests/docs/smoke-all/2025/08/20/issue-13233/styles.css @@ -0,0 +1 @@ +/* css styles */ diff --git a/tests/docs/smoke-all/brand/logo/brand-icon-small-favicon-book/.gitignore b/tests/docs/smoke-all/brand/logo/brand-icon-small-favicon-book/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/brand-icon-small-favicon-book/.gitignore +++ b/tests/docs/smoke-all/brand/logo/brand-icon-small-favicon-book/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/logo-extension-github/.gitignore b/tests/docs/smoke-all/brand/logo/logo-extension-github/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/logo-extension-github/.gitignore +++ b/tests/docs/smoke-all/brand/logo/logo-extension-github/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/logo-extension/.gitignore b/tests/docs/smoke-all/brand/logo/logo-extension/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/logo-extension/.gitignore +++ b/tests/docs/smoke-all/brand/logo/logo-extension/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/navbar/alt-text-consistency/.gitignore b/tests/docs/smoke-all/brand/logo/navbar/alt-text-consistency/.gitignore index 9ac4fee8e4f..598636242fc 100644 --- a/tests/docs/smoke-all/brand/logo/navbar/alt-text-consistency/.gitignore +++ b/tests/docs/smoke-all/brand/logo/navbar/alt-text-consistency/.gitignore @@ -1,2 +1,3 @@ _site/ -/.quarto/ \ No newline at end of file +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/navbar/brand-default/.gitignore b/tests/docs/smoke-all/brand/logo/navbar/brand-default/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/navbar/brand-default/.gitignore +++ b/tests/docs/smoke-all/brand/logo/navbar/brand-default/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/navbar/brand-override-alt/.gitignore b/tests/docs/smoke-all/brand/logo/navbar/brand-override-alt/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/navbar/brand-override-alt/.gitignore +++ b/tests/docs/smoke-all/brand/logo/navbar/brand-override-alt/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/navbar/brand-override-resources/.gitignore b/tests/docs/smoke-all/brand/logo/navbar/brand-override-resources/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/navbar/brand-override-resources/.gitignore +++ b/tests/docs/smoke-all/brand/logo/navbar/brand-override-resources/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/navbar/missing-logo-handling/.gitignore b/tests/docs/smoke-all/brand/logo/navbar/missing-logo-handling/.gitignore index 9ac4fee8e4f..598636242fc 100644 --- a/tests/docs/smoke-all/brand/logo/navbar/missing-logo-handling/.gitignore +++ b/tests/docs/smoke-all/brand/logo/navbar/missing-logo-handling/.gitignore @@ -1,2 +1,3 @@ _site/ -/.quarto/ \ No newline at end of file +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/navbar/size-preference-test/.gitignore b/tests/docs/smoke-all/brand/logo/navbar/size-preference-test/.gitignore index 9ac4fee8e4f..598636242fc 100644 --- a/tests/docs/smoke-all/brand/logo/navbar/size-preference-test/.gitignore +++ b/tests/docs/smoke-all/brand/logo/navbar/size-preference-test/.gitignore @@ -1,2 +1,3 @@ _site/ -/.quarto/ \ No newline at end of file +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/navbar/theme-dark-enables-dark-logo/.gitignore b/tests/docs/smoke-all/brand/logo/navbar/theme-dark-enables-dark-logo/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/navbar/theme-dark-enables-dark-logo/.gitignore +++ b/tests/docs/smoke-all/brand/logo/navbar/theme-dark-enables-dark-logo/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/_quarto-navbar.yml b/tests/docs/smoke-all/brand/logo/project-subdirs/_quarto-navbar.yml new file mode 100644 index 00000000000..ad012e87e0c --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/project-subdirs/_quarto-navbar.yml @@ -0,0 +1,6 @@ +website: + navbar: + left: + - href: index.qmd + text: Home + - about.qmd diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/_quarto-sidebar.yml b/tests/docs/smoke-all/brand/logo/project-subdirs/_quarto-sidebar.yml new file mode 100644 index 00000000000..7481c2bfdcb --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/project-subdirs/_quarto-sidebar.yml @@ -0,0 +1,8 @@ +website: + sidebar: + style: "docked" + search: false + contents: + - href: index.qmd + text: Home + - about.qmd diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/_quarto.yml b/tests/docs/smoke-all/brand/logo/project-subdirs/_quarto.yml new file mode 100644 index 00000000000..d3aeebda2b5 --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/project-subdirs/_quarto.yml @@ -0,0 +1,17 @@ +project: + type: website + +website: + title: "tmpsite" + +format: + html: + theme: + - cosmo + - brand + +profile: + group: + - [navbar, sidebar] + +brand: brand/brand.yml diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/about.qmd b/tests/docs/smoke-all/brand/logo/project-subdirs/about.qmd new file mode 100644 index 00000000000..e8cd8b81f28 --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/project-subdirs/about.qmd @@ -0,0 +1,8 @@ +--- +title: "Other" +--- + +Testing branding in subdirectories + +- [slides, root directory](slides.qmd) +- [slides, subdirectory](revealjs/slides.qmd) \ No newline at end of file diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/brand/brand.yml b/tests/docs/smoke-all/brand/logo/project-subdirs/brand/brand.yml new file mode 100644 index 00000000000..8c00c2073f2 --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/project-subdirs/brand/brand.yml @@ -0,0 +1,44 @@ +logo: + images: + favicon: + path: images/favicon.png + full: + path: images/logo.png + header: + path: images/header.png + small: favicon + medium: favicon + large: full + +color: + palette: + blue: "#0b3954" + teal: "#087e8b" + lightblue: "#bfd7ea" + pink: "#ff8484" + purple: "#8d6b94" + lightblue2: "#ecf3f9" + foreground: blue + background: lightblue + primary: blue + secondary: teal + +typography: + fonts: + - family: Ubuntu + source: google + - family: Telex + source: google + - family: Source Code Pro + source: google + base: Telex + headings: Ubuntu + monospace: Source Code Pro + monospace-inline: + family: Source Code Pro + background-color: lightblue2 + monospace-block: + family: Source Code Pro + background-color: lightblue2 + link: + decoration: underline diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/brand/images/favicon.png b/tests/docs/smoke-all/brand/logo/project-subdirs/brand/images/favicon.png new file mode 100644 index 00000000000..03ade4522c9 Binary files /dev/null and b/tests/docs/smoke-all/brand/logo/project-subdirs/brand/images/favicon.png differ diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/brand/images/header.png b/tests/docs/smoke-all/brand/logo/project-subdirs/brand/images/header.png new file mode 100644 index 00000000000..a9ec42c984e Binary files /dev/null and b/tests/docs/smoke-all/brand/logo/project-subdirs/brand/images/header.png differ diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/brand/images/logo.png b/tests/docs/smoke-all/brand/logo/project-subdirs/brand/images/logo.png new file mode 100644 index 00000000000..c8ba37dd5c9 Binary files /dev/null and b/tests/docs/smoke-all/brand/logo/project-subdirs/brand/images/logo.png differ diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/dashboard.qmd b/tests/docs/smoke-all/brand/logo/project-subdirs/dashboard.qmd new file mode 100644 index 00000000000..535fe1c9f0c --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/project-subdirs/dashboard.qmd @@ -0,0 +1,20 @@ +--- +title: "tmpsite" +format: dashboard +_quarto: + tests: + dashboard: + ensureFileRegexMatches: + - + - '}} + +Here's some text and [a link](https://example.com). + +{{< brand logo header >}} diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/dashboard/dashboard.qmd b/tests/docs/smoke-all/brand/logo/project-subdirs/dashboard/dashboard.qmd new file mode 100644 index 00000000000..ae7c97c07e9 --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/project-subdirs/dashboard/dashboard.qmd @@ -0,0 +1,21 @@ +--- +title: "tmpsite" +format: dashboard +_quarto: + tests: + dashboard: + ensureFileRegexMatches: + - + - '' + - '' + - '' + - '' + - [] +--- + + +{{< brand logo full >}} + +Here's some text and [a link](https://example.com). + +{{< brand logo header >}} diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/html/index.qmd b/tests/docs/smoke-all/brand/logo/project-subdirs/html/index.qmd new file mode 100644 index 00000000000..b8b0abe8e37 --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/project-subdirs/html/index.qmd @@ -0,0 +1,19 @@ +--- +title: "tmpsite" +_quarto: + tests: + html: + ensureFileRegexMatches: + - + - '}} + +Here's some text and [a link](https://example.com). + +{{< brand logo header >}} diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/index.qmd b/tests/docs/smoke-all/brand/logo/project-subdirs/index.qmd new file mode 100644 index 00000000000..747dd6c8ee7 --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/project-subdirs/index.qmd @@ -0,0 +1,18 @@ +--- +title: "tmpsite" +_quarto: + tests: + html: + ensureFileRegexMatches: + - + - '}} + +Here's some text and [a link](https://example.com). + +{{< brand logo header >}} diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/revealjs/slides.qmd b/tests/docs/smoke-all/brand/logo/project-subdirs/revealjs/slides.qmd new file mode 100644 index 00000000000..89c9850fd15 --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/project-subdirs/revealjs/slides.qmd @@ -0,0 +1,21 @@ +--- +title: "Untitled" +format: revealjs +_quarto: + tests: + revealjs: + ensureFileRegexMatches: + - + - '}} + +Here's some text and [a link](https://example.com). + +{{< brand logo header >}} diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/slides.qmd b/tests/docs/smoke-all/brand/logo/project-subdirs/slides.qmd new file mode 100644 index 00000000000..b2a8e96ddd0 --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/project-subdirs/slides.qmd @@ -0,0 +1,22 @@ +--- +title: "Untitled" +format: revealjs +_quarto: + tests: + revealjs: + ensureFileRegexMatches: + - + - '}} + +Here's some text and [a link](https://example.com). + +{{< brand logo header >}} diff --git a/tests/docs/smoke-all/brand/logo/project-subdirs/typst/typst.qmd b/tests/docs/smoke-all/brand/logo/project-subdirs/typst/typst.qmd new file mode 100644 index 00000000000..b5d5a07c210 --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/project-subdirs/typst/typst.qmd @@ -0,0 +1,22 @@ +--- +title: "tmpsite" +format: + typst: + output-ext: typ +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - + - '#set page\(background:.*image\("\.\.(/|\\\\)brand(/|\\\\)images(/|\\\\)favicon\.png' + - '#box\(image\("\.\.(/|\\\\)brand(/|\\\\)images(/|\\\\)logo.png"\)\)' + - '#box\(image\("\.\.(/|\\\\)brand(/|\\\\)images(/|\\\\)header.png"\)\)' + - [] +--- + + +{{< brand logo full >}} + +Here's some text and [a link](https://example.com). + +{{< brand logo header >}} diff --git a/tests/docs/smoke-all/brand/logo/sidebar/brand-default/.gitignore b/tests/docs/smoke-all/brand/logo/sidebar/brand-default/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/sidebar/brand-default/.gitignore +++ b/tests/docs/smoke-all/brand/logo/sidebar/brand-default/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/sidebar/brand-override-alt/.gitignore b/tests/docs/smoke-all/brand/logo/sidebar/brand-override-alt/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/sidebar/brand-override-alt/.gitignore +++ b/tests/docs/smoke-all/brand/logo/sidebar/brand-override-alt/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/sidebar/brand-override-resources/.gitignore b/tests/docs/smoke-all/brand/logo/sidebar/brand-override-resources/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/sidebar/brand-override-resources/.gitignore +++ b/tests/docs/smoke-all/brand/logo/sidebar/brand-override-resources/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/sidebar/mode-first-search/.gitignore b/tests/docs/smoke-all/brand/logo/sidebar/mode-first-search/.gitignore index 9e01345e0d5..a31c1396976 100644 --- a/tests/docs/smoke-all/brand/logo/sidebar/mode-first-search/.gitignore +++ b/tests/docs/smoke-all/brand/logo/sidebar/mode-first-search/.gitignore @@ -1,2 +1,4 @@ _site/ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/sidebar/size-preference-order/.gitignore b/tests/docs/smoke-all/brand/logo/sidebar/size-preference-order/.gitignore index 9e01345e0d5..a31c1396976 100644 --- a/tests/docs/smoke-all/brand/logo/sidebar/size-preference-order/.gitignore +++ b/tests/docs/smoke-all/brand/logo/sidebar/size-preference-order/.gitignore @@ -1,2 +1,4 @@ _site/ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/sidebar/theme-dark-mode-enables-dark-logo/.gitignore b/tests/docs/smoke-all/brand/logo/sidebar/theme-dark-mode-enables-dark-logo/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/sidebar/theme-dark-mode-enables-dark-logo/.gitignore +++ b/tests/docs/smoke-all/brand/logo/sidebar/theme-dark-mode-enables-dark-logo/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/source-sans-pro/.gitignore b/tests/docs/smoke-all/brand/logo/source-sans-pro/.gitignore new file mode 100644 index 00000000000..0a60a0663a7 --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/source-sans-pro/.gitignore @@ -0,0 +1,2 @@ +*.html +*_files/ diff --git a/tests/docs/smoke-all/brand/logo/source-sans-pro/_brand.yml b/tests/docs/smoke-all/brand/logo/source-sans-pro/_brand.yml new file mode 100644 index 00000000000..34d5763b1c2 --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/source-sans-pro/_brand.yml @@ -0,0 +1,17 @@ +typography: + fonts: + - family: "Source Sans Pro" + source: file + files: + - fonts/source-sans-pro/source-sans-pro-regular.woff + - path: fonts/source-sans-pro/source-sans-pro-italic.woff + style: italic + - path: fonts/source-sans-pro/source-sans-pro-semibold.woff + weight: 600 + - path: fonts/source-sans-pro/source-sans-pro-semibolditalic.woff + style: italic + weight: 600 + base: + family: Source Sans Pro + line-height: 1.25 + size: 1rem diff --git a/tests/docs/smoke-all/brand/logo/source-sans-pro/brand.yml b/tests/docs/smoke-all/brand/logo/source-sans-pro/brand.yml new file mode 100644 index 00000000000..f547767203c --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/source-sans-pro/brand.yml @@ -0,0 +1,28 @@ +typography: + fonts: + - family: "Source Sans Pro" + source: file + files: + - path: fonts/source-sans-pro/source-sans-pro-regular.woff + + - family: "Fira Code" + source: file + files: + - fonts/firacode/FiraCode-VF.ttf + - family: "Roboto Slab" + source: file + files: + - path: fonts/robotoslab/RobotoSlab-VariableFont_wght.ttf + weight: 600 + style: normal + base: + family: Source Sans Pro + line-height: 1.25 + size: 1rem + headings: + family: Roboto Slab + color: primary + weight: 600 + monospace: + family: Fira Code + size: 0.9em diff --git a/tests/docs/smoke-all/brand/logo/source-sans-pro/fonts/source-sans-pro/source-sans-pro-italic.woff b/tests/docs/smoke-all/brand/logo/source-sans-pro/fonts/source-sans-pro/source-sans-pro-italic.woff new file mode 100755 index 00000000000..ceecbf17f3b Binary files /dev/null and b/tests/docs/smoke-all/brand/logo/source-sans-pro/fonts/source-sans-pro/source-sans-pro-italic.woff differ diff --git a/tests/docs/smoke-all/brand/logo/source-sans-pro/fonts/source-sans-pro/source-sans-pro-regular.woff b/tests/docs/smoke-all/brand/logo/source-sans-pro/fonts/source-sans-pro/source-sans-pro-regular.woff new file mode 100755 index 00000000000..630754abf39 Binary files /dev/null and b/tests/docs/smoke-all/brand/logo/source-sans-pro/fonts/source-sans-pro/source-sans-pro-regular.woff differ diff --git a/tests/docs/smoke-all/brand/logo/source-sans-pro/fonts/source-sans-pro/source-sans-pro-semibold.woff b/tests/docs/smoke-all/brand/logo/source-sans-pro/fonts/source-sans-pro/source-sans-pro-semibold.woff new file mode 100755 index 00000000000..8888cf8d4f9 Binary files /dev/null and b/tests/docs/smoke-all/brand/logo/source-sans-pro/fonts/source-sans-pro/source-sans-pro-semibold.woff differ diff --git a/tests/docs/smoke-all/brand/logo/source-sans-pro/fonts/source-sans-pro/source-sans-pro-semibolditalic.woff b/tests/docs/smoke-all/brand/logo/source-sans-pro/fonts/source-sans-pro/source-sans-pro-semibolditalic.woff new file mode 100755 index 00000000000..7c2d3c74f19 Binary files /dev/null and b/tests/docs/smoke-all/brand/logo/source-sans-pro/fonts/source-sans-pro/source-sans-pro-semibolditalic.woff differ diff --git a/tests/docs/smoke-all/brand/logo/source-sans-pro/source-sans-pro.qmd b/tests/docs/smoke-all/brand/logo/source-sans-pro/source-sans-pro.qmd new file mode 100644 index 00000000000..0976817b984 --- /dev/null +++ b/tests/docs/smoke-all/brand/logo/source-sans-pro/source-sans-pro.qmd @@ -0,0 +1,14 @@ +--- +title: Source Sans Pro +--- + +This text **should be** in Source Sans Pro, _and_ some ***variants***. + +{{< lipsum 1 >}} + +**{{< lipsum 1 >}}** + +*{{< lipsum 1 >}}* + +***{{< lipsum 1 >}}*** + diff --git a/tests/docs/smoke-all/brand/logo/theme-dark-project-brand-file-light/.gitignore b/tests/docs/smoke-all/brand/logo/theme-dark-project-brand-file-light/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/theme-dark-project-brand-file-light/.gitignore +++ b/tests/docs/smoke-all/brand/logo/theme-dark-project-brand-file-light/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/brand/logo/url-logo.qmd b/tests/docs/smoke-all/brand/logo/url-logo.qmd index 45babc67aaa..b9aed9809a7 100644 --- a/tests/docs/smoke-all/brand/logo/url-logo.qmd +++ b/tests/docs/smoke-all/brand/logo/url-logo.qmd @@ -1,7 +1,6 @@ --- title: "Reproducible Quarto Document" format: - html: default dashboard: default revealjs: default typst: @@ -15,12 +14,6 @@ brand: small: quarto-logo _quarto: tests: - html: - ensureHtmlElements: - - - - 'img[class*="light-content"][src="https://quarto.org/quarto.png"][alt="Quarto icon"]' - - - - 'img[class*="dark-content"]' dashboard: ensureHtmlElements: - @@ -44,6 +37,4 @@ This is a reproducible Quarto document using `format: html`. It is written in Ma {{< lipsum 1 >}} -{{< brand logo small>}} - The end. \ No newline at end of file diff --git a/tests/docs/smoke-all/brand/logo/website-favicon/.gitignore b/tests/docs/smoke-all/brand/logo/website-favicon/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/brand/logo/website-favicon/.gitignore +++ b/tests/docs/smoke-all/brand/logo/website-favicon/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/issues/4820-giscus-dark-mode/README b/tests/docs/smoke-all/issues/4820-giscus-dark-mode/README new file mode 100644 index 00000000000..1876f70978c --- /dev/null +++ b/tests/docs/smoke-all/issues/4820-giscus-dark-mode/README @@ -0,0 +1 @@ +29-08-2025 - Those tests are for now ignored because they rely on a repo that is not accessible anymore. They need to be recreated, and it could take time. We don't want our test suites to be blocked by this. diff --git a/tests/docs/smoke-all/issues/4820-giscus-dark-mode/darkly-default/index.qmd b/tests/docs/smoke-all/issues/4820-giscus-dark-mode/darkly-default/_index.qmd similarity index 100% rename from tests/docs/smoke-all/issues/4820-giscus-dark-mode/darkly-default/index.qmd rename to tests/docs/smoke-all/issues/4820-giscus-dark-mode/darkly-default/_index.qmd diff --git a/tests/docs/smoke-all/issues/4820-giscus-dark-mode/darkly-light-cobalt/index.qmd b/tests/docs/smoke-all/issues/4820-giscus-dark-mode/darkly-light-cobalt/_index.qmd similarity index 100% rename from tests/docs/smoke-all/issues/4820-giscus-dark-mode/darkly-light-cobalt/index.qmd rename to tests/docs/smoke-all/issues/4820-giscus-dark-mode/darkly-light-cobalt/_index.qmd diff --git a/tests/docs/smoke-all/issues/4820-giscus-dark-mode/darkly-light-dark/index.qmd b/tests/docs/smoke-all/issues/4820-giscus-dark-mode/darkly-light-dark/_index.qmd similarity index 100% rename from tests/docs/smoke-all/issues/4820-giscus-dark-mode/darkly-light-dark/index.qmd rename to tests/docs/smoke-all/issues/4820-giscus-dark-mode/darkly-light-dark/_index.qmd diff --git a/tests/docs/smoke-all/issues/4820-giscus-dark-mode/lightly-cobalt_only/index.qmd b/tests/docs/smoke-all/issues/4820-giscus-dark-mode/lightly-cobalt_only/_index.qmd similarity index 100% rename from tests/docs/smoke-all/issues/4820-giscus-dark-mode/lightly-cobalt_only/index.qmd rename to tests/docs/smoke-all/issues/4820-giscus-dark-mode/lightly-cobalt_only/_index.qmd diff --git a/tests/docs/smoke-all/issues/4820-giscus-dark-mode/lightly-default/index.qmd b/tests/docs/smoke-all/issues/4820-giscus-dark-mode/lightly-default/_index.qmd similarity index 100% rename from tests/docs/smoke-all/issues/4820-giscus-dark-mode/lightly-default/index.qmd rename to tests/docs/smoke-all/issues/4820-giscus-dark-mode/lightly-default/_index.qmd diff --git a/tests/docs/smoke-all/issues/4820-giscus-dark-mode/lightly-light-cobalt/index.qmd b/tests/docs/smoke-all/issues/4820-giscus-dark-mode/lightly-light-cobalt/_index.qmd similarity index 100% rename from tests/docs/smoke-all/issues/4820-giscus-dark-mode/lightly-light-cobalt/index.qmd rename to tests/docs/smoke-all/issues/4820-giscus-dark-mode/lightly-light-cobalt/_index.qmd diff --git a/tests/docs/smoke-all/shortcodes/brand-logo-both.qmd b/tests/docs/smoke-all/shortcodes/brand-logo-both.qmd index d91b4708056..5780c12ddea 100644 --- a/tests/docs/smoke-all/shortcodes/brand-logo-both.qmd +++ b/tests/docs/smoke-all/shortcodes/brand-logo-both.qmd @@ -19,10 +19,10 @@ brand: _quarto: tests: html: - ensureHtmlElements: + ensureFileRegexMatches: - - - 'img[src="sun.png"][alt="sun"][class*="light-content"]' - - 'img[src="moon.png"][alt="moon"][class*="dark-content"]' + - '' + - '' - [] --- diff --git a/tests/docs/smoke-all/shortcodes/brand-logo-dark.qmd b/tests/docs/smoke-all/shortcodes/brand-logo-dark.qmd index c20a00ba174..5af745241cb 100644 --- a/tests/docs/smoke-all/shortcodes/brand-logo-dark.qmd +++ b/tests/docs/smoke-all/shortcodes/brand-logo-dark.qmd @@ -11,9 +11,9 @@ brand: _quarto: tests: html: - ensureHtmlElements: + ensureFileRegexMatches: - - - 'img[src="moon.png"][class*="dark-content"]' + - '' + - '' + - '' - [] --- diff --git a/tests/docs/smoke-all/shortcodes/brand-logo-unified-brand-both.qmd b/tests/docs/smoke-all/shortcodes/brand-logo-unified-brand-both.qmd index 418995f8f18..0eb1f368329 100644 --- a/tests/docs/smoke-all/shortcodes/brand-logo-unified-brand-both.qmd +++ b/tests/docs/smoke-all/shortcodes/brand-logo-unified-brand-both.qmd @@ -13,10 +13,10 @@ brand: _quarto: tests: html: - ensureHtmlElements: + ensureFileRegexMatches: - - - 'img[src="sun.png"][class*="light-content"]' - - 'img[src="moon.png"][class*="dark-content"]' + - ' assertFound("! LaTeX Error: File `framed.sty' not found.", "framed.sty"); assertFound("/usr/local/bin/mktexpk: line 123: mf: command not found", "mf"); assertFound("or the language definition file ngerman.ldf was not found", "ngerman.ldf"); + assertFound(`Package babel Error: Unknown option 'ngerman'. Either you misspelled it + (babel) or the language definition file ngerman.ldf + (babel) was not found. + (babel) There is a locale ini file for this language. + (babel) If it’s the main language, try adding \`provide=*' + (babel) to the babel package options.`, "ngerman.ldf") assertFound("!pdfTeX error: pdflatex (file 8r.enc): cannot open encoding file for reading", "8r.enc"); assertFound("! CTeX fontset `fandol' is unavailable in current mode", "fandol"); assertFound('Package widetext error: Install the flushend package which is a part of sttools', "flushend.sty"); diff --git a/tests/verify.ts b/tests/verify.ts index f871dfe22c3..bd8689bec0a 100644 --- a/tests/verify.ts +++ b/tests/verify.ts @@ -153,6 +153,11 @@ export const noErrorsOrWarnings: Verify = { const isErrorOrWarning = (output: ExecuteOutput) => { return output.levelName.toLowerCase() === "warn" || output.levelName.toLowerCase() === "error"; + // I'd like to do this but many many of our tests + // would fail right now because we're assuming noErrorsOrWarnings + // doesn't include warnings from the lua subsystem + // || + // output.msg.startsWith("(W)"); // this is a warning from quarto.log.warning() }; const errorsOrWarnings = outputs.some(isErrorOrWarning); diff --git a/version.txt b/version.txt index 2fef18adb3a..1ebc94b454c 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.8.21 +1.8.22