From 20aea90ae99dbb425d5927d00672c2578043aa7e Mon Sep 17 00:00:00 2001 From: Gordon Woodhull Date: Mon, 21 Jul 2025 15:03:51 -0400 Subject: [PATCH 1/4] typst logo partial moves the logo declaration to a new partial the lua filter fills the logo object in the pandoc metadata declares new brand-logo and brand-logo-images dictionaries in Typst header --- src/format/typst/format-typst.ts | 1 + .../filters/quarto-post/typst-brand-yaml.lua | 198 ++++++++++-------- .../formats/typst/pandoc/quarto/logo.typ | 3 + .../formats/typst/pandoc/quarto/template.typ | 2 + .../brand-yaml/logo/posit/brand-logo.qmd | 2 + .../logo/relative-path/brand-logo.qmd | 2 + 6 files changed, 123 insertions(+), 85 deletions(-) create mode 100644 src/resources/formats/typst/pandoc/quarto/logo.typ diff --git a/src/format/typst/format-typst.ts b/src/format/typst/format-typst.ts index f36460b64b0..04407e46392 100644 --- a/src/format/typst/format-typst.ts +++ b/src/format/typst/format-typst.ts @@ -92,6 +92,7 @@ export function typstFormat(): Format { partials: [ "definitions.typ", "typst-template.typ", + "logo.typ", "typst-show.typ", "notes.typ", "biblio.typ", diff --git a/src/resources/filters/quarto-post/typst-brand-yaml.lua b/src/resources/filters/quarto-post/typst-brand-yaml.lua index b39798f6809..08ff8c6a9bb 100644 --- a/src/resources/filters/quarto-post/typst-brand-yaml.lua +++ b/src/resources/filters/quarto-post/typst-brand-yaml.lua @@ -96,6 +96,33 @@ function render_typst_brand_yaml() local decl = '#let brand-color-background = ' .. to_typst_dict_indent(themebk) quarto.doc.include_text('in-header', decl) end + if brand.processedData.logo and next(brand.processedData.logo) then + local logo = brand.processedData.logo + if logo.images then + local declImage = {} + for name, image in pairs(logo.images) do + declImage[name] = { + path = quote_string(image.path), + alt = quote_string(image.alt), + } + end + if next(declImage) then + quarto.doc.include_text('in-header', '#let brand-logo-images = ' .. to_typst_dict_indent(declImage)) + end + end + local declLogo = {} + for _, size in pairs({'small', 'medium', 'large'}) do + if logo[size] then + declLogo[size] = { + path = quote_string(logo[size].path), + alt = quote_string(logo[size].alt), + } + end + end + if next(declLogo) then + quarto.doc.include_text('in-header', '#let brand-logo = ' .. to_typst_dict_indent(declLogo)) + end + end local function conditional_entry(key, value, quote_strings) if quote_strings == null then quote_strings = true end if not value then return '' end @@ -212,102 +239,103 @@ function render_typst_brand_yaml() ', content)' })) end - - -- logo - local logo = param('logo') - local logoOptions = {} - local foundLogo = null - if logo then - if type(logo) == 'string' then - foundLogo = _quarto.modules.brand.get_logo(brandMode, logo) or {path=logo} - elseif type(logo) == 'table' then - for k, v in pairs(logo) do - logoOptions[k] = v - end - if logo.path then - foundLogo = _quarto.modules.brand.get_logo(brandMode, logo.path) or {path=logo} - end + end + end, + Meta = function(meta) + local brand = param('brand') + local brandMode = param('brand-mode') or 'light' + brand = brand and brand[brandMode] + -- it can contain the path but we want to store an object here + if not meta.brand or pandoc.utils.type(meta.brand) == 'Inlines' then + meta.brand = {} + end + -- logo + local logo = param('logo') + local logoOptions = {} + local foundLogo = null + if logo then + if type(logo) == 'string' then + foundLogo = _quarto.modules.brand.get_logo(brandMode, logo) or {path=logo} + elseif type(logo) == 'table' then + for k, v in pairs(logo) do + logoOptions[k] = v + end + if logo.path then + foundLogo = _quarto.modules.brand.get_logo(brandMode, logo.path) or {path=logo} end end - if not foundLogo and brand.processedData.logo then - local tries = {'large', 'small', 'medium'} -- low to high priority - foundLogo = _quarto.modules.brand.get_logo(brandMode, 'medium') - or _quarto.modules.brand.get_logo(brandMode, 'small') - or _quarto.modules.brand.get_logo(brandMode, 'large') - end - if foundLogo then - logoOptions.path = foundLogo.path - logoOptions.alt = foundLogo.alt + end + if not foundLogo and brand and brand.processedData and brand.processedData.logo then + foundLogo = _quarto.modules.brand.get_logo(brandMode, 'medium') + or _quarto.modules.brand.get_logo(brandMode, 'small') + or _quarto.modules.brand.get_logo(brandMode, 'large') + end + if foundLogo then + logoOptions.path = foundLogo.path + logoOptions.alt = foundLogo.alt - local pads = {} - for k, v in _quarto.utils.table.sortedPairs(logoOptions) do - if k == 'padding' then - local widths = {} - _quarto.modules.typst.css.parse_multiple(v, 5, function(s, start) - local width, newstart = _quarto.modules.typst.css.consume_width(s, start) - table.insert(widths, width) - return newstart - end) - local sides = _quarto.modules.typst.css.expand_side_shorthand( - widths, - 'widths in padding list: ' .. v) - pads.top = sides.top - pads.right = sides.right - pads.bottom = sides.bottom - pads.left = sides.left - elseif k:find '^padding-' then - local _, ndash = k:gsub('-', '') - if ndash == 1 then - local side = k:match('^padding--(%a+)') - local padding_sides = {'left', 'top', 'right', 'bottom'} - if tcontains(padding_sides, side) then - pads[side] = _quarto.modules.typst.css.translate_length(v) - else - quarto.log.warning('invalid padding key ' .. k) - end + local pads = {} + for k, v in _quarto.utils.table.sortedPairs(logoOptions) do + if k == 'padding' then + local widths = {} + _quarto.modules.typst.css.parse_multiple(v, 5, function(s, start) + local width, newstart = _quarto.modules.typst.css.consume_width(s, start) + table.insert(widths, width) + return newstart + end) + local sides = _quarto.modules.typst.css.expand_side_shorthand( + widths, + 'widths in padding list: ' .. v) + pads.top = sides.top + pads.right = sides.right + pads.bottom = sides.bottom + pads.left = sides.left + elseif k:find '^padding-' then + local _, ndash = k:gsub('-', '') + if ndash == 1 then + local side = k:match('^padding--(%a+)') + local padding_sides = {'left', 'top', 'right', 'bottom'} + if tcontains(padding_sides, side) then + pads[side] = _quarto.modules.typst.css.translate_length(v) else quarto.log.warning('invalid padding key ' .. k) end - end - end - local inset = nil - if next(pads) then - if pads.top == pads.right and - pads.right == pads.bottom and - pads.bottom == pads.left - then - inset = pads.top - elseif pads.top == pads.bottom and pads.left == pads.right then - inset = _quarto.modules.typst.as_typst_dictionary({x = pads.left, y = pads.top}) else - inset = _quarto.modules.typst.as_typst_dictionary(pads) + quarto.log.warning('invalid padding key ' .. k) end - else - inset = '0.75in' end - logoOptions.width = _quarto.modules.typst.css.translate_length(logoOptions.width or '1.5in') - logoOptions.location = logoOptions.location and - location_to_typst_align(logoOptions.location) or 'left+top' - quarto.log.debug('logo options', logoOptions) - local altProp = logoOptions.alt and (', alt: "' .. logoOptions.alt .. '"') or '' - local imageFilename = logoOptions.path - if _quarto.modules.mediabag.should_mediabag(imageFilename) then - imageFilename = _quarto.modules.mediabag.resolved_url_cache[logoOptions.path] or _quarto.modules.mediabag.fetch_and_store_image(logoOptions.path) - imageFilename = _quarto.modules.mediabag.write_mediabag_entry(imageFilename) or imageFilename + end + local inset = nil + if next(pads) then + if pads.top == pads.right and + pads.right == pads.bottom and + pads.bottom == pads.left + then + inset = pads.top + elseif pads.top == pads.bottom and pads.left == pads.right then + inset = _quarto.modules.typst.as_typst_dictionary({x = pads.left, y = pads.top}) else - -- backslashes need to be doubled for Windows - imageFilename = string.gsub(imageFilename, '\\', '\\\\') + inset = _quarto.modules.typst.as_typst_dictionary(pads) end - quarto.doc.include_text('in-header', - '#set page(background: align(' .. logoOptions.location .. ', box(inset: ' .. inset .. ', image("' .. imageFilename .. '", width: ' .. logoOptions.width .. altProp .. '))))') - end - end - end, - Meta = function(meta) - local brandMode = param('brand-mode') or 'light' - -- it can contain the path but we want to store an object here - if not meta.brand or pandoc.utils.type(meta.brand) == 'Inlines' then - meta.brand = {} + else + inset = '0.75in' + end + logoOptions.width = _quarto.modules.typst.css.translate_length(logoOptions.width or '1.5in') + logoOptions.inset = inset + logoOptions.location = logoOptions.location and + location_to_typst_align(logoOptions.location) or 'left+top' + quarto.log.debug('logo options', logoOptions) + local imageFilename = logoOptions.path + if _quarto.modules.mediabag.should_mediabag(imageFilename) then + imageFilename = _quarto.modules.mediabag.resolved_url_cache[logoOptions.path] or _quarto.modules.mediabag.fetch_and_store_image(logoOptions.path) + imageFilename = _quarto.modules.mediabag.write_mediabag_entry(imageFilename) or imageFilename + imageFilename = imageFilename and imageFilename:gsub('\\_', '_') + else + -- backslashes need to be doubled for Windows + imageFilename = string.gsub(imageFilename, '\\', '\\\\') + end + logoOptions.path = pandoc.RawInline('typst', imageFilename) + meta.logo = logoOptions end meta.brand.typography = meta.brand.typography or {} local base = _quarto.modules.brand.get_typography(brandMode, 'base') diff --git a/src/resources/formats/typst/pandoc/quarto/logo.typ b/src/resources/formats/typst/pandoc/quarto/logo.typ new file mode 100644 index 00000000000..c2d9ee6488c --- /dev/null +++ b/src/resources/formats/typst/pandoc/quarto/logo.typ @@ -0,0 +1,3 @@ +$if(logo)$ +#set page(background: align($logo.location$, box(inset: $logo.inset$, image("$logo.path$", width: $logo.width$$if(logo.alt)$, alt: "$logo.alt$"$endif$)))) +$endif$ diff --git a/src/resources/formats/typst/pandoc/quarto/template.typ b/src/resources/formats/typst/pandoc/quarto/template.typ index 3b23beaa085..27f98d26218 100644 --- a/src/resources/formats/typst/pandoc/quarto/template.typ +++ b/src/resources/formats/typst/pandoc/quarto/template.typ @@ -6,6 +6,8 @@ $for(header-includes)$ $header-includes$ $endfor$ +$logo.typ()$ + $typst-show.typ()$ $for(include-before)$ diff --git a/tests/docs/smoke-all/typst/brand-yaml/logo/posit/brand-logo.qmd b/tests/docs/smoke-all/typst/brand-yaml/logo/posit/brand-logo.qmd index dac52630ed1..07fff252977 100644 --- a/tests/docs/smoke-all/typst/brand-yaml/logo/posit/brand-logo.qmd +++ b/tests/docs/smoke-all/typst/brand-yaml/logo/posit/brand-logo.qmd @@ -9,6 +9,8 @@ _quarto: typst: ensureTypstFileRegexMatches: - + - '#let brand-logo-images = \(\s*brand-typst-with-good-padding: \(\s*path: "good-padding\.png"\s*\),\s*posit-logo-light-medium: \(\s*alt: "Posit Logo",\s*path: "posit-logo-2024.svg"\s*\)' + - '#let brand-logo = \(\s*medium: \(\s*alt: "Posit Logo",\s*path: "posit-logo-2024\.svg"\s*\)\s*\)' - '#set page\(background: align\(left\+top, box\(inset: 0.75in, image\("posit-logo-2024.svg", width: 1.5in, alt: "Posit Logo"\)\)\)\)' - [] --- diff --git a/tests/docs/smoke-all/typst/brand-yaml/logo/relative-path/brand-logo.qmd b/tests/docs/smoke-all/typst/brand-yaml/logo/relative-path/brand-logo.qmd index b51305cdfe9..54b260893cd 100644 --- a/tests/docs/smoke-all/typst/brand-yaml/logo/relative-path/brand-logo.qmd +++ b/tests/docs/smoke-all/typst/brand-yaml/logo/relative-path/brand-logo.qmd @@ -11,6 +11,8 @@ _quarto: typst: ensureTypstFileRegexMatches: - + - '#let brand-logo-images = \(\s*large-light: \(\s*path: "resources/quarto.png"\s*\)\s*\)' + - '#let brand-logo = \(\s*large: \(\s*path: "brand_yaml/resources/quarto.png"\s*\)\s*\)' - '#set page\(background: align\(center\+top, box\(inset: 2em, image\("brand_yaml(/|\\\\)resources(/|\\\\)quarto.png", width: 225pt\)\)\)\)' - [] --- From 0002e519ef2885caa75c09efe26593de947321cd Mon Sep 17 00:00:00 2001 From: Gordon Woodhull Date: Tue, 22 Jul 2025 00:50:11 -0400 Subject: [PATCH 2/4] os-agnostic path tests --- .../typst/brand-yaml/logo/relative-path/brand-logo.qmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/docs/smoke-all/typst/brand-yaml/logo/relative-path/brand-logo.qmd b/tests/docs/smoke-all/typst/brand-yaml/logo/relative-path/brand-logo.qmd index 54b260893cd..7bd168684f8 100644 --- a/tests/docs/smoke-all/typst/brand-yaml/logo/relative-path/brand-logo.qmd +++ b/tests/docs/smoke-all/typst/brand-yaml/logo/relative-path/brand-logo.qmd @@ -11,8 +11,8 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '#let brand-logo-images = \(\s*large-light: \(\s*path: "resources/quarto.png"\s*\)\s*\)' - - '#let brand-logo = \(\s*large: \(\s*path: "brand_yaml/resources/quarto.png"\s*\)\s*\)' + - '#let brand-logo-images = \(\s*large-light: \(\s*path: "resources(/|\\\\)quarto.png"\s*\)\s*\)' + - '#let brand-logo = \(\s*large: \(\s*path: "brand_yaml(/|\\\\)resources(/|\\\\)quarto.png"\s*\)\s*\)' - '#set page\(background: align\(center\+top, box\(inset: 2em, image\("brand_yaml(/|\\\\)resources(/|\\\\)quarto.png", width: 225pt\)\)\)\)' - [] --- From 23e8b994f001c252aa1849e0a152fd3c87b8e89e Mon Sep 17 00:00:00 2001 From: Gordon Woodhull Date: Tue, 22 Jul 2025 12:22:20 -0400 Subject: [PATCH 3/4] typst logo: replace backslashes with double backslashes on windows --- src/resources/filters/quarto-post/typst-brand-yaml.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resources/filters/quarto-post/typst-brand-yaml.lua b/src/resources/filters/quarto-post/typst-brand-yaml.lua index 08ff8c6a9bb..1b737f573f9 100644 --- a/src/resources/filters/quarto-post/typst-brand-yaml.lua +++ b/src/resources/filters/quarto-post/typst-brand-yaml.lua @@ -102,7 +102,7 @@ function render_typst_brand_yaml() local declImage = {} for name, image in pairs(logo.images) do declImage[name] = { - path = quote_string(image.path), + path = quote_string(image.path):gsub('\\', '\\\\'), alt = quote_string(image.alt), } end @@ -114,7 +114,7 @@ function render_typst_brand_yaml() for _, size in pairs({'small', 'medium', 'large'}) do if logo[size] then declLogo[size] = { - path = quote_string(logo[size].path), + path = quote_string(logo[size].path):gsub('\\', '\\\\'), alt = quote_string(logo[size].alt), } end From 73d72e96b179590aa2c00e265988b8cb997d74c6 Mon Sep 17 00:00:00 2001 From: Gordon Woodhull Date: Tue, 22 Jul 2025 12:30:41 -0400 Subject: [PATCH 4/4] brand processedData also needs to resolve paths in logo.images --- src/core/brand/brand.ts | 20 +++++++++---------- .../logo/relative-path/brand-logo.qmd | 6 +++++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/core/brand/brand.ts b/src/core/brand/brand.ts index ffc50736783..bd52b21f207 100644 --- a/src/core/brand/brand.ts +++ b/src/core/brand/brand.ts @@ -10,6 +10,7 @@ import { BrandColorLightDark, BrandFont, BrandLogoExplicitResource, + BrandLogoResource, BrandLogoSingle, BrandLogoUnified, BrandNamedLogo, @@ -143,11 +144,7 @@ export class Brand { } } for (const [key, value] of Object.entries(data.logo?.images ?? {})) { - if (typeof value === "string") { - logo.images[key] = { path: value }; - } else { - logo.images[key] = value; - } + logo.images[key] = this.resolvePath(value); } return { @@ -240,11 +237,7 @@ export class Brand { return fonts ?? []; } - getLogoResource(name: string): BrandLogoExplicitResource { - const entry = this.data.logo?.images?.[name]; - if (!entry) { - return { path: name }; - } + resolvePath(entry: BrandLogoResource) { const pathPrefix = relative(this.projectDir, this.brandDir); if (typeof entry === "string") { return { path: join(pathPrefix, entry) }; @@ -255,6 +248,13 @@ export class Brand { }; } + getLogoResource(name: string): BrandLogoExplicitResource { + const entry = this.data.logo?.images?.[name]; + if (!entry) { + return { path: name }; + } + return this.resolvePath(entry); + } getLogo(name: BrandNamedLogo): BrandLogoExplicitResource | undefined { const entry = this.data.logo?.[name]; if (!entry) { diff --git a/tests/docs/smoke-all/typst/brand-yaml/logo/relative-path/brand-logo.qmd b/tests/docs/smoke-all/typst/brand-yaml/logo/relative-path/brand-logo.qmd index 7bd168684f8..f94302ae13f 100644 --- a/tests/docs/smoke-all/typst/brand-yaml/logo/relative-path/brand-logo.qmd +++ b/tests/docs/smoke-all/typst/brand-yaml/logo/relative-path/brand-logo.qmd @@ -11,11 +11,15 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '#let brand-logo-images = \(\s*large-light: \(\s*path: "resources(/|\\\\)quarto.png"\s*\)\s*\)' + - '#let brand-logo-images = \(\s*large-light: \(\s*path: "brand_yaml(/|\\\\)resources(/|\\\\)quarto.png"\s*\)\s*\)' - '#let brand-logo = \(\s*large: \(\s*path: "brand_yaml(/|\\\\)resources(/|\\\\)quarto.png"\s*\)\s*\)' - '#set page\(background: align\(center\+top, box\(inset: 2em, image\("brand_yaml(/|\\\\)resources(/|\\\\)quarto.png", width: 225pt\)\)\)\)' + - '#image\(brand-logo-images\.large-light\.path, alt:"from brand-logo-images"\)' - [] --- {{< lipsum 4 >}} +```{=typst} +#image(brand-logo-images.large-light.path, alt:"from brand-logo-images") +```