diff --git a/docs/_extensions/mcanouil/atelier/_extension.yml b/docs/_extensions/mcanouil/atelier/_extension.yml index f05145e..7cb162a 100644 --- a/docs/_extensions/mcanouil/atelier/_extension.yml +++ b/docs/_extensions/mcanouil/atelier/_extension.yml @@ -1,7 +1,7 @@ title: Atelier author: Mickaël Canouil -version: 0.10.5 -quarto-required: ">=1.9.36" +version: 0.11.0 +quarto-required: ">=1.9.38" contributes: project: project: @@ -64,8 +64,8 @@ contributes: - brand - html/theme.scss syntax-highlighting: - light: github-light - dark: github-dark + light: arrow-light + dark: arrow-dark toc: true toc-depth: 3 anchor-sections: true @@ -74,8 +74,34 @@ contributes: sidebar-width: 300px body-width: 1000px margin-width: 300px + # The embedded extensions under `_extensions/mcanouil/` are named here, + # and Quarto resolves each name to the filters or shortcodes of the + # embedded copy. A project resolves the names in its own `filters` list + # against its own `_extensions/` only. A project that keeps its own copy + # of one of these extensions and names it there runs both copies, and its + # own shortcodes replace the embedded ones. + shortcodes: + - iconify filters: - social-metadata.lua + - iconify + - code-window + - gitlink + - toc-depth + - collapse-output + # Each embedded extension keeps its own defaults, set per site under + # `extensions:`, with two exceptions. gitlink shows its repository widget, + # but does not rewrite references in prose. The widget shows only where a + # navbar item or sidebar tool has the href `#gitlink-widget`. Code Window + # draws a window only where a block has a `filename`, and does not make a + # label from the language of every other block. + extensions: + gitlink: + enabled: false + widget: + enabled: true + code-window: + auto-filename: false include-before-body: - file: html/scripts/skip-link.html include-after-body: @@ -84,5 +110,4 @@ contributes: - file: html/scripts/ordinal-dates.html - file: html/scripts/a11y-fixes.html - file: html/scripts/navbar-tooltips.html -source: mcanouil/quarto-atelier@0.10.5 -source-type: github +source: mcanouil/quarto-atelier@0.11.0 diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/LICENSE b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/LICENSE new file mode 100644 index 0000000..4b53bb8 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mickaël Canouil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_dependencies.yml b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_dependencies.yml new file mode 100644 index 0000000..8394100 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_dependencies.yml @@ -0,0 +1,36 @@ +schema: 1 +sources: + quarto-lua-modules: + origin: "https://github.com/mcanouil/quarto-lua-modules" + fetch: "{origin}/releases/download/{version}/{file}" + version: "2.5.0" + licence: MIT + files: + html.lua: + sha256: "a7aae5e69c4effce399b85e90f3ee3261d0f84a5ac2f6c7ddeb1e358ba22ddc8" + runtime: any + logging.lua: + sha256: "5d99ceb89c813fbe4491310e42b4a4cff62e11dda42f01b4f61f969aa58e0706" + runtime: any + metadata.lua: + sha256: "1186431acc50e35797eef68808e4c52321c845a3be4f29bbd7de695b1db2f6cc" + runtime: any + pandoc-helpers.lua: + sha256: "519cdfa87e0026cfaf754639c8f07faefa4e0d21819f463f8b202448fe0f4d7e" + runtime: any + schema-check.lua: + sha256: "343c4846d4bced229de2b09f84b756051cb4061099787f8001384bce680e2987" + runtime: any + string.lua: + sha256: "7801d2f217e803951313ef86f1f899cb6350a44cbb468b2d7c28dd6911a0fadc" + runtime: any + quarto-wizard: + origin: "https://github.com/mcanouil/quarto-wizard" + fetch: "{origin}/releases/download/{version}/{file}" + version: "2.2.0" + licence: MIT + tag-prefix: schema-v + files: + schema.lua: + sha256: "84e0a5a7bae8d71a6d601d581e3e819ace96613cc76c6e9dd3cf79eee629df78" + runtime: any diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_extension.yml b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_extension.yml new file mode 100644 index 0000000..f237561 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_extension.yml @@ -0,0 +1,12 @@ +title: Code Window +author: Mickaël Canouil +version: 1.6.0 +quarto-required: ">=1.9.36" +contributes: + filters: + - at: pre-quarto + path: main.lua + - at: post-quarto + path: _modules/hotfix/typst-title-fix.lua +source: mcanouil/quarto-code-window@1.6.0 +source-type: registry diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/cell-output.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/cell-output.lua new file mode 100644 index 0000000..ea892b7 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/cell-output.lua @@ -0,0 +1,69 @@ +--- @module "cell-output" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @brief Mark the code blocks that hold the output of an executed cell. +--- @description Quarto puts the result of an executed cell in a Div with the +--- `cell-output` classes, and the code block inside carries no language class. +--- A CodeBlock filter sees that block on its own and cannot tell it apart from +--- a fenced block written without a language. This pass marks it while the +--- parent Div is still in reach, so the language and window passes can leave it +--- alone. + +local M = {} + +local MARKER = 'code-window-cell-output' + +--- Check whether a Div holds the output of an executed cell. +--- @param div pandoc.Div +--- @return boolean +local function is_cell_output(div) + for _, class in ipairs(div.classes) do + if class == 'cell-output' or class:match('^cell%-output%-') then + return true + end + end + return false +end + +--- Mark every code block inside a cell-output Div. +--- @param div pandoc.Div +--- @return pandoc.Div|nil Marked Div, or nil when the Div is left as it is +function M.Div(div) + if not is_cell_output(div) then + return nil + end + return div:walk({ + CodeBlock = function(block) + block.attributes[MARKER] = 'true' + return block + end, + }) +end + +--- Check whether a code block holds the output of an executed cell. +--- @param block pandoc.CodeBlock +--- @return boolean +function M.is_marked(block) + return block.attributes[MARKER] == 'true' +end + +--- Check whether an attribute name is this module's marker. +--- The window passes remove every attribute carrying the extension's prefix, +--- and the marker carries it too, so they ask here and leave this one alone. +--- The marker outlives those passes on purpose: it answers whether a block +--- holds the output of an executed cell, and it is removed once, below, after +--- every pass that asks. +--- @param key string Attribute name +--- @return boolean +function M.is_marker(key) + return key == MARKER +end + +--- Remove the marker, so it never reaches the output document. +--- @param block pandoc.CodeBlock +function M.strip(block) + block.attributes[MARKER] = nil +end + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/hotfix/code-annotations.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/hotfix/code-annotations.lua new file mode 100644 index 0000000..2e6f56e --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/hotfix/code-annotations.lua @@ -0,0 +1,197 @@ +--- @module "code-annotations" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @brief Code annotation detection, stripping, and Typst rendering helpers. +--- Scans CodeBlock elements for inline annotation markers (e.g. # <1>, // <2>) +--- and provides utilities for converting annotations to Typst output. + +-- ============================================================================ +-- LANGUAGE COMMENT CHARACTERS +-- ============================================================================ + +--- Map of language identifiers to their single-line comment prefix. +--- @type table +local LANG_COMMENT_CHARS = { + r = '#', + python = '#', + lua = '--', + javascript = '//', + typescript = '//', + go = '//', + rust = '//', + bash = '#', + sh = '#', + zsh = '#', + fish = '#', + c = '//', + cpp = '//', + cxx = '//', + cc = '//', + cs = '//', + java = '//', + scala = '//', + kotlin = '//', + swift = '//', + objc = '//', + php = '//', + ruby = '#', + perl = '#', + julia = '#', + haskell = '--', + elm = '--', + clojure = ';', + scheme = ';', + lisp = ';', + racket = ';', + erlang = '%%', + elixir = '#', + fortran = '!', + matlab = '%%', + ada = '--', + sql = '--', + plsql = '--', + tsql = '--', + mysql = '--', + sqlite = '--', + postgresql = '--', + vb = "'", + vbnet = "'", + fsharp = '//', + stata = '//', + yaml = '#', + toml = '#', + make = '#', + cmake = '#', + dockerfile = '#', + powershell = '#', + nix = '#', + zig = '//', + dart = '//', + groovy = '//', + d = '//', + nim = '#', + crystal = '#', + v = '//', + odin = '//', + mojo = '#', +} + +-- ============================================================================ +-- ANNOTATION RESOLUTION +-- ============================================================================ + +--- Escape a string for use in a Lua pattern. +--- @param s string +--- @return string +local function escape_pattern(s) + return s:gsub('([%(%)%.%%%+%-%*%?%[%]%^%$])', '%%%1') +end + +--- Resolve annotations in a CodeBlock element. +--- Scans each line for a trailing annotation marker (e.g. # <1>) using the +--- language's comment prefix. Strips the marker from the code text and returns +--- the cleaned text along with an annotations table. +--- @param block pandoc.CodeBlock +--- @return string cleaned_text The code with annotation markers removed +--- @return table|nil annotations Maps line numbers (int) to annotation numbers (int), or nil if none found +local function resolve_annotations(block) + if not block.classes or #block.classes == 0 then + return block.text, nil + end + + local lang = block.classes[1]:lower() + local comment = LANG_COMMENT_CHARS[lang] or '#' + + local escaped_comment = escape_pattern(comment) + local pattern = '^(.-)%s*' .. escaped_comment .. '%s*<%s*(%d+)%s*>%s*$' + + local annotations = {} + local lines = {} + local found = false + + local line_num = 0 + for line in (block.text .. '\n'):gmatch('([^\n]*)\n') do + line_num = line_num + 1 + local content, annot_num = line:match(pattern) + if annot_num then + found = true + annotations[line_num] = tonumber(annot_num) + table.insert(lines, content) + else + table.insert(lines, line) + end + end + + if not found then + return block.text, nil + end + + return table.concat(lines, '\n'), annotations +end + +-- ============================================================================ +-- TYPST CONVERSION HELPERS +-- ============================================================================ + +--- Convert an annotations table to a Typst dictionary literal. +--- Keys are stringified line numbers, values are annotation numbers. +--- Example output: (1: 2, 3: 1) +--- @param annotations table Line number to annotation number mapping +--- @return string Typst dictionary literal +local function annotations_to_typst_dict(annotations) + local pairs_list = {} + local keys = {} + for k in pairs(annotations) do + table.insert(keys, k) + end + table.sort(keys) + for _, line_num in ipairs(keys) do + table.insert(pairs_list, + string.format('"%d": %d', line_num, annotations[line_num])) + end + return '(' .. table.concat(pairs_list, ', ') .. ')' +end + +--- Check whether a block is an OrderedList that looks like an annotation list. +--- Annotation lists are OrderedLists immediately following a code block, +--- where each item corresponds to an annotation number. +--- @param block pandoc.Block +--- @return boolean +local function is_annotation_ordered_list(block) + return block and block.t == 'OrderedList' +end + +--- Convert an OrderedList to Typst annotation item RawBlocks. +--- Each list item becomes a #code-window-annotation-item(block-id, n)[...] call. +--- @param ol pandoc.OrderedList The ordered list to convert +--- @param wrapper_prefix string Prefix for the Typst function name +--- @param block_id integer Unique block identifier for bidirectional linking +--- @return pandoc.List List of RawBlock elements +local function ordered_list_to_typst_blocks(ol, wrapper_prefix, block_id) + local blocks = {} + local start = ol.listAttributes and ol.listAttributes.start or 1 + for i, item in ipairs(ol.content) do + local annot_num = start + i - 1 + local content_blocks = pandoc.Blocks(item) + local rendered = pandoc.write(pandoc.Pandoc(content_blocks), 'typst') + rendered = rendered:gsub('%s+$', '') + table.insert(blocks, pandoc.RawBlock('typst', string.format( + '#%s-annotation-item(%d, %d)[%s]', + wrapper_prefix, block_id, annot_num, rendered + ))) + end + return blocks +end + +-- ============================================================================ +-- MODULE EXPORTS +-- ============================================================================ + +return { + LANG_COMMENT_CHARS = LANG_COMMENT_CHARS, + resolve_annotations = resolve_annotations, + annotations_to_typst_dict = annotations_to_typst_dict, + is_annotation_ordered_list = is_annotation_ordered_list, + ordered_list_to_typst_blocks = ordered_list_to_typst_blocks, +} diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/hotfix/skylighting-typst-fix.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/hotfix/skylighting-typst-fix.lua new file mode 100644 index 0000000..a3a48d3 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/hotfix/skylighting-typst-fix.lua @@ -0,0 +1,286 @@ +--- @module "skylighting-typst-fix" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @brief Typst skylighting styling override and inline code background. +--- Pandoc 3.8+ generates correct token colours and bgcolor from the theme, +--- but the generated Skylighting block lacks styling properties (width, inset, +--- radius, stroke) and ignores its own fill parameter. This module overrides +--- the Skylighting function with better block styling and adds inline code +--- background support for Typst output. + +local _wrapper_prefix = 'code-window' + +--- Set the wrapper prefix for Typst function name generation. +--- Called by main.lua before each handler invocation. +--- @param prefix string The wrapper prefix (e.g. 'code-window' or 'my-window') +local function set_wrapper(prefix) + _wrapper_prefix = prefix +end + +--- Build a Skylighting override with improved block styling. +--- Pandoc 3.8+ generates correct bgcolor but the block call lacks width, +--- inset, radius, and stroke properties. The fill parameter is also ignored. +--- This override fixes both issues. +--- @return string|nil Typst #let Skylighting(...) definition, or nil +local function build_skylighting_override() + local hm = PANDOC_WRITER_OPTIONS and PANDOC_WRITER_OPTIONS.highlight_method + if not hm then return nil end + + local bg = hm['background-color'] + if not bg or type(bg) ~= 'string' then return nil end + + local circled = _wrapper_prefix .. '-circled-number' + return string.format([==[ +// skylighting-typst-fix override +#let Skylighting( + fill: none, + number: false, + start: 1, + sourcelines, +) = { + let bgcolor = if fill != none { fill } else { rgb("%s") } + let has-gutter = start + sourcelines.len() > 999 + + context { + let _page-bg = _cw-page-bg() + let _fg = _cw-fg(_page-bg) + let _border-colour = color.mix((_fg, 15%%), (_page-bg, 85%%)) + let annot-data = _cw-annotations.get() + let blocks = [] + let lnum = start - 1 + let seen-annotes = (:) + + for ln in sourcelines { + lnum = lnum + 1 + if number { + blocks = blocks + box( + width: if has-gutter { 30pt } else { 24pt }, + text([ #lnum ]), + ) + } + + if annot-data != none { + let annot-num = annot-data.annotations.at(str(lnum), default: none) + if annot-num != none { + let lbl-prefix = "cw-" + str(annot-data.block-id) + "-" + if str(annot-num) not in seen-annotes { + seen-annotes.insert(str(annot-num), true) + blocks = blocks + box(width: 100%%)[ + #ln + #h(1fr) + #link(label(lbl-prefix + "item-" + str(annot-num)))[ + #%s(annot-num, bg-colour: annot-data.bg-colour) + ] + #label(lbl-prefix + "line-" + str(annot-num)) + ] + } else { + blocks = blocks + box(width: 100%%)[ + #ln + #h(1fr) + #link(label(lbl-prefix + "item-" + str(annot-num)))[ + #%s(annot-num, bg-colour: annot-data.bg-colour) + ] + ] + } + } else { + blocks = blocks + ln + } + } else { + blocks = blocks + ln + } + blocks = blocks + EndLine() + } + + block( + fill: bgcolor, + width: 100%%, + inset: 8pt, + radius: 2pt, + stroke: 0.5pt + _border-colour, + blocks, + ) + } +} +]==], bg, circled, circled) +end + +--- Build a show raw.line rule for annotation support in idiomatic mode. +--- When Skylighting is not available (idiomatic/native Typst highlighting), +--- this rule reads _cw-annotations state and adds circled annotation markers +--- to annotated lines, mirroring what the Skylighting override does. +--- @return string Typst show rule definition +local function build_raw_line_annotation_rule() + local circled = _wrapper_prefix .. '-circled-number' + return string.format([==[ +// idiomatic-mode annotation support for raw blocks +#show raw.line: it => { + context { + let annot-data = _cw-annotations.get() + if annot-data == none { + it + } else { + let annot-num = annot-data.annotations.at(str(it.number), default: none) + if annot-num == none { + it + } else { + let lbl-prefix = "cw-" + str(annot-data.block-id) + "-" + let first-line = calc.min( + ..annot-data.annotations.pairs() + .filter(p => p.at(1) == annot-num) + .map(p => int(p.at(0))) + ) + if it.number == first-line { + box(width: 100%%)[ + #it + #h(1fr) + #link(label(lbl-prefix + "item-" + str(annot-num)))[ + #%s(annot-num, bg-colour: annot-data.bg-colour) + ] + #label(lbl-prefix + "line-" + str(annot-num)) + ] + } else { + box(width: 100%%)[ + #it + #h(1fr) + #link(label(lbl-prefix + "item-" + str(annot-num)))[ + #%s(annot-num, bg-colour: annot-data.bg-colour) + ] + ] + } + } + } + } +} +]==], circled, circled) +end + +--- Shape of the box that holds inline code, after its fill. +local _inline_box_style = 'inset: (x: 3pt, y: 0pt), outset: (y: 3pt), ' + .. 'radius: 2pt, stroke: none)[' + +--- Process inline Code for Typst format. +--- Puts the Code element in a box that carries the theme background colour. +--- The element itself stays in the document, so Pandoc highlights it and +--- writes the token definitions for it. +--- @param el pandoc.Code Inline code element +--- @return pandoc.Inlines The boxed code +local function process_typst_inline(el) + local hm = PANDOC_WRITER_OPTIONS and PANDOC_WRITER_OPTIONS.highlight_method + local bg = hm and hm['background-color'] + local opening, closing + + if type(bg) == 'string' then + opening = string.format('#box(fill: rgb("%s"), ', bg) .. _inline_box_style + closing = ']' + else + opening = '#context { let _bg = _cw-page-bg(); let _f = _cw-fg(_bg); ' + .. 'box(fill: color.mix((_f, 10%), (_bg, 90%)), ' .. _inline_box_style + closing = '] }' + end + + return pandoc.Inlines({ + pandoc.RawInline('typst', opening), + el, + pandoc.RawInline('typst', closing), + }) +end + +--- Inject Skylighting override at the start of the document. +--- Always injected when code-window is enabled to support annotation markers. +function Pandoc(doc) + if not quarto.doc.is_format('typst') then + return doc + end + + -- Guard: skip if override or annotation rule already injected. + for _, blk in ipairs(doc.blocks) do + if blk.t == 'RawBlock' and blk.format == 'typst' + and (blk.text:find('// skylighting-typst-fix override', 1, true) + or blk.text:find('// idiomatic-mode annotation support', 1, true)) then + return doc + end + end + + local override = build_skylighting_override() + local rule = override or build_raw_line_annotation_rule() + + -- Insert after the code-window function definitions so the override/rule can + -- reference _cw-annotations and the wrapper-prefixed circled-number function. + local insert_pos = 1 + for idx, blk in ipairs(doc.blocks) do + if blk.t == 'RawBlock' and blk.format == 'typst' + and blk.text:find('_cw%-annotations') then + insert_pos = idx + 1 + break + end + end + table.insert(doc.blocks, insert_pos, pandoc.RawBlock('typst', rule)) + return doc +end + +--- Check if a Div is a Quarto title scaffold (inline-only content). +--- NOTE: relies on Quarto's internal `__quarto_custom_scaffold` attribute, +--- which is not part of the public API and may change without notice. +--- @param div pandoc.Div +--- @return boolean +local function is_title_scaffold(div) + if div.attributes['__quarto_custom_scaffold'] ~= 'true' then + return false + end + for _, child in ipairs(div.content) do + if child.t ~= 'Plain' and child.t ~= 'Para' then + return false + end + end + return true +end + +--- Walk the document tree and box inline Code with background styling. +--- The extension contributes the pass once, so it sees each Code element +--- once and boxes it once. +--- Code in title scaffolds is converted to plain +--- Typst backtick code to avoid Skylighting tokens with inner quotes +--- that would break the string parameter Quarto generates. +--- The typst-title-fix post-quarto filter then evaluates the string +--- as markup so the backtick code renders with proper inline styling. +local function process_inline_code(doc) + if not quarto.doc.is_format('typst') then + return doc + end + + local code_filter = { Code = function(el) return process_typst_inline(el) end } + local title_filter = { + Code = function(el) + return pandoc.RawInline('typst', '`' .. el.text .. '`') + end, + } + + local function walk_blocks(blocks) + local new_blocks = {} + for _, blk in ipairs(blocks) do + if blk.t == 'Div' then + if is_title_scaffold(blk) then + table.insert(new_blocks, blk:walk(title_filter)) + else + blk.content = walk_blocks(blk.content) + table.insert(new_blocks, blk) + end + else + table.insert(new_blocks, blk:walk(code_filter)) + end + end + return pandoc.Blocks(new_blocks) + end + + doc.blocks = walk_blocks(doc.blocks) + return doc +end + +return { + set_wrapper = set_wrapper, + filters = { + { Pandoc = Pandoc }, + { Pandoc = process_inline_code }, + }, +} diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/hotfix/typst-title-fix.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/hotfix/typst-title-fix.lua new file mode 100644 index 0000000..89c3690 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/hotfix/typst-title-fix.lua @@ -0,0 +1,111 @@ +--- @module "typst-title-fix" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @brief Hot-fix for Quarto rendering theorem titles as string parameters. +--- Quarto renders custom type titles as title: "..." (string mode) which +--- stringifies any Typst markup. This post-quarto filter scans the source +--- for cross-reference div IDs, then injects Typst wrapper functions that +--- evaluate string titles as Typst markup via eval(mode: "markup"). + +--- Mapping from Quarto cross-reference prefix to Typst function name. +local PREFIX_TO_FUNC = { + thm = 'theorem', + lem = 'lemma', + cor = 'corollary', + prp = 'proposition', + cnj = 'conjecture', + def = 'definition', + exm = 'example', + exr = 'exercise', + sol = 'solution', +} + +--- Typst wrapper template. %s is replaced with the function name. +local WRAPPER_TEMPLATE = [==[ +#let _cw-orig-%s = %s +#let %s(title: none, ..args) = { + let t = if title != none and type(title) == str { + eval(title, mode: "markup") + } else { + title + } + _cw-orig-%s(title: t, ..args) +}]==] + +--- Build Typst code that wraps each theorem function to eval string titles. +--- @param func_names table List of function names to wrap +--- @return string Typst code +local function build_wrappers(func_names) + local parts = { '// code-window: hot-fix for Quarto rendering theorem titles as strings.' } + for _, name in ipairs(func_names) do + table.insert(parts, string.format(WRAPPER_TEMPLATE, name, name, name, name)) + end + return table.concat(parts, '\n') +end + +--- Scan source files for cross-reference div IDs and return the +--- corresponding Typst function names. +--- NOTE: uses raw-text pattern matching on source files, so it will not +--- detect divs inside `include` shortcodes or other indirect sources. +--- @return table List of function names +local function detect_theorem_types() + local func_names = {} + local seen = {} + for _, input_file in ipairs(PANDOC_STATE.input_files) do + local f = io.open(input_file, 'r') + if f then + local ok, source = pcall(f.read, f, '*a') + f:close() + if not ok then source = '' end + for prefix in source:gmatch('::: *{#(%w+)%-') do + if PREFIX_TO_FUNC[prefix] and not seen[prefix] then + table.insert(func_names, PREFIX_TO_FUNC[prefix]) + seen[prefix] = true + end + end + end + end + return func_names +end + +return { + { + Pandoc = function(doc) + if not quarto.doc.is_format('typst') then + return doc + end + + -- Check if the hotfix is enabled via metadata set by the pre-quarto filter. + local hotfix_meta = doc.meta['_code-window-hotfix'] + if hotfix_meta then + local enabled = hotfix_meta['typst-title'] + if enabled and pandoc.utils.stringify(enabled) == 'false' then + return doc + end + end + + -- Guard: skip if already injected. + for _, blk in ipairs(doc.blocks) do + if blk.t == 'RawBlock' and blk.format == 'typst' + and blk.text:find('code-window: hot-fix for Quarto rendering theorem titles', 1, true) then + return doc + end + end + + local func_names = detect_theorem_types() + if #func_names == 0 then + return doc + end + + -- Insert at the start of doc.blocks. RawBlocks placed here appear + -- after the Typst template preamble (where make-frame defines the + -- theorem functions), so the wrappers can reference them. + -- This relies on Quarto emitting the template preamble before + -- doc.blocks; if that ordering changes the wrappers will break. + table.insert(doc.blocks, 1, pandoc.RawBlock('typst', build_wrappers(func_names))) + + return doc + end, + }, +} diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/language.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/language.lua new file mode 100644 index 0000000..59bdeef --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_modules/language.lua @@ -0,0 +1,67 @@ +--- @module "language" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @brief Normalise code blocks with no or unknown language class. + +local cell_output = require( + quarto.utils.resolve_path('_modules/cell-output.lua'):gsub('%.lua$', '')) + +local M = {} + +local known_language_cache = {} + +--- Check if a language is recognised by Pandoc's syntax highlighter. +--- Renders a test CodeBlock to HTML and checks for the sourceCode class. +--- @param lang string Language identifier +--- @return boolean +local function is_known_language(lang) + if not lang or lang == '' then + return false + end + + if known_language_cache[lang] ~= nil then + return known_language_cache[lang] + end + + local test_block = pandoc.CodeBlock('x', pandoc.Attr('', { lang })) + local html = pandoc.write(pandoc.Pandoc({ test_block }), 'html') + local is_known = html:find('sourceCode') ~= nil + known_language_cache[lang] = is_known + return is_known +end + +--- Normalise code blocks with no or unknown language class to "default". +--- Both are framed like a highlighted code cell rather than left as a bare +--- block: a no-language block is labelled "default", and an unknown language +--- keeps its original token as the label. The label is carried on the +--- `code-window-auto-label` attribute (not `filename`, which is reserved for +--- author-set filenames) and consumed by the auto-filename windowing path. +--- The output of an executed cell is left as Quarto wrote it. +--- @param block pandoc.CodeBlock +--- @return pandoc.CodeBlock|nil +function M.CodeBlock(block) + if cell_output.is_marked(block) then + return nil + end + + if not block.classes or #block.classes == 0 then + block.classes:insert('default') + if not block.attributes['filename'] or block.attributes['filename'] == '' then + block.attributes['code-window-auto-label'] = 'default' + end + return block + end + + local lang = block.classes[1] + if not is_known_language(lang) then + block.classes[1] = 'default' + if not block.attributes['filename'] or block.attributes['filename'] == '' then + block.attributes['code-window-auto-label'] = lang + end + end + + return block +end + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_schema.yml b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_schema.yml new file mode 100644 index 0000000..b7938d9 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_schema.yml @@ -0,0 +1,96 @@ +$schema: https://m.canouil.dev/quarto-wizard/assets/schema/v2/extension-schema.json + +options: + enabled: + type: boolean + default: true + description: "Enable or disable the code-window filter." + auto-filename: + type: boolean + default: true + description: "Automatically generate filename labels from the code block language." + style: + type: string + enum: ["default", "macos", "windows"] + default: "macos" + description: "Window decoration style: 'macos' (traffic lights), 'windows' (title bar buttons), or 'default' (plain filename). Can be overridden per block with the 'code-window-style' attribute." + cell-output: + type: boolean + default: false + description: "Frame the output of an executed cell like a code block. Off by default, so output keeps the shape Quarto gives it." + wrapper: + type: string + default: "code-window" + description: "Typst wrapper function name for code-window rendering." + collapse: + type: [boolean, string] + enum: [true, false, "open", "closed"] + default: false + description: "Wrap every code-window block in a collapsible
element (HTML only). Use 'open' to render expanded by default, 'closed' or true to render collapsed by default, false to disable. Per-block overrides use the 'code-window-collapse' attribute." + lines-label: + type: boolean + default: true + description: "Render a small chip next to the filename showing the highlighted-line spec for blocks that set 'code-line-numbers' or 'code-window-lines'." + hotfix: + type: object + description: "Temporary hot-fixes for Typst output. These will be removed when Quarto natively supports the corresponding features (see quarto-dev/quarto-cli#14170)." + additionalProperties: false + properties: + code-annotations: + type: [boolean, object] + default: true + description: "Enable the code-annotations hot-fix for Typst output. Use a boolean or a map with 'enabled' and 'quarto-version' keys." + additionalProperties: false + properties: + enabled: + type: boolean + description: "Explicitly enable or disable this hot-fix." + quarto-version: + type: string + description: "Quarto version at or above which this hot-fix is automatically disabled." + skylighting: + type: [boolean, object] + default: true + description: "Enable the Skylighting hot-fix for Typst output (overrides block styling and adds inline code background). Use a boolean or a map with 'enabled' and 'quarto-version' keys." + additionalProperties: false + properties: + enabled: + type: boolean + description: "Explicitly enable or disable this hot-fix." + quarto-version: + type: string + description: "Quarto version at or above which this hot-fix is automatically disabled." + typst-title: + type: [boolean, object] + default: true + description: "Enable the Typst title hot-fix (evaluates theorem title strings as markup). Use a boolean or a map with 'enabled' and 'quarto-version' keys." + additionalProperties: false + properties: + enabled: + type: boolean + description: "Explicitly enable or disable this hot-fix." + quarto-version: + type: string + description: "Quarto version at or above which this hot-fix is automatically disabled." + +attributes: + CodeBlock: + code-window-enabled: + type: boolean + default: true + description: "Whether to apply window chrome to this specific code block. Set to false to disable. Annotations still render when disabled." + code-window-no-auto-filename: + type: boolean + default: false + description: "Suppress the auto-generated filename for this specific code block without changing the global setting." + code-window-style: + type: string + enum: ["default", "macos", "windows"] + description: "Override the global window decoration style for this specific code block." + code-window-collapse: + type: string + enum: ["true", "false", "open", "closed"] + description: "Wrap this code block in a collapsible
element (HTML only). Use 'open' to render expanded, 'closed' or true to render collapsed, false to disable." + code-window-lines: + type: string + description: "Highlighted-lines spec rendered as a chip alongside the filename (e.g. '1,3-5'). When omitted, Quarto's 'code-line-numbers' attribute is used." diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/LICENSE b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/LICENSE new file mode 100644 index 0000000..4b53bb8 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mickaël Canouil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/html.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/html.lua new file mode 100644 index 0000000..ecb4870 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/html.lua @@ -0,0 +1,102 @@ +--- MC HTML - HTML generation and dependency management for Quarto Lua filters and shortcodes +--- @module "html" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +--- Load a sibling module from the same directory as this file. +--- @param filename string The sibling module filename (e.g., 'string.lua') +--- @return table The loaded module +local function load_sibling(filename) + local source = debug.getinfo(1, 'S').source:sub(2) + local dir = source:match('(.*[/\\])') or '' + return require((dir .. filename):gsub('%.lua$', '')) +end + +--- Load string module for escape_html and escape_attribute +local str = load_sibling('string.lua') + +-- ============================================================================ +-- HTML RAW GENERATION UTILITIES +-- ============================================================================ + +--- Generates a raw HTML header element. +--- @param level integer The header level (e.g., 2 for

) +--- @param text string|nil The header text +--- @param id string The id attribute for the header +--- @param classes table List of classes for the header +--- @param attributes table|nil Additional HTML attributes +--- @return string Raw HTML string for the header +function M.raw_header(level, text, id, classes, attributes) + local attr_str = '' + if id and id ~= '' then + attr_str = attr_str .. ' id="' .. str.escape_attribute(id) .. '"' + end + if classes and #classes > 0 then + local escaped_classes = {} + for i, cls in ipairs(classes) do + escaped_classes[i] = str.escape_attribute(cls) + end + attr_str = attr_str .. ' class="' .. table.concat(escaped_classes, ' ') .. '"' + end + if attributes then + for k, v in pairs(attributes) do + attr_str = attr_str .. ' ' .. str.escape_attribute(k) .. '="' .. str.escape_attribute(v) .. '"' + end + end + return string.format('%s', level, attr_str, str.escape_html(text or ''), level) +end + +-- ============================================================================ +-- HTML DEPENDENCY UTILITIES +-- ============================================================================ + +--- Managed HTML dependency tracker +--- Tracks which dependencies have been added to prevent duplication +--- @type table +local dependency_tracker = {} + +--- Ensure HTML dependency is added only once per document. +--- Prevents duplicate dependency injection by tracking dependencies by name. +--- Returns true if dependency was added, false if already present. +--- +--- @param config table Dependency configuration with fields: name (required), version, scripts, stylesheets, head +--- @return boolean True if dependency was added, false if already added +--- @usage M.ensure_html_dependency({name = 'my-lib', version = '1.0.0', scripts = {'lib.js'}}) +function M.ensure_html_dependency(config) + if not config or not config.name then + error("HTML dependency configuration must include a 'name' field") + end + + --- @type string Unique key for this dependency + local dep_key = config.name + + -- Check if already added + if dependency_tracker[dep_key] then + return false + end + + -- Add the dependency + quarto.doc.add_html_dependency(config) + + -- Mark as added + dependency_tracker[dep_key] = true + return true +end + +--- Reset dependency tracker. +--- Useful for testing or when processing multiple independent documents. +--- In normal usage, this should not be called as dependencies persist per document. +--- +--- @return nil +function M.reset_dependencies() + dependency_tracker = {} +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/logging.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/logging.lua new file mode 100644 index 0000000..81fba55 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/logging.lua @@ -0,0 +1,61 @@ +--- MC Logging - Formatted log output for Quarto Lua filters and shortcodes +--- @module "logging" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +-- ============================================================================ +-- LOGGING UTILITIES +-- ============================================================================ + +--- Format and log an error message with extension prefix. +--- Provides standardised error messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "external", "lua-env") +--- @param message string The error message to display +--- @usage M.log_error("external", "Could not open file 'example.md'.") +function M.log_error(extension_name, message) + quarto.log.error('[' .. extension_name .. '] ' .. message) +end + +--- Format and log a warning message with extension prefix. +--- Provides standardised warning messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "external", "lua-env") +--- @param message string The warning message to display +--- @usage M.log_warning("lua-env", "No variable name provided.") +function M.log_warning(extension_name, message) + quarto.log.warning('[' .. extension_name .. '] ' .. message) +end + +--- Format and log an output message with extension prefix. +--- Provides standardised informational messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "lua-env") +--- @param message string The informational message to display +--- @usage M.log_output("lua-env", "Exported metadata to: output.json") +function M.log_output(extension_name, message) + quarto.log.output('[' .. extension_name .. '] ' .. message) +end + +--- Format and log a debug message with extension prefix. +--- Provides standardised debug messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "lua-env") +--- @param message string The debug message to display +--- @usage M.log_debug("lua-env", "Variable 'x' has value: 42") +function M.log_debug(extension_name, message) + quarto.log.debug('[' .. extension_name .. '] ' .. message) +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/metadata.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/metadata.lua new file mode 100644 index 0000000..7bc55c1 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/metadata.lua @@ -0,0 +1,223 @@ +--- MC Metadata - Extension configuration and metadata access for Quarto Lua filters and shortcodes +--- @module "metadata" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +--- Load a sibling module from the same directory as this file. +--- @param filename string The sibling module filename (e.g., 'string.lua') +--- @return table The loaded module +local function load_sibling(filename) + local source = debug.getinfo(1, 'S').source:sub(2) + local dir = source:match('(.*[/\\])') or '' + return require((dir .. filename):gsub('%.lua$', '')) +end + +--- Load required modules +local str = load_sibling('string.lua') +local log = load_sibling('logging.lua') + +-- ============================================================================ +-- METADATA UTILITIES +-- ============================================================================ + +--- Get configuration from extensions.name namespace. +--- @param meta table Document metadata +--- @param extension_name string The extension name (e.g., "github", "iconify") +--- @return any The value/table or nil +--- @usage local value = M.get_extension_config(meta, 'section-outline') +function M.get_extension_config(meta, extension_name) + local config_ext = meta.extensions and meta.extensions[extension_name] + if not config_ext then return nil end + return config_ext +end + +--- Extract metadata value from document meta using nested structure. +--- Supports the extensions.{extension-name}.{key} pattern. +--- @param meta table The document metadata table +--- @param extension_name string The extension name (e.g., "github", "iconify") +--- @param key string The metadata key to retrieve +--- @return string|nil The metadata value as a string, or nil if not found +--- @usage local repo = M.get_metadata_value(meta, "github", "repository-name") +--- @note A boolean `false` is a value, not an absence, so the test is against +--- `nil` rather than truthiness. Stringifying it yields "false", which is what +--- a caller comparing against the string form already expects. +function M.get_metadata_value(meta, extension_name, key) + if meta['extensions'] and meta['extensions'][extension_name] + and meta['extensions'][extension_name][key] ~= nil then + return str.stringify(meta['extensions'][extension_name][key]) + end + return nil +end + +--- Check for deprecated top-level configuration and emit warning +--- @param meta table The document metadata table +--- @param extension_name string The extension name +--- @param key string|nil The configuration key being accessed (nil to check entire extension config) +--- @param deprecation_warning_shown boolean Flag to track if warning has been shown +--- @return any|nil The value from deprecated config, or nil if not found +--- @return boolean Updated deprecation warning flag +function M.check_deprecated_config(meta, extension_name, key, deprecation_warning_shown) + -- Handle array-based configuration (when key is nil) + if key == nil then + if not str.is_empty(meta[extension_name]) then + if not deprecation_warning_shown then + log.log_warning( + extension_name, + 'Top-level "' .. extension_name .. '" configuration is deprecated. ' .. + 'Please use:\n' .. + 'extensions:\n' .. + ' ' .. extension_name .. ':\n' .. + ' - (configuration array)' + ) + deprecation_warning_shown = true + end + return meta[extension_name], deprecation_warning_shown + end + return nil, deprecation_warning_shown + end + + -- Handle key-value configuration (original behaviour) + if not str.is_empty(meta[extension_name]) and not str.is_empty(meta[extension_name][key]) then + if not deprecation_warning_shown then + log.log_warning( + extension_name, + 'Top-level "' .. extension_name .. '" configuration is deprecated. ' .. + 'Please use:\n' .. + 'extensions:\n' .. + ' ' .. extension_name .. ':\n' .. + ' ' .. key .. ': value' + ) + deprecation_warning_shown = true + end + return str.stringify(meta[extension_name][key]), deprecation_warning_shown + end + return nil, deprecation_warning_shown +end + +-- ============================================================================ +-- ENHANCED METADATA/CONFIGURATION UTILITIES +-- ============================================================================ + +--- Get option value with fallback hierarchy: args -> extensions.{extension}.{key} -> defaults. +--- Provides a standardised way to read configuration values with multiple fallback levels. +--- Priority: 1. Named arguments (kwargs), 2. Document metadata, 3. Default values. +--- +--- @param spec table Configuration spec with fields: extension (string), key (string), args (table|nil), meta (table|nil), default (any|nil) +--- @return any The resolved option value (type depends on what's stored in config) +--- @usage local duration = M.get_option_with_fallbacks({extension = 'animate', key = 'duration', args = kwargs, meta = meta, default = '3s'}) +function M.get_option_with_fallbacks(spec) + -- Validate required fields + if not spec.extension or not spec.key then + error("Configuration spec must include 'extension' and 'key' fields") + end + + --- @type string The extension name + local extension = spec.extension + --- @type string The configuration key + local key = spec.key + --- @type table|nil Named arguments table + local args = spec.args + --- @type table|nil Document metadata + local meta = spec.meta + --- @type any Default value if not found elsewhere + local default = spec.default + + -- Priority 1: Check named arguments (kwargs) + if args and args[key] then + local arg_value = str.stringify(args[key]) + if not str.is_empty(arg_value) then + return arg_value + end + end + + -- Priority 2: Check metadata extensions.{extension}.{key} + if meta then + local meta_value = M.get_metadata_value(meta, extension, key) + if not str.is_empty(meta_value) then + return meta_value + end + end + + -- Priority 3: Return default value + return default +end + +--- Get multiple option values at once with fallback hierarchy. +--- Batch version of get_option_with_fallbacks for retrieving multiple configuration values. +--- Returns a table mapping each key to its resolved value. +--- +--- @param spec table Configuration spec with fields: extension (string), keys (table), args (table|nil), meta (table|nil), defaults (table|nil) +--- @return table Table mapping each key to its resolved value +--- @usage local opts = M.get_options({extension = 'animate', keys = {'duration', 'delay'}, args = kwargs, meta = meta, defaults = {duration = '3s', delay = '2s'}}) +function M.get_options(spec) + -- Validate required fields + if not spec.extension or not spec.keys then + error("Configuration spec must include 'extension' and 'keys' fields") + end + + --- @type table Result table + local result = {} + + --- @type table Default values table + local defaults = spec.defaults or {} + + -- Get each key using the single-option fallback logic + for _, key in ipairs(spec.keys) do + result[key] = M.get_option_with_fallbacks({ + extension = spec.extension, + key = key, + args = spec.args, + meta = spec.meta, + default = defaults[key] + }) + end + + return result +end + +-- ============================================================================ +-- PROJECT METADATA UTILITIES +-- ============================================================================ + +--- Get repo-url from Quarto project metadata via QUARTO_EXECUTE_INFO. +--- Reads the JSON file pointed to by the QUARTO_EXECUTE_INFO environment variable +--- and extracts repo-url from website or book metadata. +--- @return string|nil The repo-url value, or nil if not available +function M.get_project_repo_url() + local path = os.getenv("QUARTO_EXECUTE_INFO") + if not path then return nil end + + local file = io.open(path, "r") + if not file then return nil end + + local content = file:read("*a") + file:close() + + if str.is_empty(content) then return nil end + + local ok, info = pcall(quarto.json.decode, content) + if not ok or not info then return nil end + + local format_meta = info["format"] and info["format"]["metadata"] + if not format_meta then return nil end + + -- Try website.repo-url first, then book.repo-url + local repo_url = nil + if format_meta["website"] and format_meta["website"]["repo-url"] then + repo_url = format_meta["website"]["repo-url"] + elseif format_meta["book"] and format_meta["book"]["repo-url"] then + repo_url = format_meta["book"]["repo-url"] + end + + if str.is_empty(repo_url) then return nil end + return repo_url +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/pandoc-helpers.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/pandoc-helpers.lua new file mode 100644 index 0000000..f90919b --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/pandoc-helpers.lua @@ -0,0 +1,151 @@ +--- MC Pandoc Helpers - Pandoc element construction and format detection for Quarto Lua filters and shortcodes +--- @module "pandoc_helpers" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +--- Load a sibling module from the same directory as this file. +--- @param filename string The sibling module filename (e.g., 'string.lua') +--- @return table The loaded module +local function load_sibling(filename) + local source = debug.getinfo(1, 'S').source:sub(2) + local dir = source:match('(.*[/\\])') or '' + return require((dir .. filename):gsub('%.lua$', '')) +end + +--- Load string module for is_empty +local str = load_sibling('string.lua') + +-- ============================================================================ +-- PANDOC/QUARTO FORMAT UTILITIES +-- ============================================================================ + +--- Create a Pandoc Link element +--- @param text string|nil The link text +--- @param uri string|nil The URI to link to +--- @return pandoc.Link|nil A Pandoc Link element or nil if text or uri is empty +function M.create_link(text, uri) + if not str.is_empty(uri) and not str.is_empty(text) then + return pandoc.Link({ pandoc.Str(text --[[@as string]]) }, uri --[[@as string]]) + end + return nil +end + +--- Helper to build Pandoc attributes +--- @param id string|nil Element ID +--- @param classes table|nil List of CSS classes +--- @param attributes table|nil Key-value attributes +--- @return pandoc.Attr Pandoc Attr object +function M.attr(id, classes, attributes) + return pandoc.Attr(id or '', classes or {}, attributes or {}) +end + +--- Check if a class list contains a specific class name +--- @param classes table|nil List of CSS classes +--- @param name string The class name to search for +--- @return boolean True if the class is found, false otherwise +function M.has_class(classes, name) + if not classes then return false end + for _, cls in ipairs(classes) do + if cls == name then return true end + end + return false +end + +--- Add a class to the class list if it doesn't already exist +--- @param classes table List of CSS classes +--- @param name string The class name to add +function M.add_class(classes, name) + if not M.has_class(classes, name) then + table.insert(classes, name) + end +end + +--- Retrieve the current Quarto output format. +--- @return string The output format ("pptx", "html", "latex", "typst", "docx", or "unknown") +--- @return string The language of the output format +function M.get_quarto_format() + if quarto.doc.is_format('html:js') then + return 'html', 'html' + elseif quarto.doc.is_format('latex') then + return 'latex', 'latex' + elseif quarto.doc.is_format('typst') then + return 'typst', 'typst' + elseif quarto.doc.is_format('docx') then + return 'docx', 'openxml' + elseif quarto.doc.is_format('pptx') then + return 'pptx', 'openxml' + else + return 'unknown', 'unknown' + end +end + +-- ============================================================================ +-- OBJECT/TABLE UTILITIES +-- ============================================================================ + +--- Check if an object (including tables and lists) is empty or nil +--- @param obj any The object to check +--- @return boolean true if the object is nil, empty string, or empty table/list +function M.is_object_empty(obj) + local function length(x) + local count = 0 + if x ~= nil then + for _ in pairs(x) do + count = count + 1 + end + end + return count + end + if pandoc.utils.type(obj) == 'table' or pandoc.utils.type(obj) == 'List' then + return obj == nil or obj == '' or length(obj) == 0 + else + return obj == nil or obj == '' + end +end + +--- Check if an object is a simple type (string, number, or boolean) +--- @param obj any The object to check +--- @return boolean true if the object is a string, number, or boolean +function M.is_type_simple(obj) + return pandoc.utils.type(obj) == 'string' or pandoc.utils.type(obj) == 'number' or pandoc.utils.type(obj) == 'boolean' +end + +--- Check if an object is a function or userdata +--- @param obj any The object to check +--- @return boolean true if the object is a function or userdata +function M.is_function_userdata(obj) + return pandoc.utils.type(obj) == 'function' or pandoc.utils.type(obj) == 'userdata' +end + +--- Get nested value from object using field path +--- @param fields table Array of field names to traverse +--- @param obj table The object to extract value from +--- @return any The value at the nested path +--- @usage local val = M.get_value({"a", "b", "c"}, obj) +function M.get_value(fields, obj) + local value = obj + for _, field in ipairs(fields) do + value = value[field] + end + return value +end + +--- Convert Pandoc AttributeList to plain table for easier processing. +--- @param element table Element with attributes field (Div, Span, Table, Image) +--- @return table Plain table with attribute key-value pairs +function M.attributes_to_table(element) + local attrs = {} + for k, v in pairs(element.attributes) do + attrs[k] = v + end + return attrs +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/schema-check.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/schema-check.lua new file mode 100644 index 0000000..9fe2c17 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/schema-check.lua @@ -0,0 +1,581 @@ +--- MC Schema Check - Runtime schema checks for Quarto extensions +--- @module "schema-check" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- +--- Holds the wiring that every extension would otherwise copy: read the schema +--- once, check the document configuration against it, check one shortcode call +--- against it, and report what it finds through `logging`. +--- +--- The schema is `_schema.yml` beside the entry point that runs. An extension +--- whose entry points sit in a subdirectory names its own path instead, with +--- `'../_schema.yml'` for one level down. +--- +--- The validator arrives as an argument rather than through `require`. A +--- vendored copy of this module then knows nothing about where the validator +--- was vendored, so the two sources stay independent. The validator must +--- provide `load_schema`, `validate`, `validate_shortcode`, +--- `extract_meta_options`, `validate_attributes` and `validate_format`. +--- +--- The last two are newer than the rest of that list, so a validator vendored +--- before either exists is reported rather than called. The others are required +--- outright and a validator without one of them raises. +--- +--- `validate_format` must also be the one that reads the top level of the +--- metadata, which is Quarto Wizard 3.6.0 or newer. An older one looks for the +--- format name as a metadata key, which a document never has, so it would +--- report nothing whatever the document wrote. +--- +--- Nothing here stops a render. A schema is configuration, and a fault in the +--- configuration must not remove the document. + +local M = {} + +--- Load a sibling module from the same directory as this file. +--- @param filename string The sibling module filename (e.g., 'string.lua') +--- @return table The loaded module +local function load_sibling(filename) + local source = debug.getinfo(1, 'S').source:sub(2) + local dir = source:match('(.*[/\\])') or '' + return require((dir .. filename):gsub('%.lua$', '')) +end + +--- Load required modules +local log = load_sibling('logging.lua') +local str = load_sibling('string.lua') + +-- ============================================================================ +-- SEVERITY +-- ============================================================================ + +--- The level each kind of finding is reported at. This is the only place a +--- severity is decided, so a later change to what an extension must correct is +--- a change to this table and nothing else. +--- +--- The current policy reports and never stops a render. A finding is a warning, +--- with two exceptions. An unreadable schema is an error, because no check runs +--- after it. A missing required argument is an error, because the shortcode +--- renders nothing without it, which is the one finding that changes the +--- document. +--- +--- A rejected document option keeps the error level it has today: it names a +--- value the extension cannot use, and the author has to correct it. A rejected +--- format option is the same finding about the same document, one section over, +--- so it takes the same level. +--- +--- A finding about an element's attribute is a warning instead. The attribute +--- stays on the element whatever the schema says, so the rendered output does +--- not change because of it, which is the reason a shortcode attribute is a +--- warning too. +--- +--- `misuse` is the one kind that is not about the document. It reports a fault +--- in the extension calling this module, and it is an error because the caller +--- is handed no value and would otherwise carry on with nil. +--- @type table +local SEVERITY = { + schema = 'error', + option_error = 'error', + option_warning = 'warning', + call_error = 'warning', + call_warning = 'warning', + missing_argument = 'error', + misuse = 'error', + attribute_error = 'warning', + attribute_warning = 'warning', + format_error = 'error', + format_warning = 'warning', +} + +--- The reporting function for each level. +--- @type table +local REPORTERS = { + error = log.log_error, + warning = log.log_warning, +} + +-- A level with no reporter is the mistake a change to `SEVERITY` makes, and it +-- is caught here, at load, rather than at the one render that reaches that kind +-- of finding. +for kind, level in pairs(SEVERITY) do + assert(REPORTERS[level] ~= nil, + string.format('schema-check: "%s" maps to the unknown level "%s"', kind, tostring(level))) +end + +-- ============================================================================ +-- PRIVATE HELPERS +-- ============================================================================ + +--- Read an attribute value with a surrounding quote pair removed. +--- Quarto's body parser strips those quotes before the value reaches the +--- shortcode, but the parser it uses for a text or attribute string hands the +--- raw token over instead, so `aria-hidden='true'` arrives as the five +--- character string `'true'`. Stripping here is what makes a quoted and an +--- unquoted value check as the same thing. +--- @param kwargs table Key-value options for the call +--- @param key string The attribute name to read +--- @return string +local function attr_value(kwargs, key) + --- @type string + local value = str.stringify(kwargs[key]) + --- @type string + local quote = value:sub(1, 1) + if #value > 1 and (quote == '"' or quote == "'") and value:sub(-1) == quote then + return value:sub(2, -2) + end + return value +end + +--- Copy a value, and every table inside it, all the way down. +--- The copy exists so that a caller writing into what it received cannot +--- change what every later reader of the same checker sees. +--- +--- The depth comes from the schema format, not from the schemas written so +--- far. The vocabulary allows `type: array` and `type: object`, and the +--- validator compiles a declared `default` by coercing it against that type +--- and returning it untouched once it already matches. Any option declaring a +--- collection type with a literal default therefore resolves to a Lua table, +--- at whatever depth the schema nests it, so a copy one level deep would hand +--- two callers the same inner table and leave the fault one level down. +--- +--- A table already copied on this walk is reused rather than copied again, +--- which keeps shared structure shared and makes a cycle terminate. A schema +--- file cannot express a cycle, because the validator's parser refuses anchors +--- and aliases, but the validator is injected and what it returns is not this +--- module's to assume. +--- @param value any The value to copy +--- @param seen table|nil Tables already copied on this walk +--- @return any +local function deep_copy(value, seen) + if type(value) ~= 'table' then + return value + end + seen = seen or {} + if seen[value] ~= nil then + return seen[value] + end + --- @type table + local copy = {} + seen[value] = copy + for key, item in pairs(value) do + copy[key] = deep_copy(item, seen) + end + return copy +end + +--- Flatten a call's named options to plain strings for the validator. +--- @param kwargs table Key-value options for the call +--- @return table +local function plain_kwargs(kwargs) + --- @type table + local plain = {} + for key in pairs(kwargs) do + plain[tostring(key)] = attr_value(kwargs, key) + end + return plain +end + +-- ============================================================================ +-- CHECKER +-- ============================================================================ + +--- @class Checker +--- @field validator table The validator the caller injected +--- @field extension string The extension name every message carries +--- @field schema table|nil The parsed schema, nil when it could not be read +--- @field defaults table The defaults the schema declares +--- @field resolved table|nil The three tables the configuration resolves to +--- @field options_checked boolean Whether the configuration was already checked +--- @field meta table|nil The metadata `options` was given, which `format` reads +--- @field formats table What each format checked so far resolved to +--- @field unavailable table Validator functions already reported missing +local Checker = {} +Checker.__index = Checker + +--- Report one finding at the level its kind is mapped to. +--- A kind with no entry in `SEVERITY` is a fault in this module, and it says so +--- rather than reporting the finding at a level nobody chose. It is reported +--- rather than raised, because a fault here must not remove a document. +--- @param kind string A key of `SEVERITY` +--- @param message string The message to report +--- @return nil +function Checker:_report(kind, message) + --- @type string|nil + local level = SEVERITY[kind] + if level == nil then + log.log_error(self.extension, string.format( + 'schema-check has no severity for "%s": %s', tostring(kind), message)) + return + end + REPORTERS[level](self.extension, message) +end + +--- Whether the validator provides one function, reporting it once if not. +--- +--- `validate_attributes` and `validate_format` are newer than the rest of the +--- contract, so a validator vendored before either exists satisfies everything +--- else and still lacks them. Calling one raises, and a raise removes the +--- document, which is the one thing this module promises not to do. +--- +--- It is reported once for the render rather than once for each caller. A +--- filter reaches these for every element and every format it handles, and the +--- condition is a fact about the vendored pair, so the second message says +--- nothing the first did not. +--- @param name string The function the caller is about to use +--- @return boolean available +function Checker:_provides(name) + if type(self.validator[name]) == 'function' then + return true + end + if not self.unavailable[name] then + self.unavailable[name] = true + self:_report('misuse', string.format( + 'schema-check: the validator provides no `%s`, so nothing was checked with it', name)) + end + return false +end + +--- Check the document configuration and return what it resolves to. The check +--- runs once, so an extension can ask on every shortcode without repeating the +--- messages. +--- +--- The defaults come first, because that is what most extensions want, and +--- because the schema is the one place they are written. They come from a +--- second pass over an empty table, which yields the declared defaults alone. +--- +--- The second return holds three tables, because they answer different +--- questions: +--- provided what the document actually set, which is the only way to tell +--- a deliberate `false` or `0` from an absent key, +--- merged the same values with coercion and defaults applied, +--- defaults the schema defaults on their own. +--- It is nil when there is nothing to resolve against, so an extension can +--- tell an unreadable schema from a document that set nothing. +--- +--- The defaults are a fresh copy on every call, so a caller may treat them as +--- its own. The tables inside the second return are the checker's, and every +--- later reader of the same checker sees them, so they must not be written to. +--- +--- Only the first call reads `meta`. A later call returns what the first one +--- resolved, whatever it is handed. A caller that needs a second document +--- checked builds a second checker. +--- +--- The argument cannot be read on a later call without losing the single check +--- this function promises. Quarto hands a shortcode a new metadata table on +--- every call, and the content is the same each time. A checker that read it +--- again repeats every finding once per shortcode. +--- @param meta table Document metadata, read on the first call only +--- @return table defaults A copy of the defaults, empty when there is no schema +--- @return table|nil resolved {provided, merged, defaults}, nil when there is no schema +function Checker:options(meta) + if self.options_checked then + return deep_copy(self.defaults), self.resolved + end + self.options_checked = true + -- Kept for `format`, which reads the same document. Quarto merges the options + -- of the selected format into the top level of this table, so the format + -- check has nowhere else to read them from. + self.meta = meta + + --- @type table|nil + local loaded = self.schema + -- The validator is injected from an independent source, so the shape of what + -- it returns is not this module's to assume. `call` makes the same allowance + -- for `shortcodes` one field over. + if loaded == nil or next(loaded.options or {}) == nil then + return deep_copy(self.defaults), self.resolved + end + + --- @type table + local provided = self.validator.extract_meta_options(meta, self.extension) + local valid, errors, warnings, merged = self.validator.validate(provided, loaded.options) + + for _, message in ipairs(warnings) do + self:_report('option_warning', message) + end + if not valid then + for _, message in ipairs(errors) do + self:_report('option_error', message) + end + end + + local _, _, _, defaults = self.validator.validate({}, loaded.options, { unknown = 'ignore' }) + self.defaults = defaults or {} + self.resolved = { provided = provided, merged = merged, defaults = self.defaults } + + return deep_copy(self.defaults), self.resolved +end + +--- Read what one option resolves to, after `options` has run. +--- +--- This is the value the schema decides, not the text the document holds. An +--- extension that reads the metadata itself has to decide what counts as true, +--- and each one that did decided something different, so `enabled: no` turned +--- one filter off and left another on. Here the schema is the only answer: the +--- validator coerces the written value toward the declared type, and a key the +--- document never set resolves to its declared default. +--- +--- It answers nil when there is no schema, which the checker has already +--- reported once. A key the schema does not declare answers nil as well, +--- because a schema that omits an option is the author's statement that the +--- extension does not have it. +--- @param key string The option name, as the schema declares it +--- @return any value The resolved value, nil when there is nothing to resolve +function Checker:option(key) + if type(key) ~= 'string' then + self:_report('misuse', string.format( + 'schema-check: the key given to `option` must be a string, got %s', type(key))) + return nil + end + if not self.options_checked then + self:_report('misuse', string.format( + 'schema-check: `option("%s")` was called before `options`', key)) + return nil + end + if self.resolved == nil then + return nil + end + return self.resolved.merged[key] +end + +--- Check one element's attributes against the `attributes` section, and return +--- what they resolve to. +--- +--- The section declares a map of groups. A group is named after the element it +--- describes, such as `Header` or `CodeBlock`, or after the class the extension +--- gives it, such as `modal`. `_any` is the group every element takes, and it +--- is additive rather than an alternative: quarto-revealjs-tabset declares +--- `panel-tabset` for a tabset's own attributes and `_any` for one that any +--- slide can carry, and an element can meet both. +--- +--- So both apply, and the named group runs last, over what `_any` resolved. The +--- validator hands an undeclared attribute straight back, so chaining the two +--- passes is the whole of the merge and there is no rule here about which group +--- wins. The one that declares the attribute decides it. +--- +--- This reports only. The attribute stays on the element whatever the schema +--- says, so nothing about the rendered output changes, which is why a finding +--- here is a warning as it is for a shortcode attribute. +--- The attributes may arrive as Pandoc's `AttributeList` rather than as a +--- table, which is what an element filter holds, so nothing here assumes a +--- plain table. The validator reads them with `pairs`, which both answer. +--- @param attributes table The element's attributes +--- @param group string|nil The element's own group, nil when it has none +--- @return table|nil resolved The attributes with the schema applied +function Checker:attributes(attributes, group) + if group ~= nil and type(group) ~= 'string' then + self:_report('misuse', string.format( + 'schema-check: the group given to `attributes` must be a string, got %s', type(group))) + return nil + end + + attributes = attributes or {} + + --- @type table|nil + local loaded = self.schema + -- The validator is injected from an independent source, so the shape of what + -- it returns is not this module's to assume, as `options` and `call` allow + -- for `options` and `shortcodes`. + if loaded == nil or next(loaded.attributes or {}) == nil then + return attributes + end + + if not self:_provides('validate_attributes') then + return attributes + end + + --- @type table + local resolved = attributes + -- `_any` is the group every element takes, so a caller that names it has + -- already asked for the only pass there is. Running the list would report + -- each of its findings twice for every element handed over. + local groups = (group == nil or group == '_any') and { '_any' } or { '_any', group } + for _, name in ipairs(groups) do + local _, errors, warnings, merged = + self.validator.validate_attributes(resolved, name, loaded) + for _, message in ipairs(errors) do + self:_report('attribute_error', message) + end + for _, message in ipairs(warnings) do + self:_report('attribute_warning', message) + end + resolved = merged + end + return resolved +end + +--- Check one output format's options against the `formats` section, and return +--- what they resolve to. +--- +--- Quarto merges the options of the selected format into the top level of the +--- document metadata, and the format name is never a key there. So this reads +--- the metadata `options` was given, and it must be called after `options`. +--- +--- The extension names its own format, as it names an attribute group. A format +--- name such as `letter-pdf` is a name the extension contributes, and nothing +--- here can work out which of several declared formats a render selected. +--- +--- The answer is kept, so a filter that asks again in the same render gets the +--- same table and the findings are reported once. +--- @param name string The format name, as the schema declares it +--- @return table|nil resolved The format's options, nil when there is nothing to resolve +function Checker:format(name) + if type(name) ~= 'string' then + self:_report('misuse', string.format( + 'schema-check: the name given to `format` must be a string, got %s', type(name))) + return nil + end + if not self.options_checked then + self:_report('misuse', string.format( + 'schema-check: `format("%s")` was called before `options`', name)) + return nil + end + if self.formats[name] ~= nil then + return self.formats[name] + end + + --- @type table|nil + local loaded = self.schema + if loaded == nil or next(loaded.formats or {}) == nil then + return nil + end + if not self:_provides('validate_format') then + return nil + end + + local _, errors, warnings, merged = + self.validator.validate_format(self.meta, name, loaded) + for _, message in ipairs(errors) do + self:_report('format_error', message) + end + for _, message in ipairs(warnings) do + self:_report('format_warning', message) + end + + self.formats[name] = merged + return merged +end + +--- Check one shortcode call against its entry in the schema. +--- This reports only. Nothing about the rendered output changes, so an +--- unrecognised attribute is surfaced rather than dropped. +--- @param name string Shortcode name +--- @param args table Positional arguments +--- @param kwargs table Key-value options for the call +--- @return nil +function Checker:call(name, args, kwargs) + --- @type table|nil + local loaded = self.schema + if loaded == nil then return end + + --- @type table|nil + local entry = loaded.shortcodes and loaded.shortcodes[name] + if entry == nil then return end + + args = args or {} + kwargs = kwargs or {} + + --- @type table + local positional = {} + for index, value in ipairs(args) do + positional[index] = str.stringify(value) + end + + local _, errors, warnings = self.validator.validate_shortcode( + name, positional, plain_kwargs(kwargs), entry) + + for _, message in ipairs(warnings) do + self:_report('call_warning', message) + end + + -- A required argument that is absent is read here rather than out of the + -- validator's findings, because the validator reports a nested argument + -- fault under one `arguments` entry, which cannot tell a missing argument + -- from a malformed one. + --- @type table + local missing = {} + for index, argument in ipairs(entry.arguments or {}) do + if argument.required == true and str.is_empty(positional[index]) then + missing[#missing + 1] = argument + end + end + + if #missing > 0 then + -- The only message about the missing argument: the schema's own `required` + -- wording says the same thing, and reporting both would state one fault + -- twice at two severities. Attribute warnings above still stand, and every + -- other finding about this call is secondary to there being no output. + for _, argument in ipairs(missing) do + --- The example comes from the schema, so that each shortcode carries its + --- own rather than this message naming one shortcode for all of them. + --- @type string + local advice = '' + local example = type(argument.examples) == 'table' and argument.examples[1] or nil + if example ~= nil then + advice = string.format(' For example: {{< %s %s >}}.', name, tostring(example)) + end + self:_report('missing_argument', string.format( + 'The "%s" shortcode needs its "%s" argument.%s', name, argument.name, advice)) + end + return + end + + for _, message in ipairs(errors) do + self:_report('call_error', message) + end +end + +-- ============================================================================ +-- PUBLIC API +-- ============================================================================ + +--- Build a checker for one extension, reading its schema once. +--- A schema that cannot be read is reported, and the checker it returns does +--- nothing: `options` gives an empty table and `call` gives no message. +--- +--- The path is resolved with `quarto.utils.resolve_path`, which answers +--- relative to the entry point that is running rather than to the extension +--- directory. An entry point in a subdirectory therefore has to say where the +--- schema is, and the checker it builds belongs at file scope, so that the +--- schema is read once for the render and not once for each call. +--- @param validator table The validator, with `load_schema`, `validate`, +--- `validate_shortcode`, `extract_meta_options`, `validate_attributes` and +--- `validate_format` +--- @param extension_name string The extension name every message carries +--- @param schema_path string|nil The schema to read, relative to the entry +--- point that is running. Defaults to `_schema.yml`. +--- @return Checker +--- @usage local checker = M.new(validator, 'iconify', '../_schema.yml') +function M.new(validator, extension_name, schema_path) + --- @type Checker + local checker = setmetatable({ + validator = validator, + extension = extension_name, + schema = nil, + defaults = {}, + resolved = nil, + options_checked = false, + meta = nil, + formats = {}, + unavailable = {}, + }, Checker) + + -- The default is chosen here rather than in the signature, so a caller that + -- passes two arguments reads the same file it read before this argument + -- existed. + local loaded, err = validator.load_schema( + quarto.utils.resolve_path(schema_path or '_schema.yml')) + if err then + checker:_report('schema', err) + else + checker.schema = loaded + end + + return checker +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/string.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/string.lua new file mode 100644 index 0000000..e551066 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-lua-modules/string.lua @@ -0,0 +1,376 @@ +--- MC String - String manipulation and escaping for Quarto Lua filters and shortcodes +--- @module "string" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +-- ============================================================================ +-- STRING UTILITIES +-- ============================================================================ + +--- Pandoc utility function for converting values to strings +--- @type function +M.stringify = pandoc.utils.stringify + +--- Check if a string is empty or nil. +--- Utility function to determine if a value is empty or nil, +--- which is useful for parameter validation throughout the module. +--- @param s string|nil|table The value to check for emptiness +--- @return boolean True if the value is nil or empty, false otherwise +--- @usage local result = M.is_empty("") -- returns true +--- @usage local result = M.is_empty(nil) -- returns true +--- @usage local result = M.is_empty("hello") -- returns false +function M.is_empty(s) + return s == nil or s == '' +end + +--- Escape special pattern characters in a string for Lua pattern matching +--- @param s string The string to escape +--- @return string The escaped string +--- @usage local escaped = M.escape_pattern("user/repo#123") +function M.escape_pattern(s) + local escaped = s:gsub('([%^%$%(%)%%%.%[%]%*%+%-%?])', '%%%1') + return escaped +end + +--- Split a string by a separator +--- @param str string The string to split +--- @param sep string The separator pattern +--- @return table Array of string fields +--- @usage local parts = M.split("a.b.c", ".") +function M.split(str, sep) + local fields = {} + local pattern = string.format('([^%s]+)', sep) + str:gsub(pattern, function(c) fields[#fields + 1] = c end) + return fields +end + +--- Trim leading and trailing whitespace from a string +--- @param str string The string to trim +--- @return string The trimmed string +--- @usage local trimmed = M.trim(" hello world ") -- returns "hello world" +function M.trim(str) + if str == nil then return '' end + return str:match('^%s*(.-)%s*$') +end + +--- Convert any value to a string, handling Pandoc objects and empty values. +--- Returns nil for empty or nil values, otherwise returns a string representation. +--- @param val any The value to convert +--- @return string|nil The string value or nil if empty +--- @usage local str = M.to_string(kwargs.value) +function M.to_string(val) + if not val then return nil end + if type(val) == 'string' then + return val ~= '' and val or nil + end + -- Handle Pandoc objects + if pandoc and pandoc.utils and pandoc.utils.stringify then + local str = pandoc.utils.stringify(val) + return str ~= '' and str or nil + end + local str = tostring(val) + return str ~= '' and str or nil +end + +--- Strip one layer of surrounding bracket or punctuation characters. +--- Handles balanced pairs: () [] {} "" '' `` «» +--- Handles trailing-only punctuation: , . ; : ! ? +--- @param text string The input text +--- @return string prefix Characters stripped from the start (may be empty) +--- @return string inner The inner text after stripping +--- @return string suffix Characters stripped from the end (may be empty) +function M.strip_surrounding(text) + if not text or #text < 2 then + return '', text or '', '' + end + + local balanced = { + ['('] = ')', ['['] = ']', ['{'] = '}', + ['"'] = '"', ["'"] = "'", ['`'] = '`', + } + -- UTF-8 guillemets + local first_two = text:sub(1, 2) + local last_two = text:sub(-2) + if first_two == '\xC2\xAB' and last_two == '\xC2\xBB' then + return first_two, text:sub(3, -3), last_two + end + + local first = text:sub(1, 1) + local last = text:sub(-1) + + if balanced[first] and last == balanced[first] then + return first, text:sub(2, -2), last + end + + local trailing = { + [','] = true, ['.'] = true, [';'] = true, + [':'] = true, ['!'] = true, ['?'] = true, + } + if trailing[last] then + return '', text:sub(1, -2), last + end + + return '', text, '' +end + +--- Peel unbalanced surrounding brackets and trailing punctuation from a token. +--- Unlike `strip_surrounding`, this does not require a balanced pair: it removes +--- any run of leading opening-bracket characters and any run of trailing +--- closing-bracket or punctuation characters. This handles bracket groups that +--- Pandoc split across whitespace, e.g. "(#2," and "#3)" from "(#2, #3)". +--- Leading set: ( [ { " ' ` and the 2-byte UTF-8 « (\xC2\xAB). +--- Trailing set: ) ] } " ' ` , . ; : ! ? and the 2-byte UTF-8 » (\xC2\xBB). +--- @param text string The input text +--- @return string prefix Characters peeled from the start (may be empty) +--- @return string inner The inner text after peeling +--- @return string suffix Characters peeled from the end (may be empty) +function M.strip_edges(text) + if not text or text == '' then + return '', text or '', '' + end + + local leading = { + ['('] = true, ['['] = true, ['{'] = true, + ['"'] = true, ["'"] = true, ['`'] = true, + } + local trailing = { + [')'] = true, [']'] = true, ['}'] = true, + ['"'] = true, ["'"] = true, ['`'] = true, + [','] = true, ['.'] = true, [';'] = true, + [':'] = true, ['!'] = true, ['?'] = true, + } + + local first = 1 + local last = #text + local prefix = '' + local suffix = '' + + while first <= last do + if text:sub(first, first + 1) == '\xC2\xAB' then + prefix = prefix .. '\xC2\xAB' + first = first + 2 + elseif leading[text:sub(first, first)] then + prefix = prefix .. text:sub(first, first) + first = first + 1 + else + break + end + end + + while last >= first do + -- The two-byte window can only match a real «/» pair: the leading loop + -- never leaves \xC2 at first - 1 (openers are ASCII or the \xAB of a peeled + -- «), so the guillemet check cannot straddle the already-peeled prefix. + if last >= 2 and text:sub(last - 1, last) == '\xC2\xBB' then + suffix = '\xC2\xBB' .. suffix + last = last - 2 + elseif trailing[text:sub(last, last)] then + suffix = text:sub(last, last) .. suffix + last = last - 1 + else + break + end + end + + return prefix, text:sub(first, last), suffix +end + +--- Find a balanced bracket pair anywhere in the text and split around it. +--- Walks the text from `start_pos` looking for an opening bracket whose matching +--- closing bracket appears later in the string. Returns the text split into +--- a prefix (up to and including the opening bracket), the inner content, and +--- a suffix (closing bracket and everything after). +--- Supports the same bracket pairs as `strip_surrounding`: +--- () [] {} "" '' `` and the 2-byte UTF-8 guillemets «». +--- @param text string The input text +--- @param start_pos integer|nil Byte position to start searching from (default 1) +--- @return string|nil prefix Text up to and including the opening bracket +--- @return string|nil content Non-empty text between the brackets +--- @return string|nil suffix Closing bracket and trailing text +--- @return integer|nil open_pos Byte position of the opening bracket +function M.find_bracketed_content(text, start_pos) + if not text or #text < 2 then + return nil, nil, nil, nil + end + start_pos = start_pos or 1 + + local balanced = { + ['('] = ')', ['['] = ']', ['{'] = '}', + ['"'] = '"', ["'"] = "'", ['`'] = '`', + } + + local i = start_pos + while i <= #text do + -- UTF-8 guillemet «…» + if text:sub(i, i + 1) == '\xC2\xAB' then + local close_pos = text:find('\xC2\xBB', i + 2, true) + if close_pos and close_pos > i + 2 then + return text:sub(1, i + 1), text:sub(i + 2, close_pos - 1), text:sub(close_pos), i + end + i = i + 2 + else + local c = text:sub(i, i) + local close_char = balanced[c] + if close_char then + local close_pos = text:find(close_char, i + 1, true) + if close_pos and close_pos > i + 1 then + return text:sub(1, i), text:sub(i + 1, close_pos - 1), text:sub(close_pos), i + end + end + i = i + 1 + end + end + + return nil, nil, nil, nil +end + +-- ============================================================================ +-- ESCAPE UTILITIES +-- ============================================================================ + +--- Escape special LaTeX characters in text. +--- @param text string The text to escape +--- @return string The escaped text safe for LaTeX +function M.escape_latex(text) + text = string.gsub(text, '\\', '\\textbackslash{}') + text = string.gsub(text, '%{', '\\{') + text = string.gsub(text, '%}', '\\}') + text = string.gsub(text, '%$', '\\$') + text = string.gsub(text, '%&', '\\&') + text = string.gsub(text, '%%', '\\%%') + text = string.gsub(text, '%#', '\\#') + text = string.gsub(text, '%^', '\\textasciicircum{}') + text = string.gsub(text, '%_', '\\_') + text = string.gsub(text, '~', '\\textasciitilde{}') + return text +end + +--- Escape special Typst characters in text. +--- @param text string The text to escape +--- @return string The escaped text safe for Typst +function M.escape_typst(text) + text = string.gsub(text, '%#', '\\#') + return text +end + +--- Escape characters for Typst string literals (inside `"..."`). +--- Handles backslash, double quote, newline, carriage return, and tab. +--- @param text string The text to escape +--- @return string The escaped text safe for Typst string literals +function M.escape_typst_string(text) + return (text + :gsub('\\', '\\\\') + :gsub('"', '\\"') + :gsub('\n', '\\n') + :gsub('\r', '\\r') + :gsub('\t', '\\t')) +end + +--- Escape characters for JavaScript string literals (inside `"..."` or `'...'`). +--- Handles backslash, both quote styles, newlines, carriage returns, tabs, +--- form feeds, and the `` block. +--- @param text string|nil The text to escape +--- @return string The escaped text safe for JavaScript string literals +--- @usage local safe = M.escape_js_string([[a "b" ]]) +function M.escape_js_string(text) + if text == nil then return '' end + if type(text) ~= 'string' then text = tostring(text) end + return (text + :gsub('\\', '\\\\') + :gsub('"', '\\"') + :gsub("'", "\\'") + :gsub('\n', '\\n') + :gsub('\r', '\\r') + :gsub('\t', '\\t') + :gsub('\f', '\\f') + :gsub(', ", and ' to prevent XSS and ensure valid HTML. +--- @param text string The text to escape +--- @return string Escaped text safe for use in HTML +--- @usage local escaped = M.escape_html('Hello ') +function M.escape_html(text) + if text == nil then return '' end + if type(text) ~= 'string' then text = tostring(text) end + local result = text + :gsub('&', '&') + :gsub('<', '<') + :gsub('>', '>') + :gsub('"', '"') + :gsub("'", ''') + return result +end + +--- Escape special HTML attribute characters. +--- Escapes characters that could break attribute values. +--- @param value string The attribute value to escape +--- @return string Escaped value safe for use in HTML attributes +--- @usage local escaped = M.escape_attribute('Hello "World"') +function M.escape_attribute(value) + if value == nil then return '' end + if type(value) ~= 'string' then value = tostring(value) end + local result = value + :gsub('&', '&') + :gsub('"', '"') + :gsub('<', '<') + :gsub('>', '>') + return result +end + +--- Escape text for different formats. +--- @param text string The text to escape +--- @param format string The format to escape for (e.g., "latex", "typst", "lua") +--- @return string The escaped text +function M.escape_text(text, format) + local escape_functions = { + latex = M.escape_latex, + typst = M.escape_typst, + lua = M.escape_lua_pattern + } + + local escape = escape_functions[format] + if escape then + return escape(text) + else + error('Unsupported escape format: ' .. format) + end +end + +--- Converts a string to a valid HTML id by lowercasing and replacing spaces. +--- @param text string The text to convert +--- @return string The HTML id +function M.ascii_id(text) + local id = text:lower():gsub('[^a-z0-9 ]', ''):gsub(' +', '-') + return id +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-wizard/LICENSE b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-wizard/LICENSE new file mode 100644 index 0000000..4b53bb8 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-wizard/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mickaël Canouil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-wizard/schema.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-wizard/schema.lua new file mode 100644 index 0000000..add64b8 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/_vendor/quarto-wizard/schema.lua @@ -0,0 +1,2569 @@ +--- MC Schema - Reference validator for Quarto extension schemas (`_schema.yml`) +--- @module "schema" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @version 2.2.0 +--- +--- Implements the v2 extension schema vocabulary published at +--- . +--- The v1 dual-key vocabulary (`min`, `max`, `enum-case-insensitive`, +--- `element-attributes`, `pattern-exact`) is not accepted. +--- +--- The module is standalone: it requires no sibling module, and it reaches +--- `pandoc` and `quarto` only through the `M._env` table, so it can be +--- exercised outside a Quarto render with stubs in place. + +local M = {} + +--- Meta-schema this module implements. +M.SCHEMA_VERSION = 'https://m.canouil.dev/quarto-wizard/assets/schema/v2/extension-schema.json' + +-- ============================================================================ +-- ENVIRONMENT SEAM +-- ============================================================================ + +--- Indirection over the two host globals, resolved at call time so a test +--- harness can install stubs before the first call. +local _env = {} + +--- Render a Pandoc value as plain text. +--- @param value any Pandoc value +--- @return string Plain text +function _env.stringify(value) + return pandoc.utils.stringify(value) +end + +--- Report the Pandoc type of a value. +--- @param value any Pandoc value +--- @return string Pandoc type name +function _env.pandoc_type(value) + return pandoc.utils.type(value) +end + +--- Emit a warning through the host logger. +--- @param message string Message to emit +--- @return nil +function _env.warn(message) + if quarto and quarto.log and quarto.log.warning then + quarto.log.warning(message) + else + io.stderr:write(message .. '\n') + end +end + +--- Emit an error through the host logger. +--- @param message string Message to emit +--- @return nil +function _env.report_error(message) + if quarto and quarto.log and quarto.log.error then + quarto.log.error(message) + else + io.stderr:write(message .. '\n') + end +end + +M._env = _env + +-- ============================================================================ +-- CONSTANTS +-- ============================================================================ + +--- Maximum nesting depth accepted when converting a parsed schema. +local MAX_DEPTH = 16 + +--- Maximum number of Lua patterns one regex may expand into. +local MAX_PATTERN_BRANCHES = 64 + +--- Descriptor keywords holding a numeric bound. +local NUMERIC_KEYWORDS = { + 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', + 'minLength', 'maxLength', 'minItems', 'maxItems', +} + +--- Every keyword the v2 field descriptor may carry, mapped to whether this +--- module enforces it. Annotations are carried through untouched for tooling. +M.KEYWORDS = { + type = true, + required = true, + default = true, + const = true, + enum = true, + enumCaseInsensitive = true, + pattern = true, + minimum = true, + maximum = true, + exclusiveMinimum = true, + exclusiveMaximum = true, + multipleOf = true, + minLength = true, + maxLength = true, + minItems = true, + maxItems = true, + uniqueItems = true, + items = true, + properties = true, + additionalProperties = true, + propertyNames = true, + dependentRequired = true, + aliases = true, + deprecated = true, + name = true, + description = false, + title = false, + examples = false, + format = false, + completion = false, + contentEncoding = false, + contentMediaType = false, +} + +-- ============================================================================ +-- PRIVATE HELPERS +-- ============================================================================ + +--- Report whether a table is a dense array. +--- @param value any Value to inspect +--- @return boolean True when the table has only keys 1..n +local function _is_array(value) + if type(value) ~= 'table' then + return false + end + local count = 0 + for _ in pairs(value) do + count = count + 1 + end + return count == #value +end + +--- Read a key from a map, accepting the hyphen and underscore spellings. +--- Keys are never rewritten, so a schema keeps the names its author wrote. +--- @param map table|nil Map to read +--- @param key string|nil Key to read +--- @return any value The stored value, or nil +--- @return string|nil found_key The spelling that matched +local function _lookup(map, key) + if type(map) ~= 'table' or type(key) ~= 'string' then + return nil, nil + end + if map[key] ~= nil then + return map[key], key + end + local underscored = (key:gsub('%-', '_')) + if underscored ~= key and map[underscored] ~= nil then + return map[underscored], underscored + end + local hyphenated = (key:gsub('_', '-')) + if hyphenated ~= key and map[hyphenated] ~= nil then + return map[hyphenated], hyphenated + end + return nil, nil +end + +--- Render any value for an error message, including mixed-type arrays. +--- Replaces `table.concat`, which throws on a boolean entry. +--- @param value any Value to render +--- @return string Readable representation +local function _format_value(value) + local kind = type(value) + if kind == 'string' then + if value == '' then + return '""' + end + return value + elseif kind == 'number' or kind == 'boolean' then + return tostring(value) + elseif kind == 'nil' then + return 'null' + elseif kind == 'table' then + if _is_array(value) then + local parts = {} + for index = 1, #value do + parts[index] = _format_value(value[index]) + end + return '[' .. table.concat(parts, ', ') .. ']' + end + return '{object}' + end + return tostring(value) +end + +--- Render a list of values as a comma-separated string. +--- @param values table Array of values +--- @return string Readable list +local function _format_list(values) + local parts = {} + for index = 1, #values do + parts[index] = _format_value(values[index]) + end + return table.concat(parts, ', ') +end + +--- Normalise a type spec to an array of type names. +--- @param type_spec string|table|nil Declared type +--- @return table|nil Array of type names, or nil when no type is declared +local function _type_names(type_spec) + if type_spec == nil then + return nil + end + if type(type_spec) == 'table' then + return type_spec + end + return { type_spec } +end + +--- Render a type spec for an error message. +--- @param type_spec string|table Declared type +--- @return string Readable type +local function _format_type(type_spec) + if type(type_spec) == 'table' then + return table.concat(type_spec, ' | ') + end + return tostring(type_spec) +end + +--- Report the type name of a value, distinguishing arrays from objects. +--- @param value any Value to inspect +--- @return string Type name +local function _actual_type(value) + local kind = type(value) + if kind == 'table' then + return _is_array(value) and 'array' or 'object' + end + if kind == 'nil' then + return 'null' + end + return kind +end + +--- Report whether a value satisfies a single type name. +--- @param value any Value to check +--- @param name string Type name +--- @return boolean True when the value matches +local function _matches_type(value, name) + if name == 'content' then + return value ~= nil + end + if name == 'null' then + return value == nil + end + if name == 'integer' then + return type(value) == 'number' + and value == value + and value ~= math.huge + and value ~= -math.huge + and value == math.floor(value) + end + if name == 'number' then + return type(value) == 'number' + end + if name == 'boolean' then + return type(value) == 'boolean' + end + if name == 'string' then + return type(value) == 'string' + end + if name == 'array' then + return type(value) == 'table' and _is_array(value) + end + if name == 'object' then + -- An array is not an object. An empty table is both, and Lua cannot tell + -- the two apart, so it satisfies either. + return type(value) == 'table' and (next(value) == nil or not _is_array(value)) + end + return false +end + +--- Report whether a value satisfies any declared type. +--- @param value any Value to check +--- @param type_spec string|table|nil Declared type +--- @return boolean True when the value matches, or no type is declared +local function _matches_type_spec(value, type_spec) + local names = _type_names(type_spec) + if names == nil then + return true + end + for _, name in ipairs(names) do + if _matches_type(value, name) then + return true + end + end + return false +end + +--- Coerce a string toward a single scalar type. +--- Pandoc renders every YAML number as a string, so the incoming value and +--- the schema's own literals both need this before any comparison. +--- @param value any Value to coerce +--- @param name string Target type name +--- @return any coerced Coerced value, or nil +--- @return boolean ok True when the coercion applied +local function _coerce_scalar(value, name) + if type(value) ~= 'string' then + return nil, false + end + if name == 'number' then + local number = tonumber(value) + if number then + return number, true + end + elseif name == 'integer' then + local number = tonumber(value) + if number and number == math.floor(number) then + return number, true + end + elseif name == 'boolean' then + -- Only `true` and `false`, which is what YAML 1.2 calls a boolean and what + -- Pandoc reads. `yes` and `no` are booleans in YAML 1.1 alone, and Pandoc + -- hands them over as strings, so accepting them here made this validator + -- disagree with the YAML the documents are written in. + -- + -- The coercion is still needed for the words that do name a boolean, + -- because an attribute and a shortcode argument always arrive as strings. + local lowered = value:lower() + if lowered == 'true' then + return true, true + end + if lowered == 'false' then + return false, true + end + end + return nil, false +end + +--- Coerce a value toward its declared type. +--- A value already matching one declared type is returned untouched, so +--- `type: [boolean, string]` keeps the string "fenced" a string. +--- @param value any Value to coerce +--- @param type_spec string|table|nil Declared type +--- @return any Coerced value +local function _coerce(value, type_spec) + local names = _type_names(type_spec) + if names == nil or value == nil then + return value + end + for _, name in ipairs(names) do + if _matches_type(value, name) then + return value + end + end + for _, name in ipairs(names) do + local coerced, ok = _coerce_scalar(value, name) + if ok then + return coerced + end + end + return value +end + +-- ============================================================================ +-- PATTERN COMPILATION +-- ============================================================================ + +--- Lua replacements for JS shorthand classes, outside a character class. +local ESCAPE_OUTSIDE = { + d = '%d', D = '%D', w = '[%w_]', W = '[^%w_]', s = '%s', S = '%S', +} + +--- Lua replacements for JS shorthand classes, inside a character class. +--- `\W` has no in-class form, because the underscore cannot be excluded there. +local ESCAPE_INSIDE = { + d = '%d', D = '%D', w = '%w_', s = '%s', S = '%S', +} + +--- Escapes with a real Lua equivalent. Anything else alphanumeric is refused, +--- because compiling it to the bare letter would accept the wrong values. +local CONTROL_ESCAPES = { + n = '\n', + t = '\t', + r = '\r', + f = '\f', + v = '\v', +} + +--- Characters Lua treats as magic outside a character class. +local LUA_MAGIC = '^$()%.[]*+-?' + +--- Escape one literal character for use in a Lua pattern. +--- @param char string Single character +--- @return string Escaped character +local function _escape_literal(char) + if LUA_MAGIC:find(char, 1, true) then + return '%' .. char + end + return char +end + +--- Compile a JS regular expression into a list of equivalent Lua patterns. +--- Alternation is expanded into one pattern per branch, which is how Lua, +--- lacking alternation, can honour the commonest JSON Schema idiom. +--- Anything that cannot be expressed is reported rather than accepted. +--- @param regex string JS regular expression +--- @return table|nil branches Array of Lua patterns, or nil on failure +--- @return string|nil reason Why compilation failed +local function _compile_pattern(regex) + local position = 1 + local length = #regex + local anchor_start = false + local anchor_end = false + local has_top_level_alternation = false + local parse_alternation + + --- Combine a set of prefixes with a set of continuations. + local function cross(prefixes, continuations) + local out = {} + for _, prefix in ipairs(prefixes) do + for _, continuation in ipairs(continuations) do + if #out >= MAX_PATTERN_BRANCHES then + return nil, 'alternation expands past ' .. MAX_PATTERN_BRANCHES .. ' branches' + end + out[#out + 1] = prefix .. continuation + end + end + return out + end + + --- Parse a bracketed character class starting at the current position. + local function parse_class() + local out = { '[' } + position = position + 1 + if regex:sub(position, position) == '^' then + out[#out + 1] = '^' + position = position + 1 + end + if regex:sub(position, position) == ']' then + out[#out + 1] = '%]' + position = position + 1 + end + while position <= length do + local char = regex:sub(position, position) + if char == ']' then + position = position + 1 + out[#out + 1] = ']' + return table.concat(out) + elseif char == '\\' then + local next_char = regex:sub(position + 1, position + 1) + if next_char == '' then + return nil, 'trailing backslash' + end + if next_char == 'W' then + return nil, 'unsupported "\\W" inside a character class' + end + local mapped = ESCAPE_INSIDE[next_char] + if mapped then + out[#out + 1] = mapped + elseif next_char:match('[1-9]') then + return nil, 'unsupported backreference "\\' .. next_char .. '"' + elseif next_char == '%' then + out[#out + 1] = '%%' + elseif CONTROL_ESCAPES[next_char] then + out[#out + 1] = CONTROL_ESCAPES[next_char] + elseif next_char:match('%w') then + return nil, 'unsupported escape "\\' .. next_char .. '"' + else + out[#out + 1] = '%' .. next_char + end + position = position + 2 + elseif char == '%' then + out[#out + 1] = '%%' + position = position + 1 + else + out[#out + 1] = char + position = position + 1 + end + end + return nil, 'unterminated character class' + end + + --- Read a trailing quantifier, rejecting the forms Lua cannot express. + local function parse_quantifier() + local char = regex:sub(position, position) + if char ~= '*' and char ~= '+' and char ~= '?' then + return '' + end + if regex:sub(position + 1, position + 1) == '?' then + return nil, 'unsupported non-greedy quantifier "' .. char .. '?"' + end + position = position + 1 + return char + end + + --- Parse a single atom and return the branches it contributes. + local function parse_atom(depth, at_sequence_start) + local char = regex:sub(position, position) + + if char == '(' then + if regex:sub(position + 1, position + 1) == '?' then + local marker = regex:sub(position + 2, position + 2) + if marker == '=' or marker == '!' or marker == '<' then + return nil, 'unsupported lookahead or lookbehind' + end + if marker ~= ':' then + return nil, 'unsupported group modifier "(?' .. marker .. '"' + end + position = position + 3 + else + position = position + 1 + end + local branches, reason = parse_alternation(depth + 1) + if not branches then + return nil, reason + end + if regex:sub(position, position) ~= ')' then + return nil, 'unterminated group' + end + position = position + 1 + local quantifier = regex:sub(position, position) + if quantifier == '*' or quantifier == '+' or quantifier == '?' then + return nil, 'unsupported quantifier applied to a group' + end + return branches + end + + if char == '[' then + local class, reason = parse_class() + if not class then + return nil, reason + end + local quantifier, quantifier_reason = parse_quantifier() + if not quantifier then + return nil, quantifier_reason + end + return { class .. quantifier } + end + + if char == '\\' then + local next_char = regex:sub(position + 1, position + 1) + if next_char == '' then + return nil, 'trailing backslash' + end + if next_char:match('[1-9]') then + return nil, 'unsupported backreference "\\' .. next_char .. '"' + end + position = position + 2 + local mapped = ESCAPE_OUTSIDE[next_char] + local atom + if mapped then + atom = mapped + elseif CONTROL_ESCAPES[next_char] then + atom = CONTROL_ESCAPES[next_char] + elseif next_char:match('%w') then + return nil, 'unsupported escape "\\' .. next_char .. '"' + else + atom = _escape_literal(next_char) + end + local quantifier, quantifier_reason = parse_quantifier() + if not quantifier then + return nil, quantifier_reason + end + return { atom .. quantifier } + end + + if char == '^' then + if depth ~= 0 or not at_sequence_start or position ~= 1 then + return nil, 'unsupported anchor "^" away from the start of the pattern' + end + position = position + 1 + anchor_start = true + return { '' } + end + + if char == '$' then + if depth ~= 0 or position ~= length then + return nil, 'unsupported anchor "$" away from the end of the pattern' + end + position = position + 1 + anchor_end = true + return { '' } + end + + if char == '{' then + return nil, 'unsupported counted quantifier "{n,m}"' + end + + if char == '*' or char == '+' or char == '?' then + return nil, 'quantifier "' .. char .. '" with nothing to repeat' + end + + if char == '.' then + position = position + 1 + local quantifier, quantifier_reason = parse_quantifier() + if not quantifier then + return nil, quantifier_reason + end + return { '.' .. quantifier } + end + + position = position + 1 + local atom = _escape_literal(char) + local quantifier, quantifier_reason = parse_quantifier() + if not quantifier then + return nil, quantifier_reason + end + return { atom .. quantifier } + end + + --- Parse one alternative: a run of atoms up to `|`, `)` or the end. + local function parse_sequence(depth) + local branches = { '' } + local first = true + while position <= length do + local char = regex:sub(position, position) + if char == '|' or char == ')' then + break + end + local atom_branches, reason = parse_atom(depth, first) + if not atom_branches then + return nil, reason + end + first = false + local crossed, cross_reason = cross(branches, atom_branches) + if not crossed then + return nil, cross_reason + end + branches = crossed + end + return branches + end + + parse_alternation = function(depth) + local all = {} + local iterations = 0 + while true do + iterations = iterations + 1 + local branches, reason = parse_sequence(depth) + if not branches then + return nil, reason + end + for _, branch in ipairs(branches) do + if #all >= MAX_PATTERN_BRANCHES then + return nil, 'alternation expands past ' .. MAX_PATTERN_BRANCHES .. ' branches' + end + all[#all + 1] = branch + end + if regex:sub(position, position) == '|' then + position = position + 1 + else + break + end + end + if depth == 0 and iterations > 1 then + has_top_level_alternation = true + end + return all + end + + local branches, reason = parse_alternation(0) + if not branches then + return nil, reason + end + if position <= length then + return nil, 'unbalanced ")" in pattern' + end + + -- The anchors are collected for the expression as a whole, so applying them + -- to multiple top-level branches would anchor branches the author did not anchor. + if has_top_level_alternation and (anchor_start or anchor_end) then + return nil, 'unsupported anchor in a top-level alternation' + end + + local prefix = anchor_start and '^' or '' + local suffix = anchor_end and '$' or '' + local out = {} + for index, branch in ipairs(branches) do + out[index] = prefix .. branch .. suffix + end + return out +end + +M._compile_pattern = _compile_pattern + +--- Report whether a string satisfies any compiled branch. +--- @param value string Value to test +--- @param branches table Array of Lua patterns +--- @return boolean True when a branch matches +local function _pattern_matches(value, branches) + for _, branch in ipairs(branches) do + local ok, matched = pcall(string.match, value, branch) + if ok and matched ~= nil then + return true + end + end + return false +end + +-- ============================================================================ +-- PANDOC VALUE CONVERSION +-- ============================================================================ + +--- Convert a Pandoc metadata value to a native Lua value. +--- Pandoc's Lua objects carry no `t` field, so the kind has to come from +--- `pandoc.utils.type`; testing `value.t` silently turns every string option +--- into a list of its inline elements. +--- @param value any Pandoc metadata value +--- @return any Native Lua value +local function _convert_pandoc_value(value) + local kind = _env.pandoc_type(value) + + -- A bare key, `null` and `~` all arrive as an empty Pandoc `string`, which + -- is how Pandoc collapses YAML null. An explicit `""` arrives as an empty + -- `Inlines` instead, so the two are told apart before either is unwrapped. + if kind == 'string' and value == '' then + return nil + end + + if type(value) ~= 'table' then + return value + end + + if kind == 'Inlines' or kind == 'Blocks' or kind == 'Inline' or kind == 'Block' then + return _env.stringify(value) + end + + if kind == 'List' then + -- A null element is absent, the same rule this module applies to a null + -- key. Writing it into a running count rather than the source index + -- keeps the result a proper sequence instead of leaving a gap. + local result = {} + for index = 1, #value do + local converted = _convert_pandoc_value(value[index]) + if converted ~= nil then + result[#result + 1] = converted + end + end + return result + end + + if _is_array(value) and #value > 0 then + -- A null element is absent, the same rule this module applies to a null + -- key. Writing it into a running count rather than the source index + -- keeps the result a proper sequence instead of leaving a gap. + local result = {} + for index = 1, #value do + local converted = _convert_pandoc_value(value[index]) + if converted ~= nil then + result[#result + 1] = converted + end + end + return result + end + + local result = {} + for key, item in pairs(value) do + result[tostring(key)] = _convert_pandoc_value(item) + end + return result +end + +-- ============================================================================ +-- YAML PARSING +-- ============================================================================ + +--- Pandoc parses YAML string scalars as Markdown, which rewrites a schema in +--- place: `^[a-z]+$` arrives as `+$`, and `^\d+\.?\d*m?s$` arrives as +--- `^.??s$`. The schema file is therefore parsed here instead. +--- +--- The subset covers what an extension schema needs: block maps and +--- sequences, plain and quoted scalars, folded and literal block scalars, +--- flow sequences and flow maps (including ones spanning several lines), and +--- `#` comments. Anchors, aliases, tags, multiple documents and complex keys +--- are not supported and are reported rather than misread. +--- +--- Every function here reports a failure by returning a message rather than +--- by calling `error`. Quarto replaces `error` with a logger that returns +--- instead of unwinding, so a module that trusts `error` to stop execution +--- carries on with invalid state and fails later somewhere unrelated. +--- +--- One deliberate deviation from YAML: a block scalar is always stripped of +--- its trailing newline unless it is chomped with `+`. Schema text is +--- annotation, and a trailing newline there is noise rather than meaning. + +--- Build a parse error message carrying the offending line number. +--- @param line_number number Line number +--- @param message string Explanation +--- @return string Formatted message +local function _yaml_error(line_number, message) + return string.format('line %d: %s', line_number, message) +end + +--- Remove a trailing `#` comment, respecting quoted spans. +--- @param text string Line content +--- @return string Content without its comment +local function _strip_comment(text) + local out = {} + local quote = nil + local index = 1 + while index <= #text do + local char = text:sub(index, index) + if quote then + out[#out + 1] = char + if char == '\\' and quote == '"' then + index = index + 1 + out[#out + 1] = text:sub(index, index) + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + out[#out + 1] = char + elseif char == '#' and (index == 1 or text:sub(index - 1, index - 1):match('%s')) then + break + else + out[#out + 1] = char + end + index = index + 1 + end + return (table.concat(out):gsub('%s+$', '')) +end + +--- Trim leading and trailing whitespace. +--- @param text string Text to trim +--- @return string Trimmed text +local function _trim(text) + return (text:gsub('^%s+', ''):gsub('%s+$', '')) +end + +--- Parse a scalar, giving numbers and booleans their real Lua types. +--- @param text string Scalar text +--- @param line_number number Line number for errors +--- @return any value Parsed value, nil for an explicit null +--- @return string|nil err +local function _parse_scalar(text, line_number) + text = _trim(text) + + if text == '' or text == '~' or text == 'null' or text == 'Null' or text == 'NULL' then + return nil, nil + end + + local first = text:sub(1, 1) + + if first == '"' then + if #text < 2 or text:sub(-1) ~= '"' then + return nil, _yaml_error(line_number, 'unterminated double-quoted scalar') + end + return (text:sub(2, -2):gsub('\\(.)', function(escaped) + if escaped == 'n' then return '\n' end + if escaped == 't' then return '\t' end + if escaped == 'r' then return '\r' end + return escaped + end)), nil + end + + if first == "'" then + if #text < 2 or text:sub(-1) ~= "'" then + return nil, _yaml_error(line_number, 'unterminated single-quoted scalar') + end + return (text:sub(2, -2):gsub("''", "'")), nil + end + + if first == '&' or first == '*' or first == '!' then + return nil, _yaml_error(line_number, 'anchors, aliases and tags are not supported') + end + + if text == 'true' or text == 'True' or text == 'TRUE' or text == 'yes' or text == 'Yes' then + return true, nil + end + if text == 'false' or text == 'False' or text == 'FALSE' or text == 'no' or text == 'No' then + return false, nil + end + + local number = tonumber(text) + if number ~= nil and text:match('^[%-+]?[%d%.eE%+%-]+$') then + return number, nil + end + + return text, nil +end + +--- Split a flow collection body on its top-level commas. +--- @param body string Text between the brackets +--- @return table Array of raw item strings +local function _split_flow(body) + local items = {} + local depth = 0 + local quote = nil + local start = 1 + local index = 1 + while index <= #body do + local char = body:sub(index, index) + if quote then + if char == '\\' and quote == '"' then + index = index + 1 + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + elseif char == '[' or char == '{' then + depth = depth + 1 + elseif char == ']' or char == '}' then + depth = depth - 1 + elseif char == ',' and depth == 0 then + items[#items + 1] = body:sub(start, index - 1) + start = index + 1 + end + index = index + 1 + end + local tail = body:sub(start) + if tail:match('%S') then + items[#items + 1] = tail + end + return items +end + +--- Strip the quotes from a mapping key. +--- @param key string Raw key text +--- @param line_number number Line number for errors +--- @return string|nil key +--- @return string|nil err +local function _unquote_key(key, line_number) + key = _trim(key) + local first = key:sub(1, 1) + if first == '"' or first == "'" then + local parsed, err = _parse_scalar(key, line_number) + if err then + return nil, err + end + return tostring(parsed), nil + end + return key, nil +end + +local _parse_flow + +--- Parse a value written in flow style, or a plain scalar. +--- @param text string Value text +--- @param line_number number Line number for errors +--- @param depth number|nil Current nesting depth +--- @return any value +--- @return string|nil err +_parse_flow = function(text, line_number, depth) + depth = depth or 0 + if depth > MAX_DEPTH then + return nil, _yaml_error(line_number, 'value nests deeper than ' .. MAX_DEPTH .. ' levels') + end + + text = _trim(text) + local first = text:sub(1, 1) + + if first == '[' then + if text:sub(-1) ~= ']' then + return nil, _yaml_error(line_number, 'unterminated flow sequence') + end + local out = {} + for index, item in ipairs(_split_flow(text:sub(2, -2))) do + local value, err = _parse_flow(item, line_number, depth + 1) + if err then + return nil, err + end + out[index] = value + end + return out, nil + end + + if first == '{' then + if text:sub(-1) ~= '}' then + return nil, _yaml_error(line_number, 'unterminated flow mapping') + end + local out = {} + for _, item in ipairs(_split_flow(text:sub(2, -2))) do + local key, rest = item:match('^%s*(.-)%s*:%s*(.*)$') + if not key or key == '' then + return nil, _yaml_error(line_number, 'expected "key: value" inside a flow mapping') + end + local name, key_err = _unquote_key(key, line_number) + if key_err then + return nil, key_err + end + local value, err = _parse_flow(rest, line_number, depth + 1) + if err then + return nil, err + end + out[name] = value + end + return out, nil + end + + return _parse_scalar(text, line_number) +end + +--- Split a mapping line into its key and the rest of the line. +--- The separator is the first unquoted colon followed by a space or the end +--- of the line, so a URL value such as `https://...` is not split. +--- @param text string Line content +--- @return string|nil key +--- @return string|nil rest +local function _split_key(text) + local quote = nil + local index = 1 + while index <= #text do + local char = text:sub(index, index) + if quote then + if char == '\\' and quote == '"' then + index = index + 1 + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + elseif char == ':' then + local following = text:sub(index + 1, index + 1) + if following == '' or following == ' ' then + return text:sub(1, index - 1), _trim(text:sub(index + 2)) + end + end + index = index + 1 + end + return nil, nil +end + +--- Break the source into classified lines. +--- @param source string YAML text +--- @return table|nil lines Array of {raw, indent, number, blank, comment} +--- @return string|nil err +local function _scan_lines(source) + local lines = {} + local number = 0 + for raw in (source .. '\n'):gmatch('([^\n]*)\n') do + number = number + 1 + if raw:find('\t') then + return nil, _yaml_error(number, 'tabs are not valid YAML indentation') + end + local indent = #(raw:match('^ *') or '') + local body = raw:sub(indent + 1) + lines[#lines + 1] = { + raw = raw, + indent = indent, + number = number, + blank = body == '', + comment = body:sub(1, 1) == '#', + } + end + return lines, nil +end + +--- Advance past blank and comment lines. +--- @param lines table Scanned lines +--- @param index number Starting index +--- @return number Index of the next significant line +local function _skip_insignificant(lines, index) + while index <= #lines and (lines[index].blank or lines[index].comment) do + index = index + 1 + end + return index +end + +--- Read the comment-stripped content of a line. +--- @param line table Scanned line +--- @return string Content +local function _content(line) + return _strip_comment(line.raw:sub(line.indent + 1)) +end + +--- Track bracket depth across one line, ignoring quoted spans. +--- @param text string Line content +--- @param depth number Depth carried in +--- @param quote string|nil Open quote carried in +--- @return number depth +--- @return string|nil quote +local function _scan_flow_depth(text, depth, quote) + local index = 1 + while index <= #text do + local char = text:sub(index, index) + if quote then + if char == '\\' and quote == '"' then + index = index + 1 + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + elseif char == '[' or char == '{' then + depth = depth + 1 + elseif char == ']' or char == '}' then + depth = depth - 1 + end + index = index + 1 + end + return depth, quote +end + +--- Collect a flow collection that may run across several lines. +--- @param lines table Scanned lines +--- @param start number Index of the line holding the opening bracket +--- @param initial string Flow text already read from that line +--- @param line_number number Line number for errors +--- @return string|nil text The whole flow collection on one line +--- @return number next_index Index of the first line after it +--- @return string|nil err +local function _gather_flow(lines, start, initial, line_number) + local pieces = { initial } + local depth, quote = _scan_flow_depth(initial, 0, nil) + local index = start + + while depth > 0 do + index = index + 1 + if index > #lines then + return nil, index, _yaml_error(line_number, 'unterminated flow collection') + end + local content = _content(lines[index]) + pieces[#pieces + 1] = content + depth, quote = _scan_flow_depth(content, depth, quote) + end + + return table.concat(pieces, ' '), index + 1, nil +end + +--- Read a folded or literal block scalar. +--- @param lines table Scanned lines +--- @param start number First candidate line +--- @param parent_indent number Indentation of the owning key +--- @param style string Block style, such as '>', '>-' or '|' +--- @return string text +--- @return number next_index +local function _read_block_scalar(lines, start, parent_indent, style) + local kind = style:sub(1, 1) + local chomp = style:sub(2, 2) + + local collected = {} + local block_indent = nil + local index = start + + while index <= #lines do + local line = lines[index] + if line.blank then + collected[#collected + 1] = '' + index = index + 1 + elseif line.indent > parent_indent then + block_indent = block_indent or line.indent + collected[#collected + 1] = line.raw:sub(block_indent + 1) + index = index + 1 + else + break + end + end + + while #collected > 0 and collected[#collected] == '' do + table.remove(collected) + end + + local text + if kind == '>' then + local parts = {} + local buffer = {} + for _, line in ipairs(collected) do + if line == '' then + if #buffer > 0 then + parts[#parts + 1] = table.concat(buffer, ' ') + buffer = {} + end + parts[#parts + 1] = '\n' + else + buffer[#buffer + 1] = (line:gsub('%s+$', '')) + end + end + if #buffer > 0 then + parts[#parts + 1] = table.concat(buffer, ' ') + end + text = table.concat(parts) + else + text = table.concat(collected, '\n') + end + + if chomp == '+' then + text = text .. '\n' + end + + return text, index +end + +local _parse_block +local _parse_sequence + +--- The order in which each parsed mapping declared its keys. +--- Lua tables have no key order, but a schema is authored in a meaningful one, +--- and a generator that turns a schema into documentation needs it. The order +--- is kept beside the data rather than inside it, so it cannot be mistaken for +--- a key of the mapping. Weak keys let a discarded mapping take its order with +--- it. +local _key_order = setmetatable({}, { __mode = 'k' }) + +--- Parse a block mapping at a known indentation. +--- @return table|nil map +--- @return number next_index +--- @return string|nil err +local function _parse_map(lines, start, indent, depth) + local map = {} + local order = {} + local index = start + + while index <= #lines do + local line = lines[index] + if line.blank or line.comment then + index = index + 1 + elseif line.indent < indent then + break + else + if line.indent > indent then + return nil, index, _yaml_error(line.number, 'unexpected indentation inside a mapping') + end + + local content = _content(line) + if content == '' then + index = index + 1 + else + if content == '---' or content == '...' then + return nil, index, + _yaml_error(line.number, 'multiple YAML documents are not supported') + end + + if content:sub(1, 1) == '-' then + return nil, index, + _yaml_error(line.number, 'a sequence item cannot appear inside a mapping here') + end + + local raw_key, rest = _split_key(content) + if raw_key == nil or _trim(raw_key) == '' then + return nil, index, _yaml_error(line.number, 'expected "key: value"') + end + + local key, key_err = _unquote_key(raw_key, line.number) + if key_err then + return nil, index, key_err + end + if map[key] == nil then + order[#order + 1] = key + end + index = index + 1 + + -- An explicit indentation indicator (`|2`, `>2-`) is reported rather + -- than misread: without this the whole block is stored as the string + -- "|2" and its body is then parsed as further mapping lines. + if rest:match('^[|>]%d') or rest:match('^[|>][+%-]%d') then + return nil, index, _yaml_error( + line.number, + 'a block scalar indentation indicator is not supported: ' .. rest + ) + end + + local block_style = rest:match('^([|>][+%-]?)$') + if block_style then + local text, next_index = _read_block_scalar(lines, index, indent, block_style) + map[key] = text + index = next_index + elseif rest == '' then + local value, next_index, err = _parse_block(lines, index, indent, depth + 1) + if err then + return nil, next_index, err + end + + -- A block sequence may sit at the column of its own key, which + -- `_parse_block` declines because its guard wants a deeper line. + -- Nothing was consumed in that case, so read the sequence here. + if value == nil and next_index == index then + local peek = _skip_insignificant(lines, index) + if peek <= #lines and lines[peek].indent == indent then + local ahead = _content(lines[peek]) + if ahead == '-' or ahead:sub(1, 2) == '- ' then + value, next_index, err = _parse_sequence(lines, peek, indent, depth + 1) + if err then + return nil, next_index, err + end + end + end + end + + map[key] = value + index = next_index + elseif rest:sub(1, 1) == '[' or rest:sub(1, 1) == '{' then + local text, next_index, err = _gather_flow(lines, index - 1, rest, line.number) + if err then + return nil, next_index, err + end + local value, value_err = _parse_flow(text, line.number, depth) + if value_err then + return nil, next_index, value_err + end + map[key] = value + index = next_index + else + local value, err = _parse_flow(rest, line.number, depth) + if err then + return nil, index, err + end + map[key] = value + end + end + end + end + + _key_order[map] = order + return map, index, nil +end + +--- Parse a block sequence at a known indentation. +--- @return table|nil sequence +--- @return number next_index +--- @return string|nil err +_parse_sequence = function(lines, start, indent, depth) + local sequence = {} + local index = start + + while index <= #lines do + local line = lines[index] + if line.blank or line.comment then + index = index + 1 + elseif line.indent < indent then + break + else + if line.indent > indent then + return nil, index, _yaml_error(line.number, 'unexpected indentation inside a sequence') + end + + local content = _content(line) + local rest = content:match('^%-%s*(.*)$') + if rest == nil then + break + end + + if rest == '' then + local value, next_index, err = _parse_block(lines, index + 1, indent, depth + 1) + if err then + return nil, next_index, err + end + -- A Lua array cannot hold an interior nil, so an empty item would be + -- dropped and every later item would shift down one index, moving the + -- element that `minItems` and an `items` path such as `preload[2]` + -- refer to. Report it instead. + if value == nil then + return nil, index, _yaml_error(line.number, 'an empty sequence item is not supported') + end + sequence[#sequence + 1] = value + index = next_index + elseif _split_key(rest) == nil then + -- A plain or flow scalar item, such as `- password` or `- [a, b]`. + if rest:sub(1, 1) == '[' or rest:sub(1, 1) == '{' then + local text, next_index, err = _gather_flow(lines, index, rest, line.number) + if err then + return nil, next_index, err + end + local value, value_err = _parse_flow(text, line.number, depth) + if value_err then + return nil, next_index, value_err + end + sequence[#sequence + 1] = value + index = next_index + else + local value, err = _parse_flow(rest, line.number, depth) + if err then + return nil, index, err + end + sequence[#sequence + 1] = value + index = index + 1 + end + else + -- The remainder of the dash line is the first line of the item, and it + -- sits at the column where that remainder begins. + local offset = #content - #rest + local synthetic = { + raw = string.rep(' ', indent + offset) .. rest, + indent = indent + offset, + number = line.number, + blank = false, + comment = false, + } + local saved = lines[index] + lines[index] = synthetic + local value, next_index, err = _parse_block(lines, index, indent, depth + 1) + lines[index] = saved + if err then + return nil, next_index, err + end + sequence[#sequence + 1] = value + index = next_index + end + end + end + + return sequence, index, nil +end + +--- Parse whichever block collection begins at `start`. +--- @param lines table Scanned lines +--- @param start number Index to start from +--- @param parent_indent number|nil Indentation that the block must exceed +--- @param depth number Current nesting depth +--- @return any value +--- @return number next_index +--- @return string|nil err +_parse_block = function(lines, start, parent_indent, depth) + depth = depth or 0 + local index = _skip_insignificant(lines, start) + if index > #lines then + return nil, index, nil + end + + local line = lines[index] + if parent_indent ~= nil and line.indent <= parent_indent then + return nil, start, nil + end + + if depth > MAX_DEPTH then + return nil, index, _yaml_error(line.number, 'schema nests deeper than ' .. MAX_DEPTH .. ' levels') + end + + local indent = line.indent + local content = _content(line) + + if content:sub(1, 1) == '[' or content:sub(1, 1) == '{' then + local text, next_index, err = _gather_flow(lines, index, content, line.number) + if err then + return nil, next_index, err + end + local value, value_err = _parse_flow(text, line.number, depth) + if value_err then + return nil, next_index, value_err + end + return value, next_index, nil + end + + if content == '-' or content:sub(1, 2) == '- ' then + return _parse_sequence(lines, index, indent, depth) + end + return _parse_map(lines, index, indent, depth) +end + +--- Parse YAML text into a native Lua table. +--- @param source string YAML text +--- @return table|nil tree +--- @return string|nil err +local function _parse_yaml_text(source) + local lines, scan_err = _scan_lines(source) + if scan_err then + return nil, scan_err + end + + -- A single leading `---` opens one document, which is valid and common. + -- A later one opens a second document, which `_parse_map` reports. Testing + -- the raw text instead would also reject a `---` inside a block scalar. + local first = _skip_insignificant(lines, 1) + if first <= #lines and _content(lines[first]) == '---' then + lines[first].blank = true + end + + local value, _, err = _parse_block(lines, 1, nil, 0) + if err then + return nil, err + end + if value == nil then + return {}, nil + end + if type(value) ~= 'table' or (_is_array(value) and #value > 0) then + return nil, 'the file must contain a mapping at its top level' + end + return value, nil +end + +M._parse_yaml_text = _parse_yaml_text + +--- Read and parse a YAML schema file. +--- @param filename string Path to the schema file +--- @return table|nil tree +--- @return string|nil err +local function _parse_yaml_file(filename) + local handle = io.open(filename, 'r') + if not handle then + return nil, string.format('Could not open schema file: %s', filename) + end + + local content = handle:read('*a') + handle:close() + + local tree, err = _parse_yaml_text(content) + if err then + return nil, string.format('Could not parse schema file %s: %s', filename, err) + end + return tree, nil +end + + +-- ============================================================================ +-- DESCRIPTOR COMPILATION +-- ============================================================================ + +local _compiled_cache = setmetatable({}, { __mode = 'k' }) + +local _compile + +--- Compile a field descriptor: coerce its literals against its own declared +--- type, compile its patterns, and recurse into nested descriptors. +--- Returns a new table, so the caller's schema is never mutated. +--- @param spec table Raw field descriptor +--- @return table Compiled descriptor +_compile = function(spec) + if type(spec) ~= 'table' then + return spec + end + + local cached = _compiled_cache[spec] + if cached then + return cached + end + + local out = {} + for key, value in pairs(spec) do + out[key] = value + end + + local type_spec = out.type + + if out.default ~= nil then + out.default = _coerce(out.default, type_spec) + end + if out.const ~= nil then + out.const = _coerce(out.const, type_spec) + end + if type(out.enum) == 'table' then + local values = {} + for index = 1, #out.enum do + values[index] = _coerce(out.enum[index], type_spec) + end + out.enum = values + end + + for _, keyword in ipairs(NUMERIC_KEYWORDS) do + if out[keyword] ~= nil then + out[keyword] = tonumber(out[keyword]) or out[keyword] + end + end + + if type(out.required) == 'string' then + out.required = out.required:lower() == 'true' or out.required:lower() == 'yes' + end + if type(out.uniqueItems) == 'string' then + out.uniqueItems = out.uniqueItems:lower() == 'true' + end + if type(out.enumCaseInsensitive) == 'string' then + out.enumCaseInsensitive = out.enumCaseInsensitive:lower() == 'true' + end + if out.additionalProperties == 'false' then + out.additionalProperties = false + elseif out.additionalProperties == 'true' then + out.additionalProperties = true + end + + if type(out.pattern) == 'string' then + out._pattern, out._pattern_error = _compile_pattern(out.pattern) + end + if type(out.propertyNames) == 'string' then + out._property_names, out._property_names_error = _compile_pattern(out.propertyNames) + end + + if type(out.items) == 'table' then + out.items = _compile(out.items) + end + if type(out.properties) == 'table' then + local properties = {} + for key, value in pairs(out.properties) do + properties[key] = _compile(value) + end + out.properties = properties + end + if type(out.additionalProperties) == 'table' then + out.additionalProperties = _compile(out.additionalProperties) + end + + _compiled_cache[spec] = out + return out +end + +-- ============================================================================ +-- FINDINGS +-- ============================================================================ + +--- Record a finding against a path. +--- @param context table Validation context +--- @param severity string 'error' or 'warning' +--- @param path string Dotted path of the offending value +--- @param keyword string Keyword that produced the finding +--- @param message string Human-readable explanation +--- @return nil +local function _report(context, severity, path, keyword, message) + table.insert(context.findings, { + path = path, + keyword = keyword, + message = message, + severity = severity, + }) +end + +--- Split findings into the string arrays the public API returns. +--- @param findings table Array of findings +--- @return table errors Array of error strings +--- @return table warnings Array of warning strings +local function _split_findings(findings) + local errors = {} + local warnings = {} + for _, finding in ipairs(findings) do + local line = finding.path and (finding.path .. ': ' .. finding.message) or finding.message + if finding.severity == 'warning' then + table.insert(warnings, line) + else + table.insert(errors, line) + end + end + return errors, warnings +end + +-- ============================================================================ +-- KEYWORD CHECKS +-- ============================================================================ + +local _validate_value +local _validate_map + +--- Check `enum`, honouring `enumCaseInsensitive`. +local function _check_enum(value, spec, path, context) + if type(spec.enum) ~= 'table' then + return + end + local case_insensitive = spec.enumCaseInsensitive == true + for _, allowed in ipairs(spec.enum) do + if value == allowed then + return + end + if case_insensitive and type(value) == 'string' and type(allowed) == 'string' + and value:lower() == allowed:lower() then + return + end + end + _report(context, 'error', path, 'enum', string.format( + 'must be one of: %s, got %s.', _format_list(spec.enum), _format_value(value) + )) +end + +--- Check `pattern`, reporting a pattern the module cannot compile. +local function _check_pattern(value, spec, path, context) + if spec.pattern == nil or type(value) ~= 'string' then + return + end + if spec._pattern == nil then + _report(context, 'error', path, 'pattern', string.format( + 'schema declares a pattern this validator cannot compile (%s): %s.', + spec._pattern_error or 'unsupported construct', spec.pattern + )) + return + end + if not _pattern_matches(value, spec._pattern) then + _report(context, 'error', path, 'pattern', string.format( + 'does not match required pattern: %s.', spec.pattern + )) + end +end + +--- Check the numeric bounds. +local function _check_numeric(value, spec, path, context) + if type(value) ~= 'number' then + return + end + if type(spec.minimum) == 'number' and value < spec.minimum then + _report(context, 'error', path, 'minimum', string.format( + 'must be at least %s, got %s.', _format_value(spec.minimum), _format_value(value) + )) + end + if type(spec.maximum) == 'number' and value > spec.maximum then + _report(context, 'error', path, 'maximum', string.format( + 'must be at most %s, got %s.', _format_value(spec.maximum), _format_value(value) + )) + end + if type(spec.exclusiveMinimum) == 'number' and value <= spec.exclusiveMinimum then + _report(context, 'error', path, 'exclusiveMinimum', string.format( + 'must be greater than %s, got %s.', _format_value(spec.exclusiveMinimum), _format_value(value) + )) + end + if type(spec.exclusiveMaximum) == 'number' and value >= spec.exclusiveMaximum then + _report(context, 'error', path, 'exclusiveMaximum', string.format( + 'must be less than %s, got %s.', _format_value(spec.exclusiveMaximum), _format_value(value) + )) + end + if type(spec.multipleOf) == 'number' and spec.multipleOf ~= 0 then + local quotient = value / spec.multipleOf + if quotient ~= math.floor(quotient) then + _report(context, 'error', path, 'multipleOf', string.format( + 'must be a multiple of %s, got %s.', _format_value(spec.multipleOf), _format_value(value) + )) + end + end +end + +--- Check string length. +local function _check_string_length(value, spec, path, context) + if type(value) ~= 'string' then + return + end + -- Count characters rather than bytes, so a multi-byte glyph counts as one. + local length = utf8 and utf8.len(value) or nil + if length == nil then + length = #value + end + if type(spec.minLength) == 'number' and length < spec.minLength then + _report(context, 'error', path, 'minLength', string.format( + 'must be at least %d characters, got %d.', spec.minLength, length + )) + end + if type(spec.maxLength) == 'number' and length > spec.maxLength then + _report(context, 'error', path, 'maxLength', string.format( + 'must be at most %d characters, got %d.', spec.maxLength, length + )) + end +end + +--- Check array length and uniqueness. +local function _check_array(value, spec, path, context) + if type(value) ~= 'table' then + return + end + local length = #value + if type(spec.minItems) == 'number' and length < spec.minItems then + _report(context, 'error', path, 'minItems', string.format( + 'must have at least %d items, got %d.', spec.minItems, length + )) + end + if type(spec.maxItems) == 'number' and length > spec.maxItems then + _report(context, 'error', path, 'maxItems', string.format( + 'must have at most %d items, got %d.', spec.maxItems, length + )) + end + if spec.uniqueItems == true then + local seen = {} + for index = 1, length do + local element = value[index] + local rendered = _format_value(element) + -- The key carries the type so 1 and "1" are different items. A string + -- keys on its own text rather than on the rendering, because + -- `_format_value` renders an empty string as `""`, which would make it + -- collide with the two-character string `""`. The message carries only + -- the rendering, because the key is not the author's text. + local raw = type(element) == 'string' and element or rendered + local key = type(element) .. '\0' .. raw + if seen[key] then + _report(context, 'error', path, 'uniqueItems', string.format( + 'must not repeat items, but %s appears more than once.', rendered + )) + break + end + seen[key] = true + end + end +end + +--- Validate array elements against `items`. +local function _check_items(value, spec, path, context) + if type(spec.items) ~= 'table' or type(value) ~= 'table' then + return + end + for index = 1, #value do + local element = _coerce(value[index], spec.items.type) + value[index] = element + if element ~= nil then + _validate_value(element, spec.items, string.format('%s[%d]', path, index), context) + end + end +end + +--- Validate object members against `properties` and its companions. +local function _check_object(value, spec, path, context) + if type(value) ~= 'table' then + return + end + + if spec._property_names ~= nil or spec._property_names_error ~= nil then + if spec._property_names == nil then + _report(context, 'error', path, 'propertyNames', string.format( + 'schema declares a propertyNames pattern this validator cannot compile (%s): %s.', + spec._property_names_error or 'unsupported construct', spec.propertyNames + )) + else + for key in pairs(value) do + if type(key) == 'string' and not _pattern_matches(key, spec._property_names) then + _report(context, 'error', path .. '.' .. key, 'propertyNames', string.format( + 'key does not match required pattern: %s.', spec.propertyNames + )) + end + end + end + end + + local defaulted = {} + + if type(spec.properties) == 'table' then + local sub, filled = _validate_map(value, spec.properties, path, context, { + unknown = spec.additionalProperties == false and 'warn' or 'ignore', + additional = type(spec.additionalProperties) == 'table' and spec.additionalProperties or nil, + }) + + -- `_validate_map` builds a new table, so its defaults, coercion and alias + -- resolution have to be written back into the value the parent holds. + -- Without this a nested `properties` contributes nothing to `merged`. + local stale = {} + for key in pairs(value) do + if sub[key] == nil then + stale[#stale + 1] = key + end + end + for _, key in ipairs(stale) do + value[key] = nil + end + for key, member in pairs(sub) do + value[key] = member + end + defaulted = filled + elseif type(spec.additionalProperties) == 'table' then + for key, member in pairs(value) do + local coerced = _coerce(member, spec.additionalProperties.type) + value[key] = coerced + if coerced ~= nil then + _validate_value(coerced, spec.additionalProperties, path .. '.' .. tostring(key), context) + end + end + end + + -- Runs after the properties block, which writes the resolved members back + -- into `value`. Before that, a dependent supplied under an alias is not yet + -- there to be found. A `default` is an annotation and does not change the + -- instance, so a key that only holds one neither triggers a requirement nor + -- satisfies it. + if type(spec.dependentRequired) == 'table' then + for key, dependents in pairs(spec.dependentRequired) do + local trigger, trigger_key = _lookup(value, key) + if trigger ~= nil and not defaulted[trigger_key] and type(dependents) == 'table' then + for _, dependent in ipairs(dependents) do + local supplied, dependent_key = _lookup(value, dependent) + if supplied == nil or defaulted[dependent_key] then + _report(context, 'error', path, 'dependentRequired', string.format( + 'requires "%s" when "%s" is present.', dependent, key + )) + end + end + end + end + end +end + +--- Run every keyword check against one value. +--- A failed type check short-circuits, so one mistake yields one message. +--- @param value any Value to validate +--- @param spec table Compiled descriptor +--- @param path string Dotted path of the value +--- @param context table Validation context +--- @return nil +_validate_value = function(value, spec, path, context) + local names = _type_names(spec.type) + + if names then + for _, name in ipairs(names) do + if name == 'content' then + return + end + end + end + + if not _matches_type_spec(value, spec.type) then + _report(context, 'error', path, 'type', string.format( + 'must be of type "%s", got "%s".', _format_type(spec.type), _actual_type(value) + )) + return + end + + if spec.const ~= nil and value ~= spec.const then + _report(context, 'error', path, 'const', string.format( + 'must be %s, got %s.', _format_value(spec.const), _format_value(value) + )) + end + + _check_enum(value, spec, path, context) + _check_pattern(value, spec, path, context) + _check_numeric(value, spec, path, context) + _check_string_length(value, spec, path, context) + _check_array(value, spec, path, context) + _check_items(value, spec, path, context) + _check_object(value, spec, path, context) +end + +-- ============================================================================ +-- MAP VALIDATION +-- ============================================================================ + +--- Build the deprecation warning for a descriptor, and forward its value. +--- @param field string Field name +--- @param spec table Compiled descriptor +--- @param value any Current value +--- @param merged table Working values +--- @return string message Warning text +--- @return boolean cleared True when the deprecated key was removed +local function _apply_deprecation(field, spec, value, merged) + local deprecated = spec.deprecated + + if type(deprecated) == 'string' then + return string.format('option "%s" is deprecated. %s', field, deprecated), false + end + + if type(deprecated) ~= 'table' then + return string.format('option "%s" is deprecated.', field), false + end + + local message + if deprecated.since then + message = string.format('option "%s" is deprecated since %s.', field, deprecated.since) + else + message = string.format('option "%s" is deprecated.', field) + end + if deprecated.message then + message = message .. ' ' .. deprecated.message + end + + -- A v1 schema writes `replace-with`, which this vocabulary does not accept. + -- Say so, rather than dropping the forwarding without a word. + local unrecognised = {} + for key in pairs(deprecated) do + if key ~= 'since' and key ~= 'message' and key ~= 'replaceWith' then + unrecognised[#unrecognised + 1] = key + end + end + if #unrecognised > 0 then + table.sort(unrecognised) + message = message .. string.format( + ' The deprecation declares %s, which this vocabulary does not accept; use "replaceWith".', + table.concat(unrecognised, ', ') + ) + end + + local cleared = false + if deprecated.replaceWith then + local replacement = deprecated.replaceWith + message = message .. string.format(' Use "%s" instead.', replacement) + if _lookup(merged, replacement) == nil then + merged[replacement] = value + end + merged[field] = nil + cleared = true + end + + return message, cleared +end + +--- Record that a descriptor accepts a spelling, and report a contested one. +--- +--- Two descriptors can name the same spelling, when one declares as an alias +--- what another declares as its own name, or when two of them declare the same +--- alias. `pairs` yields descriptors in no particular order, so which field a +--- contested spelling resolves to is not decidable from the schema. It is +--- reported rather than resolved in silence, because the schema is what is +--- wrong and only its author can settle it. +--- +--- @param sources table Map of spelling to declared name +--- @param spelling string The spelling being declared +--- @param field string The declared name that accepts it +--- @param base_path string|nil Path prefix for findings +--- @param context table Validation context +local function _declare_spelling(sources, spelling, field, base_path, context) + -- The owner is read through `_lookup`, like every other name comparison in + -- this module, so a contest between the hyphen and the underscore spelling + -- of one name is found as well. Those sit under different keys but reach the + -- same document key at merge time, so they contest just as directly. + local owner = _lookup(sources, spelling) + if owner ~= nil and owner ~= field then + _report(context, 'warning', base_path and (base_path .. '.' .. spelling) or spelling, + 'aliases', string.format( + 'is declared by both "%s" and "%s"; which one accepts it is not defined.', + owner, field)) + end + -- Stored under the literal spelling either way. The map is also the set of + -- keys a descriptor claimed, and a contested spelling is still claimed: the + -- schema is ambiguous, which the warning says, and treating the key as + -- undeclared on top of that would report it as unknown as well. + sources[spelling] = sources[spelling] or field +end + + +--- Validate a map of values against a map of field descriptors. +--- @param values table Values to validate +--- @param descriptors table Field descriptor map +--- @param base_path string|nil Path prefix for findings +--- @param context table Validation context +--- @param options table|nil {unknown = 'warn'|'error'|'ignore', additional = descriptor} +--- @return table merged Values with aliases, coercion and defaults applied +--- @return table defaulted Set of field names whose value came from `default` +--- @return table sources Map of every spelling the schema accepts to the +--- declared name it resolves to. A reader that has to answer a question +--- about a name the caller wrote asks this rather than walking the +--- descriptors again, because the merge has already decided the answer. +--- It doubles as the set of keys a descriptor claimed, which is what the +--- unknown key pass below reads. +_validate_map = function(values, descriptors, base_path, context, options) + options = options or {} + local unknown_policy = options.unknown or 'ignore' + + local merged = {} + for key, value in pairs(values) do + merged[key] = value + end + + local defaulted = {} + local sources = {} + local fields = {} + + -- Three passes, because `pairs` yields descriptors in no particular order. + -- Aliases must land before deprecation reads a value, and a `replaceWith` + -- target must be populated before its own descriptor is validated. + for field, raw_spec in pairs(descriptors) do + local spec = _compile(raw_spec) + fields[#fields + 1] = { + name = field, + spec = spec, + path = base_path and (base_path .. '.' .. field) or field, + } + -- Two writes with different weight. Declaring a spelling fills an empty + -- slot only, while claiming the value under one overwrites whatever was + -- there. The value lands under whichever descriptor claimed it, so the + -- claim is what the map has to follow: a declaration that outranked it + -- would send a reader to a field the merge has already emptied. + _declare_spelling(sources, field, field, base_path, context) + + -- A value found under an alias, or under the other spelling, moves to the + -- name the schema declares. The key it came from is removed, so `merged` + -- never carries the same value twice, once coerced and once raw. + -- The declared name is read first, so it wins over any alias. + local supplied_key + local found, found_key = _lookup(merged, field) + if found ~= nil then + merged[field] = found + sources[found_key] = field + supplied_key = found_key + if found_key ~= field then + merged[found_key] = nil + end + end + + if type(spec.aliases) == 'table' then + for _, alias in ipairs(spec.aliases) do + _declare_spelling(sources, alias, field, base_path, context) + local aliased, alias_key = _lookup(merged, alias) + if aliased ~= nil then + sources[alias_key] = field + if merged[field] == nil then + merged[field] = aliased + supplied_key = alias_key + elseif alias_key ~= field then + -- Two spellings were supplied. The first one read wins, and the + -- other is reported rather than left behind unvalidated. Both + -- names come from the document, never from the schema. + _report(context, 'warning', + base_path and (base_path .. '.' .. field) or field, + 'aliases', + string.format('was given as both "%s" and "%s"; "%s" was used.', + supplied_key, alias_key, supplied_key)) + end + if alias_key ~= field then + merged[alias_key] = nil + end + end + end + end + end + + for _, entry in ipairs(fields) do + local value = merged[entry.name] + if entry.spec.deprecated and value ~= nil then + local message, cleared = _apply_deprecation(entry.name, entry.spec, value, merged) + _report(context, 'warning', entry.path, 'deprecated', message) + entry.cleared = cleared + end + end + + for _, entry in ipairs(fields) do + local field, spec, path = entry.name, entry.spec, entry.path + local value + + if entry.cleared then + value = nil + else + value = _coerce(merged[field], spec.type) + merged[field] = value + + if value == nil and spec.default ~= nil then + value = spec.default + merged[field] = value + defaulted[field] = true + end + end + + -- An empty string is a value the author wrote. Only a missing key is + -- absent, so `minLength`, `const` and `enum` can be tested against ''. + if spec.required == true and value == nil then + _report(context, 'error', path, 'required', 'is required but was not provided.') + elseif value ~= nil then + _validate_value(value, spec, path, context) + end + end + + if unknown_policy ~= 'ignore' or options.additional then + for key, value in pairs(values) do + if sources[key] == nil then + local path = base_path and (base_path .. '.' .. tostring(key)) or tostring(key) + if options.additional then + local coerced = _coerce(value, options.additional.type) + merged[key] = coerced + if coerced ~= nil then + _validate_value(coerced, options.additional, path, context) + end + elseif unknown_policy == 'error' then + _report(context, 'error', path, 'additionalProperties', 'is not a recognised key.') + else + _report(context, 'warning', path, 'additionalProperties', 'is not a recognised key and was ignored.') + end + end + end + end + + return merged, defaulted, sources +end + +--- Start a validation run. +--- @return table Fresh validation context +local function _new_context() + return { findings = {} } +end + +--- Close a validation run and shape the public return values. +--- @param context table Validation context +--- @param merged table Merged values +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged +--- @return table findings +local function _finish(context, merged) + local errors, warnings = _split_findings(context.findings) + return #errors == 0, errors, warnings, merged, context.findings +end + +-- ============================================================================ +-- PUBLIC API +-- ============================================================================ + +--- Load and parse a schema file. +--- Sections absent from the file default to an empty table. +--- +--- A failure is returned, not raised. Quarto replaces `error` with a logger +--- that returns instead of unwinding, so raising here would let a caller +--- carry on with a nil schema and fail later somewhere unrelated. +--- +--- @param filename string|nil Path to the schema file (defaults to '_schema.yml') +--- @return table|nil schema Schema with {['$schema'], options, shortcodes, formats, projects, attributes, classes} +--- @return string|nil err Why the schema could not be read +--- @usage local schema, err = M.load_schema('_schema.yml') +function M.load_schema(filename) + local raw, err = _parse_yaml_file(filename or '_schema.yml') + if err then + return nil, err + end + + return { + ['$schema'] = raw['$schema'], + options = raw.options or {}, + shortcodes = raw.shortcodes or {}, + formats = raw.formats or {}, + projects = raw.projects or {}, + attributes = raw.attributes or {}, + classes = raw.classes or {}, + }, nil +end + +--- List the keys of a parsed mapping in the order the schema declared them. +--- A schema is authored in a meaningful order, and a generator that turns one +--- into documentation needs that order rather than an arbitrary one. +--- Falls back to sorted keys for a table this module did not parse. +--- @param map table A mapping from a loaded schema +--- @return table Array of keys +--- @usage for _, name in ipairs(schema.key_order(loaded.options)) do +function M.key_order(map) + if type(map) ~= 'table' then + return {} + end + + local order = _key_order[map] + if order ~= nil then + local copy = {} + for index, key in ipairs(order) do + copy[index] = key + end + return copy + end + + local keys = {} + for key in pairs(map) do + keys[#keys + 1] = key + end + table.sort(keys) + return keys +end + +--- Validate a map of values against a map of field descriptors. +--- @param values table Values to validate +--- @param descriptors table Field descriptor map +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid True when no error was reported +--- @return table errors Array of error strings +--- @return table warnings Array of warning strings +--- @return table merged Values with aliases, coercion and defaults applied +--- @return table findings Array of structured findings +--- @usage local valid, errors, warnings, merged = M.validate(options, schema.options) +function M.validate(values, descriptors, options) + options = options or {} + local context = _new_context() + local merged = _validate_map(values or {}, descriptors or {}, nil, context, { + unknown = options.unknown or 'warn', + }) + return _finish(context, merged) +end + +--- Validate positional shortcode arguments against an argument descriptor array. +--- Each descriptor should carry a `name`; the merged table is keyed by name. +--- @param args table Array of positional argument values +--- @param argument_specs table Array of argument descriptors +--- @param options table|nil {unknown = 'warn'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged Map keyed by argument name +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_arguments(args, specs) +function M.validate_arguments(args, argument_specs, options) + options = options or {} + args = args or {} + argument_specs = argument_specs or {} + + local context = _new_context() + local merged = {} + + for index, raw_spec in ipairs(argument_specs) do + local spec = _compile(raw_spec) + local name = spec.name or tostring(index) + local path = string.format('argument %d ("%s")', index, name) + + local value = _coerce(args[index], spec.type) + if value == nil and spec.default ~= nil then + value = spec.default + end + merged[name] = value + + if spec.required == true and value == nil then + _report(context, 'error', path, 'required', 'is required but was not provided.') + elseif value ~= nil then + _validate_value(value, spec, path, context) + end + end + + if (options.unknown or 'warn') ~= 'ignore' and #args > #argument_specs then + _report(context, 'warning', nil, 'arguments', string.format( + 'Received %d arguments but only %d are defined. The extra arguments were ignored.', + #args, #argument_specs + )) + end + + return _finish(context, merged) +end + +--- Validate one shortcode call against its schema entry. +--- Covers the positional `arguments`, the named `attributes`, and the +--- parent-level `required` array of attribute names. +--- @param name string Shortcode name, used as the path prefix +--- @param args table Array of positional argument values +--- @param kwargs table Map of named attribute values +--- @param entry table Shortcode entry from `schema.shortcodes[name]` +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged {arguments = ..., attributes = ...} +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_shortcode('iconify', args, kwargs, schema.shortcodes.iconify) +function M.validate_shortcode(name, args, kwargs, entry, options) + options = options or {} + entry = entry or {} + + local context = _new_context() + local merged = { arguments = {}, attributes = {} } + + if type(entry.arguments) == 'table' then + local _, errors, warnings, arguments = M.validate_arguments(args or {}, entry.arguments, options) + merged.arguments = arguments + for _, message in ipairs(errors) do + _report(context, 'error', nil, 'arguments', name .. ' ' .. message) + end + for _, message in ipairs(warnings) do + _report(context, 'warning', nil, 'arguments', name .. ' ' .. message) + end + end + + local declared = type(entry.attributes) == 'table' + local spellings + if declared then + local attributes, _, resolved = _validate_map(kwargs or {}, entry.attributes, name, context, { + unknown = options.unknown or 'warn', + }) + merged.attributes = attributes + spellings = resolved + end + + if type(entry.required) == 'table' then + -- `merged.attributes` is filled only when the entry declares `attributes`, + -- and it is authoritative when it is: it keeps every unclaimed key and it + -- drops a key that a deprecation cleared. The vocabulary allows `required` + -- on its own, so fall back to what the caller supplied only when the + -- entry declares no `attributes`. A name that names an alias is not + -- missing either: `_validate_map` moves its value to the field that + -- declares the alias and clears the alias spelling, the same way it + -- clears a deprecated key, so look for the value under that field. The + -- merge reports which declared name each spelling resolves to, so that + -- is one lookup rather than a second walk over the descriptors. It goes + -- through `_lookup`, the same as every other name comparison in this + -- module, so a hyphen in one spelling and an underscore in the other + -- still match. + -- + -- This asks what the shortcode receives, not what the author wrote, so + -- a `default` present in `merged.attributes` satisfies a name here. + -- That is the opposite of `dependentRequired` above, which asks what + -- the author wrote and treats a default as an annotation nobody + -- supplied. Neither reading is a bug: they answer different questions + -- about the same instance. + local function _required_satisfied(required) + local field = (spellings and _lookup(spellings, required)) or required + if _lookup(merged.attributes, field) ~= nil then + return true + end + if not declared then + return _lookup(kwargs or {}, required) ~= nil + end + return false + end + + for _, required in ipairs(entry.required) do + if not _required_satisfied(required) then + _report(context, 'error', name .. '.' .. required, 'required', + 'is required but was not provided.') + end + end + end + + return _finish(context, merged) +end + +--- Validate Pandoc element attributes against one group of the `attributes` +--- section. Undeclared attributes are ignored by default, because a Pandoc +--- element legitimately carries attributes from Quarto and other filters. +--- @param attributes table Map of attribute values +--- @param group string Group key, such as a class, identifier or element name +--- @param schema table Loaded schema +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_attributes(el.attributes, 'callout-note', schema) +function M.validate_attributes(attributes, group, schema, options) + options = options or {} + local descriptors = schema and schema.attributes and _lookup(schema.attributes, group) or nil + + local context = _new_context() + if descriptors == nil then + return _finish(context, attributes or {}) + end + + local merged = _validate_map(attributes or {}, descriptors, group, context, { + unknown = options.unknown or 'ignore', + }) + return _finish(context, merged) +end + +--- Validate the options of one output format against the `formats` section. +--- Quarto merges the options of the selected format into the top level of the +--- metadata, so the values are read from there. The format name is never a key. +--- A value is collected only when a descriptor declares its name, which keeps +--- the document's own keys, such as `title`, out of the merge. The `unknown` +--- option therefore has nothing to report here, and is kept so that the entry +--- points keep one signature. +--- @param meta table Document metadata +--- @param format string Format name, such as 'html' or 'typst' +--- @param schema table Loaded schema +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_format(meta, 'typst', schema) +function M.validate_format(meta, format, schema, options) + options = options or {} + local descriptors = schema and schema.formats and _lookup(schema.formats, format) or nil + + local context = _new_context() + if descriptors == nil then + return _finish(context, {}) + end + + -- Each value is stored under the spelling the document wrote, never under the + -- declared name. `_validate_map` is what moves an alias or the other spelling + -- to the declared name, and it needs the original key to say which two + -- spellings a document supplied for one field. + -- + -- `_lookup` returns the first spelling it finds, so a document that wrote one + -- name in both spellings loses the other. Every other entry point reports the + -- loser through the `unknown` option, which has nothing to report here, so the + -- collection reports it in the same words `_validate_map` uses. + local values = {} + for field, raw_spec in pairs(descriptors) do + local spec = _compile(raw_spec) + + -- One entry per name, and not one per spelling. `_lookup` reads both + -- spellings of whichever entry is kept, so an alias that is only the other + -- spelling of a name already listed adds nothing. Probing it as well would + -- collect the two spellings under separate keys, and one mistake would then + -- be reported three times, once by each probe and once by `_validate_map`. + local names = { field } + local listed = { [(field:gsub('_', '-'))] = true } + if type(spec) == 'table' and type(spec.aliases) == 'table' then + for _, alias in ipairs(spec.aliases) do + -- A schema file is not read against the meta-schema, so an unquoted + -- `no` reaches here as a boolean. `_lookup` skips a key that is not a + -- string everywhere else, and so does this. + if type(alias) == 'string' then + local normalised = (alias:gsub('_', '-')) + if not listed[normalised] then + listed[normalised] = true + names[#names + 1] = alias + end + end + end + end + + for _, name in ipairs(names) do + local value, found_key = _lookup(meta, name) + if value ~= nil then + values[found_key] = _convert_pandoc_value(value) + + for _, other in ipairs({ (name:gsub('%-', '_')), (name:gsub('_', '-')) }) do + if other ~= found_key and meta[other] ~= nil then + _report(context, 'warning', format .. '.' .. field, 'aliases', + string.format('was given as both "%s" and "%s"; "%s" was used.', + found_key, other, found_key)) + end + end + end + end + end + + local merged = _validate_map(values, descriptors, format, context, { + unknown = options.unknown or 'ignore', + }) + return _finish(context, merged) +end + +--- Extract an extension's options from document metadata. +--- Reads `meta.extensions[extension_name]` and converts Pandoc values to +--- native Lua values, keeping every key exactly as the document wrote it. +--- @param meta table Document metadata +--- @param extension_name string Extension name +--- @return table Map of option values +--- @usage local options = M.extract_meta_options(meta, 'iconify') +function M.extract_meta_options(meta, extension_name) + local extension_meta = meta and meta['extensions'] and meta['extensions'][extension_name] + if not extension_meta then + return {} + end + + local options = {} + for key, value in pairs(extension_meta) do + options[tostring(key)] = _convert_pandoc_value(value) + end + return options +end + +--- Load a schema, validate an extension's options, and log the outcome. +--- A schema that cannot be loaded is reported and the render continues with +--- no validated options, rather than aborting on a configuration file. +--- @param meta table Document metadata +--- @param extension_name string Extension name +--- @param schema_path string Path to the schema file, resolved by the caller +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return table merged Validated options with defaults applied +--- @usage local options = M.validate_options(meta, 'iconify', quarto.utils.resolve_path('_schema.yml')) +function M.validate_options(meta, extension_name, schema_path, options) + local schema, err = M.load_schema(schema_path) + if err then + _env.report_error(string.format('Extension "%s": %s', extension_name, err)) + return {} + end + + if not schema.options or next(schema.options) == nil then + return {} + end + + local values = M.extract_meta_options(meta, extension_name) + local valid, errors, warnings, merged = M.validate(values, schema.options, options) + + if #warnings > 0 then + _env.warn(M.format_warnings(warnings, extension_name)) + end + if not valid then + _env.report_error(M.format_errors(errors, extension_name)) + end + + return merged +end + +--- Format error messages as one readable block. +--- @param errors table Array of error strings +--- @param extension_name string|nil Extension name for context +--- @return string Formatted message, or an empty string +--- @usage local message = M.format_errors(errors, 'iconify') +function M.format_errors(errors, extension_name) + if #errors == 0 then + return '' + end + + local prefix = extension_name and ('Extension "' .. extension_name .. '": ') or '' + local lines = {} + for index, message in ipairs(errors) do + table.insert(lines, ' ' .. index .. '. ' .. message) + end + + return prefix .. 'Configuration validation failed:\n' .. table.concat(lines, '\n') +end + +--- Format warning messages as one readable block. +--- @param warnings table Array of warning strings +--- @param extension_name string|nil Extension name for context +--- @return string Formatted message, or an empty string +--- @usage local message = M.format_warnings(warnings, 'iconify') +function M.format_warnings(warnings, extension_name) + if #warnings == 0 then + return '' + end + + local prefix = extension_name and ('Extension "' .. extension_name .. '": ') or '' + local lines = {} + for index, message in ipairs(warnings) do + table.insert(lines, ' ' .. index .. '. ' .. message) + end + + return prefix .. 'Configuration warnings:\n' .. table.concat(lines, '\n') +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/code-window.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/code-window.lua new file mode 100644 index 0000000..d67c3c0 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/code-window.lua @@ -0,0 +1,1395 @@ +--- @module "code-window" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @brief Code block window decorations with multiple styles +--- @description Adds window chrome (macOS traffic lights, Windows title bar +--- buttons, or plain filename) to code blocks in HTML, Reveal.js, and Typst +--- formats. Registered at pre-quarto to process all formats in a single pass. + +-- ============================================================================ +-- EXTENSION NAME +-- ============================================================================ + +local EXTENSION_NAME = 'code-window' +local str = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/string.lua'):gsub('%.lua$', '')) +local log = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/logging.lua'):gsub('%.lua$', '')) +local meta_mod = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/metadata.lua'):gsub('%.lua$', '')) +local pdoc = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/pandoc-helpers.lua'):gsub('%.lua$', '')) +local html_mod = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/html.lua'):gsub('%.lua$', '')) +local cell_output = require(quarto.utils.resolve_path('_modules/cell-output.lua'):gsub('%.lua$', '')) +local code_annotations = nil +local checker = nil + +-- ============================================================================ +-- DEFAULTS AND STATE +-- ============================================================================ + +--- @class CodeWindowConfig +--- @field enabled boolean Whether code-window styling is enabled +--- @field auto_filename boolean Whether to auto-generate filename from language +--- @field style string Window decoration style ('macos', 'windows', 'default') +--- @field cell_output boolean Whether the output of an executed cell is framed +--- @field typst_wrapper string Typst wrapper function name +--- @field hotfix_code_annotations boolean Whether to apply the code-annotations hot-fix for Typst +--- @field hotfix_skylighting boolean Whether to apply the Skylighting hot-fix for Typst + +local VALID_STYLES = { ['default'] = true, ['macos'] = true, ['windows'] = true } + +--- Prefix of every code block attribute this extension owns, the ones the +--- schema declares and the internal label the language module writes alike. +local ATTRIBUTE_PREFIX = 'code-window-' + +--- Defaults for the two cases where the schema cannot answer: a format the +--- extension does not act on, where the check never runs, and a schema that +--- could not be read, which is a state this extension renders through rather +--- than stopping for. Everywhere else _schema.yml decides these seven, so the +--- values below are a fallback and not a second place to change one of them. +--- The hotfix defaults are a different story, and are still kept by hand in +--- HOTFIX_DEFAULTS: they are nested in the schema and read on their own path, +--- so changing one in _schema.yml alone still changes nothing. +--- All seven stay listed, because the second case has nothing else to read +--- from, and an option added to the schema needs an entry here as well or it +--- has no default at all on a format the extension does not act on. +--- Where the two disagree, the schema wins on html and typst, and a format +--- the extension does not act on reads "enabled" alone, which is the one that +--- chooses whether a block's attributes are checked. So keep that one in step +--- with the schema, and treat a difference in the other six as a thing to +--- correct rather than a thing that shows. +local FALLBACK_DEFAULTS = { + ['enabled'] = 'true', + ['auto-filename'] = 'true', + ['style'] = 'macos', + ['cell-output'] = 'false', + ['wrapper'] = 'code-window', + ['collapse'] = 'false', + ['lines-label'] = 'true', +} + +local VALID_COLLAPSE = { + ['false'] = false, + ['true'] = 'closed', + ['closed'] = 'closed', + ['open'] = 'open', +} + +local HOTFIX_DEFAULTS = { + ['code-annotations'] = true, + ['skylighting'] = true, + ['typst-title'] = true, +} + +local CURRENT_FORMAT = nil +local CONFIG = nil +local TYPST_BG_COLOUR = nil +local ANNOTATION_BLOCK_COUNTER = 0 + +-- ============================================================================ +-- CELL OUTPUT +-- ============================================================================ + +--- Check whether the extension acts on the format being rendered. It draws +--- chrome for html, which covers Reveal.js, and for typst, and leaves every +--- other format as Quarto writes it. +--- @return boolean +local function acts_on_format() + return CURRENT_FORMAT == 'html' or CURRENT_FORMAT == 'typst' +end + +--- Check whether this render draws chrome at all: the extension is on, and the +--- format is one it acts on. Every pass that exists only to serve the chrome +--- asks this before it does any work, so none of them has to carry its own +--- copy of the two conditions. +--- @return boolean +local function draws_chrome() + return CONFIG ~= nil and CONFIG.enabled and acts_on_format() +end + +--- Check whether a block holds the output of an executed cell that the engine +--- did not name. Such a block keeps the shape Quarto gave it. +--- @param block pandoc.CodeBlock Code block element +--- @return boolean +local function is_unnamed_cell_output(block) + return cell_output.is_marked(block) and str.is_empty(block.attributes['filename']) +end + +-- ============================================================================ +-- BLOCK-LEVEL STYLE OVERRIDE +-- ============================================================================ + +--- Turn a schema-resolved boolean back into the "true"/"false" string this +--- file's own comparisons already use. A value the schema rejects arrives +--- here unchanged (the original string the document wrote), and is passed +--- through as-is. +--- @param value any Resolved attribute value +--- @return any +local function stringify_bool(value) + if type(value) == 'boolean' then + return value and 'true' or 'false' + end + return value +end + +--- Take the attributes an author can write off a block the filter processes, +--- and answer the label the language module wrote. +--- Pandoc writes an attribute nothing removes into the HTML as +--- data-code-window-*, so a block this filter ran on carries none of them +--- onward: one call per format path, above every early return, is what keeps +--- that true, and an attribute added to the schema later is covered with no +--- change here. +--- Two prefixed attributes are not author-written and are treated on their +--- own terms. code-window-auto-label is returned rather than dropped, because +--- the windowing paths still need its value. The cell-output marker is left +--- alone, because the Typst pass reads it after this runs, and the +--- cell-output module removes it itself. +--- The HTML path writes code-window-lines-label back afterwards, for the +--- injected script to read and remove in the browser. +--- @param block pandoc.CodeBlock Code block element +--- @return string|nil auto_label Label written by the language module, if any +local function take_block_attributes(block) + local auto_label = block.attributes['code-window-auto-label'] + local owned = {} + for key, _ in pairs(block.attributes) do + if key:sub(1, #ATTRIBUTE_PREFIX) == ATTRIBUTE_PREFIX and not cell_output.is_marker(key) then + owned[#owned + 1] = key + end + end + for _, key in ipairs(owned) do + block.attributes[key] = nil + end + return auto_label +end + +--- Resolve a block's attributes against the schema, then take this +--- extension's attributes off the block. +--- The resolved values are copied into a table of their own before anything +--- is removed. checker:attributes hands back the list it was given when it +--- has no schema to check against, which is a case this extension carries on +--- through by design, and the caller would then hold the block's live +--- attributes. Taking them off would empty the very table every reader below +--- reads, and each per-block override would be ignored with no warning. +--- @param block pandoc.CodeBlock Code block element +--- @return table resolved What this block's attributes resolve to +--- @return string|nil auto_label Label written by the language module, if any +local function resolve_and_take(block) + local resolved = {} + for key, value in pairs(checker:attributes(block.attributes, 'CodeBlock')) do + resolved[key] = value + end + return resolved, take_block_attributes(block) +end + +--- Read the block-level style override from code-window-style attribute. +--- Returns the validated style value or nil. +--- An unrecognised value is not warned about here: the schema check this +--- block's caller already ran (checker:attributes, group "CodeBlock") reports +--- the same mistake in its own words, and this file's own message added +--- nothing beyond restating the fallback, which the schema check's +--- documented policy ("no finding stops a render") already implies for every +--- attribute. +--- @param resolved table This block's attributes, resolved against the schema +--- @return string|nil Style override value +local function read_block_style(resolved) + local block_style = resolved['code-window-style'] + if not block_style or block_style == '' then + return nil + end + if type(block_style) == 'string' and VALID_STYLES[block_style] then + return block_style + end + return nil +end + +--- Resolve a collapse value coming from extension options or a code-block +--- attribute. Returns "open"/"closed" when the window should be collapsible, +--- nil when collapsing is off or the value is unrecognised. +--- Both callers are now covered by a schema check that reports an +--- unrecognised value in its own words: the "collapse" document option by +--- checker:options (Meta, below), and the "code-window-collapse" block +--- attribute by checker:attributes on the "CodeBlock" group +--- (read_block_collapse, below). This function used to warn again for both, +--- restating the same enum with no remedy, legal alternative, or +--- format-specific consequence the schema message lacks, so neither call +--- site asks for it any more. +--- raw is never a Lua boolean here, for either caller: the "collapse" +--- document option is read through meta_mod.get_options, which always +--- stringifies, and "code-window-collapse" declares type: string in +--- _schema.yml (not a [boolean, string] union), specifically because a +--- Pandoc attribute value is always a string, so a union type paired with +--- an enum holding an unquoted true/false can never match it: `_coerce` +--- returns a value unchanged as soon as its current Lua type already +--- matches a member of the declared type list, so the string always wins +--- and boolean coercion is never attempted, which made the enum check +--- compare the string "true" against the schema's own boolean `true` and +--- fail on every legal value. No stringify step is needed on this path. +--- @param raw string|nil Raw collapse value +--- @return string|nil Resolved collapse mode ("open"/"closed") or nil +local function resolve_collapse(raw) + if raw == nil or raw == '' then + return nil + end + local resolved = VALID_COLLAPSE[raw] + if resolved == nil then + return nil + end + if resolved == false then + return nil + end + return resolved +end + +--- Read the per-block collapse override from code-window-collapse attribute. +--- code-window-collapse layers over the "collapse" document option +--- (CONFIG.collapse), which is the reason it must not be read with a bare +--- "or": a resolved boolean false has to reach resolve_collapse and disable +--- collapsing, not fall through to the document option. The schema declares +--- no default for this attribute, so the merged table already answers nil +--- exactly when the document left it unwritten, with no separate presence +--- test needed. +--- @param resolved table This block's attributes, resolved against the schema +--- @return string|nil Resolved collapse mode ("open"/"closed") or nil when off +--- @return boolean Whether this block decided, which is what separates an +--- explicit "false" from an attribute the block never wrote +local function read_block_collapse(resolved) + local raw = resolved['code-window-collapse'] + if raw == nil then + return nil, false + end + local mode = VALID_COLLAPSE[raw] + if mode == nil then + -- A value the schema does not accept. The check reports it, and the + -- document option decides as though the block had written nothing. + return nil, false + end + return mode or nil, true +end + +--- Read a highlight-lines spec from the block, looking at the +--- code-window-lines attribute first and falling back to Quarto's +--- code-line-numbers attribute when it carries a non-boolean spec. +--- Returns the cleaned spec string or nil. +--- The "lines-label" option is answered here rather than at each call site, +--- so neither format path repeats the test. code-line-numbers belongs to +--- Quarto and is read, never written. +--- @param block pandoc.CodeBlock Code block element +--- @param resolved table This block's attributes, resolved against the schema +--- @return string|nil Line spec to display in the title bar +local function read_block_lines_label(block, resolved) + if not CONFIG.lines_label then + return nil + end + + local raw = resolved['code-window-lines'] + if raw ~= nil and raw ~= '' then + return raw + end + + local cln = block.attributes['code-line-numbers'] + if not cln or cln == '' or cln == 'true' or cln == 'false' then + return nil + end + return cln +end + +--- @class WindowOverrides +--- @field enabled boolean Whether this block asks for chrome at all +--- @field style string|nil Style this block asks for, in place of the document's +--- @field lines_label string|nil Highlighted-lines spec for the title bar +--- @field no_auto_filename boolean Whether this block refuses a derived name + +--- Read what a block asks of the chrome, in the order and the way both format +--- paths ask for it. Collapse is left out, because it is the one override that +--- is HTML only and the Typst path never reads. +--- code-window-enabled declares default: true, and code-window-no-auto-filename +--- declares default: false, each matching this file's own fallback. Neither +--- layers over a document option, so the resolved value answers directly, and +--- the stringified value decides the flag, which is what makes an explicit +--- "false" behave like absence rather than like "true". +--- All four are lookups on the resolved table and on the block, and take +--- nothing off the block, which take_block_attributes already did. So a caller +--- may read them all before it tests the opt-out, and their order is free. +--- @param block pandoc.CodeBlock Code block element +--- @param resolved table This block's attributes, resolved against the schema +--- @return WindowOverrides +local function read_window_overrides(block, resolved) + return { + enabled = stringify_bool(resolved['code-window-enabled']) ~= 'false', + style = read_block_style(resolved), + lines_label = read_block_lines_label(block, resolved), + no_auto_filename = stringify_bool(resolved['code-window-no-auto-filename']) == 'true', + } +end + +-- ============================================================================ +-- TYPST FUNCTION DEFINITION +-- ============================================================================ + +--- Typst colour helpers for adaptive theme support. +--- Always injected so the code-window function can derive border, surface, +--- muted, and label colours from the page background at render time. +local TYPST_COLOUR_HELPERS = [==[ +// code-window: adaptive colour helpers (derive UI tones from page background) +#let _cw-page-bg() = { + let f = page.fill + if type(f) == color { f } else { luma(255) } +} + +#let _cw-fg(bg) = { + let comps = bg.components(alpha: false) + let lum = if comps.len() == 1 { + comps.at(0) / 100% + } else { + 0.2126 * comps.at(0) / 100% + 0.7152 * comps.at(1) / 100% + 0.0722 * comps.at(2) / 100% + } + if lum < 0.5 { luma(255) } else { luma(0) } +} +]==] + +--- Typst annotation helper functions (state, colour, circled numbers, annotation items). +--- Only injected when at least one hot-fix is active. +local TYPST_ANNOTATION_DEF = [==[ +// code-window: annotation state passed to Skylighting via Typst state +#let _cw-annotations = state("cw-annotations", none) + +// Derive a contrasting annotation colour from a background fill. +// Light backgrounds get dark circles; dark backgrounds get light circles. +// Uses ITU-R BT.709 luminance coefficients, matching quarto-cli PR #14170. +#let code-window-annote-colour(bg) = { + if type(bg) == color { + let comps = bg.components(alpha: false) + let lum = if comps.len() == 1 { + comps.at(0) / 100% + } else { + 0.2126 * comps.at(0) / 100% + 0.7152 * comps.at(1) / 100% + 0.0722 * comps.at(2) / 100% + } + if lum < 0.5 { luma(200) } else { luma(60) } + } else { + luma(60) + } +} + +#let code-window-circled-number(n, bg-colour: none) = { + let c = if bg-colour != none { code-window-annote-colour(bg-colour) } else { luma(120) } + box(baseline: 20%, circle( + radius: 4.5pt, + stroke: 0.5pt + c, + )[#set text(size: 5.5pt, fill: c); #align(center + horizon, str(n))]) +} + +#let code-window-annotation-item(block-id, n, content) = { + let lbl-prefix = "cw-" + str(block-id) + "-" + context { + let target = label(lbl-prefix + "line-" + str(n)) + let has-target = query(target).len() > 0 + [#block(above: 0.4em, below: 0.4em)[ + #if has-target { + link(target)[#code-window-circled-number(n)] + } else { + code-window-circled-number(n) + } + #h(0.4em) + #content + ] #label(lbl-prefix + "item-" + str(n))] + } +} + +#let code-window-annotated-content(content, annotations: (:), bg-colour: none, block-id: 0) = { + if annotations.len() > 0 { + _cw-annotations.update((annotations: annotations, bg-colour: bg-colour, block-id: block-id)) + content + _cw-annotations.update(none) + } else { + content + } +} +]==] + +--- Typst code-window body content template. +--- Uses annotation wrapper when hot-fixes are active, plain content otherwise. +local TYPST_CONTENT_WITH_ANNOTATIONS = [==[ + code-window-annotated-content( + content, + annotations: annotations, + bg-colour: bg-colour, + block-id: block-id, + ) +]==] + +local TYPST_CONTENT_PLAIN = [==[ + content +]==] + +--- Build the complete Typst code-window function definition. +--- @param has_hotfixes boolean Whether at least one hot-fix is active +--- @return string Typst function definition(s) +local function build_typst_function_def(has_hotfixes) + local content_block = has_hotfixes + and TYPST_CONTENT_WITH_ANNOTATIONS + or TYPST_CONTENT_PLAIN + + local fn_def = string.format([==[ +#let code-window( + content, + filename: none, + is-auto: false, + style: "macos", + annotations: (:), + bg-colour: none, + block-id: 0, + lines-label: none, +) = { + context { + let page-bg = _cw-page-bg() + let fg = _cw-fg(page-bg) + let border-colour = color.mix((fg, 15%%), (page-bg, 85%%)) + let surface-fill = color.mix((fg, 5%%), (page-bg, 95%%)) + let muted-colour = color.mix((fg, 50%%), (page-bg, 50%%)) + let label-colour = color.mix((fg, 65%%), (page-bg, 35%%)) + + let filename-label = if filename != none { + text( + size: if is-auto { 0.7em } else { 0.85em }, + weight: 500, + fill: label-colour, + if is-auto { upper(filename) } else { filename }, + ) + } + + let lines-chip = if lines-label != none { + box( + inset: (x: 0.4em, y: 0.05em), + outset: (y: 0.15em), + radius: 3pt, + fill: color.mix((fg, 10%%), (page-bg, 90%%)), + stroke: 0.5pt + border-colour, + text(size: 0.7em, weight: 500, fill: label-colour, lines-label), + ) + } + + let traffic-lights = box( + inset: (right: 0.5em), + stack( + dir: ltr, + spacing: 0.425em, + circle(radius: 0.425em, fill: rgb("#ff5f56"), stroke: none), + circle(radius: 0.425em, fill: rgb("#ffbd2e"), stroke: none), + circle(radius: 0.425em, fill: rgb("#27c93f"), stroke: none), + ), + ) + + let window-buttons = box( + inset: (left: 0.5em), + { + set line(stroke: 1pt + muted-colour) + stack( + dir: ltr, + spacing: 0.8em, + // Minimise (horizontal line) + box(width: 0.6em, height: 0.6em, align(horizon, line(length: 100%%))), + // Maximise (square) + box(width: 0.6em, height: 0.6em, stroke: 1pt + muted-colour), + // Close (x) + box(width: 0.6em, height: 0.6em, { + place(line(start: (0%%, 0%%), end: (100%%, 100%%))) + place(line(start: (100%%, 0%%), end: (0%%, 100%%))) + }), + ) + }, + ) + + let _label-with-chip = if lines-chip != none { + stack(dir: ltr, spacing: 0.5em, filename-label, lines-chip) + } else { + filename-label + } + + let title-bar = if style == "macos" { + grid( + columns: (auto, 1fr), + align: (left + horizon, right + horizon), + gutter: 0.5em, + stroke: 0pt, + traffic-lights, + _label-with-chip, + ) + } else if style == "windows" { + grid( + columns: (1fr, auto), + align: (left + horizon, right + horizon), + gutter: 0.5em, + stroke: 0pt, + _label-with-chip, + window-buttons, + ) + } else { + // default: plain filename, left-aligned + _label-with-chip + } + + block( + width: 100%%, + stroke: 1pt + border-colour, + radius: 8pt, + clip: true, + { + block( + width: 100%%, + fill: surface-fill, + inset: (x: 1em, y: 0.6em), + below: 0pt, + radius: 0pt, + stroke: (bottom: 1pt + border-colour), + title-bar, + ) + // Strip code block chrome so content fills flush against the window body. + // set block() provides defaults for Skylighting blocks (explicit fill preserved). + // show raw overrides the document-level raw block styling (fill, radius). + { + set block( + width: 100%%, + inset: 8pt, + radius: 0pt, + stroke: none, + above: 0pt, + below: 0pt, + ) + show raw.where(block: true): set block( + fill: none, + width: 100%%, + radius: 0pt, + stroke: none, + above: 0pt, + below: 0pt, + ) +%s } + }, + ) + } +} +]==], content_block) + + local result = TYPST_COLOUR_HELPERS + if has_hotfixes then + result = result .. TYPST_ANNOTATION_DEF + end + return result .. fn_def +end + +-- ============================================================================ +-- TYPST PROCESSING +-- ============================================================================ + +--- Get the next unique block ID for annotation linking. +--- @return integer +local function next_block_id() + ANNOTATION_BLOCK_COUNTER = ANNOTATION_BLOCK_COUNTER + 1 + return ANNOTATION_BLOCK_COUNTER +end + +--- Build the Typst bg-colour parameter string. +--- @return string Empty string or ', bg-colour: rgb("...")' +local function typst_bg_colour_param() + if not TYPST_BG_COLOUR then + return '' + end + return string.format(', bg-colour: rgb("%s")', TYPST_BG_COLOUR) +end + +--- Escape a string for embedding in a Typst double-quoted string literal. +--- @param s string +--- @return string +local function typst_escape_string(s) + return (s:gsub('\\', '\\\\'):gsub('"', '\\"')) +end + +--- Build a code-window opening RawBlock for Typst. +--- @param filename string +--- @param is_auto boolean +--- @param style string +--- @param annotations table|nil +--- @param block_id integer +--- @param lines_label string|nil Highlighted-lines spec to display in the title bar +--- @return pandoc.RawBlock +local function typst_code_window_open(filename, is_auto, style, annotations, block_id, lines_label) + local annot_param = '' + if annotations and next(annotations) then + annot_param = string.format(', annotations: %s, block-id: %d', + code_annotations.annotations_to_typst_dict(annotations), block_id) + end + + local lines_param = '' + if lines_label and lines_label ~= '' then + lines_param = string.format(', lines-label: "%s"', typst_escape_string(lines_label)) + end + + return pandoc.RawBlock('typst', string.format( + '#%s(filename: "%s", is-auto: %s, style: "%s"%s%s%s)[', + CONFIG.typst_wrapper, + typst_escape_string(filename), + is_auto and 'true' or 'false', + style, + annot_param, + typst_bg_colour_param(), + lines_param + )) +end + +--- Build a standalone annotation wrapper for non-windowed blocks. +--- @param annotations table +--- @param block_id integer +--- @return pandoc.RawBlock opening, pandoc.RawBlock closing +local function typst_annotation_wrapper(annotations, block_id) + local open = pandoc.RawBlock('typst', string.format( + '#%s-annotated-content(annotations: %s, block-id: %d%s)[', + CONFIG.typst_wrapper, + code_annotations.annotations_to_typst_dict(annotations), + block_id, + typst_bg_colour_param() + )) + local close = pandoc.RawBlock('typst', ']') + return open, close +end + +-- ============================================================================ +-- HTML PROCESSING +-- ============================================================================ + +--- Process CodeBlock for HTML/Reveal.js formats. +--- Explicit-filename blocks are returned for Quarto to wrap; a marker class +--- is added when a block-level style override is present. +--- Auto-filename blocks are wrapped directly with the style class. +--- The block reaches here with its code-window attributes already taken off +--- by take_block_attributes, so every value below is read from the resolved +--- table and the block carries none of them onward. +--- @param block pandoc.CodeBlock Code block element +--- @param resolved table This block's attributes, resolved against the schema +--- @param auto_label string|nil Label written by the language module, if any +--- @return pandoc.Div|pandoc.CodeBlock Wrapped block or original +local function process_html(block, resolved, auto_label) + local overrides = read_window_overrides(block, resolved) + + -- Per-block opt-out: code-window-enabled="false" skips window chrome. + if not overrides.enabled then + return block + end + + local block_collapse, block_decided = read_block_collapse(resolved) + local effective_collapse = block_collapse + if not block_decided then + effective_collapse = CONFIG.collapse + end + local explicit_filename = block.attributes['filename'] + + -- Add the marker classes and the chip the injected script reads. Both + -- branches below end this way, on the same block this closure captures. + local function mark() + if overrides.style then + table.insert(block.classes, 'cw-style-' .. overrides.style) + end + -- A block that turned collapsing off is marked as well. The script reads + -- the absence of a marker as "this block said nothing" and falls back to + -- the document setting, so an opt-out needs a marker of its own. + if effective_collapse then + table.insert(block.classes, 'cw-collapse-' .. effective_collapse) + elseif block_decided and CONFIG.collapse then + table.insert(block.classes, 'cw-collapse-none') + end + if overrides.lines_label then + block.attributes['code-window-lines-label'] = overrides.lines_label + end + end + + if explicit_filename and explicit_filename ~= '' then + -- Let Quarto create the .code-with-filename wrapper, and mark the block so + -- the injected script can promote a block-level override onto that wrapper. + mark() + return block + end + + if not CONFIG.auto_filename or overrides.no_auto_filename then + return block + end + + -- Blocks with a language of their own are labelled with its class. + -- Set the filename attribute so Quarto creates its own .code-with-filename + -- wrapper. This preserves the CodeBlock+OrderedList sibling structure + -- needed by Quarto's code-annotations processor. + block.attributes['filename'] = auto_label or block.classes[1] + table.insert(block.classes, 'cw-auto') + mark() + + return block +end + +-- ============================================================================ +-- FILTER HANDLERS +-- ============================================================================ + +--- Generate a JS snippet that builds .code-with-filename wrappers for +--- auto-filename blocks (marked with cw-auto at pre-quarto), applies the +--- configured default style class to all .code-with-filename wrappers, +--- promotes block-level cw-style-* / cw-collapse-* marker classes, inserts +--- a highlighted-lines chip when data-code-window-lines-label is set on the inner +--- pre, and wraps collapsible windows in a
element where the +--- title bar acts as the . +--- @param default_style string The configured default style +--- @param default_collapse string|nil Default collapse mode ("open"/"closed"/nil) +--- @return string JavaScript code +local function make_style_js(default_style, default_collapse) + local default_collapse_js = default_collapse and ('"' .. default_collapse .. '"') or 'null' + return string.format([=[ +document.addEventListener("DOMContentLoaded",function(){ + var DEFAULT_COLLAPSE=%s; + document.querySelectorAll("pre.cw-auto").forEach(function(pre){ + var fn=pre.closest("[data-filename]"); + if(!fn)return; + var name=fn.getAttribute("data-filename"); + if(!name)return; + var scaffold=pre.closest(".code-copy-outer-scaffold")||pre.closest(".sourceCode"); + if(!scaffold)return; + var w=document.createElement("div"); + w.className="code-with-filename code-window-auto"; + var tb=document.createElement("div"); + tb.className="code-with-filename-file"; + var tp=document.createElement("pre"); + var ts=document.createElement("strong"); + ts.textContent=name; + tp.appendChild(ts);tb.appendChild(tp); + scaffold.parentNode.insertBefore(w,scaffold); + w.appendChild(tb);w.appendChild(scaffold); + pre.classList.remove("cw-auto"); + }); + function findMarkerSource(el){ + return el.querySelector('pre[class*="cw-style-"],pre[class*="cw-collapse-"],pre[data-code-window-lines-label]') + ||el.querySelector('[class*="cw-style-"],[class*="cw-collapse-"],[data-code-window-lines-label]'); + } + document.querySelectorAll(".code-with-filename").forEach(function(el){ + var marker=findMarkerSource(el); + var styleApplied=/\bcode-window-(macos|windows|default)\b/.test(el.className); + if(!styleApplied){ + var sm=marker&&marker.className.match(/cw-style-(\w+)/); + if(sm){el.classList.add("code-window-"+sm[1]);marker.classList.remove(sm[0]);} + else{el.classList.add("code-window-%s");} + } + var collapse=null; + if(marker){ + var cm=marker.className.match(/cw-collapse-(open|closed|none)/); + if(cm){collapse=cm[1]==='none'?false:cm[1];marker.classList.remove(cm[0]);} + } + if(collapse===null&&DEFAULT_COLLAPSE){collapse=DEFAULT_COLLAPSE;} + if(marker&&marker.hasAttribute("data-code-window-lines-label")){ + var spec=marker.getAttribute("data-code-window-lines-label"); + marker.removeAttribute("data-code-window-lines-label"); + var titleBar=el.querySelector(".code-with-filename-file"); + if(titleBar&&!titleBar.querySelector(".code-with-filename-lines")){ + var chip=document.createElement("span"); + chip.className="code-with-filename-lines"; + chip.textContent="L"+spec.replace(/,/g,",\u200B"); + titleBar.appendChild(chip); + } + } + if(collapse&&!el.classList.contains("code-window-collapsible")){ + var titleBar=el.querySelector(".code-with-filename-file"); + if(titleBar){ + var details=document.createElement("details"); + details.className=el.className+" code-window-collapsible"; + if(collapse==="open"){details.open=true;} + var summaryEl=document.createElement("summary"); + summaryEl.className="code-with-filename-file"; + while(titleBar.firstChild){summaryEl.appendChild(titleBar.firstChild);} + titleBar.parentNode.removeChild(titleBar); + details.appendChild(summaryEl); + while(el.firstChild){details.appendChild(el.firstChild);} + el.parentNode.replaceChild(details,el); + } + } + }); +});]=], default_collapse_js, default_style) +end + +--- Load configuration and inject CSS/JS dependencies. +function Meta(meta) + CURRENT_FORMAT = pdoc.get_quarto_format() + + -- This is the pass that reads the configuration, so the check runs here, + -- before the first option is read. An option the check rejects is still + -- read below, because the report says what the extension cannot use and the + -- document renders either way. Only a format the extension acts on reports, + -- because nothing it could say applies anywhere else. + -- The check also answers what the schema declares each option defaults to, + -- so those values are kept rather than dropped and rebuilt by hand here. + -- They arrive typed, and every comparison below reads a string, so each one + -- goes through stringify_bool on the way into the table. + local schema_defaults = {} + if acts_on_format() then + schema_defaults = checker:options(meta) + end + + -- The schema is read first and the fallback fills only what it leaves + -- unanswered, so on html and typst an option added to _schema.yml and to the + -- key list below needs nothing more. Anywhere else the check above never + -- runs, so the same option also needs an entry in FALLBACK_DEFAULTS or it + -- has no default there. Reading the fallback first would have made it the + -- list of options allowed to have a default at all, on every format, which + -- is the coupling this change exists to remove. What arrives is one entry + -- per option that declares a default of its own, and get_options reads only + -- the keys named below. + local defaults = {} + for key, declared in pairs(schema_defaults) do + defaults[key] = stringify_bool(declared) + end + for key, fallback in pairs(FALLBACK_DEFAULTS) do + if defaults[key] == nil then + defaults[key] = fallback + end + end + + local opts = meta_mod.get_options({ + extension = EXTENSION_NAME, + keys = { + 'enabled', 'auto-filename', 'style', 'cell-output', 'wrapper', 'collapse', 'lines-label', + }, + meta = meta, + defaults = defaults, + }) + + -- checker:options (above) already reports an unrecognised "style" value in + -- its own words; this used to warn again with no remedy, legal + -- alternative, or format-specific consequence the schema message lacks + -- (the same shape already removed for "collapse"), so it is silent here. + -- The fallback (VALID_STYLES[opts['style']] and opts['style'] or 'macos', + -- below in CONFIG) is unchanged. + + local global_collapse = resolve_collapse(opts['collapse']) + + -- Read code-annotations metadata (Quarto standard option). + local annot_meta = meta['code-annotations'] + local annot_value = annot_meta and pandoc.utils.stringify(annot_meta) or '' + local annotations_enabled = annot_value ~= 'none' and annot_value ~= 'false' + + -- Read hotfix sub-table from extensions.code-window.hotfix. + local ext_config = meta_mod.get_extension_config(meta, EXTENSION_NAME) + local hotfix_meta = ext_config and ext_config['hotfix'] or nil + + -- Deprecation check for old flat skylighting-fix key. + if ext_config and ext_config['skylighting-fix'] ~= nil then + log.log_warning(EXTENSION_NAME, + '"skylighting-fix" is deprecated. Use "hotfix: { skylighting: true/false }" instead.') + end + + -- Parse hotfix options with per-hotfix version-based auto-disable. + -- Each hotfix value can be: + -- boolean/string: true/false to enable/disable + -- map: { enabled: true/false, quarto-version: "x.y.z" } + local hotfix = {} + for key, default in pairs(HOTFIX_DEFAULTS) do + local entry = hotfix_meta and hotfix_meta[key] + if entry ~= nil and pandoc.utils.type(entry) == 'table' then + -- Map form: { enabled: bool, quarto-version: "x.y.z" } + local enabled = true + if entry['enabled'] ~= nil then + enabled = pandoc.utils.stringify(entry['enabled']) == 'true' + end + local ver = entry['quarto-version'] + if ver then + ver = pandoc.utils.stringify(ver) + if ver ~= '' then + local ok, threshold = pcall(pandoc.types.Version, ver) + if ok and quarto.version >= threshold then + enabled = false + end + end + end + hotfix[key] = enabled + elseif entry ~= nil then + -- Simple boolean/string form + hotfix[key] = pandoc.utils.stringify(entry) == 'true' + else + hotfix[key] = default + end + end + + CONFIG = { + enabled = opts['enabled'] == 'true', + auto_filename = opts['auto-filename'] == 'true', + style = VALID_STYLES[opts['style']] and opts['style'] or 'macos', + cell_output = opts['cell-output'] == 'true', + typst_wrapper = opts['wrapper'], + collapse = global_collapse, + lines_label = opts['lines-label'] == 'true', + hotfix_code_annotations = hotfix['code-annotations'], + hotfix_skylighting = hotfix['skylighting'], + hotfix_typst_title = hotfix['typst-title'], + code_annotations = annotations_enabled, + } + + -- Store hotfix state in metadata so the post-quarto typst-title-fix filter + -- can read it (it runs as a separate filter and has no access to CONFIG). + if not meta['_code-window-hotfix'] then + meta['_code-window-hotfix'] = pandoc.MetaMap({}) + end + meta['_code-window-hotfix']['typst-title'] = pandoc.MetaString( + CONFIG.enabled and hotfix['typst-title'] and 'true' or 'false' + ) + + -- Cache syntax highlighting background colour for Typst contrast-aware annotations. + if CURRENT_FORMAT == 'typst' then + local hm = PANDOC_WRITER_OPTIONS and PANDOC_WRITER_OPTIONS.highlight_method + if hm then + local bg = hm['background-color'] + if bg and type(bg) == 'string' then + TYPST_BG_COLOUR = bg + end + end + end + + if CURRENT_FORMAT == 'html' and CONFIG.enabled then + html_mod.ensure_html_dependency({ + name = EXTENSION_NAME, + version = '0.1.0', + stylesheets = { 'style.css' }, + }) + html_mod.ensure_html_dependency({ + name = EXTENSION_NAME .. '-style-init', + version = '0.1.0', + head = '', + }) + end + + return meta +end + +--- Process CodeBlock elements for HTML/Reveal.js only. +--- Typst processing is handled by the Blocks filter. +--- This handler runs on every CodeBlock structurally, for every format, +--- because a plain `CodeBlock` filter entry is not format-scoped the way +--- CONFIG.enabled or CURRENT_FORMAT are. For a Typst render, the same block +--- is already checked once by process_typst_block through the Pandoc filter +--- above it in main.lua's filter list, so checker:attributes is called here +--- only in the two branches that do not overlap with that pass: the +--- disabled/unconfigured branch (where the Typst pass never runs at all, +--- because Pandoc() returns before reaching it) and the html branch (where +--- the Typst pass never runs either, because it is typst-only). Calling it +--- unconditionally at the top of this function double-validated every +--- CodeBlock-group attribute the Typst path leaves unstripped, such as +--- code-window-collapse, which is HTML-only and never read on that path. +--- Within the html branch, the check runs before the is_plain_output return, +--- not after: process_typst_block (below) checks every attribute +--- unconditionally, before its own is_unnamed_cell_output test, so an +--- output-of-an-executed-cell block is validated on the Typst path even +--- though nothing about it is otherwise touched. The html branch matches +--- that rather than skipping validation for the same kind of block, so the +--- same document reports the same finding in both formats. +function CodeBlock(block) + -- The Typst path reads the marker in the Pandoc filter, which runs first, so + -- this pass is where it is removed for every format. + local is_plain_output = is_unnamed_cell_output(block) + if cell_output.is_marked(block) then + cell_output.strip(block) + end + + -- A filter that draws nothing changes nothing an author wrote. The + -- attributes it would read stay on the block and reach the output, which is + -- also what a document with this extension not installed produces. Only + -- code-window-auto-label goes. The language module no longer writes it in + -- either of the two branches that clear it, since it asks the same question + -- before it runs, so what is left to clear is a document that wrote the + -- extension's own attribute name on a fence by hand. This holds for a filter + -- switched off, below, and for a format the extension does not act on, at + -- the end of this function. + if not CURRENT_FORMAT or not CONFIG or not CONFIG.enabled then + checker:attributes(block.attributes, 'CodeBlock') + block.attributes['code-window-auto-label'] = nil + return block + end + + if CURRENT_FORMAT == 'html' then + local resolved, auto_label = resolve_and_take(block) + if is_plain_output then + return block + end + return process_html(block, resolved, auto_label) + end + + -- Typst is finished by the Pandoc filter ahead of this one, which takes the + -- attributes off there. Every other format draws no chrome, so the block + -- keeps what its author wrote and loses only the label, which a writer that + -- preserves attributes would otherwise print. Nothing writes that label here + -- any more, for the reason given above, so this guards a hand-written one. + block.attributes['code-window-auto-label'] = nil + return block +end + +-- ============================================================================ +-- TYPST BLOCKS FILTER +-- ============================================================================ + +--- Determine whether a CodeBlock should get code-window chrome. +--- The block reaches here with its code-window attributes already taken off +--- by take_block_attributes, as on the HTML path. +--- @param block pandoc.CodeBlock +--- @param resolved table This block's attributes, resolved against the schema +--- @param auto_label string|nil Label written by the language module, if any +--- @return string|nil filename +--- @return boolean is_auto +--- @return string|nil block_style +--- @return string|nil lines_label Highlighted-lines spec for the title bar +local function resolve_window_params(block, resolved, auto_label) + local overrides = read_window_overrides(block, resolved) + + -- Per-block opt-out: code-window-enabled="false" skips window chrome. + if not overrides.enabled then + return nil, false, nil, nil + end + + local filename = block.attributes['filename'] + local is_auto = false + + if (not filename or filename == '') and not overrides.no_auto_filename then + if CONFIG.auto_filename and block.classes and #block.classes > 0 then + -- Default/unknown/no-language blocks carry their label on + -- code-window-auto-label; everything else uses its language class. + filename = auto_label or block.classes[1] + is_auto = true + end + end + + return filename, is_auto, overrides.style, overrides.lines_label +end + +--- Process a single CodeBlock for Typst, returning replacement blocks. +--- Handles both code-window wrapping and standalone annotation rendering. +--- @param block pandoc.CodeBlock +--- @param next_block pandoc.Block|nil The block following this CodeBlock +--- @return pandoc.List replacement_blocks Blocks to splice in +--- @return boolean consumed_next Whether the next block was consumed +--- @return integer|nil annotation_block_id Block ID if annotations were found (for parent propagation) +local function process_typst_block(block, next_block) + -- Check this block's own attributes against the CodeBlock group of the + -- schema before anything below reads or rewrites them, in either branch. + local resolved, auto_label = resolve_and_take(block) + + -- The output of an executed cell keeps the shape Quarto gave it, annotations + -- included, unless the engine gave it a filename of its own. + if is_unnamed_cell_output(block) then + return { block }, false, nil + end + + local filename, is_auto, block_style, lines_label = + resolve_window_params(block, resolved, auto_label) + local has_window = filename and filename ~= '' + local effective_style = block_style or CONFIG.style + + -- Resolve annotations if enabled and the code-annotations hot-fix is active. + local annotations = nil + local should_handle_annotations = CONFIG.code_annotations and CONFIG.hotfix_code_annotations + + if should_handle_annotations then + local cleaned_text + cleaned_text, annotations = code_annotations.resolve_annotations(block) + if annotations then + block.text = cleaned_text + end + end + + -- Strip filename attribute so the CodeBlock renders as plain code inside the + -- code-window wrapper (the DecoratedCodeBlock Div is already unwrapped above). + if has_window and block.attributes['filename'] then + block.attributes['filename'] = nil + end + + local has_annotations = annotations and next(annotations) + local consumed_next = false + local result = {} + local block_id = has_annotations and next_block_id() or 0 + + if has_window and has_annotations then + table.insert(result, typst_code_window_open( + filename, is_auto, effective_style, annotations, block_id, lines_label)) + table.insert(result, block) + table.insert(result, pandoc.RawBlock('typst', ']')) + elseif has_window then + table.insert(result, typst_code_window_open( + filename, is_auto, effective_style, nil, 0, lines_label)) + table.insert(result, block) + table.insert(result, pandoc.RawBlock('typst', ']')) + elseif has_annotations then + local open, close = typst_annotation_wrapper(annotations, block_id) + table.insert(result, open) + table.insert(result, block) + table.insert(result, close) + else + table.insert(result, block) + end + + -- Consume the following OrderedList if it is an annotation list. + if has_annotations + and next_block + and code_annotations.is_annotation_ordered_list(next_block) then + local wrapper_prefix = CONFIG.typst_wrapper + local annot_blocks = code_annotations.ordered_list_to_typst_blocks( + next_block, wrapper_prefix, block_id) + for _, ab in ipairs(annot_blocks) do + table.insert(result, ab) + end + consumed_next = true + end + + local returned_block_id = has_annotations and (not consumed_next) and block_id or nil + return result, consumed_next, returned_block_id +end + +--- Check if a Div is Quarto's DecoratedCodeBlock wrapper. +--- @param div pandoc.Div +--- @return boolean +local function is_decorated_codeblock(div) + return div.attributes['__quarto_custom_type'] == 'DecoratedCodeBlock' +end + +--- Extract the CodeBlock from a DecoratedCodeBlock Div. +--- Structure: DecoratedCodeBlock Div > scaffold Div > CodeBlock +--- @param div pandoc.Div +--- @return pandoc.CodeBlock|nil +local function extract_codeblock(div) + for _, child in ipairs(div.content) do + if child.t == 'CodeBlock' then + return child + elseif child.t == 'Div' then + local found = extract_codeblock(child) + if found then return found end + end + end + return nil +end + +--- Read the filename from Quarto's internal custom node registry for a +--- DecoratedCodeBlock Div. Quarto stores the #| filename: value there rather +--- than as a plain Pandoc attribute on the inner CodeBlock. +--- @param div pandoc.Div A DecoratedCodeBlock Div +--- @return string|nil filename, or nil when the data is unavailable +local function get_decorated_codeblock_filename(div) + local custom_id = div.attributes['__quarto_custom_id'] + if not custom_id then return nil end + if not (_quarto and _quarto.ast and _quarto.ast.custom_node_data) then return nil end + local entry = _quarto.ast.custom_node_data[custom_id] + if type(entry) == 'table' and entry.filename and entry.filename ~= '' then + return entry.filename + end + return nil +end + +--- Process a flat list of blocks for Typst, handling CodeBlocks and their +--- following OrderedLists. Called recursively on Div contents. +--- @param blocks pandoc.Blocks|pandoc.List +--- @return pandoc.Blocks processed_blocks +--- @return integer|nil pending_annotation_block_id Block ID if the last block had annotations (for parent consumption) +local function process_typst_blocks(blocks, wrap_codeblocks) + local new_blocks = {} + local pending_annot_block_id = nil + local i = 1 + while i <= #blocks do + local blk = blocks[i] + + if blk.t == 'CodeBlock' then + local next_blk = blocks[i + 1] + local replacement, consumed_next, annot_id = process_typst_block(blk, next_blk) + local to_insert = (wrap_codeblocks and #replacement > 1) + and { pandoc.Div(replacement) } or replacement + for _, rb in ipairs(to_insert) do + table.insert(new_blocks, rb) + end + if consumed_next then + pending_annot_block_id = nil + i = i + 2 + else + pending_annot_block_id = annot_id + i = i + 1 + end + elseif blk.t == 'Div' and is_decorated_codeblock(blk) then + -- Unwrap Quarto's DecoratedCodeBlock to prevent double filename wrapping. + -- Process the inner CodeBlock directly, replacing the entire Div. + local inner_block = extract_codeblock(blk) + if inner_block then + -- Quarto stores the #| filename: value in its custom node registry, not as + -- a plain Pandoc attribute on the inner CodeBlock. Transfer it here so + -- resolve_window_params sees the explicit filename instead of falling back + -- to auto-filename. + if str.is_empty(inner_block.attributes['filename']) then + local fn = get_decorated_codeblock_filename(blk) + if fn then + inner_block.attributes['filename'] = fn + end + end + local next_blk = blocks[i + 1] + local replacement, consumed_next, annot_id = process_typst_block(inner_block, next_blk) + local to_insert = (wrap_codeblocks and #replacement > 1) + and { pandoc.Div(replacement) } or replacement + for _, rb in ipairs(to_insert) do + table.insert(new_blocks, rb) + end + if consumed_next then + pending_annot_block_id = nil + i = i + 2 + else + pending_annot_block_id = annot_id + i = i + 1 + end + else + -- Fallback: keep the Div as-is if no CodeBlock found. + local processed, inner_pending = process_typst_blocks(blk.content) + blk.content = processed + table.insert(new_blocks, blk) + pending_annot_block_id = inner_pending + i = i + 1 + end + elseif blk.t == 'Div' then + local is_layout = blk.attributes['layout-ncol'] + or blk.attributes['layout-nrow'] + or blk.attributes['layout'] + local processed, inner_pending = process_typst_blocks(blk.content, is_layout) + blk.content = processed + table.insert(new_blocks, blk) + -- If the Div's last processed block had pending annotations, + -- check if the next sibling is an OrderedList to consume. + if inner_pending then + local next_blk = blocks[i + 1] + if next_blk and code_annotations.is_annotation_ordered_list(next_blk) then + local annot_blocks = code_annotations.ordered_list_to_typst_blocks( + next_blk, CONFIG.typst_wrapper, inner_pending) + for _, ab in ipairs(annot_blocks) do + table.insert(new_blocks, ab) + end + pending_annot_block_id = nil + i = i + 2 + else + pending_annot_block_id = inner_pending + i = i + 1 + end + else + pending_annot_block_id = nil + i = i + 1 + end + elseif blk.t == 'BulletList' or blk.t == 'OrderedList' then + for j, item in ipairs(blk.content) do + blk.content[j] = process_typst_blocks(pandoc.Blocks(item)) + end + table.insert(new_blocks, blk) + pending_annot_block_id = nil + i = i + 1 + elseif blk.t == 'BlockQuote' then + blk.content = process_typst_blocks(blk.content) + table.insert(new_blocks, blk) + pending_annot_block_id = nil + i = i + 1 + elseif blk.t == 'DefinitionList' then + for j, def_item in ipairs(blk.content) do + for k, body in ipairs(def_item[2]) do + def_item[2][k] = process_typst_blocks(pandoc.Blocks(body)) + end + end + table.insert(new_blocks, blk) + pending_annot_block_id = nil + i = i + 1 + else + pending_annot_block_id = nil + table.insert(new_blocks, blk) + i = i + 1 + end + end + return pandoc.Blocks(new_blocks), pending_annot_block_id +end + +--- Walk HTML document blocks and transfer the filename from any DecoratedCodeBlock +--- outer Div to its inner CodeBlock, so process_html sees an explicit filename +--- and does not trigger auto-filename (which would otherwise produce a second +--- .code-with-filename wrapper alongside Quarto's own). +--- @param blocks pandoc.Blocks +--- @return pandoc.Blocks +local function fix_html_decorated_filenames(blocks) + return blocks:walk({ + Div = function(div) + if not is_decorated_codeblock(div) then return nil end + local fn = get_decorated_codeblock_filename(div) + if not fn then return nil end + local inner_block = extract_codeblock(div) + if inner_block and str.is_empty(inner_block.attributes['filename']) then + inner_block.attributes['filename'] = fn + end + return div + end + }) +end + +--- Inject Typst function definition and process code blocks for Typst format. +--- Also repairs HTML DecoratedCodeBlock filename propagation. +--- Runs as a Pandoc filter to have full control over the document tree. +function Pandoc(doc) + if not CONFIG or not CONFIG.enabled then + return doc + end + + -- For HTML: transfer filenames from DecoratedCodeBlock Divs to inner + -- CodeBlocks before the CodeBlock filter runs, preventing auto-filename + -- from adding a second .code-with-filename wrapper. + if CURRENT_FORMAT == 'html' then + doc.blocks = fix_html_decorated_filenames(doc.blocks) + return doc + end + + if CURRENT_FORMAT ~= 'typst' then + return doc + end + + -- Process code blocks and annotations throughout the document tree. + doc.blocks = process_typst_blocks(doc.blocks) + + -- Guard: check if the function definition is already present. + local fn_pattern = '#let ' .. CONFIG.typst_wrapper + for _, blk in ipairs(doc.blocks) do + if blk.t == 'RawBlock' and blk.format == 'typst' + and blk.text:find(fn_pattern, 1, true) then + return doc + end + end + + local has_hotfixes = CONFIG.hotfix_code_annotations or CONFIG.hotfix_skylighting + local fn_def = build_typst_function_def(has_hotfixes) + if CONFIG.typst_wrapper ~= 'code-window' then + fn_def = fn_def:gsub('code%-window%-annote%-colour', CONFIG.typst_wrapper .. '-annote-colour') + fn_def = fn_def:gsub('code%-window%-circled%-number', CONFIG.typst_wrapper .. '-circled-number') + fn_def = fn_def:gsub('code%-window%-annotation%-item', CONFIG.typst_wrapper .. '-annotation-item') + fn_def = fn_def:gsub('code%-window%-annotated%-content', CONFIG.typst_wrapper .. '-annotated-content') + fn_def = fn_def:gsub('#let code%-window%(', '#let ' .. CONFIG.typst_wrapper .. '(') + end + table.insert(doc.blocks, 1, pandoc.RawBlock('typst', fn_def)) + + return doc +end + +-- ============================================================================ +-- MODULE EXPORTS +-- ============================================================================ + +--- Inject the code-annotations module dependency. +--- Called by main.lua before any filter handlers run. +--- @param mod table The code-annotations module +local function set_code_annotations(mod) + code_annotations = mod +end + +--- Inject the schema checker. +--- Called by main.lua before any filter handlers run. +--- @param mod table The checker built from the vendored validator +local function set_checker(mod) + checker = mod +end + +return { + set_code_annotations = set_code_annotations, + set_checker = set_checker, + Meta = Meta, + Pandoc = Pandoc, + CodeBlock = CodeBlock, + CONFIG = function() return CONFIG end, + draws_chrome = draws_chrome, +} diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/main.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/main.lua new file mode 100644 index 0000000..59727d4 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/main.lua @@ -0,0 +1,153 @@ +--- @module "main" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @brief Entry point for the code-window extension. +--- Loads all submodules, wires dependencies, and assembles the filter list. + +local EXTENSION_NAME = 'code-window' +local log = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/logging.lua'):gsub('%.lua$', '')) +local schema = require(quarto.utils.resolve_path('_vendor/quarto-wizard/schema.lua'):gsub('%.lua$', '')) +local check = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/schema-check.lua'):gsub('%.lua$', '')) + +--- The checker reports what "extensions.code-window" declares that this +--- extension cannot use. The validator is handed to it rather than required by +--- it, so the two vendored sources stay independent of each other. Building it +--- here reads "_schema.yml" once for the whole render. A schema that cannot be +--- read is reported and the render carries on: a fault in the configuration +--- must not remove the document. +local checker = check.new(schema, EXTENSION_NAME) + +-- ============================================================================ +-- LOAD SUBMODULES +-- ============================================================================ + +local cell_output = require( + quarto.utils.resolve_path('_modules/cell-output.lua'):gsub('%.lua$', '')) + +local language = require( + quarto.utils.resolve_path('_modules/language.lua'):gsub('%.lua$', '')) + +local code_annotations = require( + quarto.utils.resolve_path('_modules/hotfix/code-annotations.lua'):gsub('%.lua$', '')) + +local code_window = require( + quarto.utils.resolve_path('code-window.lua'):gsub('%.lua$', '')) + +code_window.set_code_annotations(code_annotations) +code_window.set_checker(checker) + +-- ============================================================================ +-- CELL OUTPUT +-- ============================================================================ + +--- Mark the code blocks that hold the output of an executed cell, so the later +--- passes leave them as Quarto wrote them. Walks the document only when there +--- is a pass to hold back, which means this render draws chrome and the output +--- has to stay unframed. Every reader that acts on the mark asks draws_chrome +--- first: the language pass below, and the two window paths. CodeBlock reads it +--- too, but only to remove it, and a mark that was never set costs nothing +--- there. So a render that draws no chrome would walk the whole document to set +--- an attribute nothing goes on to act on. draws_chrome answers false when +--- there is no configuration yet, so the second test below always has one in +--- hand. +--- @param doc pandoc.Pandoc +--- @return pandoc.Pandoc|nil Marked document, or nil when the pass is skipped +local function mark_cell_output(doc) + if not code_window.draws_chrome() or code_window.CONFIG().cell_output then + return nil + end + doc.blocks = doc.blocks:walk({ Div = cell_output.Div }) + return doc +end + +-- ============================================================================ +-- LANGUAGE +-- ============================================================================ + +--- Normalise a block's language where the render draws chrome. +--- The pass labels a block whose language Pandoc cannot highlight, and the +--- derived filename is the only reader of that label. Nothing derives a +--- filename in a render that draws no chrome, so the pass would rewrite a +--- class for nobody and hand the author back a language they did not write. +--- "auto-filename" belongs in the same question, because it is the reader +--- itself: with no derived name to build, both window paths return before they +--- read the label, so the pass would rewrite a class for nobody again. +--- Every question this asks is about the render, not about one block. A block +--- can still draw no chrome inside a render that does, through +--- "code-window-no-auto-filename" or "code-window-enabled", and its class is +--- rewritten with no reader either. Answering that per block means relabelling +--- where the name is built, which is a change to the two window paths rather +--- than to this gate. +--- @param block pandoc.CodeBlock +--- @return pandoc.CodeBlock|nil Relabelled block, or nil when the pass is skipped +local function normalise_language(block) + if not code_window.draws_chrome() or not code_window.CONFIG().auto_filename then + return nil + end + return language.CodeBlock(block) +end + +-- ============================================================================ +-- SKYLIGHTING HOT-FIX +-- ============================================================================ + +--- Load optional skylighting hot-fix module from sibling file. +--- @return table Module table with .filters and .set_wrapper, or empty table +local function load_skylighting_hotfix_module() + local ok, result = pcall(require, + quarto.utils.resolve_path('_modules/hotfix/skylighting-typst-fix.lua'):gsub('%.lua$', '')) + if not ok then + log.log_warning(EXTENSION_NAME, + 'Failed to load optional skylighting hot-fix: ' .. tostring(result)) + return {} + end + if type(result) ~= 'table' then + log.log_warning(EXTENSION_NAME, + 'Skylighting hot-fix did not return a module table.') + return {} + end + return result +end + +-- ============================================================================ +-- FILTER ASSEMBLY +-- ============================================================================ + +-- Meta runs first because the cell-output pass needs the configuration, and +-- that pass runs before the language pass so a marked block is never relabelled. +local filters = { + { Meta = code_window.Meta }, + { Pandoc = mark_cell_output }, + { CodeBlock = normalise_language }, + { Pandoc = code_window.Pandoc }, + { CodeBlock = code_window.CodeBlock }, +} + +local skylighting_mod = load_skylighting_hotfix_module() + +-- The hot-fix exists only to serve the chrome, so it asks draws_chrome like +-- every other pass that does. Its Skylighting override calls the colour helpers, +-- and code_window.Pandoc is what declares them, so asking hotfix_skylighting +-- alone left the override in a document that declared none. +for _, subfilter in ipairs(skylighting_mod.filters or {}) do + local wrapped = {} + for element_type, handler in pairs(subfilter) do + wrapped[element_type] = function(...) + if not code_window.draws_chrome() then + return nil + end + local cfg = code_window.CONFIG() + if not cfg.hotfix_skylighting then + return nil + end + if skylighting_mod.set_wrapper then + skylighting_mod.set_wrapper(cfg.typst_wrapper) + end + return handler(...) + end + end + table.insert(filters, wrapped) +end + +return filters diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/style.css b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/style.css new file mode 100644 index 0000000..c739eb2 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/code-window/style.css @@ -0,0 +1,212 @@ +/* Code Window Extension for Quarto + * Code block window decorations with multiple styles + * MIT License - Mickaël Canouil 2026 + */ + +/* ============================================================================ + * Theming variables (override to customise the window background, window-button icons and chip) + * ========================================================================= */ + +:where(:root) { + --code-window-macos-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='10'%3E%3Ccircle cx='5' cy='5' r='5' fill='%23ff5f56'/%3E%3Ccircle cx='20' cy='5' r='5' fill='%23ffbd2e'/%3E%3Ccircle cx='35' cy='5' r='5' fill='%2327c93f'/%3E%3C/svg%3E"); + --code-window-windows-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='10' fill='none'%3E%3Cline x1='3' y1='5' x2='11' y2='5' stroke='%23888' stroke-width='1.5' stroke-linecap='round'/%3E%3Crect x='17' y='1' width='8' height='8' rx='1' stroke='%23888' stroke-width='1.5' fill='none'/%3E%3Cline x1='31' y1='1' x2='39' y2='9' stroke='%23888' stroke-width='1.5' stroke-linecap='round'/%3E%3Cline x1='39' y1='1' x2='31' y2='9' stroke='%23888' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E"); + --code-window-macos-icon-width: 3.4em; + --code-window-windows-icon-width: 3.6em; + --code-window-icon-height: 0.85em; + /* Window background: Bootstrap theme, then Reveal.js theme, then the system colour */ + --code-window-bg: var(--bs-body-bg, var(--r-background-color, Canvas)); + --code-window-title-bg: color-mix(in srgb, currentColor 5%, var(--code-window-bg)); +} + +/* ============================================================================ + * Base container + * ========================================================================= */ + +.code-with-filename { + margin: 1.5em 0; + border-radius: 8px; + border: 1px solid color-mix(in srgb, currentColor 15%, var(--code-window-bg)); + background-color: var(--code-window-bg); + overflow: hidden; + page-break-inside: avoid; + break-inside: avoid; +} + +/* Title bar (shared across all styles) */ +.code-with-filename .code-with-filename-file { + display: flex; + align-items: center; + gap: 0.75em; + background-color: var(--code-window-title-bg); + border: none; + border-bottom: 1px solid color-mix(in srgb, currentColor 15%, var(--code-window-bg)); + border-radius: 0; + padding: 0.2em 1em; + line-height: 1.2; + margin: 0; + page-break-after: avoid; + break-after: avoid; +} + +/* Outrank Quarto's own title bar colours: its dark-mode grey, and the light + * grey that Reveal.js loads after this file. A theme sets + * --code-window-title-bg to change the title bar in both modes. */ +.reveal .code-with-filename .code-with-filename-file, +body.quarto-dark .code-with-filename .code-with-filename-file { + background-color: var(--code-window-title-bg); +} + +/* Shrink title bar ~35% on Reveal.js slides (scales padding, decorations, and filename text proportionally) */ +.reveal .code-with-filename .code-with-filename-file { + font-size: 0.65em; + padding: 0.3em 1em; +} + +/* Filename styling (base) */ +.code-with-filename .code-with-filename-file pre { + flex: 1; + margin: 0; + padding: 0; + background-color: transparent; + border: none; + font-size: 0.85em; + font-weight: 500; + color: color-mix(in srgb, currentColor 80%, var(--code-window-bg)); + /* Wrap long filenames: a scrolling title bar has no keyboard access */ + min-width: 0; + overflow: visible; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +/* Highlighted-lines chip rendered next to the filename in the title bar */ +.code-with-filename .code-with-filename-file .code-with-filename-lines { + flex: 0 0 auto; + max-width: 50%; + font-size: 0.7em; + font-weight: 500; + font-family: var(--bs-font-monospace, monospace); + color: color-mix(in srgb, currentColor 85%, var(--code-window-bg)); + background-color: color-mix(in srgb, currentColor 10%, var(--code-window-bg)); + border: 1px solid color-mix(in srgb, currentColor 15%, var(--code-window-bg)); + border-radius: 3px; + padding: 0.05em 0.4em; + line-height: 1.4; + overflow-wrap: break-word; +} + +/* Code block content area */ +.code-with-filename .sourceCode, +.code-with-filename div.sourceCode, +.code-with-filename .code-copy-outer-scaffold { + margin: 0; + border: none; + border-radius: 0; +} + +.code-with-filename .sourceCode pre, +.code-with-filename div.sourceCode pre, +.code-with-filename .code-copy-outer-scaffold pre { + margin: 0; + border: none; + border-radius: 0; +} + +/* Plain code blocks (unknown language without sourceCode wrapper) */ +.code-with-filename > pre { + margin: 0; + border: none; + border-radius: 0; +} + +/* Auto-generated filenames display in simulated small caps */ +.code-with-filename.code-window-auto .code-with-filename-file pre strong { + text-transform: uppercase; + font-size: 0.8em; +} + +/* Code block bottom border-radius */ +.code-with-filename .code-copy-outer-scaffold { + border-radius: 0 0 8px 8px; + overflow: hidden; +} + +/* ============================================================================ + * Style: default (plain filename, no decorations) + * ========================================================================= */ + +.code-with-filename.code-window-default .code-with-filename-file pre { + text-align: left; +} + +/* ============================================================================ + * Style: macos (traffic lights left, filename right) + * ========================================================================= */ + +.code-with-filename.code-window-macos .code-with-filename-file::before { + content: ''; + display: inline-flex; + flex-shrink: 0; + width: var(--code-window-macos-icon-width); + height: var(--code-window-icon-height); + background-image: var(--code-window-macos-icon); + background-size: contain; + background-repeat: no-repeat; +} + +.code-with-filename.code-window-macos .code-with-filename-file pre { + text-align: right; +} + +/* ============================================================================ + * Style: windows (filename left, window buttons right) + * ========================================================================= */ + +.code-with-filename.code-window-windows .code-with-filename-file pre { + text-align: left; +} + +.code-with-filename.code-window-windows .code-with-filename-file::after { + content: ''; + display: inline-flex; + flex-shrink: 0; + width: var(--code-window-windows-icon-width); + height: var(--code-window-icon-height); + background-image: var(--code-window-windows-icon); + background-position: right; + background-size: contain; + background-repeat: no-repeat; +} + +/* ============================================================================ + * Collapsible code window (
) + * ========================================================================= */ + +.code-with-filename.code-window-collapsible { + padding: 0; +} + +.code-with-filename.code-window-collapsible > summary.code-with-filename-file { + cursor: pointer; + list-style: none; + user-select: none; +} + +.code-with-filename.code-window-collapsible > summary.code-with-filename-file::-webkit-details-marker { + display: none; +} + +/* Bottom-border on summary only when expanded so the closed state has no + * separator line beneath an already-rounded container. */ +.code-with-filename.code-window-collapsible:not([open]) > summary.code-with-filename-file { + border-bottom: none; +} + +/* Round the title bar corners when the window is closed. */ +.code-with-filename.code-window-collapsible:not([open]) { + border-radius: 8px; +} + +.code-with-filename.code-window-collapsible:not([open]) > summary.code-with-filename-file { + border-radius: 8px; +} diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/LICENSE b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/LICENSE new file mode 100644 index 0000000..4b53bb8 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mickaël Canouil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_dependencies.yml b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_dependencies.yml new file mode 100644 index 0000000..c253e4d --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_dependencies.yml @@ -0,0 +1,33 @@ +schema: 1 +sources: + quarto-lua-modules: + origin: "https://github.com/mcanouil/quarto-lua-modules" + fetch: "{origin}/releases/download/{version}/{file}" + version: "2.5.0" + licence: MIT + files: + html.lua: + sha256: "a7aae5e69c4effce399b85e90f3ee3261d0f84a5ac2f6c7ddeb1e358ba22ddc8" + runtime: any + logging.lua: + sha256: "5d99ceb89c813fbe4491310e42b4a4cff62e11dda42f01b4f61f969aa58e0706" + runtime: any + metadata.lua: + sha256: "1186431acc50e35797eef68808e4c52321c845a3be4f29bbd7de695b1db2f6cc" + runtime: any + schema-check.lua: + sha256: "343c4846d4bced229de2b09f84b756051cb4061099787f8001384bce680e2987" + runtime: any + string.lua: + sha256: "7801d2f217e803951313ef86f1f899cb6350a44cbb468b2d7c28dd6911a0fadc" + runtime: any + quarto-wizard: + origin: "https://github.com/mcanouil/quarto-wizard" + fetch: "{origin}/releases/download/{version}/{file}" + version: "2.2.0" + licence: MIT + tag-prefix: schema-v + files: + schema.lua: + sha256: "84e0a5a7bae8d71a6d601d581e3e819ace96613cc76c6e9dd3cf79eee629df78" + runtime: any diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_extension.yml b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_extension.yml new file mode 100644 index 0000000..6df6c66 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_extension.yml @@ -0,0 +1,9 @@ +title: Collapse Output +author: Mickaël Canouil +version: 1.6.1 +quarto-required: ">=1.8.25" +contributes: + filters: + - collapse-output.lua +source: mcanouil/quarto-collapse-output@1.6.1 +source-type: registry diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_schema.yml b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_schema.yml new file mode 100644 index 0000000..8086b62 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_schema.yml @@ -0,0 +1,48 @@ +# Schema for the collapse-output extension +# Provides configuration validation and IDE support for options +# and attributes. + +$schema: https://m.canouil.dev/quarto-wizard/assets/schema/v2/extension-schema.json + +options: + method: + type: string + default: "lua" + enum: [lua, javascript] + enumCaseInsensitive: true + description: "Collapsing method. 'lua' renders server-side; 'javascript' renders client-side." + collapse-all-outputs: + type: boolean + default: false + description: "Wrap every cell output in a collapsible details element. Per-cell 'output-fold' still overrides this." + default-open: + type: boolean + default: false + description: "Initial state of the collapsible details element." + auto-collapse-size: + type: integer + minimum: 0 + description: "Line threshold above which outputs are forced closed regardless of 'default-open'." + output-types: + type: [string, array] + description: "Comma-separated list (or YAML list) of output types to fold. Allowed: stdout, stderr, display, output." + summary-template: + type: string + default: "{type}" + description: "Template used for the summary text. Placeholders: {type}, {lines}." + summaries: + type: object + description: "Per-output-type summary overrides, keyed by stdout, stderr, display, or output." + +attributes: + _any: + output-fold: + type: boolean + default: false + description: "Wrap cell output in a collapsible details element." + output-summary: + type: string + description: "Summary text shown on the collapsed output toggle, taking precedence over both `summaries` and `summary-template`. Unset, the summary comes from `summaries` for the detected output type, then from `summary-template`." + output-open: + type: boolean + description: "Per-cell override of the document 'default-open' setting." diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_snippets.json b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_snippets.json new file mode 100644 index 0000000..16ede76 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_snippets.json @@ -0,0 +1,17 @@ +{ + "Output fold": { + "prefix": "output-fold", + "body": "#| output-fold: true", + "description": "Collapse code cell output with a fold" + }, + "Output fold with summary": { + "prefix": "output-summary", + "body": ["#| output-fold: true", "#| output-summary: \"${1:Click to view}\""], + "description": "Collapse code cell output with a custom summary label" + }, + "Output fold open by default": { + "prefix": "output-open", + "body": ["#| output-fold: true", "#| output-open: true"], + "description": "Collapse code cell output but render it open by default" + } +} diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/LICENSE b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/LICENSE new file mode 100644 index 0000000..4b53bb8 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mickaël Canouil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/html.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/html.lua new file mode 100644 index 0000000..ecb4870 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/html.lua @@ -0,0 +1,102 @@ +--- MC HTML - HTML generation and dependency management for Quarto Lua filters and shortcodes +--- @module "html" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +--- Load a sibling module from the same directory as this file. +--- @param filename string The sibling module filename (e.g., 'string.lua') +--- @return table The loaded module +local function load_sibling(filename) + local source = debug.getinfo(1, 'S').source:sub(2) + local dir = source:match('(.*[/\\])') or '' + return require((dir .. filename):gsub('%.lua$', '')) +end + +--- Load string module for escape_html and escape_attribute +local str = load_sibling('string.lua') + +-- ============================================================================ +-- HTML RAW GENERATION UTILITIES +-- ============================================================================ + +--- Generates a raw HTML header element. +--- @param level integer The header level (e.g., 2 for

) +--- @param text string|nil The header text +--- @param id string The id attribute for the header +--- @param classes table List of classes for the header +--- @param attributes table|nil Additional HTML attributes +--- @return string Raw HTML string for the header +function M.raw_header(level, text, id, classes, attributes) + local attr_str = '' + if id and id ~= '' then + attr_str = attr_str .. ' id="' .. str.escape_attribute(id) .. '"' + end + if classes and #classes > 0 then + local escaped_classes = {} + for i, cls in ipairs(classes) do + escaped_classes[i] = str.escape_attribute(cls) + end + attr_str = attr_str .. ' class="' .. table.concat(escaped_classes, ' ') .. '"' + end + if attributes then + for k, v in pairs(attributes) do + attr_str = attr_str .. ' ' .. str.escape_attribute(k) .. '="' .. str.escape_attribute(v) .. '"' + end + end + return string.format('%s', level, attr_str, str.escape_html(text or ''), level) +end + +-- ============================================================================ +-- HTML DEPENDENCY UTILITIES +-- ============================================================================ + +--- Managed HTML dependency tracker +--- Tracks which dependencies have been added to prevent duplication +--- @type table +local dependency_tracker = {} + +--- Ensure HTML dependency is added only once per document. +--- Prevents duplicate dependency injection by tracking dependencies by name. +--- Returns true if dependency was added, false if already present. +--- +--- @param config table Dependency configuration with fields: name (required), version, scripts, stylesheets, head +--- @return boolean True if dependency was added, false if already added +--- @usage M.ensure_html_dependency({name = 'my-lib', version = '1.0.0', scripts = {'lib.js'}}) +function M.ensure_html_dependency(config) + if not config or not config.name then + error("HTML dependency configuration must include a 'name' field") + end + + --- @type string Unique key for this dependency + local dep_key = config.name + + -- Check if already added + if dependency_tracker[dep_key] then + return false + end + + -- Add the dependency + quarto.doc.add_html_dependency(config) + + -- Mark as added + dependency_tracker[dep_key] = true + return true +end + +--- Reset dependency tracker. +--- Useful for testing or when processing multiple independent documents. +--- In normal usage, this should not be called as dependencies persist per document. +--- +--- @return nil +function M.reset_dependencies() + dependency_tracker = {} +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/logging.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/logging.lua new file mode 100644 index 0000000..81fba55 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/logging.lua @@ -0,0 +1,61 @@ +--- MC Logging - Formatted log output for Quarto Lua filters and shortcodes +--- @module "logging" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +-- ============================================================================ +-- LOGGING UTILITIES +-- ============================================================================ + +--- Format and log an error message with extension prefix. +--- Provides standardised error messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "external", "lua-env") +--- @param message string The error message to display +--- @usage M.log_error("external", "Could not open file 'example.md'.") +function M.log_error(extension_name, message) + quarto.log.error('[' .. extension_name .. '] ' .. message) +end + +--- Format and log a warning message with extension prefix. +--- Provides standardised warning messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "external", "lua-env") +--- @param message string The warning message to display +--- @usage M.log_warning("lua-env", "No variable name provided.") +function M.log_warning(extension_name, message) + quarto.log.warning('[' .. extension_name .. '] ' .. message) +end + +--- Format and log an output message with extension prefix. +--- Provides standardised informational messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "lua-env") +--- @param message string The informational message to display +--- @usage M.log_output("lua-env", "Exported metadata to: output.json") +function M.log_output(extension_name, message) + quarto.log.output('[' .. extension_name .. '] ' .. message) +end + +--- Format and log a debug message with extension prefix. +--- Provides standardised debug messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "lua-env") +--- @param message string The debug message to display +--- @usage M.log_debug("lua-env", "Variable 'x' has value: 42") +function M.log_debug(extension_name, message) + quarto.log.debug('[' .. extension_name .. '] ' .. message) +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/metadata.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/metadata.lua new file mode 100644 index 0000000..7bc55c1 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/metadata.lua @@ -0,0 +1,223 @@ +--- MC Metadata - Extension configuration and metadata access for Quarto Lua filters and shortcodes +--- @module "metadata" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +--- Load a sibling module from the same directory as this file. +--- @param filename string The sibling module filename (e.g., 'string.lua') +--- @return table The loaded module +local function load_sibling(filename) + local source = debug.getinfo(1, 'S').source:sub(2) + local dir = source:match('(.*[/\\])') or '' + return require((dir .. filename):gsub('%.lua$', '')) +end + +--- Load required modules +local str = load_sibling('string.lua') +local log = load_sibling('logging.lua') + +-- ============================================================================ +-- METADATA UTILITIES +-- ============================================================================ + +--- Get configuration from extensions.name namespace. +--- @param meta table Document metadata +--- @param extension_name string The extension name (e.g., "github", "iconify") +--- @return any The value/table or nil +--- @usage local value = M.get_extension_config(meta, 'section-outline') +function M.get_extension_config(meta, extension_name) + local config_ext = meta.extensions and meta.extensions[extension_name] + if not config_ext then return nil end + return config_ext +end + +--- Extract metadata value from document meta using nested structure. +--- Supports the extensions.{extension-name}.{key} pattern. +--- @param meta table The document metadata table +--- @param extension_name string The extension name (e.g., "github", "iconify") +--- @param key string The metadata key to retrieve +--- @return string|nil The metadata value as a string, or nil if not found +--- @usage local repo = M.get_metadata_value(meta, "github", "repository-name") +--- @note A boolean `false` is a value, not an absence, so the test is against +--- `nil` rather than truthiness. Stringifying it yields "false", which is what +--- a caller comparing against the string form already expects. +function M.get_metadata_value(meta, extension_name, key) + if meta['extensions'] and meta['extensions'][extension_name] + and meta['extensions'][extension_name][key] ~= nil then + return str.stringify(meta['extensions'][extension_name][key]) + end + return nil +end + +--- Check for deprecated top-level configuration and emit warning +--- @param meta table The document metadata table +--- @param extension_name string The extension name +--- @param key string|nil The configuration key being accessed (nil to check entire extension config) +--- @param deprecation_warning_shown boolean Flag to track if warning has been shown +--- @return any|nil The value from deprecated config, or nil if not found +--- @return boolean Updated deprecation warning flag +function M.check_deprecated_config(meta, extension_name, key, deprecation_warning_shown) + -- Handle array-based configuration (when key is nil) + if key == nil then + if not str.is_empty(meta[extension_name]) then + if not deprecation_warning_shown then + log.log_warning( + extension_name, + 'Top-level "' .. extension_name .. '" configuration is deprecated. ' .. + 'Please use:\n' .. + 'extensions:\n' .. + ' ' .. extension_name .. ':\n' .. + ' - (configuration array)' + ) + deprecation_warning_shown = true + end + return meta[extension_name], deprecation_warning_shown + end + return nil, deprecation_warning_shown + end + + -- Handle key-value configuration (original behaviour) + if not str.is_empty(meta[extension_name]) and not str.is_empty(meta[extension_name][key]) then + if not deprecation_warning_shown then + log.log_warning( + extension_name, + 'Top-level "' .. extension_name .. '" configuration is deprecated. ' .. + 'Please use:\n' .. + 'extensions:\n' .. + ' ' .. extension_name .. ':\n' .. + ' ' .. key .. ': value' + ) + deprecation_warning_shown = true + end + return str.stringify(meta[extension_name][key]), deprecation_warning_shown + end + return nil, deprecation_warning_shown +end + +-- ============================================================================ +-- ENHANCED METADATA/CONFIGURATION UTILITIES +-- ============================================================================ + +--- Get option value with fallback hierarchy: args -> extensions.{extension}.{key} -> defaults. +--- Provides a standardised way to read configuration values with multiple fallback levels. +--- Priority: 1. Named arguments (kwargs), 2. Document metadata, 3. Default values. +--- +--- @param spec table Configuration spec with fields: extension (string), key (string), args (table|nil), meta (table|nil), default (any|nil) +--- @return any The resolved option value (type depends on what's stored in config) +--- @usage local duration = M.get_option_with_fallbacks({extension = 'animate', key = 'duration', args = kwargs, meta = meta, default = '3s'}) +function M.get_option_with_fallbacks(spec) + -- Validate required fields + if not spec.extension or not spec.key then + error("Configuration spec must include 'extension' and 'key' fields") + end + + --- @type string The extension name + local extension = spec.extension + --- @type string The configuration key + local key = spec.key + --- @type table|nil Named arguments table + local args = spec.args + --- @type table|nil Document metadata + local meta = spec.meta + --- @type any Default value if not found elsewhere + local default = spec.default + + -- Priority 1: Check named arguments (kwargs) + if args and args[key] then + local arg_value = str.stringify(args[key]) + if not str.is_empty(arg_value) then + return arg_value + end + end + + -- Priority 2: Check metadata extensions.{extension}.{key} + if meta then + local meta_value = M.get_metadata_value(meta, extension, key) + if not str.is_empty(meta_value) then + return meta_value + end + end + + -- Priority 3: Return default value + return default +end + +--- Get multiple option values at once with fallback hierarchy. +--- Batch version of get_option_with_fallbacks for retrieving multiple configuration values. +--- Returns a table mapping each key to its resolved value. +--- +--- @param spec table Configuration spec with fields: extension (string), keys (table), args (table|nil), meta (table|nil), defaults (table|nil) +--- @return table Table mapping each key to its resolved value +--- @usage local opts = M.get_options({extension = 'animate', keys = {'duration', 'delay'}, args = kwargs, meta = meta, defaults = {duration = '3s', delay = '2s'}}) +function M.get_options(spec) + -- Validate required fields + if not spec.extension or not spec.keys then + error("Configuration spec must include 'extension' and 'keys' fields") + end + + --- @type table Result table + local result = {} + + --- @type table Default values table + local defaults = spec.defaults or {} + + -- Get each key using the single-option fallback logic + for _, key in ipairs(spec.keys) do + result[key] = M.get_option_with_fallbacks({ + extension = spec.extension, + key = key, + args = spec.args, + meta = spec.meta, + default = defaults[key] + }) + end + + return result +end + +-- ============================================================================ +-- PROJECT METADATA UTILITIES +-- ============================================================================ + +--- Get repo-url from Quarto project metadata via QUARTO_EXECUTE_INFO. +--- Reads the JSON file pointed to by the QUARTO_EXECUTE_INFO environment variable +--- and extracts repo-url from website or book metadata. +--- @return string|nil The repo-url value, or nil if not available +function M.get_project_repo_url() + local path = os.getenv("QUARTO_EXECUTE_INFO") + if not path then return nil end + + local file = io.open(path, "r") + if not file then return nil end + + local content = file:read("*a") + file:close() + + if str.is_empty(content) then return nil end + + local ok, info = pcall(quarto.json.decode, content) + if not ok or not info then return nil end + + local format_meta = info["format"] and info["format"]["metadata"] + if not format_meta then return nil end + + -- Try website.repo-url first, then book.repo-url + local repo_url = nil + if format_meta["website"] and format_meta["website"]["repo-url"] then + repo_url = format_meta["website"]["repo-url"] + elseif format_meta["book"] and format_meta["book"]["repo-url"] then + repo_url = format_meta["book"]["repo-url"] + end + + if str.is_empty(repo_url) then return nil end + return repo_url +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/schema-check.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/schema-check.lua new file mode 100644 index 0000000..9fe2c17 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/schema-check.lua @@ -0,0 +1,581 @@ +--- MC Schema Check - Runtime schema checks for Quarto extensions +--- @module "schema-check" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- +--- Holds the wiring that every extension would otherwise copy: read the schema +--- once, check the document configuration against it, check one shortcode call +--- against it, and report what it finds through `logging`. +--- +--- The schema is `_schema.yml` beside the entry point that runs. An extension +--- whose entry points sit in a subdirectory names its own path instead, with +--- `'../_schema.yml'` for one level down. +--- +--- The validator arrives as an argument rather than through `require`. A +--- vendored copy of this module then knows nothing about where the validator +--- was vendored, so the two sources stay independent. The validator must +--- provide `load_schema`, `validate`, `validate_shortcode`, +--- `extract_meta_options`, `validate_attributes` and `validate_format`. +--- +--- The last two are newer than the rest of that list, so a validator vendored +--- before either exists is reported rather than called. The others are required +--- outright and a validator without one of them raises. +--- +--- `validate_format` must also be the one that reads the top level of the +--- metadata, which is Quarto Wizard 3.6.0 or newer. An older one looks for the +--- format name as a metadata key, which a document never has, so it would +--- report nothing whatever the document wrote. +--- +--- Nothing here stops a render. A schema is configuration, and a fault in the +--- configuration must not remove the document. + +local M = {} + +--- Load a sibling module from the same directory as this file. +--- @param filename string The sibling module filename (e.g., 'string.lua') +--- @return table The loaded module +local function load_sibling(filename) + local source = debug.getinfo(1, 'S').source:sub(2) + local dir = source:match('(.*[/\\])') or '' + return require((dir .. filename):gsub('%.lua$', '')) +end + +--- Load required modules +local log = load_sibling('logging.lua') +local str = load_sibling('string.lua') + +-- ============================================================================ +-- SEVERITY +-- ============================================================================ + +--- The level each kind of finding is reported at. This is the only place a +--- severity is decided, so a later change to what an extension must correct is +--- a change to this table and nothing else. +--- +--- The current policy reports and never stops a render. A finding is a warning, +--- with two exceptions. An unreadable schema is an error, because no check runs +--- after it. A missing required argument is an error, because the shortcode +--- renders nothing without it, which is the one finding that changes the +--- document. +--- +--- A rejected document option keeps the error level it has today: it names a +--- value the extension cannot use, and the author has to correct it. A rejected +--- format option is the same finding about the same document, one section over, +--- so it takes the same level. +--- +--- A finding about an element's attribute is a warning instead. The attribute +--- stays on the element whatever the schema says, so the rendered output does +--- not change because of it, which is the reason a shortcode attribute is a +--- warning too. +--- +--- `misuse` is the one kind that is not about the document. It reports a fault +--- in the extension calling this module, and it is an error because the caller +--- is handed no value and would otherwise carry on with nil. +--- @type table +local SEVERITY = { + schema = 'error', + option_error = 'error', + option_warning = 'warning', + call_error = 'warning', + call_warning = 'warning', + missing_argument = 'error', + misuse = 'error', + attribute_error = 'warning', + attribute_warning = 'warning', + format_error = 'error', + format_warning = 'warning', +} + +--- The reporting function for each level. +--- @type table +local REPORTERS = { + error = log.log_error, + warning = log.log_warning, +} + +-- A level with no reporter is the mistake a change to `SEVERITY` makes, and it +-- is caught here, at load, rather than at the one render that reaches that kind +-- of finding. +for kind, level in pairs(SEVERITY) do + assert(REPORTERS[level] ~= nil, + string.format('schema-check: "%s" maps to the unknown level "%s"', kind, tostring(level))) +end + +-- ============================================================================ +-- PRIVATE HELPERS +-- ============================================================================ + +--- Read an attribute value with a surrounding quote pair removed. +--- Quarto's body parser strips those quotes before the value reaches the +--- shortcode, but the parser it uses for a text or attribute string hands the +--- raw token over instead, so `aria-hidden='true'` arrives as the five +--- character string `'true'`. Stripping here is what makes a quoted and an +--- unquoted value check as the same thing. +--- @param kwargs table Key-value options for the call +--- @param key string The attribute name to read +--- @return string +local function attr_value(kwargs, key) + --- @type string + local value = str.stringify(kwargs[key]) + --- @type string + local quote = value:sub(1, 1) + if #value > 1 and (quote == '"' or quote == "'") and value:sub(-1) == quote then + return value:sub(2, -2) + end + return value +end + +--- Copy a value, and every table inside it, all the way down. +--- The copy exists so that a caller writing into what it received cannot +--- change what every later reader of the same checker sees. +--- +--- The depth comes from the schema format, not from the schemas written so +--- far. The vocabulary allows `type: array` and `type: object`, and the +--- validator compiles a declared `default` by coercing it against that type +--- and returning it untouched once it already matches. Any option declaring a +--- collection type with a literal default therefore resolves to a Lua table, +--- at whatever depth the schema nests it, so a copy one level deep would hand +--- two callers the same inner table and leave the fault one level down. +--- +--- A table already copied on this walk is reused rather than copied again, +--- which keeps shared structure shared and makes a cycle terminate. A schema +--- file cannot express a cycle, because the validator's parser refuses anchors +--- and aliases, but the validator is injected and what it returns is not this +--- module's to assume. +--- @param value any The value to copy +--- @param seen table|nil Tables already copied on this walk +--- @return any +local function deep_copy(value, seen) + if type(value) ~= 'table' then + return value + end + seen = seen or {} + if seen[value] ~= nil then + return seen[value] + end + --- @type table + local copy = {} + seen[value] = copy + for key, item in pairs(value) do + copy[key] = deep_copy(item, seen) + end + return copy +end + +--- Flatten a call's named options to plain strings for the validator. +--- @param kwargs table Key-value options for the call +--- @return table +local function plain_kwargs(kwargs) + --- @type table + local plain = {} + for key in pairs(kwargs) do + plain[tostring(key)] = attr_value(kwargs, key) + end + return plain +end + +-- ============================================================================ +-- CHECKER +-- ============================================================================ + +--- @class Checker +--- @field validator table The validator the caller injected +--- @field extension string The extension name every message carries +--- @field schema table|nil The parsed schema, nil when it could not be read +--- @field defaults table The defaults the schema declares +--- @field resolved table|nil The three tables the configuration resolves to +--- @field options_checked boolean Whether the configuration was already checked +--- @field meta table|nil The metadata `options` was given, which `format` reads +--- @field formats table What each format checked so far resolved to +--- @field unavailable table Validator functions already reported missing +local Checker = {} +Checker.__index = Checker + +--- Report one finding at the level its kind is mapped to. +--- A kind with no entry in `SEVERITY` is a fault in this module, and it says so +--- rather than reporting the finding at a level nobody chose. It is reported +--- rather than raised, because a fault here must not remove a document. +--- @param kind string A key of `SEVERITY` +--- @param message string The message to report +--- @return nil +function Checker:_report(kind, message) + --- @type string|nil + local level = SEVERITY[kind] + if level == nil then + log.log_error(self.extension, string.format( + 'schema-check has no severity for "%s": %s', tostring(kind), message)) + return + end + REPORTERS[level](self.extension, message) +end + +--- Whether the validator provides one function, reporting it once if not. +--- +--- `validate_attributes` and `validate_format` are newer than the rest of the +--- contract, so a validator vendored before either exists satisfies everything +--- else and still lacks them. Calling one raises, and a raise removes the +--- document, which is the one thing this module promises not to do. +--- +--- It is reported once for the render rather than once for each caller. A +--- filter reaches these for every element and every format it handles, and the +--- condition is a fact about the vendored pair, so the second message says +--- nothing the first did not. +--- @param name string The function the caller is about to use +--- @return boolean available +function Checker:_provides(name) + if type(self.validator[name]) == 'function' then + return true + end + if not self.unavailable[name] then + self.unavailable[name] = true + self:_report('misuse', string.format( + 'schema-check: the validator provides no `%s`, so nothing was checked with it', name)) + end + return false +end + +--- Check the document configuration and return what it resolves to. The check +--- runs once, so an extension can ask on every shortcode without repeating the +--- messages. +--- +--- The defaults come first, because that is what most extensions want, and +--- because the schema is the one place they are written. They come from a +--- second pass over an empty table, which yields the declared defaults alone. +--- +--- The second return holds three tables, because they answer different +--- questions: +--- provided what the document actually set, which is the only way to tell +--- a deliberate `false` or `0` from an absent key, +--- merged the same values with coercion and defaults applied, +--- defaults the schema defaults on their own. +--- It is nil when there is nothing to resolve against, so an extension can +--- tell an unreadable schema from a document that set nothing. +--- +--- The defaults are a fresh copy on every call, so a caller may treat them as +--- its own. The tables inside the second return are the checker's, and every +--- later reader of the same checker sees them, so they must not be written to. +--- +--- Only the first call reads `meta`. A later call returns what the first one +--- resolved, whatever it is handed. A caller that needs a second document +--- checked builds a second checker. +--- +--- The argument cannot be read on a later call without losing the single check +--- this function promises. Quarto hands a shortcode a new metadata table on +--- every call, and the content is the same each time. A checker that read it +--- again repeats every finding once per shortcode. +--- @param meta table Document metadata, read on the first call only +--- @return table defaults A copy of the defaults, empty when there is no schema +--- @return table|nil resolved {provided, merged, defaults}, nil when there is no schema +function Checker:options(meta) + if self.options_checked then + return deep_copy(self.defaults), self.resolved + end + self.options_checked = true + -- Kept for `format`, which reads the same document. Quarto merges the options + -- of the selected format into the top level of this table, so the format + -- check has nowhere else to read them from. + self.meta = meta + + --- @type table|nil + local loaded = self.schema + -- The validator is injected from an independent source, so the shape of what + -- it returns is not this module's to assume. `call` makes the same allowance + -- for `shortcodes` one field over. + if loaded == nil or next(loaded.options or {}) == nil then + return deep_copy(self.defaults), self.resolved + end + + --- @type table + local provided = self.validator.extract_meta_options(meta, self.extension) + local valid, errors, warnings, merged = self.validator.validate(provided, loaded.options) + + for _, message in ipairs(warnings) do + self:_report('option_warning', message) + end + if not valid then + for _, message in ipairs(errors) do + self:_report('option_error', message) + end + end + + local _, _, _, defaults = self.validator.validate({}, loaded.options, { unknown = 'ignore' }) + self.defaults = defaults or {} + self.resolved = { provided = provided, merged = merged, defaults = self.defaults } + + return deep_copy(self.defaults), self.resolved +end + +--- Read what one option resolves to, after `options` has run. +--- +--- This is the value the schema decides, not the text the document holds. An +--- extension that reads the metadata itself has to decide what counts as true, +--- and each one that did decided something different, so `enabled: no` turned +--- one filter off and left another on. Here the schema is the only answer: the +--- validator coerces the written value toward the declared type, and a key the +--- document never set resolves to its declared default. +--- +--- It answers nil when there is no schema, which the checker has already +--- reported once. A key the schema does not declare answers nil as well, +--- because a schema that omits an option is the author's statement that the +--- extension does not have it. +--- @param key string The option name, as the schema declares it +--- @return any value The resolved value, nil when there is nothing to resolve +function Checker:option(key) + if type(key) ~= 'string' then + self:_report('misuse', string.format( + 'schema-check: the key given to `option` must be a string, got %s', type(key))) + return nil + end + if not self.options_checked then + self:_report('misuse', string.format( + 'schema-check: `option("%s")` was called before `options`', key)) + return nil + end + if self.resolved == nil then + return nil + end + return self.resolved.merged[key] +end + +--- Check one element's attributes against the `attributes` section, and return +--- what they resolve to. +--- +--- The section declares a map of groups. A group is named after the element it +--- describes, such as `Header` or `CodeBlock`, or after the class the extension +--- gives it, such as `modal`. `_any` is the group every element takes, and it +--- is additive rather than an alternative: quarto-revealjs-tabset declares +--- `panel-tabset` for a tabset's own attributes and `_any` for one that any +--- slide can carry, and an element can meet both. +--- +--- So both apply, and the named group runs last, over what `_any` resolved. The +--- validator hands an undeclared attribute straight back, so chaining the two +--- passes is the whole of the merge and there is no rule here about which group +--- wins. The one that declares the attribute decides it. +--- +--- This reports only. The attribute stays on the element whatever the schema +--- says, so nothing about the rendered output changes, which is why a finding +--- here is a warning as it is for a shortcode attribute. +--- The attributes may arrive as Pandoc's `AttributeList` rather than as a +--- table, which is what an element filter holds, so nothing here assumes a +--- plain table. The validator reads them with `pairs`, which both answer. +--- @param attributes table The element's attributes +--- @param group string|nil The element's own group, nil when it has none +--- @return table|nil resolved The attributes with the schema applied +function Checker:attributes(attributes, group) + if group ~= nil and type(group) ~= 'string' then + self:_report('misuse', string.format( + 'schema-check: the group given to `attributes` must be a string, got %s', type(group))) + return nil + end + + attributes = attributes or {} + + --- @type table|nil + local loaded = self.schema + -- The validator is injected from an independent source, so the shape of what + -- it returns is not this module's to assume, as `options` and `call` allow + -- for `options` and `shortcodes`. + if loaded == nil or next(loaded.attributes or {}) == nil then + return attributes + end + + if not self:_provides('validate_attributes') then + return attributes + end + + --- @type table + local resolved = attributes + -- `_any` is the group every element takes, so a caller that names it has + -- already asked for the only pass there is. Running the list would report + -- each of its findings twice for every element handed over. + local groups = (group == nil or group == '_any') and { '_any' } or { '_any', group } + for _, name in ipairs(groups) do + local _, errors, warnings, merged = + self.validator.validate_attributes(resolved, name, loaded) + for _, message in ipairs(errors) do + self:_report('attribute_error', message) + end + for _, message in ipairs(warnings) do + self:_report('attribute_warning', message) + end + resolved = merged + end + return resolved +end + +--- Check one output format's options against the `formats` section, and return +--- what they resolve to. +--- +--- Quarto merges the options of the selected format into the top level of the +--- document metadata, and the format name is never a key there. So this reads +--- the metadata `options` was given, and it must be called after `options`. +--- +--- The extension names its own format, as it names an attribute group. A format +--- name such as `letter-pdf` is a name the extension contributes, and nothing +--- here can work out which of several declared formats a render selected. +--- +--- The answer is kept, so a filter that asks again in the same render gets the +--- same table and the findings are reported once. +--- @param name string The format name, as the schema declares it +--- @return table|nil resolved The format's options, nil when there is nothing to resolve +function Checker:format(name) + if type(name) ~= 'string' then + self:_report('misuse', string.format( + 'schema-check: the name given to `format` must be a string, got %s', type(name))) + return nil + end + if not self.options_checked then + self:_report('misuse', string.format( + 'schema-check: `format("%s")` was called before `options`', name)) + return nil + end + if self.formats[name] ~= nil then + return self.formats[name] + end + + --- @type table|nil + local loaded = self.schema + if loaded == nil or next(loaded.formats or {}) == nil then + return nil + end + if not self:_provides('validate_format') then + return nil + end + + local _, errors, warnings, merged = + self.validator.validate_format(self.meta, name, loaded) + for _, message in ipairs(errors) do + self:_report('format_error', message) + end + for _, message in ipairs(warnings) do + self:_report('format_warning', message) + end + + self.formats[name] = merged + return merged +end + +--- Check one shortcode call against its entry in the schema. +--- This reports only. Nothing about the rendered output changes, so an +--- unrecognised attribute is surfaced rather than dropped. +--- @param name string Shortcode name +--- @param args table Positional arguments +--- @param kwargs table Key-value options for the call +--- @return nil +function Checker:call(name, args, kwargs) + --- @type table|nil + local loaded = self.schema + if loaded == nil then return end + + --- @type table|nil + local entry = loaded.shortcodes and loaded.shortcodes[name] + if entry == nil then return end + + args = args or {} + kwargs = kwargs or {} + + --- @type table + local positional = {} + for index, value in ipairs(args) do + positional[index] = str.stringify(value) + end + + local _, errors, warnings = self.validator.validate_shortcode( + name, positional, plain_kwargs(kwargs), entry) + + for _, message in ipairs(warnings) do + self:_report('call_warning', message) + end + + -- A required argument that is absent is read here rather than out of the + -- validator's findings, because the validator reports a nested argument + -- fault under one `arguments` entry, which cannot tell a missing argument + -- from a malformed one. + --- @type table + local missing = {} + for index, argument in ipairs(entry.arguments or {}) do + if argument.required == true and str.is_empty(positional[index]) then + missing[#missing + 1] = argument + end + end + + if #missing > 0 then + -- The only message about the missing argument: the schema's own `required` + -- wording says the same thing, and reporting both would state one fault + -- twice at two severities. Attribute warnings above still stand, and every + -- other finding about this call is secondary to there being no output. + for _, argument in ipairs(missing) do + --- The example comes from the schema, so that each shortcode carries its + --- own rather than this message naming one shortcode for all of them. + --- @type string + local advice = '' + local example = type(argument.examples) == 'table' and argument.examples[1] or nil + if example ~= nil then + advice = string.format(' For example: {{< %s %s >}}.', name, tostring(example)) + end + self:_report('missing_argument', string.format( + 'The "%s" shortcode needs its "%s" argument.%s', name, argument.name, advice)) + end + return + end + + for _, message in ipairs(errors) do + self:_report('call_error', message) + end +end + +-- ============================================================================ +-- PUBLIC API +-- ============================================================================ + +--- Build a checker for one extension, reading its schema once. +--- A schema that cannot be read is reported, and the checker it returns does +--- nothing: `options` gives an empty table and `call` gives no message. +--- +--- The path is resolved with `quarto.utils.resolve_path`, which answers +--- relative to the entry point that is running rather than to the extension +--- directory. An entry point in a subdirectory therefore has to say where the +--- schema is, and the checker it builds belongs at file scope, so that the +--- schema is read once for the render and not once for each call. +--- @param validator table The validator, with `load_schema`, `validate`, +--- `validate_shortcode`, `extract_meta_options`, `validate_attributes` and +--- `validate_format` +--- @param extension_name string The extension name every message carries +--- @param schema_path string|nil The schema to read, relative to the entry +--- point that is running. Defaults to `_schema.yml`. +--- @return Checker +--- @usage local checker = M.new(validator, 'iconify', '../_schema.yml') +function M.new(validator, extension_name, schema_path) + --- @type Checker + local checker = setmetatable({ + validator = validator, + extension = extension_name, + schema = nil, + defaults = {}, + resolved = nil, + options_checked = false, + meta = nil, + formats = {}, + unavailable = {}, + }, Checker) + + -- The default is chosen here rather than in the signature, so a caller that + -- passes two arguments reads the same file it read before this argument + -- existed. + local loaded, err = validator.load_schema( + quarto.utils.resolve_path(schema_path or '_schema.yml')) + if err then + checker:_report('schema', err) + else + checker.schema = loaded + end + + return checker +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/string.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/string.lua new file mode 100644 index 0000000..e551066 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-lua-modules/string.lua @@ -0,0 +1,376 @@ +--- MC String - String manipulation and escaping for Quarto Lua filters and shortcodes +--- @module "string" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +-- ============================================================================ +-- STRING UTILITIES +-- ============================================================================ + +--- Pandoc utility function for converting values to strings +--- @type function +M.stringify = pandoc.utils.stringify + +--- Check if a string is empty or nil. +--- Utility function to determine if a value is empty or nil, +--- which is useful for parameter validation throughout the module. +--- @param s string|nil|table The value to check for emptiness +--- @return boolean True if the value is nil or empty, false otherwise +--- @usage local result = M.is_empty("") -- returns true +--- @usage local result = M.is_empty(nil) -- returns true +--- @usage local result = M.is_empty("hello") -- returns false +function M.is_empty(s) + return s == nil or s == '' +end + +--- Escape special pattern characters in a string for Lua pattern matching +--- @param s string The string to escape +--- @return string The escaped string +--- @usage local escaped = M.escape_pattern("user/repo#123") +function M.escape_pattern(s) + local escaped = s:gsub('([%^%$%(%)%%%.%[%]%*%+%-%?])', '%%%1') + return escaped +end + +--- Split a string by a separator +--- @param str string The string to split +--- @param sep string The separator pattern +--- @return table Array of string fields +--- @usage local parts = M.split("a.b.c", ".") +function M.split(str, sep) + local fields = {} + local pattern = string.format('([^%s]+)', sep) + str:gsub(pattern, function(c) fields[#fields + 1] = c end) + return fields +end + +--- Trim leading and trailing whitespace from a string +--- @param str string The string to trim +--- @return string The trimmed string +--- @usage local trimmed = M.trim(" hello world ") -- returns "hello world" +function M.trim(str) + if str == nil then return '' end + return str:match('^%s*(.-)%s*$') +end + +--- Convert any value to a string, handling Pandoc objects and empty values. +--- Returns nil for empty or nil values, otherwise returns a string representation. +--- @param val any The value to convert +--- @return string|nil The string value or nil if empty +--- @usage local str = M.to_string(kwargs.value) +function M.to_string(val) + if not val then return nil end + if type(val) == 'string' then + return val ~= '' and val or nil + end + -- Handle Pandoc objects + if pandoc and pandoc.utils and pandoc.utils.stringify then + local str = pandoc.utils.stringify(val) + return str ~= '' and str or nil + end + local str = tostring(val) + return str ~= '' and str or nil +end + +--- Strip one layer of surrounding bracket or punctuation characters. +--- Handles balanced pairs: () [] {} "" '' `` «» +--- Handles trailing-only punctuation: , . ; : ! ? +--- @param text string The input text +--- @return string prefix Characters stripped from the start (may be empty) +--- @return string inner The inner text after stripping +--- @return string suffix Characters stripped from the end (may be empty) +function M.strip_surrounding(text) + if not text or #text < 2 then + return '', text or '', '' + end + + local balanced = { + ['('] = ')', ['['] = ']', ['{'] = '}', + ['"'] = '"', ["'"] = "'", ['`'] = '`', + } + -- UTF-8 guillemets + local first_two = text:sub(1, 2) + local last_two = text:sub(-2) + if first_two == '\xC2\xAB' and last_two == '\xC2\xBB' then + return first_two, text:sub(3, -3), last_two + end + + local first = text:sub(1, 1) + local last = text:sub(-1) + + if balanced[first] and last == balanced[first] then + return first, text:sub(2, -2), last + end + + local trailing = { + [','] = true, ['.'] = true, [';'] = true, + [':'] = true, ['!'] = true, ['?'] = true, + } + if trailing[last] then + return '', text:sub(1, -2), last + end + + return '', text, '' +end + +--- Peel unbalanced surrounding brackets and trailing punctuation from a token. +--- Unlike `strip_surrounding`, this does not require a balanced pair: it removes +--- any run of leading opening-bracket characters and any run of trailing +--- closing-bracket or punctuation characters. This handles bracket groups that +--- Pandoc split across whitespace, e.g. "(#2," and "#3)" from "(#2, #3)". +--- Leading set: ( [ { " ' ` and the 2-byte UTF-8 « (\xC2\xAB). +--- Trailing set: ) ] } " ' ` , . ; : ! ? and the 2-byte UTF-8 » (\xC2\xBB). +--- @param text string The input text +--- @return string prefix Characters peeled from the start (may be empty) +--- @return string inner The inner text after peeling +--- @return string suffix Characters peeled from the end (may be empty) +function M.strip_edges(text) + if not text or text == '' then + return '', text or '', '' + end + + local leading = { + ['('] = true, ['['] = true, ['{'] = true, + ['"'] = true, ["'"] = true, ['`'] = true, + } + local trailing = { + [')'] = true, [']'] = true, ['}'] = true, + ['"'] = true, ["'"] = true, ['`'] = true, + [','] = true, ['.'] = true, [';'] = true, + [':'] = true, ['!'] = true, ['?'] = true, + } + + local first = 1 + local last = #text + local prefix = '' + local suffix = '' + + while first <= last do + if text:sub(first, first + 1) == '\xC2\xAB' then + prefix = prefix .. '\xC2\xAB' + first = first + 2 + elseif leading[text:sub(first, first)] then + prefix = prefix .. text:sub(first, first) + first = first + 1 + else + break + end + end + + while last >= first do + -- The two-byte window can only match a real «/» pair: the leading loop + -- never leaves \xC2 at first - 1 (openers are ASCII or the \xAB of a peeled + -- «), so the guillemet check cannot straddle the already-peeled prefix. + if last >= 2 and text:sub(last - 1, last) == '\xC2\xBB' then + suffix = '\xC2\xBB' .. suffix + last = last - 2 + elseif trailing[text:sub(last, last)] then + suffix = text:sub(last, last) .. suffix + last = last - 1 + else + break + end + end + + return prefix, text:sub(first, last), suffix +end + +--- Find a balanced bracket pair anywhere in the text and split around it. +--- Walks the text from `start_pos` looking for an opening bracket whose matching +--- closing bracket appears later in the string. Returns the text split into +--- a prefix (up to and including the opening bracket), the inner content, and +--- a suffix (closing bracket and everything after). +--- Supports the same bracket pairs as `strip_surrounding`: +--- () [] {} "" '' `` and the 2-byte UTF-8 guillemets «». +--- @param text string The input text +--- @param start_pos integer|nil Byte position to start searching from (default 1) +--- @return string|nil prefix Text up to and including the opening bracket +--- @return string|nil content Non-empty text between the brackets +--- @return string|nil suffix Closing bracket and trailing text +--- @return integer|nil open_pos Byte position of the opening bracket +function M.find_bracketed_content(text, start_pos) + if not text or #text < 2 then + return nil, nil, nil, nil + end + start_pos = start_pos or 1 + + local balanced = { + ['('] = ')', ['['] = ']', ['{'] = '}', + ['"'] = '"', ["'"] = "'", ['`'] = '`', + } + + local i = start_pos + while i <= #text do + -- UTF-8 guillemet «…» + if text:sub(i, i + 1) == '\xC2\xAB' then + local close_pos = text:find('\xC2\xBB', i + 2, true) + if close_pos and close_pos > i + 2 then + return text:sub(1, i + 1), text:sub(i + 2, close_pos - 1), text:sub(close_pos), i + end + i = i + 2 + else + local c = text:sub(i, i) + local close_char = balanced[c] + if close_char then + local close_pos = text:find(close_char, i + 1, true) + if close_pos and close_pos > i + 1 then + return text:sub(1, i), text:sub(i + 1, close_pos - 1), text:sub(close_pos), i + end + end + i = i + 1 + end + end + + return nil, nil, nil, nil +end + +-- ============================================================================ +-- ESCAPE UTILITIES +-- ============================================================================ + +--- Escape special LaTeX characters in text. +--- @param text string The text to escape +--- @return string The escaped text safe for LaTeX +function M.escape_latex(text) + text = string.gsub(text, '\\', '\\textbackslash{}') + text = string.gsub(text, '%{', '\\{') + text = string.gsub(text, '%}', '\\}') + text = string.gsub(text, '%$', '\\$') + text = string.gsub(text, '%&', '\\&') + text = string.gsub(text, '%%', '\\%%') + text = string.gsub(text, '%#', '\\#') + text = string.gsub(text, '%^', '\\textasciicircum{}') + text = string.gsub(text, '%_', '\\_') + text = string.gsub(text, '~', '\\textasciitilde{}') + return text +end + +--- Escape special Typst characters in text. +--- @param text string The text to escape +--- @return string The escaped text safe for Typst +function M.escape_typst(text) + text = string.gsub(text, '%#', '\\#') + return text +end + +--- Escape characters for Typst string literals (inside `"..."`). +--- Handles backslash, double quote, newline, carriage return, and tab. +--- @param text string The text to escape +--- @return string The escaped text safe for Typst string literals +function M.escape_typst_string(text) + return (text + :gsub('\\', '\\\\') + :gsub('"', '\\"') + :gsub('\n', '\\n') + :gsub('\r', '\\r') + :gsub('\t', '\\t')) +end + +--- Escape characters for JavaScript string literals (inside `"..."` or `'...'`). +--- Handles backslash, both quote styles, newlines, carriage returns, tabs, +--- form feeds, and the `` block. +--- @param text string|nil The text to escape +--- @return string The escaped text safe for JavaScript string literals +--- @usage local safe = M.escape_js_string([[a "b" ]]) +function M.escape_js_string(text) + if text == nil then return '' end + if type(text) ~= 'string' then text = tostring(text) end + return (text + :gsub('\\', '\\\\') + :gsub('"', '\\"') + :gsub("'", "\\'") + :gsub('\n', '\\n') + :gsub('\r', '\\r') + :gsub('\t', '\\t') + :gsub('\f', '\\f') + :gsub(', ", and ' to prevent XSS and ensure valid HTML. +--- @param text string The text to escape +--- @return string Escaped text safe for use in HTML +--- @usage local escaped = M.escape_html('Hello ') +function M.escape_html(text) + if text == nil then return '' end + if type(text) ~= 'string' then text = tostring(text) end + local result = text + :gsub('&', '&') + :gsub('<', '<') + :gsub('>', '>') + :gsub('"', '"') + :gsub("'", ''') + return result +end + +--- Escape special HTML attribute characters. +--- Escapes characters that could break attribute values. +--- @param value string The attribute value to escape +--- @return string Escaped value safe for use in HTML attributes +--- @usage local escaped = M.escape_attribute('Hello "World"') +function M.escape_attribute(value) + if value == nil then return '' end + if type(value) ~= 'string' then value = tostring(value) end + local result = value + :gsub('&', '&') + :gsub('"', '"') + :gsub('<', '<') + :gsub('>', '>') + return result +end + +--- Escape text for different formats. +--- @param text string The text to escape +--- @param format string The format to escape for (e.g., "latex", "typst", "lua") +--- @return string The escaped text +function M.escape_text(text, format) + local escape_functions = { + latex = M.escape_latex, + typst = M.escape_typst, + lua = M.escape_lua_pattern + } + + local escape = escape_functions[format] + if escape then + return escape(text) + else + error('Unsupported escape format: ' .. format) + end +end + +--- Converts a string to a valid HTML id by lowercasing and replacing spaces. +--- @param text string The text to convert +--- @return string The HTML id +function M.ascii_id(text) + local id = text:lower():gsub('[^a-z0-9 ]', ''):gsub(' +', '-') + return id +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-wizard/LICENSE b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-wizard/LICENSE new file mode 100644 index 0000000..4b53bb8 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-wizard/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mickaël Canouil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-wizard/schema.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-wizard/schema.lua new file mode 100644 index 0000000..add64b8 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/_vendor/quarto-wizard/schema.lua @@ -0,0 +1,2569 @@ +--- MC Schema - Reference validator for Quarto extension schemas (`_schema.yml`) +--- @module "schema" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @version 2.2.0 +--- +--- Implements the v2 extension schema vocabulary published at +--- . +--- The v1 dual-key vocabulary (`min`, `max`, `enum-case-insensitive`, +--- `element-attributes`, `pattern-exact`) is not accepted. +--- +--- The module is standalone: it requires no sibling module, and it reaches +--- `pandoc` and `quarto` only through the `M._env` table, so it can be +--- exercised outside a Quarto render with stubs in place. + +local M = {} + +--- Meta-schema this module implements. +M.SCHEMA_VERSION = 'https://m.canouil.dev/quarto-wizard/assets/schema/v2/extension-schema.json' + +-- ============================================================================ +-- ENVIRONMENT SEAM +-- ============================================================================ + +--- Indirection over the two host globals, resolved at call time so a test +--- harness can install stubs before the first call. +local _env = {} + +--- Render a Pandoc value as plain text. +--- @param value any Pandoc value +--- @return string Plain text +function _env.stringify(value) + return pandoc.utils.stringify(value) +end + +--- Report the Pandoc type of a value. +--- @param value any Pandoc value +--- @return string Pandoc type name +function _env.pandoc_type(value) + return pandoc.utils.type(value) +end + +--- Emit a warning through the host logger. +--- @param message string Message to emit +--- @return nil +function _env.warn(message) + if quarto and quarto.log and quarto.log.warning then + quarto.log.warning(message) + else + io.stderr:write(message .. '\n') + end +end + +--- Emit an error through the host logger. +--- @param message string Message to emit +--- @return nil +function _env.report_error(message) + if quarto and quarto.log and quarto.log.error then + quarto.log.error(message) + else + io.stderr:write(message .. '\n') + end +end + +M._env = _env + +-- ============================================================================ +-- CONSTANTS +-- ============================================================================ + +--- Maximum nesting depth accepted when converting a parsed schema. +local MAX_DEPTH = 16 + +--- Maximum number of Lua patterns one regex may expand into. +local MAX_PATTERN_BRANCHES = 64 + +--- Descriptor keywords holding a numeric bound. +local NUMERIC_KEYWORDS = { + 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', + 'minLength', 'maxLength', 'minItems', 'maxItems', +} + +--- Every keyword the v2 field descriptor may carry, mapped to whether this +--- module enforces it. Annotations are carried through untouched for tooling. +M.KEYWORDS = { + type = true, + required = true, + default = true, + const = true, + enum = true, + enumCaseInsensitive = true, + pattern = true, + minimum = true, + maximum = true, + exclusiveMinimum = true, + exclusiveMaximum = true, + multipleOf = true, + minLength = true, + maxLength = true, + minItems = true, + maxItems = true, + uniqueItems = true, + items = true, + properties = true, + additionalProperties = true, + propertyNames = true, + dependentRequired = true, + aliases = true, + deprecated = true, + name = true, + description = false, + title = false, + examples = false, + format = false, + completion = false, + contentEncoding = false, + contentMediaType = false, +} + +-- ============================================================================ +-- PRIVATE HELPERS +-- ============================================================================ + +--- Report whether a table is a dense array. +--- @param value any Value to inspect +--- @return boolean True when the table has only keys 1..n +local function _is_array(value) + if type(value) ~= 'table' then + return false + end + local count = 0 + for _ in pairs(value) do + count = count + 1 + end + return count == #value +end + +--- Read a key from a map, accepting the hyphen and underscore spellings. +--- Keys are never rewritten, so a schema keeps the names its author wrote. +--- @param map table|nil Map to read +--- @param key string|nil Key to read +--- @return any value The stored value, or nil +--- @return string|nil found_key The spelling that matched +local function _lookup(map, key) + if type(map) ~= 'table' or type(key) ~= 'string' then + return nil, nil + end + if map[key] ~= nil then + return map[key], key + end + local underscored = (key:gsub('%-', '_')) + if underscored ~= key and map[underscored] ~= nil then + return map[underscored], underscored + end + local hyphenated = (key:gsub('_', '-')) + if hyphenated ~= key and map[hyphenated] ~= nil then + return map[hyphenated], hyphenated + end + return nil, nil +end + +--- Render any value for an error message, including mixed-type arrays. +--- Replaces `table.concat`, which throws on a boolean entry. +--- @param value any Value to render +--- @return string Readable representation +local function _format_value(value) + local kind = type(value) + if kind == 'string' then + if value == '' then + return '""' + end + return value + elseif kind == 'number' or kind == 'boolean' then + return tostring(value) + elseif kind == 'nil' then + return 'null' + elseif kind == 'table' then + if _is_array(value) then + local parts = {} + for index = 1, #value do + parts[index] = _format_value(value[index]) + end + return '[' .. table.concat(parts, ', ') .. ']' + end + return '{object}' + end + return tostring(value) +end + +--- Render a list of values as a comma-separated string. +--- @param values table Array of values +--- @return string Readable list +local function _format_list(values) + local parts = {} + for index = 1, #values do + parts[index] = _format_value(values[index]) + end + return table.concat(parts, ', ') +end + +--- Normalise a type spec to an array of type names. +--- @param type_spec string|table|nil Declared type +--- @return table|nil Array of type names, or nil when no type is declared +local function _type_names(type_spec) + if type_spec == nil then + return nil + end + if type(type_spec) == 'table' then + return type_spec + end + return { type_spec } +end + +--- Render a type spec for an error message. +--- @param type_spec string|table Declared type +--- @return string Readable type +local function _format_type(type_spec) + if type(type_spec) == 'table' then + return table.concat(type_spec, ' | ') + end + return tostring(type_spec) +end + +--- Report the type name of a value, distinguishing arrays from objects. +--- @param value any Value to inspect +--- @return string Type name +local function _actual_type(value) + local kind = type(value) + if kind == 'table' then + return _is_array(value) and 'array' or 'object' + end + if kind == 'nil' then + return 'null' + end + return kind +end + +--- Report whether a value satisfies a single type name. +--- @param value any Value to check +--- @param name string Type name +--- @return boolean True when the value matches +local function _matches_type(value, name) + if name == 'content' then + return value ~= nil + end + if name == 'null' then + return value == nil + end + if name == 'integer' then + return type(value) == 'number' + and value == value + and value ~= math.huge + and value ~= -math.huge + and value == math.floor(value) + end + if name == 'number' then + return type(value) == 'number' + end + if name == 'boolean' then + return type(value) == 'boolean' + end + if name == 'string' then + return type(value) == 'string' + end + if name == 'array' then + return type(value) == 'table' and _is_array(value) + end + if name == 'object' then + -- An array is not an object. An empty table is both, and Lua cannot tell + -- the two apart, so it satisfies either. + return type(value) == 'table' and (next(value) == nil or not _is_array(value)) + end + return false +end + +--- Report whether a value satisfies any declared type. +--- @param value any Value to check +--- @param type_spec string|table|nil Declared type +--- @return boolean True when the value matches, or no type is declared +local function _matches_type_spec(value, type_spec) + local names = _type_names(type_spec) + if names == nil then + return true + end + for _, name in ipairs(names) do + if _matches_type(value, name) then + return true + end + end + return false +end + +--- Coerce a string toward a single scalar type. +--- Pandoc renders every YAML number as a string, so the incoming value and +--- the schema's own literals both need this before any comparison. +--- @param value any Value to coerce +--- @param name string Target type name +--- @return any coerced Coerced value, or nil +--- @return boolean ok True when the coercion applied +local function _coerce_scalar(value, name) + if type(value) ~= 'string' then + return nil, false + end + if name == 'number' then + local number = tonumber(value) + if number then + return number, true + end + elseif name == 'integer' then + local number = tonumber(value) + if number and number == math.floor(number) then + return number, true + end + elseif name == 'boolean' then + -- Only `true` and `false`, which is what YAML 1.2 calls a boolean and what + -- Pandoc reads. `yes` and `no` are booleans in YAML 1.1 alone, and Pandoc + -- hands them over as strings, so accepting them here made this validator + -- disagree with the YAML the documents are written in. + -- + -- The coercion is still needed for the words that do name a boolean, + -- because an attribute and a shortcode argument always arrive as strings. + local lowered = value:lower() + if lowered == 'true' then + return true, true + end + if lowered == 'false' then + return false, true + end + end + return nil, false +end + +--- Coerce a value toward its declared type. +--- A value already matching one declared type is returned untouched, so +--- `type: [boolean, string]` keeps the string "fenced" a string. +--- @param value any Value to coerce +--- @param type_spec string|table|nil Declared type +--- @return any Coerced value +local function _coerce(value, type_spec) + local names = _type_names(type_spec) + if names == nil or value == nil then + return value + end + for _, name in ipairs(names) do + if _matches_type(value, name) then + return value + end + end + for _, name in ipairs(names) do + local coerced, ok = _coerce_scalar(value, name) + if ok then + return coerced + end + end + return value +end + +-- ============================================================================ +-- PATTERN COMPILATION +-- ============================================================================ + +--- Lua replacements for JS shorthand classes, outside a character class. +local ESCAPE_OUTSIDE = { + d = '%d', D = '%D', w = '[%w_]', W = '[^%w_]', s = '%s', S = '%S', +} + +--- Lua replacements for JS shorthand classes, inside a character class. +--- `\W` has no in-class form, because the underscore cannot be excluded there. +local ESCAPE_INSIDE = { + d = '%d', D = '%D', w = '%w_', s = '%s', S = '%S', +} + +--- Escapes with a real Lua equivalent. Anything else alphanumeric is refused, +--- because compiling it to the bare letter would accept the wrong values. +local CONTROL_ESCAPES = { + n = '\n', + t = '\t', + r = '\r', + f = '\f', + v = '\v', +} + +--- Characters Lua treats as magic outside a character class. +local LUA_MAGIC = '^$()%.[]*+-?' + +--- Escape one literal character for use in a Lua pattern. +--- @param char string Single character +--- @return string Escaped character +local function _escape_literal(char) + if LUA_MAGIC:find(char, 1, true) then + return '%' .. char + end + return char +end + +--- Compile a JS regular expression into a list of equivalent Lua patterns. +--- Alternation is expanded into one pattern per branch, which is how Lua, +--- lacking alternation, can honour the commonest JSON Schema idiom. +--- Anything that cannot be expressed is reported rather than accepted. +--- @param regex string JS regular expression +--- @return table|nil branches Array of Lua patterns, or nil on failure +--- @return string|nil reason Why compilation failed +local function _compile_pattern(regex) + local position = 1 + local length = #regex + local anchor_start = false + local anchor_end = false + local has_top_level_alternation = false + local parse_alternation + + --- Combine a set of prefixes with a set of continuations. + local function cross(prefixes, continuations) + local out = {} + for _, prefix in ipairs(prefixes) do + for _, continuation in ipairs(continuations) do + if #out >= MAX_PATTERN_BRANCHES then + return nil, 'alternation expands past ' .. MAX_PATTERN_BRANCHES .. ' branches' + end + out[#out + 1] = prefix .. continuation + end + end + return out + end + + --- Parse a bracketed character class starting at the current position. + local function parse_class() + local out = { '[' } + position = position + 1 + if regex:sub(position, position) == '^' then + out[#out + 1] = '^' + position = position + 1 + end + if regex:sub(position, position) == ']' then + out[#out + 1] = '%]' + position = position + 1 + end + while position <= length do + local char = regex:sub(position, position) + if char == ']' then + position = position + 1 + out[#out + 1] = ']' + return table.concat(out) + elseif char == '\\' then + local next_char = regex:sub(position + 1, position + 1) + if next_char == '' then + return nil, 'trailing backslash' + end + if next_char == 'W' then + return nil, 'unsupported "\\W" inside a character class' + end + local mapped = ESCAPE_INSIDE[next_char] + if mapped then + out[#out + 1] = mapped + elseif next_char:match('[1-9]') then + return nil, 'unsupported backreference "\\' .. next_char .. '"' + elseif next_char == '%' then + out[#out + 1] = '%%' + elseif CONTROL_ESCAPES[next_char] then + out[#out + 1] = CONTROL_ESCAPES[next_char] + elseif next_char:match('%w') then + return nil, 'unsupported escape "\\' .. next_char .. '"' + else + out[#out + 1] = '%' .. next_char + end + position = position + 2 + elseif char == '%' then + out[#out + 1] = '%%' + position = position + 1 + else + out[#out + 1] = char + position = position + 1 + end + end + return nil, 'unterminated character class' + end + + --- Read a trailing quantifier, rejecting the forms Lua cannot express. + local function parse_quantifier() + local char = regex:sub(position, position) + if char ~= '*' and char ~= '+' and char ~= '?' then + return '' + end + if regex:sub(position + 1, position + 1) == '?' then + return nil, 'unsupported non-greedy quantifier "' .. char .. '?"' + end + position = position + 1 + return char + end + + --- Parse a single atom and return the branches it contributes. + local function parse_atom(depth, at_sequence_start) + local char = regex:sub(position, position) + + if char == '(' then + if regex:sub(position + 1, position + 1) == '?' then + local marker = regex:sub(position + 2, position + 2) + if marker == '=' or marker == '!' or marker == '<' then + return nil, 'unsupported lookahead or lookbehind' + end + if marker ~= ':' then + return nil, 'unsupported group modifier "(?' .. marker .. '"' + end + position = position + 3 + else + position = position + 1 + end + local branches, reason = parse_alternation(depth + 1) + if not branches then + return nil, reason + end + if regex:sub(position, position) ~= ')' then + return nil, 'unterminated group' + end + position = position + 1 + local quantifier = regex:sub(position, position) + if quantifier == '*' or quantifier == '+' or quantifier == '?' then + return nil, 'unsupported quantifier applied to a group' + end + return branches + end + + if char == '[' then + local class, reason = parse_class() + if not class then + return nil, reason + end + local quantifier, quantifier_reason = parse_quantifier() + if not quantifier then + return nil, quantifier_reason + end + return { class .. quantifier } + end + + if char == '\\' then + local next_char = regex:sub(position + 1, position + 1) + if next_char == '' then + return nil, 'trailing backslash' + end + if next_char:match('[1-9]') then + return nil, 'unsupported backreference "\\' .. next_char .. '"' + end + position = position + 2 + local mapped = ESCAPE_OUTSIDE[next_char] + local atom + if mapped then + atom = mapped + elseif CONTROL_ESCAPES[next_char] then + atom = CONTROL_ESCAPES[next_char] + elseif next_char:match('%w') then + return nil, 'unsupported escape "\\' .. next_char .. '"' + else + atom = _escape_literal(next_char) + end + local quantifier, quantifier_reason = parse_quantifier() + if not quantifier then + return nil, quantifier_reason + end + return { atom .. quantifier } + end + + if char == '^' then + if depth ~= 0 or not at_sequence_start or position ~= 1 then + return nil, 'unsupported anchor "^" away from the start of the pattern' + end + position = position + 1 + anchor_start = true + return { '' } + end + + if char == '$' then + if depth ~= 0 or position ~= length then + return nil, 'unsupported anchor "$" away from the end of the pattern' + end + position = position + 1 + anchor_end = true + return { '' } + end + + if char == '{' then + return nil, 'unsupported counted quantifier "{n,m}"' + end + + if char == '*' or char == '+' or char == '?' then + return nil, 'quantifier "' .. char .. '" with nothing to repeat' + end + + if char == '.' then + position = position + 1 + local quantifier, quantifier_reason = parse_quantifier() + if not quantifier then + return nil, quantifier_reason + end + return { '.' .. quantifier } + end + + position = position + 1 + local atom = _escape_literal(char) + local quantifier, quantifier_reason = parse_quantifier() + if not quantifier then + return nil, quantifier_reason + end + return { atom .. quantifier } + end + + --- Parse one alternative: a run of atoms up to `|`, `)` or the end. + local function parse_sequence(depth) + local branches = { '' } + local first = true + while position <= length do + local char = regex:sub(position, position) + if char == '|' or char == ')' then + break + end + local atom_branches, reason = parse_atom(depth, first) + if not atom_branches then + return nil, reason + end + first = false + local crossed, cross_reason = cross(branches, atom_branches) + if not crossed then + return nil, cross_reason + end + branches = crossed + end + return branches + end + + parse_alternation = function(depth) + local all = {} + local iterations = 0 + while true do + iterations = iterations + 1 + local branches, reason = parse_sequence(depth) + if not branches then + return nil, reason + end + for _, branch in ipairs(branches) do + if #all >= MAX_PATTERN_BRANCHES then + return nil, 'alternation expands past ' .. MAX_PATTERN_BRANCHES .. ' branches' + end + all[#all + 1] = branch + end + if regex:sub(position, position) == '|' then + position = position + 1 + else + break + end + end + if depth == 0 and iterations > 1 then + has_top_level_alternation = true + end + return all + end + + local branches, reason = parse_alternation(0) + if not branches then + return nil, reason + end + if position <= length then + return nil, 'unbalanced ")" in pattern' + end + + -- The anchors are collected for the expression as a whole, so applying them + -- to multiple top-level branches would anchor branches the author did not anchor. + if has_top_level_alternation and (anchor_start or anchor_end) then + return nil, 'unsupported anchor in a top-level alternation' + end + + local prefix = anchor_start and '^' or '' + local suffix = anchor_end and '$' or '' + local out = {} + for index, branch in ipairs(branches) do + out[index] = prefix .. branch .. suffix + end + return out +end + +M._compile_pattern = _compile_pattern + +--- Report whether a string satisfies any compiled branch. +--- @param value string Value to test +--- @param branches table Array of Lua patterns +--- @return boolean True when a branch matches +local function _pattern_matches(value, branches) + for _, branch in ipairs(branches) do + local ok, matched = pcall(string.match, value, branch) + if ok and matched ~= nil then + return true + end + end + return false +end + +-- ============================================================================ +-- PANDOC VALUE CONVERSION +-- ============================================================================ + +--- Convert a Pandoc metadata value to a native Lua value. +--- Pandoc's Lua objects carry no `t` field, so the kind has to come from +--- `pandoc.utils.type`; testing `value.t` silently turns every string option +--- into a list of its inline elements. +--- @param value any Pandoc metadata value +--- @return any Native Lua value +local function _convert_pandoc_value(value) + local kind = _env.pandoc_type(value) + + -- A bare key, `null` and `~` all arrive as an empty Pandoc `string`, which + -- is how Pandoc collapses YAML null. An explicit `""` arrives as an empty + -- `Inlines` instead, so the two are told apart before either is unwrapped. + if kind == 'string' and value == '' then + return nil + end + + if type(value) ~= 'table' then + return value + end + + if kind == 'Inlines' or kind == 'Blocks' or kind == 'Inline' or kind == 'Block' then + return _env.stringify(value) + end + + if kind == 'List' then + -- A null element is absent, the same rule this module applies to a null + -- key. Writing it into a running count rather than the source index + -- keeps the result a proper sequence instead of leaving a gap. + local result = {} + for index = 1, #value do + local converted = _convert_pandoc_value(value[index]) + if converted ~= nil then + result[#result + 1] = converted + end + end + return result + end + + if _is_array(value) and #value > 0 then + -- A null element is absent, the same rule this module applies to a null + -- key. Writing it into a running count rather than the source index + -- keeps the result a proper sequence instead of leaving a gap. + local result = {} + for index = 1, #value do + local converted = _convert_pandoc_value(value[index]) + if converted ~= nil then + result[#result + 1] = converted + end + end + return result + end + + local result = {} + for key, item in pairs(value) do + result[tostring(key)] = _convert_pandoc_value(item) + end + return result +end + +-- ============================================================================ +-- YAML PARSING +-- ============================================================================ + +--- Pandoc parses YAML string scalars as Markdown, which rewrites a schema in +--- place: `^[a-z]+$` arrives as `+$`, and `^\d+\.?\d*m?s$` arrives as +--- `^.??s$`. The schema file is therefore parsed here instead. +--- +--- The subset covers what an extension schema needs: block maps and +--- sequences, plain and quoted scalars, folded and literal block scalars, +--- flow sequences and flow maps (including ones spanning several lines), and +--- `#` comments. Anchors, aliases, tags, multiple documents and complex keys +--- are not supported and are reported rather than misread. +--- +--- Every function here reports a failure by returning a message rather than +--- by calling `error`. Quarto replaces `error` with a logger that returns +--- instead of unwinding, so a module that trusts `error` to stop execution +--- carries on with invalid state and fails later somewhere unrelated. +--- +--- One deliberate deviation from YAML: a block scalar is always stripped of +--- its trailing newline unless it is chomped with `+`. Schema text is +--- annotation, and a trailing newline there is noise rather than meaning. + +--- Build a parse error message carrying the offending line number. +--- @param line_number number Line number +--- @param message string Explanation +--- @return string Formatted message +local function _yaml_error(line_number, message) + return string.format('line %d: %s', line_number, message) +end + +--- Remove a trailing `#` comment, respecting quoted spans. +--- @param text string Line content +--- @return string Content without its comment +local function _strip_comment(text) + local out = {} + local quote = nil + local index = 1 + while index <= #text do + local char = text:sub(index, index) + if quote then + out[#out + 1] = char + if char == '\\' and quote == '"' then + index = index + 1 + out[#out + 1] = text:sub(index, index) + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + out[#out + 1] = char + elseif char == '#' and (index == 1 or text:sub(index - 1, index - 1):match('%s')) then + break + else + out[#out + 1] = char + end + index = index + 1 + end + return (table.concat(out):gsub('%s+$', '')) +end + +--- Trim leading and trailing whitespace. +--- @param text string Text to trim +--- @return string Trimmed text +local function _trim(text) + return (text:gsub('^%s+', ''):gsub('%s+$', '')) +end + +--- Parse a scalar, giving numbers and booleans their real Lua types. +--- @param text string Scalar text +--- @param line_number number Line number for errors +--- @return any value Parsed value, nil for an explicit null +--- @return string|nil err +local function _parse_scalar(text, line_number) + text = _trim(text) + + if text == '' or text == '~' or text == 'null' or text == 'Null' or text == 'NULL' then + return nil, nil + end + + local first = text:sub(1, 1) + + if first == '"' then + if #text < 2 or text:sub(-1) ~= '"' then + return nil, _yaml_error(line_number, 'unterminated double-quoted scalar') + end + return (text:sub(2, -2):gsub('\\(.)', function(escaped) + if escaped == 'n' then return '\n' end + if escaped == 't' then return '\t' end + if escaped == 'r' then return '\r' end + return escaped + end)), nil + end + + if first == "'" then + if #text < 2 or text:sub(-1) ~= "'" then + return nil, _yaml_error(line_number, 'unterminated single-quoted scalar') + end + return (text:sub(2, -2):gsub("''", "'")), nil + end + + if first == '&' or first == '*' or first == '!' then + return nil, _yaml_error(line_number, 'anchors, aliases and tags are not supported') + end + + if text == 'true' or text == 'True' or text == 'TRUE' or text == 'yes' or text == 'Yes' then + return true, nil + end + if text == 'false' or text == 'False' or text == 'FALSE' or text == 'no' or text == 'No' then + return false, nil + end + + local number = tonumber(text) + if number ~= nil and text:match('^[%-+]?[%d%.eE%+%-]+$') then + return number, nil + end + + return text, nil +end + +--- Split a flow collection body on its top-level commas. +--- @param body string Text between the brackets +--- @return table Array of raw item strings +local function _split_flow(body) + local items = {} + local depth = 0 + local quote = nil + local start = 1 + local index = 1 + while index <= #body do + local char = body:sub(index, index) + if quote then + if char == '\\' and quote == '"' then + index = index + 1 + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + elseif char == '[' or char == '{' then + depth = depth + 1 + elseif char == ']' or char == '}' then + depth = depth - 1 + elseif char == ',' and depth == 0 then + items[#items + 1] = body:sub(start, index - 1) + start = index + 1 + end + index = index + 1 + end + local tail = body:sub(start) + if tail:match('%S') then + items[#items + 1] = tail + end + return items +end + +--- Strip the quotes from a mapping key. +--- @param key string Raw key text +--- @param line_number number Line number for errors +--- @return string|nil key +--- @return string|nil err +local function _unquote_key(key, line_number) + key = _trim(key) + local first = key:sub(1, 1) + if first == '"' or first == "'" then + local parsed, err = _parse_scalar(key, line_number) + if err then + return nil, err + end + return tostring(parsed), nil + end + return key, nil +end + +local _parse_flow + +--- Parse a value written in flow style, or a plain scalar. +--- @param text string Value text +--- @param line_number number Line number for errors +--- @param depth number|nil Current nesting depth +--- @return any value +--- @return string|nil err +_parse_flow = function(text, line_number, depth) + depth = depth or 0 + if depth > MAX_DEPTH then + return nil, _yaml_error(line_number, 'value nests deeper than ' .. MAX_DEPTH .. ' levels') + end + + text = _trim(text) + local first = text:sub(1, 1) + + if first == '[' then + if text:sub(-1) ~= ']' then + return nil, _yaml_error(line_number, 'unterminated flow sequence') + end + local out = {} + for index, item in ipairs(_split_flow(text:sub(2, -2))) do + local value, err = _parse_flow(item, line_number, depth + 1) + if err then + return nil, err + end + out[index] = value + end + return out, nil + end + + if first == '{' then + if text:sub(-1) ~= '}' then + return nil, _yaml_error(line_number, 'unterminated flow mapping') + end + local out = {} + for _, item in ipairs(_split_flow(text:sub(2, -2))) do + local key, rest = item:match('^%s*(.-)%s*:%s*(.*)$') + if not key or key == '' then + return nil, _yaml_error(line_number, 'expected "key: value" inside a flow mapping') + end + local name, key_err = _unquote_key(key, line_number) + if key_err then + return nil, key_err + end + local value, err = _parse_flow(rest, line_number, depth + 1) + if err then + return nil, err + end + out[name] = value + end + return out, nil + end + + return _parse_scalar(text, line_number) +end + +--- Split a mapping line into its key and the rest of the line. +--- The separator is the first unquoted colon followed by a space or the end +--- of the line, so a URL value such as `https://...` is not split. +--- @param text string Line content +--- @return string|nil key +--- @return string|nil rest +local function _split_key(text) + local quote = nil + local index = 1 + while index <= #text do + local char = text:sub(index, index) + if quote then + if char == '\\' and quote == '"' then + index = index + 1 + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + elseif char == ':' then + local following = text:sub(index + 1, index + 1) + if following == '' or following == ' ' then + return text:sub(1, index - 1), _trim(text:sub(index + 2)) + end + end + index = index + 1 + end + return nil, nil +end + +--- Break the source into classified lines. +--- @param source string YAML text +--- @return table|nil lines Array of {raw, indent, number, blank, comment} +--- @return string|nil err +local function _scan_lines(source) + local lines = {} + local number = 0 + for raw in (source .. '\n'):gmatch('([^\n]*)\n') do + number = number + 1 + if raw:find('\t') then + return nil, _yaml_error(number, 'tabs are not valid YAML indentation') + end + local indent = #(raw:match('^ *') or '') + local body = raw:sub(indent + 1) + lines[#lines + 1] = { + raw = raw, + indent = indent, + number = number, + blank = body == '', + comment = body:sub(1, 1) == '#', + } + end + return lines, nil +end + +--- Advance past blank and comment lines. +--- @param lines table Scanned lines +--- @param index number Starting index +--- @return number Index of the next significant line +local function _skip_insignificant(lines, index) + while index <= #lines and (lines[index].blank or lines[index].comment) do + index = index + 1 + end + return index +end + +--- Read the comment-stripped content of a line. +--- @param line table Scanned line +--- @return string Content +local function _content(line) + return _strip_comment(line.raw:sub(line.indent + 1)) +end + +--- Track bracket depth across one line, ignoring quoted spans. +--- @param text string Line content +--- @param depth number Depth carried in +--- @param quote string|nil Open quote carried in +--- @return number depth +--- @return string|nil quote +local function _scan_flow_depth(text, depth, quote) + local index = 1 + while index <= #text do + local char = text:sub(index, index) + if quote then + if char == '\\' and quote == '"' then + index = index + 1 + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + elseif char == '[' or char == '{' then + depth = depth + 1 + elseif char == ']' or char == '}' then + depth = depth - 1 + end + index = index + 1 + end + return depth, quote +end + +--- Collect a flow collection that may run across several lines. +--- @param lines table Scanned lines +--- @param start number Index of the line holding the opening bracket +--- @param initial string Flow text already read from that line +--- @param line_number number Line number for errors +--- @return string|nil text The whole flow collection on one line +--- @return number next_index Index of the first line after it +--- @return string|nil err +local function _gather_flow(lines, start, initial, line_number) + local pieces = { initial } + local depth, quote = _scan_flow_depth(initial, 0, nil) + local index = start + + while depth > 0 do + index = index + 1 + if index > #lines then + return nil, index, _yaml_error(line_number, 'unterminated flow collection') + end + local content = _content(lines[index]) + pieces[#pieces + 1] = content + depth, quote = _scan_flow_depth(content, depth, quote) + end + + return table.concat(pieces, ' '), index + 1, nil +end + +--- Read a folded or literal block scalar. +--- @param lines table Scanned lines +--- @param start number First candidate line +--- @param parent_indent number Indentation of the owning key +--- @param style string Block style, such as '>', '>-' or '|' +--- @return string text +--- @return number next_index +local function _read_block_scalar(lines, start, parent_indent, style) + local kind = style:sub(1, 1) + local chomp = style:sub(2, 2) + + local collected = {} + local block_indent = nil + local index = start + + while index <= #lines do + local line = lines[index] + if line.blank then + collected[#collected + 1] = '' + index = index + 1 + elseif line.indent > parent_indent then + block_indent = block_indent or line.indent + collected[#collected + 1] = line.raw:sub(block_indent + 1) + index = index + 1 + else + break + end + end + + while #collected > 0 and collected[#collected] == '' do + table.remove(collected) + end + + local text + if kind == '>' then + local parts = {} + local buffer = {} + for _, line in ipairs(collected) do + if line == '' then + if #buffer > 0 then + parts[#parts + 1] = table.concat(buffer, ' ') + buffer = {} + end + parts[#parts + 1] = '\n' + else + buffer[#buffer + 1] = (line:gsub('%s+$', '')) + end + end + if #buffer > 0 then + parts[#parts + 1] = table.concat(buffer, ' ') + end + text = table.concat(parts) + else + text = table.concat(collected, '\n') + end + + if chomp == '+' then + text = text .. '\n' + end + + return text, index +end + +local _parse_block +local _parse_sequence + +--- The order in which each parsed mapping declared its keys. +--- Lua tables have no key order, but a schema is authored in a meaningful one, +--- and a generator that turns a schema into documentation needs it. The order +--- is kept beside the data rather than inside it, so it cannot be mistaken for +--- a key of the mapping. Weak keys let a discarded mapping take its order with +--- it. +local _key_order = setmetatable({}, { __mode = 'k' }) + +--- Parse a block mapping at a known indentation. +--- @return table|nil map +--- @return number next_index +--- @return string|nil err +local function _parse_map(lines, start, indent, depth) + local map = {} + local order = {} + local index = start + + while index <= #lines do + local line = lines[index] + if line.blank or line.comment then + index = index + 1 + elseif line.indent < indent then + break + else + if line.indent > indent then + return nil, index, _yaml_error(line.number, 'unexpected indentation inside a mapping') + end + + local content = _content(line) + if content == '' then + index = index + 1 + else + if content == '---' or content == '...' then + return nil, index, + _yaml_error(line.number, 'multiple YAML documents are not supported') + end + + if content:sub(1, 1) == '-' then + return nil, index, + _yaml_error(line.number, 'a sequence item cannot appear inside a mapping here') + end + + local raw_key, rest = _split_key(content) + if raw_key == nil or _trim(raw_key) == '' then + return nil, index, _yaml_error(line.number, 'expected "key: value"') + end + + local key, key_err = _unquote_key(raw_key, line.number) + if key_err then + return nil, index, key_err + end + if map[key] == nil then + order[#order + 1] = key + end + index = index + 1 + + -- An explicit indentation indicator (`|2`, `>2-`) is reported rather + -- than misread: without this the whole block is stored as the string + -- "|2" and its body is then parsed as further mapping lines. + if rest:match('^[|>]%d') or rest:match('^[|>][+%-]%d') then + return nil, index, _yaml_error( + line.number, + 'a block scalar indentation indicator is not supported: ' .. rest + ) + end + + local block_style = rest:match('^([|>][+%-]?)$') + if block_style then + local text, next_index = _read_block_scalar(lines, index, indent, block_style) + map[key] = text + index = next_index + elseif rest == '' then + local value, next_index, err = _parse_block(lines, index, indent, depth + 1) + if err then + return nil, next_index, err + end + + -- A block sequence may sit at the column of its own key, which + -- `_parse_block` declines because its guard wants a deeper line. + -- Nothing was consumed in that case, so read the sequence here. + if value == nil and next_index == index then + local peek = _skip_insignificant(lines, index) + if peek <= #lines and lines[peek].indent == indent then + local ahead = _content(lines[peek]) + if ahead == '-' or ahead:sub(1, 2) == '- ' then + value, next_index, err = _parse_sequence(lines, peek, indent, depth + 1) + if err then + return nil, next_index, err + end + end + end + end + + map[key] = value + index = next_index + elseif rest:sub(1, 1) == '[' or rest:sub(1, 1) == '{' then + local text, next_index, err = _gather_flow(lines, index - 1, rest, line.number) + if err then + return nil, next_index, err + end + local value, value_err = _parse_flow(text, line.number, depth) + if value_err then + return nil, next_index, value_err + end + map[key] = value + index = next_index + else + local value, err = _parse_flow(rest, line.number, depth) + if err then + return nil, index, err + end + map[key] = value + end + end + end + end + + _key_order[map] = order + return map, index, nil +end + +--- Parse a block sequence at a known indentation. +--- @return table|nil sequence +--- @return number next_index +--- @return string|nil err +_parse_sequence = function(lines, start, indent, depth) + local sequence = {} + local index = start + + while index <= #lines do + local line = lines[index] + if line.blank or line.comment then + index = index + 1 + elseif line.indent < indent then + break + else + if line.indent > indent then + return nil, index, _yaml_error(line.number, 'unexpected indentation inside a sequence') + end + + local content = _content(line) + local rest = content:match('^%-%s*(.*)$') + if rest == nil then + break + end + + if rest == '' then + local value, next_index, err = _parse_block(lines, index + 1, indent, depth + 1) + if err then + return nil, next_index, err + end + -- A Lua array cannot hold an interior nil, so an empty item would be + -- dropped and every later item would shift down one index, moving the + -- element that `minItems` and an `items` path such as `preload[2]` + -- refer to. Report it instead. + if value == nil then + return nil, index, _yaml_error(line.number, 'an empty sequence item is not supported') + end + sequence[#sequence + 1] = value + index = next_index + elseif _split_key(rest) == nil then + -- A plain or flow scalar item, such as `- password` or `- [a, b]`. + if rest:sub(1, 1) == '[' or rest:sub(1, 1) == '{' then + local text, next_index, err = _gather_flow(lines, index, rest, line.number) + if err then + return nil, next_index, err + end + local value, value_err = _parse_flow(text, line.number, depth) + if value_err then + return nil, next_index, value_err + end + sequence[#sequence + 1] = value + index = next_index + else + local value, err = _parse_flow(rest, line.number, depth) + if err then + return nil, index, err + end + sequence[#sequence + 1] = value + index = index + 1 + end + else + -- The remainder of the dash line is the first line of the item, and it + -- sits at the column where that remainder begins. + local offset = #content - #rest + local synthetic = { + raw = string.rep(' ', indent + offset) .. rest, + indent = indent + offset, + number = line.number, + blank = false, + comment = false, + } + local saved = lines[index] + lines[index] = synthetic + local value, next_index, err = _parse_block(lines, index, indent, depth + 1) + lines[index] = saved + if err then + return nil, next_index, err + end + sequence[#sequence + 1] = value + index = next_index + end + end + end + + return sequence, index, nil +end + +--- Parse whichever block collection begins at `start`. +--- @param lines table Scanned lines +--- @param start number Index to start from +--- @param parent_indent number|nil Indentation that the block must exceed +--- @param depth number Current nesting depth +--- @return any value +--- @return number next_index +--- @return string|nil err +_parse_block = function(lines, start, parent_indent, depth) + depth = depth or 0 + local index = _skip_insignificant(lines, start) + if index > #lines then + return nil, index, nil + end + + local line = lines[index] + if parent_indent ~= nil and line.indent <= parent_indent then + return nil, start, nil + end + + if depth > MAX_DEPTH then + return nil, index, _yaml_error(line.number, 'schema nests deeper than ' .. MAX_DEPTH .. ' levels') + end + + local indent = line.indent + local content = _content(line) + + if content:sub(1, 1) == '[' or content:sub(1, 1) == '{' then + local text, next_index, err = _gather_flow(lines, index, content, line.number) + if err then + return nil, next_index, err + end + local value, value_err = _parse_flow(text, line.number, depth) + if value_err then + return nil, next_index, value_err + end + return value, next_index, nil + end + + if content == '-' or content:sub(1, 2) == '- ' then + return _parse_sequence(lines, index, indent, depth) + end + return _parse_map(lines, index, indent, depth) +end + +--- Parse YAML text into a native Lua table. +--- @param source string YAML text +--- @return table|nil tree +--- @return string|nil err +local function _parse_yaml_text(source) + local lines, scan_err = _scan_lines(source) + if scan_err then + return nil, scan_err + end + + -- A single leading `---` opens one document, which is valid and common. + -- A later one opens a second document, which `_parse_map` reports. Testing + -- the raw text instead would also reject a `---` inside a block scalar. + local first = _skip_insignificant(lines, 1) + if first <= #lines and _content(lines[first]) == '---' then + lines[first].blank = true + end + + local value, _, err = _parse_block(lines, 1, nil, 0) + if err then + return nil, err + end + if value == nil then + return {}, nil + end + if type(value) ~= 'table' or (_is_array(value) and #value > 0) then + return nil, 'the file must contain a mapping at its top level' + end + return value, nil +end + +M._parse_yaml_text = _parse_yaml_text + +--- Read and parse a YAML schema file. +--- @param filename string Path to the schema file +--- @return table|nil tree +--- @return string|nil err +local function _parse_yaml_file(filename) + local handle = io.open(filename, 'r') + if not handle then + return nil, string.format('Could not open schema file: %s', filename) + end + + local content = handle:read('*a') + handle:close() + + local tree, err = _parse_yaml_text(content) + if err then + return nil, string.format('Could not parse schema file %s: %s', filename, err) + end + return tree, nil +end + + +-- ============================================================================ +-- DESCRIPTOR COMPILATION +-- ============================================================================ + +local _compiled_cache = setmetatable({}, { __mode = 'k' }) + +local _compile + +--- Compile a field descriptor: coerce its literals against its own declared +--- type, compile its patterns, and recurse into nested descriptors. +--- Returns a new table, so the caller's schema is never mutated. +--- @param spec table Raw field descriptor +--- @return table Compiled descriptor +_compile = function(spec) + if type(spec) ~= 'table' then + return spec + end + + local cached = _compiled_cache[spec] + if cached then + return cached + end + + local out = {} + for key, value in pairs(spec) do + out[key] = value + end + + local type_spec = out.type + + if out.default ~= nil then + out.default = _coerce(out.default, type_spec) + end + if out.const ~= nil then + out.const = _coerce(out.const, type_spec) + end + if type(out.enum) == 'table' then + local values = {} + for index = 1, #out.enum do + values[index] = _coerce(out.enum[index], type_spec) + end + out.enum = values + end + + for _, keyword in ipairs(NUMERIC_KEYWORDS) do + if out[keyword] ~= nil then + out[keyword] = tonumber(out[keyword]) or out[keyword] + end + end + + if type(out.required) == 'string' then + out.required = out.required:lower() == 'true' or out.required:lower() == 'yes' + end + if type(out.uniqueItems) == 'string' then + out.uniqueItems = out.uniqueItems:lower() == 'true' + end + if type(out.enumCaseInsensitive) == 'string' then + out.enumCaseInsensitive = out.enumCaseInsensitive:lower() == 'true' + end + if out.additionalProperties == 'false' then + out.additionalProperties = false + elseif out.additionalProperties == 'true' then + out.additionalProperties = true + end + + if type(out.pattern) == 'string' then + out._pattern, out._pattern_error = _compile_pattern(out.pattern) + end + if type(out.propertyNames) == 'string' then + out._property_names, out._property_names_error = _compile_pattern(out.propertyNames) + end + + if type(out.items) == 'table' then + out.items = _compile(out.items) + end + if type(out.properties) == 'table' then + local properties = {} + for key, value in pairs(out.properties) do + properties[key] = _compile(value) + end + out.properties = properties + end + if type(out.additionalProperties) == 'table' then + out.additionalProperties = _compile(out.additionalProperties) + end + + _compiled_cache[spec] = out + return out +end + +-- ============================================================================ +-- FINDINGS +-- ============================================================================ + +--- Record a finding against a path. +--- @param context table Validation context +--- @param severity string 'error' or 'warning' +--- @param path string Dotted path of the offending value +--- @param keyword string Keyword that produced the finding +--- @param message string Human-readable explanation +--- @return nil +local function _report(context, severity, path, keyword, message) + table.insert(context.findings, { + path = path, + keyword = keyword, + message = message, + severity = severity, + }) +end + +--- Split findings into the string arrays the public API returns. +--- @param findings table Array of findings +--- @return table errors Array of error strings +--- @return table warnings Array of warning strings +local function _split_findings(findings) + local errors = {} + local warnings = {} + for _, finding in ipairs(findings) do + local line = finding.path and (finding.path .. ': ' .. finding.message) or finding.message + if finding.severity == 'warning' then + table.insert(warnings, line) + else + table.insert(errors, line) + end + end + return errors, warnings +end + +-- ============================================================================ +-- KEYWORD CHECKS +-- ============================================================================ + +local _validate_value +local _validate_map + +--- Check `enum`, honouring `enumCaseInsensitive`. +local function _check_enum(value, spec, path, context) + if type(spec.enum) ~= 'table' then + return + end + local case_insensitive = spec.enumCaseInsensitive == true + for _, allowed in ipairs(spec.enum) do + if value == allowed then + return + end + if case_insensitive and type(value) == 'string' and type(allowed) == 'string' + and value:lower() == allowed:lower() then + return + end + end + _report(context, 'error', path, 'enum', string.format( + 'must be one of: %s, got %s.', _format_list(spec.enum), _format_value(value) + )) +end + +--- Check `pattern`, reporting a pattern the module cannot compile. +local function _check_pattern(value, spec, path, context) + if spec.pattern == nil or type(value) ~= 'string' then + return + end + if spec._pattern == nil then + _report(context, 'error', path, 'pattern', string.format( + 'schema declares a pattern this validator cannot compile (%s): %s.', + spec._pattern_error or 'unsupported construct', spec.pattern + )) + return + end + if not _pattern_matches(value, spec._pattern) then + _report(context, 'error', path, 'pattern', string.format( + 'does not match required pattern: %s.', spec.pattern + )) + end +end + +--- Check the numeric bounds. +local function _check_numeric(value, spec, path, context) + if type(value) ~= 'number' then + return + end + if type(spec.minimum) == 'number' and value < spec.minimum then + _report(context, 'error', path, 'minimum', string.format( + 'must be at least %s, got %s.', _format_value(spec.minimum), _format_value(value) + )) + end + if type(spec.maximum) == 'number' and value > spec.maximum then + _report(context, 'error', path, 'maximum', string.format( + 'must be at most %s, got %s.', _format_value(spec.maximum), _format_value(value) + )) + end + if type(spec.exclusiveMinimum) == 'number' and value <= spec.exclusiveMinimum then + _report(context, 'error', path, 'exclusiveMinimum', string.format( + 'must be greater than %s, got %s.', _format_value(spec.exclusiveMinimum), _format_value(value) + )) + end + if type(spec.exclusiveMaximum) == 'number' and value >= spec.exclusiveMaximum then + _report(context, 'error', path, 'exclusiveMaximum', string.format( + 'must be less than %s, got %s.', _format_value(spec.exclusiveMaximum), _format_value(value) + )) + end + if type(spec.multipleOf) == 'number' and spec.multipleOf ~= 0 then + local quotient = value / spec.multipleOf + if quotient ~= math.floor(quotient) then + _report(context, 'error', path, 'multipleOf', string.format( + 'must be a multiple of %s, got %s.', _format_value(spec.multipleOf), _format_value(value) + )) + end + end +end + +--- Check string length. +local function _check_string_length(value, spec, path, context) + if type(value) ~= 'string' then + return + end + -- Count characters rather than bytes, so a multi-byte glyph counts as one. + local length = utf8 and utf8.len(value) or nil + if length == nil then + length = #value + end + if type(spec.minLength) == 'number' and length < spec.minLength then + _report(context, 'error', path, 'minLength', string.format( + 'must be at least %d characters, got %d.', spec.minLength, length + )) + end + if type(spec.maxLength) == 'number' and length > spec.maxLength then + _report(context, 'error', path, 'maxLength', string.format( + 'must be at most %d characters, got %d.', spec.maxLength, length + )) + end +end + +--- Check array length and uniqueness. +local function _check_array(value, spec, path, context) + if type(value) ~= 'table' then + return + end + local length = #value + if type(spec.minItems) == 'number' and length < spec.minItems then + _report(context, 'error', path, 'minItems', string.format( + 'must have at least %d items, got %d.', spec.minItems, length + )) + end + if type(spec.maxItems) == 'number' and length > spec.maxItems then + _report(context, 'error', path, 'maxItems', string.format( + 'must have at most %d items, got %d.', spec.maxItems, length + )) + end + if spec.uniqueItems == true then + local seen = {} + for index = 1, length do + local element = value[index] + local rendered = _format_value(element) + -- The key carries the type so 1 and "1" are different items. A string + -- keys on its own text rather than on the rendering, because + -- `_format_value` renders an empty string as `""`, which would make it + -- collide with the two-character string `""`. The message carries only + -- the rendering, because the key is not the author's text. + local raw = type(element) == 'string' and element or rendered + local key = type(element) .. '\0' .. raw + if seen[key] then + _report(context, 'error', path, 'uniqueItems', string.format( + 'must not repeat items, but %s appears more than once.', rendered + )) + break + end + seen[key] = true + end + end +end + +--- Validate array elements against `items`. +local function _check_items(value, spec, path, context) + if type(spec.items) ~= 'table' or type(value) ~= 'table' then + return + end + for index = 1, #value do + local element = _coerce(value[index], spec.items.type) + value[index] = element + if element ~= nil then + _validate_value(element, spec.items, string.format('%s[%d]', path, index), context) + end + end +end + +--- Validate object members against `properties` and its companions. +local function _check_object(value, spec, path, context) + if type(value) ~= 'table' then + return + end + + if spec._property_names ~= nil or spec._property_names_error ~= nil then + if spec._property_names == nil then + _report(context, 'error', path, 'propertyNames', string.format( + 'schema declares a propertyNames pattern this validator cannot compile (%s): %s.', + spec._property_names_error or 'unsupported construct', spec.propertyNames + )) + else + for key in pairs(value) do + if type(key) == 'string' and not _pattern_matches(key, spec._property_names) then + _report(context, 'error', path .. '.' .. key, 'propertyNames', string.format( + 'key does not match required pattern: %s.', spec.propertyNames + )) + end + end + end + end + + local defaulted = {} + + if type(spec.properties) == 'table' then + local sub, filled = _validate_map(value, spec.properties, path, context, { + unknown = spec.additionalProperties == false and 'warn' or 'ignore', + additional = type(spec.additionalProperties) == 'table' and spec.additionalProperties or nil, + }) + + -- `_validate_map` builds a new table, so its defaults, coercion and alias + -- resolution have to be written back into the value the parent holds. + -- Without this a nested `properties` contributes nothing to `merged`. + local stale = {} + for key in pairs(value) do + if sub[key] == nil then + stale[#stale + 1] = key + end + end + for _, key in ipairs(stale) do + value[key] = nil + end + for key, member in pairs(sub) do + value[key] = member + end + defaulted = filled + elseif type(spec.additionalProperties) == 'table' then + for key, member in pairs(value) do + local coerced = _coerce(member, spec.additionalProperties.type) + value[key] = coerced + if coerced ~= nil then + _validate_value(coerced, spec.additionalProperties, path .. '.' .. tostring(key), context) + end + end + end + + -- Runs after the properties block, which writes the resolved members back + -- into `value`. Before that, a dependent supplied under an alias is not yet + -- there to be found. A `default` is an annotation and does not change the + -- instance, so a key that only holds one neither triggers a requirement nor + -- satisfies it. + if type(spec.dependentRequired) == 'table' then + for key, dependents in pairs(spec.dependentRequired) do + local trigger, trigger_key = _lookup(value, key) + if trigger ~= nil and not defaulted[trigger_key] and type(dependents) == 'table' then + for _, dependent in ipairs(dependents) do + local supplied, dependent_key = _lookup(value, dependent) + if supplied == nil or defaulted[dependent_key] then + _report(context, 'error', path, 'dependentRequired', string.format( + 'requires "%s" when "%s" is present.', dependent, key + )) + end + end + end + end + end +end + +--- Run every keyword check against one value. +--- A failed type check short-circuits, so one mistake yields one message. +--- @param value any Value to validate +--- @param spec table Compiled descriptor +--- @param path string Dotted path of the value +--- @param context table Validation context +--- @return nil +_validate_value = function(value, spec, path, context) + local names = _type_names(spec.type) + + if names then + for _, name in ipairs(names) do + if name == 'content' then + return + end + end + end + + if not _matches_type_spec(value, spec.type) then + _report(context, 'error', path, 'type', string.format( + 'must be of type "%s", got "%s".', _format_type(spec.type), _actual_type(value) + )) + return + end + + if spec.const ~= nil and value ~= spec.const then + _report(context, 'error', path, 'const', string.format( + 'must be %s, got %s.', _format_value(spec.const), _format_value(value) + )) + end + + _check_enum(value, spec, path, context) + _check_pattern(value, spec, path, context) + _check_numeric(value, spec, path, context) + _check_string_length(value, spec, path, context) + _check_array(value, spec, path, context) + _check_items(value, spec, path, context) + _check_object(value, spec, path, context) +end + +-- ============================================================================ +-- MAP VALIDATION +-- ============================================================================ + +--- Build the deprecation warning for a descriptor, and forward its value. +--- @param field string Field name +--- @param spec table Compiled descriptor +--- @param value any Current value +--- @param merged table Working values +--- @return string message Warning text +--- @return boolean cleared True when the deprecated key was removed +local function _apply_deprecation(field, spec, value, merged) + local deprecated = spec.deprecated + + if type(deprecated) == 'string' then + return string.format('option "%s" is deprecated. %s', field, deprecated), false + end + + if type(deprecated) ~= 'table' then + return string.format('option "%s" is deprecated.', field), false + end + + local message + if deprecated.since then + message = string.format('option "%s" is deprecated since %s.', field, deprecated.since) + else + message = string.format('option "%s" is deprecated.', field) + end + if deprecated.message then + message = message .. ' ' .. deprecated.message + end + + -- A v1 schema writes `replace-with`, which this vocabulary does not accept. + -- Say so, rather than dropping the forwarding without a word. + local unrecognised = {} + for key in pairs(deprecated) do + if key ~= 'since' and key ~= 'message' and key ~= 'replaceWith' then + unrecognised[#unrecognised + 1] = key + end + end + if #unrecognised > 0 then + table.sort(unrecognised) + message = message .. string.format( + ' The deprecation declares %s, which this vocabulary does not accept; use "replaceWith".', + table.concat(unrecognised, ', ') + ) + end + + local cleared = false + if deprecated.replaceWith then + local replacement = deprecated.replaceWith + message = message .. string.format(' Use "%s" instead.', replacement) + if _lookup(merged, replacement) == nil then + merged[replacement] = value + end + merged[field] = nil + cleared = true + end + + return message, cleared +end + +--- Record that a descriptor accepts a spelling, and report a contested one. +--- +--- Two descriptors can name the same spelling, when one declares as an alias +--- what another declares as its own name, or when two of them declare the same +--- alias. `pairs` yields descriptors in no particular order, so which field a +--- contested spelling resolves to is not decidable from the schema. It is +--- reported rather than resolved in silence, because the schema is what is +--- wrong and only its author can settle it. +--- +--- @param sources table Map of spelling to declared name +--- @param spelling string The spelling being declared +--- @param field string The declared name that accepts it +--- @param base_path string|nil Path prefix for findings +--- @param context table Validation context +local function _declare_spelling(sources, spelling, field, base_path, context) + -- The owner is read through `_lookup`, like every other name comparison in + -- this module, so a contest between the hyphen and the underscore spelling + -- of one name is found as well. Those sit under different keys but reach the + -- same document key at merge time, so they contest just as directly. + local owner = _lookup(sources, spelling) + if owner ~= nil and owner ~= field then + _report(context, 'warning', base_path and (base_path .. '.' .. spelling) or spelling, + 'aliases', string.format( + 'is declared by both "%s" and "%s"; which one accepts it is not defined.', + owner, field)) + end + -- Stored under the literal spelling either way. The map is also the set of + -- keys a descriptor claimed, and a contested spelling is still claimed: the + -- schema is ambiguous, which the warning says, and treating the key as + -- undeclared on top of that would report it as unknown as well. + sources[spelling] = sources[spelling] or field +end + + +--- Validate a map of values against a map of field descriptors. +--- @param values table Values to validate +--- @param descriptors table Field descriptor map +--- @param base_path string|nil Path prefix for findings +--- @param context table Validation context +--- @param options table|nil {unknown = 'warn'|'error'|'ignore', additional = descriptor} +--- @return table merged Values with aliases, coercion and defaults applied +--- @return table defaulted Set of field names whose value came from `default` +--- @return table sources Map of every spelling the schema accepts to the +--- declared name it resolves to. A reader that has to answer a question +--- about a name the caller wrote asks this rather than walking the +--- descriptors again, because the merge has already decided the answer. +--- It doubles as the set of keys a descriptor claimed, which is what the +--- unknown key pass below reads. +_validate_map = function(values, descriptors, base_path, context, options) + options = options or {} + local unknown_policy = options.unknown or 'ignore' + + local merged = {} + for key, value in pairs(values) do + merged[key] = value + end + + local defaulted = {} + local sources = {} + local fields = {} + + -- Three passes, because `pairs` yields descriptors in no particular order. + -- Aliases must land before deprecation reads a value, and a `replaceWith` + -- target must be populated before its own descriptor is validated. + for field, raw_spec in pairs(descriptors) do + local spec = _compile(raw_spec) + fields[#fields + 1] = { + name = field, + spec = spec, + path = base_path and (base_path .. '.' .. field) or field, + } + -- Two writes with different weight. Declaring a spelling fills an empty + -- slot only, while claiming the value under one overwrites whatever was + -- there. The value lands under whichever descriptor claimed it, so the + -- claim is what the map has to follow: a declaration that outranked it + -- would send a reader to a field the merge has already emptied. + _declare_spelling(sources, field, field, base_path, context) + + -- A value found under an alias, or under the other spelling, moves to the + -- name the schema declares. The key it came from is removed, so `merged` + -- never carries the same value twice, once coerced and once raw. + -- The declared name is read first, so it wins over any alias. + local supplied_key + local found, found_key = _lookup(merged, field) + if found ~= nil then + merged[field] = found + sources[found_key] = field + supplied_key = found_key + if found_key ~= field then + merged[found_key] = nil + end + end + + if type(spec.aliases) == 'table' then + for _, alias in ipairs(spec.aliases) do + _declare_spelling(sources, alias, field, base_path, context) + local aliased, alias_key = _lookup(merged, alias) + if aliased ~= nil then + sources[alias_key] = field + if merged[field] == nil then + merged[field] = aliased + supplied_key = alias_key + elseif alias_key ~= field then + -- Two spellings were supplied. The first one read wins, and the + -- other is reported rather than left behind unvalidated. Both + -- names come from the document, never from the schema. + _report(context, 'warning', + base_path and (base_path .. '.' .. field) or field, + 'aliases', + string.format('was given as both "%s" and "%s"; "%s" was used.', + supplied_key, alias_key, supplied_key)) + end + if alias_key ~= field then + merged[alias_key] = nil + end + end + end + end + end + + for _, entry in ipairs(fields) do + local value = merged[entry.name] + if entry.spec.deprecated and value ~= nil then + local message, cleared = _apply_deprecation(entry.name, entry.spec, value, merged) + _report(context, 'warning', entry.path, 'deprecated', message) + entry.cleared = cleared + end + end + + for _, entry in ipairs(fields) do + local field, spec, path = entry.name, entry.spec, entry.path + local value + + if entry.cleared then + value = nil + else + value = _coerce(merged[field], spec.type) + merged[field] = value + + if value == nil and spec.default ~= nil then + value = spec.default + merged[field] = value + defaulted[field] = true + end + end + + -- An empty string is a value the author wrote. Only a missing key is + -- absent, so `minLength`, `const` and `enum` can be tested against ''. + if spec.required == true and value == nil then + _report(context, 'error', path, 'required', 'is required but was not provided.') + elseif value ~= nil then + _validate_value(value, spec, path, context) + end + end + + if unknown_policy ~= 'ignore' or options.additional then + for key, value in pairs(values) do + if sources[key] == nil then + local path = base_path and (base_path .. '.' .. tostring(key)) or tostring(key) + if options.additional then + local coerced = _coerce(value, options.additional.type) + merged[key] = coerced + if coerced ~= nil then + _validate_value(coerced, options.additional, path, context) + end + elseif unknown_policy == 'error' then + _report(context, 'error', path, 'additionalProperties', 'is not a recognised key.') + else + _report(context, 'warning', path, 'additionalProperties', 'is not a recognised key and was ignored.') + end + end + end + end + + return merged, defaulted, sources +end + +--- Start a validation run. +--- @return table Fresh validation context +local function _new_context() + return { findings = {} } +end + +--- Close a validation run and shape the public return values. +--- @param context table Validation context +--- @param merged table Merged values +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged +--- @return table findings +local function _finish(context, merged) + local errors, warnings = _split_findings(context.findings) + return #errors == 0, errors, warnings, merged, context.findings +end + +-- ============================================================================ +-- PUBLIC API +-- ============================================================================ + +--- Load and parse a schema file. +--- Sections absent from the file default to an empty table. +--- +--- A failure is returned, not raised. Quarto replaces `error` with a logger +--- that returns instead of unwinding, so raising here would let a caller +--- carry on with a nil schema and fail later somewhere unrelated. +--- +--- @param filename string|nil Path to the schema file (defaults to '_schema.yml') +--- @return table|nil schema Schema with {['$schema'], options, shortcodes, formats, projects, attributes, classes} +--- @return string|nil err Why the schema could not be read +--- @usage local schema, err = M.load_schema('_schema.yml') +function M.load_schema(filename) + local raw, err = _parse_yaml_file(filename or '_schema.yml') + if err then + return nil, err + end + + return { + ['$schema'] = raw['$schema'], + options = raw.options or {}, + shortcodes = raw.shortcodes or {}, + formats = raw.formats or {}, + projects = raw.projects or {}, + attributes = raw.attributes or {}, + classes = raw.classes or {}, + }, nil +end + +--- List the keys of a parsed mapping in the order the schema declared them. +--- A schema is authored in a meaningful order, and a generator that turns one +--- into documentation needs that order rather than an arbitrary one. +--- Falls back to sorted keys for a table this module did not parse. +--- @param map table A mapping from a loaded schema +--- @return table Array of keys +--- @usage for _, name in ipairs(schema.key_order(loaded.options)) do +function M.key_order(map) + if type(map) ~= 'table' then + return {} + end + + local order = _key_order[map] + if order ~= nil then + local copy = {} + for index, key in ipairs(order) do + copy[index] = key + end + return copy + end + + local keys = {} + for key in pairs(map) do + keys[#keys + 1] = key + end + table.sort(keys) + return keys +end + +--- Validate a map of values against a map of field descriptors. +--- @param values table Values to validate +--- @param descriptors table Field descriptor map +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid True when no error was reported +--- @return table errors Array of error strings +--- @return table warnings Array of warning strings +--- @return table merged Values with aliases, coercion and defaults applied +--- @return table findings Array of structured findings +--- @usage local valid, errors, warnings, merged = M.validate(options, schema.options) +function M.validate(values, descriptors, options) + options = options or {} + local context = _new_context() + local merged = _validate_map(values or {}, descriptors or {}, nil, context, { + unknown = options.unknown or 'warn', + }) + return _finish(context, merged) +end + +--- Validate positional shortcode arguments against an argument descriptor array. +--- Each descriptor should carry a `name`; the merged table is keyed by name. +--- @param args table Array of positional argument values +--- @param argument_specs table Array of argument descriptors +--- @param options table|nil {unknown = 'warn'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged Map keyed by argument name +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_arguments(args, specs) +function M.validate_arguments(args, argument_specs, options) + options = options or {} + args = args or {} + argument_specs = argument_specs or {} + + local context = _new_context() + local merged = {} + + for index, raw_spec in ipairs(argument_specs) do + local spec = _compile(raw_spec) + local name = spec.name or tostring(index) + local path = string.format('argument %d ("%s")', index, name) + + local value = _coerce(args[index], spec.type) + if value == nil and spec.default ~= nil then + value = spec.default + end + merged[name] = value + + if spec.required == true and value == nil then + _report(context, 'error', path, 'required', 'is required but was not provided.') + elseif value ~= nil then + _validate_value(value, spec, path, context) + end + end + + if (options.unknown or 'warn') ~= 'ignore' and #args > #argument_specs then + _report(context, 'warning', nil, 'arguments', string.format( + 'Received %d arguments but only %d are defined. The extra arguments were ignored.', + #args, #argument_specs + )) + end + + return _finish(context, merged) +end + +--- Validate one shortcode call against its schema entry. +--- Covers the positional `arguments`, the named `attributes`, and the +--- parent-level `required` array of attribute names. +--- @param name string Shortcode name, used as the path prefix +--- @param args table Array of positional argument values +--- @param kwargs table Map of named attribute values +--- @param entry table Shortcode entry from `schema.shortcodes[name]` +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged {arguments = ..., attributes = ...} +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_shortcode('iconify', args, kwargs, schema.shortcodes.iconify) +function M.validate_shortcode(name, args, kwargs, entry, options) + options = options or {} + entry = entry or {} + + local context = _new_context() + local merged = { arguments = {}, attributes = {} } + + if type(entry.arguments) == 'table' then + local _, errors, warnings, arguments = M.validate_arguments(args or {}, entry.arguments, options) + merged.arguments = arguments + for _, message in ipairs(errors) do + _report(context, 'error', nil, 'arguments', name .. ' ' .. message) + end + for _, message in ipairs(warnings) do + _report(context, 'warning', nil, 'arguments', name .. ' ' .. message) + end + end + + local declared = type(entry.attributes) == 'table' + local spellings + if declared then + local attributes, _, resolved = _validate_map(kwargs or {}, entry.attributes, name, context, { + unknown = options.unknown or 'warn', + }) + merged.attributes = attributes + spellings = resolved + end + + if type(entry.required) == 'table' then + -- `merged.attributes` is filled only when the entry declares `attributes`, + -- and it is authoritative when it is: it keeps every unclaimed key and it + -- drops a key that a deprecation cleared. The vocabulary allows `required` + -- on its own, so fall back to what the caller supplied only when the + -- entry declares no `attributes`. A name that names an alias is not + -- missing either: `_validate_map` moves its value to the field that + -- declares the alias and clears the alias spelling, the same way it + -- clears a deprecated key, so look for the value under that field. The + -- merge reports which declared name each spelling resolves to, so that + -- is one lookup rather than a second walk over the descriptors. It goes + -- through `_lookup`, the same as every other name comparison in this + -- module, so a hyphen in one spelling and an underscore in the other + -- still match. + -- + -- This asks what the shortcode receives, not what the author wrote, so + -- a `default` present in `merged.attributes` satisfies a name here. + -- That is the opposite of `dependentRequired` above, which asks what + -- the author wrote and treats a default as an annotation nobody + -- supplied. Neither reading is a bug: they answer different questions + -- about the same instance. + local function _required_satisfied(required) + local field = (spellings and _lookup(spellings, required)) or required + if _lookup(merged.attributes, field) ~= nil then + return true + end + if not declared then + return _lookup(kwargs or {}, required) ~= nil + end + return false + end + + for _, required in ipairs(entry.required) do + if not _required_satisfied(required) then + _report(context, 'error', name .. '.' .. required, 'required', + 'is required but was not provided.') + end + end + end + + return _finish(context, merged) +end + +--- Validate Pandoc element attributes against one group of the `attributes` +--- section. Undeclared attributes are ignored by default, because a Pandoc +--- element legitimately carries attributes from Quarto and other filters. +--- @param attributes table Map of attribute values +--- @param group string Group key, such as a class, identifier or element name +--- @param schema table Loaded schema +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_attributes(el.attributes, 'callout-note', schema) +function M.validate_attributes(attributes, group, schema, options) + options = options or {} + local descriptors = schema and schema.attributes and _lookup(schema.attributes, group) or nil + + local context = _new_context() + if descriptors == nil then + return _finish(context, attributes or {}) + end + + local merged = _validate_map(attributes or {}, descriptors, group, context, { + unknown = options.unknown or 'ignore', + }) + return _finish(context, merged) +end + +--- Validate the options of one output format against the `formats` section. +--- Quarto merges the options of the selected format into the top level of the +--- metadata, so the values are read from there. The format name is never a key. +--- A value is collected only when a descriptor declares its name, which keeps +--- the document's own keys, such as `title`, out of the merge. The `unknown` +--- option therefore has nothing to report here, and is kept so that the entry +--- points keep one signature. +--- @param meta table Document metadata +--- @param format string Format name, such as 'html' or 'typst' +--- @param schema table Loaded schema +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_format(meta, 'typst', schema) +function M.validate_format(meta, format, schema, options) + options = options or {} + local descriptors = schema and schema.formats and _lookup(schema.formats, format) or nil + + local context = _new_context() + if descriptors == nil then + return _finish(context, {}) + end + + -- Each value is stored under the spelling the document wrote, never under the + -- declared name. `_validate_map` is what moves an alias or the other spelling + -- to the declared name, and it needs the original key to say which two + -- spellings a document supplied for one field. + -- + -- `_lookup` returns the first spelling it finds, so a document that wrote one + -- name in both spellings loses the other. Every other entry point reports the + -- loser through the `unknown` option, which has nothing to report here, so the + -- collection reports it in the same words `_validate_map` uses. + local values = {} + for field, raw_spec in pairs(descriptors) do + local spec = _compile(raw_spec) + + -- One entry per name, and not one per spelling. `_lookup` reads both + -- spellings of whichever entry is kept, so an alias that is only the other + -- spelling of a name already listed adds nothing. Probing it as well would + -- collect the two spellings under separate keys, and one mistake would then + -- be reported three times, once by each probe and once by `_validate_map`. + local names = { field } + local listed = { [(field:gsub('_', '-'))] = true } + if type(spec) == 'table' and type(spec.aliases) == 'table' then + for _, alias in ipairs(spec.aliases) do + -- A schema file is not read against the meta-schema, so an unquoted + -- `no` reaches here as a boolean. `_lookup` skips a key that is not a + -- string everywhere else, and so does this. + if type(alias) == 'string' then + local normalised = (alias:gsub('_', '-')) + if not listed[normalised] then + listed[normalised] = true + names[#names + 1] = alias + end + end + end + end + + for _, name in ipairs(names) do + local value, found_key = _lookup(meta, name) + if value ~= nil then + values[found_key] = _convert_pandoc_value(value) + + for _, other in ipairs({ (name:gsub('%-', '_')), (name:gsub('_', '-')) }) do + if other ~= found_key and meta[other] ~= nil then + _report(context, 'warning', format .. '.' .. field, 'aliases', + string.format('was given as both "%s" and "%s"; "%s" was used.', + found_key, other, found_key)) + end + end + end + end + end + + local merged = _validate_map(values, descriptors, format, context, { + unknown = options.unknown or 'ignore', + }) + return _finish(context, merged) +end + +--- Extract an extension's options from document metadata. +--- Reads `meta.extensions[extension_name]` and converts Pandoc values to +--- native Lua values, keeping every key exactly as the document wrote it. +--- @param meta table Document metadata +--- @param extension_name string Extension name +--- @return table Map of option values +--- @usage local options = M.extract_meta_options(meta, 'iconify') +function M.extract_meta_options(meta, extension_name) + local extension_meta = meta and meta['extensions'] and meta['extensions'][extension_name] + if not extension_meta then + return {} + end + + local options = {} + for key, value in pairs(extension_meta) do + options[tostring(key)] = _convert_pandoc_value(value) + end + return options +end + +--- Load a schema, validate an extension's options, and log the outcome. +--- A schema that cannot be loaded is reported and the render continues with +--- no validated options, rather than aborting on a configuration file. +--- @param meta table Document metadata +--- @param extension_name string Extension name +--- @param schema_path string Path to the schema file, resolved by the caller +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return table merged Validated options with defaults applied +--- @usage local options = M.validate_options(meta, 'iconify', quarto.utils.resolve_path('_schema.yml')) +function M.validate_options(meta, extension_name, schema_path, options) + local schema, err = M.load_schema(schema_path) + if err then + _env.report_error(string.format('Extension "%s": %s', extension_name, err)) + return {} + end + + if not schema.options or next(schema.options) == nil then + return {} + end + + local values = M.extract_meta_options(meta, extension_name) + local valid, errors, warnings, merged = M.validate(values, schema.options, options) + + if #warnings > 0 then + _env.warn(M.format_warnings(warnings, extension_name)) + end + if not valid then + _env.report_error(M.format_errors(errors, extension_name)) + end + + return merged +end + +--- Format error messages as one readable block. +--- @param errors table Array of error strings +--- @param extension_name string|nil Extension name for context +--- @return string Formatted message, or an empty string +--- @usage local message = M.format_errors(errors, 'iconify') +function M.format_errors(errors, extension_name) + if #errors == 0 then + return '' + end + + local prefix = extension_name and ('Extension "' .. extension_name .. '": ') or '' + local lines = {} + for index, message in ipairs(errors) do + table.insert(lines, ' ' .. index .. '. ' .. message) + end + + return prefix .. 'Configuration validation failed:\n' .. table.concat(lines, '\n') +end + +--- Format warning messages as one readable block. +--- @param warnings table Array of warning strings +--- @param extension_name string|nil Extension name for context +--- @return string Formatted message, or an empty string +--- @usage local message = M.format_warnings(warnings, 'iconify') +function M.format_warnings(warnings, extension_name) + if #warnings == 0 then + return '' + end + + local prefix = extension_name and ('Extension "' .. extension_name .. '": ') or '' + local lines = {} + for index, message in ipairs(warnings) do + table.insert(lines, ' ' .. index .. '. ' .. message) + end + + return prefix .. 'Configuration warnings:\n' .. table.concat(lines, '\n') +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/collapse-output.js b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/collapse-output.js new file mode 100644 index 0000000..09332dd --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/collapse-output.js @@ -0,0 +1,174 @@ +/** +* MIT License +* +* Copyright (c) 2026 Mickaël Canouil +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: + +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. + +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +// Map of supported output types to the Quarto-emitted class names. +const OUTPUT_TYPE_CLASSES = { + stdout: 'cell-output-stdout', + stderr: 'cell-output-stderr', + display: 'cell-output-display', + output: 'cell-output', +}; + +// Ordered output-type identifiers (first match wins during detection). +const OUTPUT_TYPE_ORDER = ['stdout', 'stderr', 'display', 'output']; + +// Default summary text per output type. +const DEFAULT_TYPE_SUMMARIES = { + stdout: 'Standard Output', + stderr: 'Standard Error', + display: 'Display Output', + output: 'Code Output', +}; + +const DEFAULT_SUMMARY_TEMPLATE = '{type}'; + +/** + * Parse a boolean from a data attribute. Accepts true/false/yes/no/1/0. + * @param {string | null | undefined} value + * @returns {boolean | null} + */ +function parseBoolean(value) { + if (value === null || value === undefined || value === '') return null; + const normalised = String(value).toLowerCase(); + if (normalised === 'true' || normalised === 'yes' || normalised === '1') return true; + if (normalised === 'false' || normalised === 'no' || normalised === '0') return false; + return null; +} + +/** + * Parse a non-negative integer from a data attribute. + * @param {string | null | undefined} value + * @returns {number | null} + */ +function parseInteger(value) { + if (value === null || value === undefined || value === '') return null; + const parsed = Number.parseInt(String(value), 10); + if (Number.isNaN(parsed) || parsed < 0) return null; + return parsed; +} + +/** + * Resolve the output-type key for an element by inspecting its class list. + * @param {Element} element + * @returns {string | null} + */ +function detectOutputType(element) { + for (const key of OUTPUT_TYPE_ORDER) { + if (element.classList.contains(OUTPUT_TYPE_CLASSES[key])) return key; + } + return null; +} + +/** + * Count the number of textual lines for an element. + * @param {Element} element + * @returns {number} + */ +function countLines(element) { + const text = element.textContent || ''; + if (text.trim() === '') return 0; + return text.split('\n').length; +} + +/** + * Render the summary text using the configured template and per-type overrides. + * @param {string} outputType + * @param {string | null} explicitSummary + * @param {number} lineCount + * @param {{ template: string, perType: Record }} options + * @returns {string} + */ +function renderSummary(outputType, explicitSummary, lineCount, options) { + if (explicitSummary && explicitSummary !== '') return explicitSummary; + const override = options.perType[outputType]; + if (override) return override; + const label = DEFAULT_TYPE_SUMMARIES[outputType] || DEFAULT_TYPE_SUMMARIES.output; + return (options.template || DEFAULT_SUMMARY_TEMPLATE) + .replace(/\{type\}/g, label) + .replace(/\{lines\}/g, String(lineCount)); +} + +/** + * Wrap a single output element in a
block. + * @param {Element} cell + * @param {Element} output + * @param {{ template: string, perType: Record, open: boolean, autoCollapse: number | null }} options + */ +function wrapOutput(cell, output, options) { + const outputType = detectOutputType(output); + if (!outputType) return; + + const lineCount = countLines(output); + const forceCollapsed = options.autoCollapse !== null && lineCount >= options.autoCollapse; + + const details = document.createElement('details'); + if (options.open && !forceCollapsed) details.open = true; + + const summary = document.createElement('summary'); + summary.textContent = renderSummary( + outputType, + cell.dataset.outputSummary || null, + lineCount, + options, + ); + + details.appendChild(summary); + output.parentNode.insertBefore(details, output); + details.appendChild(output); +} + +document.addEventListener('DOMContentLoaded', () => { + const cells = document.querySelectorAll('.cell[data-output-fold="true"]'); + + cells.forEach((cell) => { + const allowedTypes = (cell.dataset.outputTypes || 'stdout,stderr,display,output') + .split(',') + .map((part) => part.trim()) + .filter((part) => part.length > 0); + + const perType = {}; + for (const key of Object.keys(OUTPUT_TYPE_CLASSES)) { + const attr = cell.dataset[`outputSummary${key.charAt(0).toUpperCase()}${key.slice(1)}`]; + if (attr) perType[key] = attr; + } + + const options = { + template: cell.dataset.outputSummaryTemplate || DEFAULT_SUMMARY_TEMPLATE, + perType, + open: parseBoolean(cell.dataset.outputOpen) === true, + autoCollapse: parseInteger(cell.dataset.outputAutoCollapse), + }; + + const outputs = []; + for (const key of allowedTypes) { + const className = OUTPUT_TYPE_CLASSES[key]; + if (!className) continue; + cell.querySelectorAll(`.${className}`).forEach((element) => { + if (!outputs.includes(element)) outputs.push(element); + }); + } + + outputs.forEach((output) => wrapOutput(cell, output, options)); + }); +}); diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/collapse-output.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/collapse-output.lua new file mode 100644 index 0000000..42a1d3e --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/collapse-output.lua @@ -0,0 +1,453 @@ +--- @module "collapse-output" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +--- Extension name constant. +local EXTENSION_NAME = 'collapse-output' + +--- Load modules. +local str = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/string.lua'):gsub('%.lua$', '')) +local log = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/logging.lua'):gsub('%.lua$', '')) +local meta_mod = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/metadata.lua'):gsub('%.lua$', '')) +local html_mod = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/html.lua'):gsub('%.lua$', '')) +local schema = require(quarto.utils.resolve_path('_vendor/quarto-wizard/schema.lua'):gsub('%.lua$', '')) +local check = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/schema-check.lua'):gsub('%.lua$', '')) + +--- The schema check, built once and reused by the whole render. It reads +--- `_schema.yml` on the way in and checks the document configuration once. +--- +--- The validator is injected rather than required by the check module, so the +--- two vendored sources stay independent of where the other was placed. +--- +--- The extension contributes a filter and no shortcode, so the check runs from +--- the `Meta` handler, which is the only place the document configuration is +--- available. +--- +--- A schema that cannot be read is reported by the module as an error and the +--- render carries on: a configuration file must not stop a document. +local checker = check.new(schema, EXTENSION_NAME) + +--- Supported output-type identifiers and the Pandoc Div classes they map to. +local OUTPUT_TYPE_CLASSES = { + stdout = 'cell-output-stdout', + stderr = 'cell-output-stderr', + display = 'cell-output-display', + output = 'cell-output', +} + +--- Ordered output-type identifiers (first match wins during detection). +local OUTPUT_TYPE_ORDER = { 'stdout', 'stderr', 'display', 'output' } + +--- Default summary text per output type, used when no template/summary is set. +local DEFAULT_TYPE_SUMMARIES = { + stdout = 'Standard Output', + stderr = 'Standard Error', + display = 'Display Output', + output = 'Code Output', +} + +--- Default summary template placeholders. +--- `{type}` resolves to the human label of the matched output type. +--- `{lines}` resolves to the line count of the wrapped block. +local DEFAULT_SUMMARY_TEMPLATE = '{type}' + +--- Filter configuration, refreshed at every Meta pass to avoid state bleed +--- across batch renders. +--- @class collapse_config +--- @field method string Rendering method ('lua' or 'javascript'). +--- @field collapse_all_outputs boolean Wrap every cell output, regardless of per-cell flag. +--- @field default_open boolean Initial state of the details element. +--- @field auto_collapse_size integer|nil Line threshold above which outputs auto-fold (Lua method). +--- @field output_types table Set of enabled output-type keys. +--- @field summary_template string Template string for the summary text. +--- @field per_type_summaries table Override summaries keyed by output type. +local config = {} + +--- Reset the filter configuration to documented defaults. +--- @return nil +local function reset_config() + config = { + method = 'lua', + collapse_all_outputs = false, + default_open = false, + auto_collapse_size = nil, + output_types = { + stdout = true, + stderr = true, + display = true, + output = true, + }, + summary_template = DEFAULT_SUMMARY_TEMPLATE, + per_type_summaries = {}, + } +end + +reset_config() + +--- Parse a metadata value into a boolean using documented conventions. +--- Deliberately wider than the schema's own boolean coercion, which accepts +--- only 'true'/'false': this parser also accepts 'yes'/'no' and '1'/'0', and +--- stays the source of every boolean value this filter reads, attribute or +--- option, whatever `checker:attributes`/`checker:options` reports about the +--- same value. +--- @param value any Raw metadata value (Pandoc Inlines, boolean, string, nil). +--- @return boolean|nil True/false when parseable, nil when value is empty. +local function parse_boolean(value) + if value == nil then return nil end + if type(value) == 'boolean' then return value end + local text = str.stringify(value) + if str.is_empty(text) then return nil end + text = text:lower() + if text == 'true' or text == 'yes' or text == '1' then return true end + if text == 'false' or text == 'no' or text == '0' then return false end + return nil +end + +--- Parse a metadata value into a positive integer. +--- Returns nil, silently, when the value is not a positive integer: the +--- schema check (`checker:options`, run from `get_configuration`) already +--- names the same fault for 'auto-collapse-size', the only caller of this +--- function, so no extension-side echo is added here. +--- @param value any Raw metadata value. +--- @return integer|nil Parsed integer, or nil when invalid/empty. +local function parse_positive_integer(value) + if value == nil then return nil end + local text = str.stringify(value) + if str.is_empty(text) then return nil end + local number = tonumber(text) + if not number or number < 0 or number ~= math.floor(number) then + return nil + end + return math.floor(number) +end + +--- Parse the `output-types` metadata into a set of enabled keys. +--- Accepts a comma-separated string or a YAML list of strings. +--- A YAML list arrives as a Lua table with no `.t` field on this Pandoc +--- version, so `pandoc.utils.type`, which reports 'List' for one and +--- 'Inlines' for a scalar string, is what tells the two apart; the field +--- this function used to test, `.t == 'MetaList'`, is never set here and +--- would leave every YAML list silently mistaken for one unbroken word. +--- @param value any Raw metadata value. +--- @return table|nil Set of enabled keys, or nil when unset. +local function parse_output_types(value) + if value == nil then return nil end + + --- @type table + local items = {} + if pandoc.utils.type(value) == 'List' then + for _, item in ipairs(value) do + local entry = str.stringify(item) + if not str.is_empty(entry) then + table.insert(items, entry) + end + end + else + local text = str.stringify(value) + if str.is_empty(text) then return nil end + for item in text:gmatch('[^,%s]+') do + table.insert(items, item) + end + end + + --- @type table + local set = {} + for _, item in ipairs(items) do + local key = item:lower() + if OUTPUT_TYPE_CLASSES[key] then + set[key] = true + else + log.log_warning( + EXTENSION_NAME, + 'Unknown output-type \'' .. item .. '\'. Known types: stdout, stderr, display, output.' + ) + end + end + + if next(set) == nil then return nil end + return set +end + +--- Parse the `summaries` mapping into per-type overrides. +--- Accepts a metadata table keyed by output-type name. +--- @param value any Raw metadata value. +--- @return table Per-type summary overrides (possibly empty). +local function parse_per_type_summaries(value) + --- @type table + local result = {} + if value == nil or type(value) ~= 'table' then return result end + for key, summary in pairs(value) do + if type(key) == 'string' and OUTPUT_TYPE_CLASSES[key:lower()] then + result[key:lower()] = str.stringify(summary) + end + end + return result +end + +--- Read filter configuration from document metadata. +--- Checks the document configuration against the schema, resets per-document +--- state, then populates `config` from `extensions.collapse-output`. +--- @param meta table The document metadata table. +--- @return table The metadata table (unchanged). +local function get_configuration(meta) + if quarto.doc.is_format('html') then + checker:options(meta) + end + + reset_config() + + local meta_method = meta_mod.get_metadata_value(meta, EXTENSION_NAME, 'method') + if not str.is_empty(meta_method) then + --- @cast meta_method string + local method = meta_method:lower() + if method ~= 'lua' and method ~= 'javascript' then + log.log_warning( + EXTENSION_NAME, + 'Invalid method \'' .. method .. '\'. Using default \'lua\'.' + ) + method = 'lua' + end + config.method = method + end + + local raw_config = meta_mod.get_extension_config(meta, EXTENSION_NAME) + if type(raw_config) == 'table' then + local collapse_all = parse_boolean(raw_config['collapse-all-outputs']) + if collapse_all ~= nil then config.collapse_all_outputs = collapse_all end + + local default_open = parse_boolean(raw_config['default-open']) + if default_open ~= nil then config.default_open = default_open end + + local auto_size = parse_positive_integer(raw_config['auto-collapse-size']) + if auto_size ~= nil then config.auto_collapse_size = auto_size end + + local output_types = parse_output_types(raw_config['output-types']) + if output_types ~= nil then config.output_types = output_types end + + local template = raw_config['summary-template'] + if template ~= nil then + local text = str.stringify(template) + if not str.is_empty(text) then + config.summary_template = text + end + end + + config.per_type_summaries = parse_per_type_summaries(raw_config['summaries']) + end + + return meta +end + +--- Resolve the output-type key for a Div by inspecting its classes. +--- @param block pandoc.Div The candidate block. +--- @return string|nil The output-type key, or nil if not an output block. +local function detect_output_type(block) + for _, key in ipairs(OUTPUT_TYPE_ORDER) do + if block.classes:find(OUTPUT_TYPE_CLASSES[key]) then + return key + end + end + return nil +end + +--- Count the number of textual lines for a block. +--- CodeBlock and RawBlock preserve newlines in their `.text` field; other +--- block kinds fall back to `pandoc.utils.stringify` so each child paragraph +--- is treated as one line. +--- @param block pandoc.Block The block to measure. +--- @return integer Number of lines (minimum 1 for any non-empty content). +local function count_lines(block) + --- @type string + local text = '' + if block.t == 'CodeBlock' or block.t == 'RawBlock' then + text = block.text or '' + elseif block.t == 'Div' then + --- @type table + local parts = {} + for _, child in ipairs(block.content) do + if child.t == 'CodeBlock' or child.t == 'RawBlock' then + table.insert(parts, child.text or '') + else + table.insert(parts, pandoc.utils.stringify(child)) + end + end + text = table.concat(parts, '\n') + else + text = pandoc.utils.stringify(block) + end + if str.is_empty(text) then return 0 end + --- @type integer + local lines = 1 + for _ in text:gmatch('\n') do + lines = lines + 1 + end + return lines +end + +--- Render the summary text using the configured template. +--- Substitutes `{type}` and `{lines}` placeholders. +--- @param output_type string The detected output-type key. +--- @param explicit string|nil The per-cell `output-summary` attribute, when set. +--- @param line_count integer The measured line count for the output. +--- @return string The rendered, HTML-escaped summary text. +local function render_summary(output_type, explicit, line_count) + if explicit and not str.is_empty(explicit) then + return str.escape_html(explicit) + end + + local per_type = config.per_type_summaries[output_type] + if per_type and not str.is_empty(per_type) then + return str.escape_html(per_type) + end + + local label = DEFAULT_TYPE_SUMMARIES[output_type] or DEFAULT_TYPE_SUMMARIES.output + local rendered = config.summary_template + :gsub('{type}', label) + :gsub('{lines}', tostring(line_count)) + return str.escape_html(rendered) +end + +--- Decide whether a per-cell directive (or the global toggle) requests folding. +--- Per-cell `output-fold` always wins over `collapse-all-outputs`. +--- @param div pandoc.Div The cell-level Div carrying the directive. +--- @return boolean True when the cell's outputs should be wrapped. +local function should_fold_cell(div) + local explicit = div.attributes['output-fold'] + if explicit ~= nil and explicit ~= '' then + local parsed = parse_boolean(explicit) + if parsed ~= nil then return parsed end + end + return config.collapse_all_outputs +end + +--- Decide whether a single output block should start expanded. +--- Per-cell `output-open` overrides the document `default-open`. +--- `auto-collapse-size` forces collapsed state for outputs above the threshold. +--- @param div pandoc.Div The cell-level Div carrying the directive. +--- @param line_count integer Line count of the candidate output block. +--- @return boolean True when the details element should be rendered open. +local function should_open(div, line_count) + if config.auto_collapse_size and line_count >= config.auto_collapse_size then + return false + end + + local explicit = div.attributes['output-open'] + if explicit ~= nil and explicit ~= '' then + local parsed = parse_boolean(explicit) + if parsed ~= nil then return parsed end + end + + return config.default_open +end + +--- Wrap a cell-output Div in `
` elements (server-side rendering). +--- @param div pandoc.Div The parent cell Div whose content is being rewritten. +--- @return pandoc.Div The same Div with wrapped content. +local function wrap_with_details(div) + --- @type string|nil + local explicit_summary = div.attributes['output-summary'] + + --- @type table + local new_content = {} + + for _, block in ipairs(div.content) do + local wrapped = false + if block.t == 'Div' then + local output_type = detect_output_type(block) + if output_type and config.output_types[output_type] then + local lines = count_lines(block) + local summary = render_summary(output_type, explicit_summary, lines) + local open_attr = should_open(div, lines) and ' open' or '' + table.insert( + new_content, + pandoc.RawBlock( + 'html', + '' .. summary .. '' + ) + ) + table.insert(new_content, block) + table.insert(new_content, pandoc.RawBlock('html', '
')) + wrapped = true + end + end + if not wrapped then + table.insert(new_content, block) + end + end + + div.content = new_content + return div +end + +--- Inject runtime data attributes consumed by the JavaScript renderer. +--- @param div pandoc.Div The cell-level Div being annotated. +--- @return pandoc.Div The same Div with additional attributes. +local function annotate_for_javascript(div) + div.attributes['output-fold'] = 'true' + if config.default_open then + div.attributes['output-open'] = div.attributes['output-open'] or 'true' + end + if config.auto_collapse_size then + div.attributes['output-auto-collapse'] = tostring(config.auto_collapse_size) + end + local enabled_types = {} + for key, enabled in pairs(config.output_types) do + if enabled then table.insert(enabled_types, key) end + end + table.sort(enabled_types) + div.attributes['output-types'] = table.concat(enabled_types, ',') + if config.summary_template ~= DEFAULT_SUMMARY_TEMPLATE then + div.attributes['output-summary-template'] = config.summary_template + end + for key, summary in pairs(config.per_type_summaries) do + div.attributes['output-summary-' .. key] = summary + end + return div +end + +--- Process every Quarto cell Div: decide whether to fold and how to render. +--- @param div pandoc.Div The Div element to process. +--- @return pandoc.Div|nil The modified Div, or nil when no changes are needed. +local function process_div(div) + if not quarto.doc.is_format('html') then + return nil + end + + -- Validate this Div's attributes against the `_any` group before any of + -- them are read below (`should_fold_cell`, `should_open`, + -- `wrap_with_details`) or written back (`annotate_for_javascript`), so an + -- invalid value is named once. `output-fold` declares a schema default, so + -- the merged table this returns carries that default for a Div that never + -- wrote the attribute; nothing here reads that merged table as a VALUE, + -- only `parse_boolean` on the raw attribute does, so a Div's own + -- `collapse-all-outputs` fallback is never shadowed by the schema default. + checker:attributes(div.attributes, nil) + + if not should_fold_cell(div) then + return nil + end + + if config.method == 'javascript' then + html_mod.ensure_html_dependency({ + name = 'collapse-output', + version = '1.0.0', + scripts = { + { path = 'collapse-output.min.js', afterBody = true } + } + }) + return annotate_for_javascript(div) + end + + return wrap_with_details(div) +end + +--- Pandoc filter configuration. +--- Order: +--- 1. Reset and read configuration from metadata. +--- 2. Process Div elements for collapse functionality. +--- @type table +return { + { Meta = get_configuration }, + { Div = process_div }, +} diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/collapse-output.min.js b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/collapse-output.min.js new file mode 100644 index 0000000..38bfe88 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/collapse-output/collapse-output.min.js @@ -0,0 +1,24 @@ +/** +* MIT License +* +* Copyright (c) 2026 Mickaël Canouil +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: + +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. + +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ +const OUTPUT_TYPE_CLASSES={stdout:"cell-output-stdout",stderr:"cell-output-stderr",display:"cell-output-display",output:"cell-output"},OUTPUT_TYPE_ORDER=["stdout","stderr","display","output"],DEFAULT_TYPE_SUMMARIES={stdout:"Standard Output",stderr:"Standard Error",display:"Display Output",output:"Code Output"},DEFAULT_SUMMARY_TEMPLATE="{type}";function parseBoolean(t){if(null==t||""===t)return null;const e=String(t).toLowerCase();return"true"===e||"yes"===e||"1"===e||"false"!==e&&"no"!==e&&"0"!==e&&null}function parseInteger(t){if(null==t||""===t)return null;const e=Number.parseInt(String(t),10);return Number.isNaN(e)||e<0?null:e}function detectOutputType(t){for(const e of OUTPUT_TYPE_ORDER)if(t.classList.contains(OUTPUT_TYPE_CLASSES[e]))return e;return null}function countLines(t){const e=t.textContent||"";return""===e.trim()?0:e.split("\n").length}function renderSummary(t,e,u,n){if(e&&""!==e)return e;const o=n.perType[t];if(o)return o;const r=DEFAULT_TYPE_SUMMARIES[t]||DEFAULT_TYPE_SUMMARIES.output;return(n.template||"{type}").replace(/\{type\}/g,r).replace(/\{lines\}/g,String(u))}function wrapOutput(t,e,u){const n=detectOutputType(e);if(!n)return;const o=countLines(e),r=null!==u.autoCollapse&&o>=u.autoCollapse,l=document.createElement("details");u.open&&!r&&(l.open=!0);const p=document.createElement("summary");p.textContent=renderSummary(n,t.dataset.outputSummary||null,o,u),l.appendChild(p),e.parentNode.insertBefore(l,e),l.appendChild(e)}document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll('.cell[data-output-fold="true"]').forEach(t=>{const e=(t.dataset.outputTypes||"stdout,stderr,display,output").split(",").map(t=>t.trim()).filter(t=>t.length>0),u={};for(const e of Object.keys(OUTPUT_TYPE_CLASSES)){const n=t.dataset[`outputSummary${e.charAt(0).toUpperCase()}${e.slice(1)}`];n&&(u[e]=n)}const n={template:t.dataset.outputSummaryTemplate||"{type}",perType:u,open:!0===parseBoolean(t.dataset.outputOpen),autoCollapse:parseInteger(t.dataset.outputAutoCollapse)},o=[];for(const u of e){const e=OUTPUT_TYPE_CLASSES[u];e&&t.querySelectorAll(`.${e}`).forEach(t=>{o.includes(t)||o.push(t)})}o.forEach(e=>wrapOutput(t,e,n))})}); diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/LICENSE b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/LICENSE new file mode 100644 index 0000000..4b53bb8 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mickaël Canouil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_dependencies.yml b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_dependencies.yml new file mode 100644 index 0000000..eda9d99 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_dependencies.yml @@ -0,0 +1,42 @@ +schema: 1 +sources: + quarto-lua-modules: + origin: "https://github.com/mcanouil/quarto-lua-modules" + fetch: "{origin}/releases/download/{version}/{file}" + version: "2.8.0" + licence: MIT + files: + colour.lua: + sha256: "eab8e12368b1ea3ff91ae5919863d9d435be39674a63e1514c9778e60790ba03" + runtime: any + git.lua: + sha256: "db774ef327d19a651e9fef9609b0942254441c5c88562a8885fcbcf07b71221f" + runtime: any + html.lua: + sha256: "ffc9987cdc12ee32d7fae7714ab29087129a6dd124de39acaff5b40a56727a08" + runtime: any + logging.lua: + sha256: "5d99ceb89c813fbe4491310e42b4a4cff62e11dda42f01b4f61f969aa58e0706" + runtime: any + metadata.lua: + sha256: "29d7a6c7568d5e2e03838fdd9f175de98a185d139cd83813d33ee2164cdb5f2e" + runtime: any + paths.lua: + sha256: "c880f09e3367bd6e871e0890952447c13cc3f0f715b86e2b49c2ea59d1cdea05" + runtime: any + schema-check.lua: + sha256: "6858ca45239565c0f0fac270b5a541c8fb46ba39f7ee55c94f006044e3db3fe3" + runtime: any + string.lua: + sha256: "7801d2f217e803951313ef86f1f899cb6350a44cbb468b2d7c28dd6911a0fadc" + runtime: any + quarto-wizard: + origin: "https://github.com/mcanouil/quarto-wizard" + fetch: "{origin}/releases/download/{version}/{file}" + version: "2.3.0" + licence: MIT + tag-prefix: schema-v + files: + schema.lua: + sha256: "e76f30ad7578c08768ebc598bc4986d0f1d4c391720585bf976a42174b50ca48" + runtime: any diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_extension.yml b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_extension.yml new file mode 100644 index 0000000..9fe4afe --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_extension.yml @@ -0,0 +1,10 @@ +title: gitlink +author: Mickaël Canouil +version: 1.12.0 +quarto-required: ">=1.9.38" +contributes: + filters: + - path: gitlink.lua + at: post-quarto +source: mcanouil/quarto-gitlink@1.12.0 +source-type: registry diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_modules/bitbucket.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_modules/bitbucket.lua new file mode 100644 index 0000000..ef86e2c --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_modules/bitbucket.lua @@ -0,0 +1,195 @@ +--- MC Bitbucket - Bitbucket-specific functionality for gitlink extension +--- @module "bitbucket" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local str = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/string.lua'):gsub('%.lua$', '')) + +local bitbucket_module = {} + +-- ============================================================================ +-- HELPER FUNCTIONS +-- ============================================================================ + +--- Create a Bitbucket link with platform label +--- @param text string The link text +--- @param uri string The URI +--- @param create_link_fn function The link creation function from main filter +--- @return pandoc.Link A Pandoc Link element with platform label +local function create_bitbucket_link(text, uri, create_link_fn) + return create_link_fn(text, uri, "bitbucket") +end + +--- Try matching a Str element's text against a pattern, with stripping fallback. +--- @param elem_text string The text to match +--- @param pattern string The Lua pattern (anchored with ^ and $) +--- @return string|nil prefix Stripped prefix (empty if direct match) +--- @return string|nil ... Captures from the pattern match +--- @return string|nil suffix Stripped suffix (empty if direct match) +local function match_with_stripping(elem_text, pattern) + local captures = { elem_text:match("^" .. pattern .. "$") } + if #captures > 0 then + return "", captures, "" + end + + local prefix, inner, suffix = str.strip_surrounding(elem_text) + if (prefix ~= "" or suffix ~= "") and inner ~= "" then + captures = { inner:match("^" .. pattern .. "$") } + if #captures > 0 then + return prefix, captures, suffix + end + end + + local search_pos = 1 + while true do + local b_prefix, b_content, b_suffix, open_pos = str.find_bracketed_content(elem_text, search_pos) + if not b_content then + break + end + captures = { b_content:match("^" .. pattern .. "$") } + if #captures > 0 then + return b_prefix, captures, b_suffix + end + search_pos = open_pos + 1 + end + + return nil, nil, nil +end + +-- ============================================================================ +-- BITBUCKET MULTI-WORD PATTERN PROCESSING +-- ============================================================================ + +--- Process Bitbucket-style multi-word patterns in inline sequences +--- This function handles patterns like "issue #123" and "pull request #456" +--- according to https://support.atlassian.com/bitbucket-cloud/docs/markup-comments/ +--- @param inlines table List of inline elements +--- @param base_url string The base URL for the Bitbucket instance +--- @param repository_name string|nil The repository name (e.g., "owner/repo") +--- @param create_link_fn function The link creation function from main filter +--- @return table Modified list of inline elements +--- @usage local result = bitbucket_module.process_inlines(inlines, "https://bitbucket.org", "owner/repo", create_platform_link) +function bitbucket_module.process_inlines(inlines, base_url, repository_name, create_link_fn) + local result = {} + local i = 1 + + while i <= #inlines do + local matched = false + + -- Try to match "issue #123" pattern + if i + 2 <= #inlines then + local elem1, elem2, elem3 = inlines[i], inlines[i + 1], inlines[i + 2] + if elem1.t == "Str" and elem1.text == "issue" and + elem2.t == "Space" and + elem3.t == "Str" then + local prefix, captures, suffix = match_with_stripping(elem3.text, "#(%d+)") + if captures then + local number = captures[1] + local uri + if repository_name then + uri = base_url .. "/" .. repository_name .. "/issues/" .. number + else + return inlines + end + local link = create_bitbucket_link("issue #" .. number, uri, create_link_fn) + if link then + if prefix ~= "" then table.insert(result, pandoc.Str(prefix)) end + table.insert(result, link) + if suffix ~= "" then table.insert(result, pandoc.Str(suffix)) end + i = i + 3 + matched = true + end + end + end + end + + -- Try to match "issue owner/repo#123" pattern + if not matched and i + 2 <= #inlines then + local elem1, elem2, elem3 = inlines[i], inlines[i + 1], inlines[i + 2] + if elem1.t == "Str" and elem1.text == "issue" and + elem2.t == "Space" and + elem3.t == "Str" then + local prefix, captures, suffix = match_with_stripping(elem3.text, "([^/]+/[^/#]+)#(%d+)") + if captures then + local repo, number = captures[1], captures[2] + local uri = base_url .. "/" .. repo .. "/issues/" .. number + local link = create_bitbucket_link("issue " .. repo .. "#" .. number, uri, create_link_fn) + if link then + if prefix ~= "" then table.insert(result, pandoc.Str(prefix)) end + table.insert(result, link) + if suffix ~= "" then table.insert(result, pandoc.Str(suffix)) end + i = i + 3 + matched = true + end + end + end + end + + -- Try to match "pull request #456" pattern + if not matched and i + 4 <= #inlines then + local elem1, elem2, elem3, elem4, elem5 = inlines[i], inlines[i + 1], inlines[i + 2], inlines[i + 3], inlines[i + 4] + if elem1.t == "Str" and elem1.text == "pull" and + elem2.t == "Space" and + elem3.t == "Str" and elem3.text == "request" and + elem4.t == "Space" and + elem5.t == "Str" then + local prefix, captures, suffix = match_with_stripping(elem5.text, "#(%d+)") + if captures then + local number = captures[1] + local uri + if repository_name then + uri = base_url .. "/" .. repository_name .. "/pull-requests/" .. number + else + return inlines + end + local link = create_bitbucket_link("pull request #" .. number, uri, create_link_fn) + if link then + if prefix ~= "" then table.insert(result, pandoc.Str(prefix)) end + table.insert(result, link) + if suffix ~= "" then table.insert(result, pandoc.Str(suffix)) end + i = i + 5 + matched = true + end + end + end + end + + -- Try to match "pull request owner/repo#456" pattern + if not matched and i + 4 <= #inlines then + local elem1, elem2, elem3, elem4, elem5 = inlines[i], inlines[i + 1], inlines[i + 2], inlines[i + 3], inlines[i + 4] + if elem1.t == "Str" and elem1.text == "pull" and + elem2.t == "Space" and + elem3.t == "Str" and elem3.text == "request" and + elem4.t == "Space" and + elem5.t == "Str" then + local prefix, captures, suffix = match_with_stripping(elem5.text, "([^/]+/[^/#]+)#(%d+)") + if captures then + local repo, number = captures[1], captures[2] + local uri = base_url .. "/" .. repo .. "/pull-requests/" .. number + local link = create_bitbucket_link("pull request " .. repo .. "#" .. number, uri, create_link_fn) + if link then + if prefix ~= "" then table.insert(result, pandoc.Str(prefix)) end + table.insert(result, link) + if suffix ~= "" then table.insert(result, pandoc.Str(suffix)) end + i = i + 5 + matched = true + end + end + end + end + + if not matched then + table.insert(result, inlines[i]) + i = i + 1 + end + end + + return result +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return bitbucket_module diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_modules/platform-schema.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_modules/platform-schema.lua new file mode 100644 index 0000000..f188db5 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_modules/platform-schema.lua @@ -0,0 +1,440 @@ +--- MC Platform Schema - Validator for gitlink platform configurations (platforms.yml) +--- @module "platform-schema" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local platform_schema = {} + +-- ============================================================================ +-- CONSTANTS +-- ============================================================================ + +--- Required pattern types that must exist in every platform configuration +--- @type table +local REQUIRED_PATTERN_TYPES = { 'issue', 'merge_request', 'commit', 'user' } + +--- Required URL format types for platform configurations +--- @type table +local REQUIRED_URL_FORMAT_TYPES = { 'issue', 'merge_request', 'pull', 'commit', 'user' } + +--- Validation error severity levels +--- @type table +local ERROR_LEVELS = { + ERROR = 1, + WARNING = 2, + INFO = 3 +} + +-- ============================================================================ +-- VALIDATION RESULT CLASS +-- ============================================================================ + +--- Validation result object containing errors, warnings, and metadata +--- @class ValidationResult +--- @field valid boolean Whether validation passed without errors +--- @field errors table List of error messages +--- @field warnings table List of warning messages +--- @field info table List of informational messages + +--- Create a new validation result +--- @return ValidationResult +local function create_validation_result() + return { + valid = true, + errors = {}, + warnings = {}, + info = {} + } +end + +--- Add an error to the validation result +--- @param result ValidationResult The validation result to update +--- @param message string The error message +--- @return nil +local function add_error(result, message) + table.insert(result.errors, message) + result.valid = false +end + +--- Add a warning to the validation result +--- @param result ValidationResult The validation result to update +--- @param message string The warning message +--- @return nil +local function add_warning(result, message) + table.insert(result.warnings, message) +end + +--- Add an informational message to the validation result +--- @param result ValidationResult The validation result to update +--- @param message string The informational message +--- @return nil +local function add_info(result, message) + table.insert(result.info, message) +end + +-- ============================================================================ +-- TYPE VALIDATION HELPERS +-- ============================================================================ + +--- Check if a value is a string +--- @param val any The value to check +--- @return boolean +local function is_string(val) + return type(val) == 'string' +end + +--- Check if a value is a table +--- @param val any The value to check +--- @return boolean +local function is_table(val) + return type(val) == 'table' +end + +--- Check if a value is an array (table with numeric keys) +--- @param val any The value to check +--- @return boolean +local function is_array(val) + if not is_table(val) then + return false + end + for k, _ in pairs(val) do + if not (type(k) == 'number' and k > 0 and k == math.floor(k)) then + return false + end + end + return true +end + +--- Check if a Lua regex pattern is valid +--- @param pattern string The pattern to validate +--- @return boolean, string|nil Whether valid, and error message if invalid +local function is_valid_lua_pattern(pattern) + if not is_string(pattern) then + return false, 'Pattern must be a string' + end + + local success, err = pcall(function() + _ = string.find('test', pattern) + end) + + if not success then + return false, tostring(err) + end + + return true, nil +end + +--- Check if a URL format template is valid +--- @param url_format string The URL format string to validate +--- @return boolean, string|nil Whether valid, and error message if invalid +local function is_valid_url_format(url_format) + if not is_string(url_format) then + return false, 'URL format must be a string' + end + + if not url_format:find('/', 1, true) then + return false, 'URL format must start with a forward slash (e.g., "/{repo}/issues/{number}")' + end + + if not url_format:match('{') then + return false, 'URL format must contain at least one placeholder (e.g., {repo}, {number})' + end + + return true, nil +end + +--- Check if a URL is valid format +--- @param url string The URL to validate +--- @return boolean, string|nil Whether valid, and error message if invalid +local function is_valid_base_url(url) + if not is_string(url) then + return false, 'Base URL must be a string' + end + + if url:find('^https?://', 1) == nil then + return false, 'Base URL must start with http:// or https://' + end + + return true, nil +end + +-- ============================================================================ +-- SCHEMA VALIDATORS +-- ============================================================================ + +--- Count the capture groups of a Lua pattern. +--- Escaped characters, balanced matches (`%b()`) and character classes count as one plain character. +--- A position capture `()` returns a number, which the filter cannot use, so it makes the pattern invalid. +--- @param pattern string The Lua pattern +--- @return integer|nil The number of capture groups, or nil if the pattern has a position capture +local function count_captures(pattern) + local structure = pattern:gsub('%%b..', 'x'):gsub('%%.', 'x'):gsub('%[%^?%]?[^%]]*%]', 'x') + if structure:find('()', 1, true) then + return nil + end + local _, opening = structure:gsub('%(', '') + return opening +end + +--- Validate a pattern object (array of patterns or single string for user) +--- @param patterns any The patterns to validate +--- @param pattern_type string The type of pattern (for error messages) +--- @param result ValidationResult The validation result to update +--- @return nil +local function validate_patterns(patterns, pattern_type, result) + if not patterns then + local hint = 'Expected: patterns:\n ' .. pattern_type:gsub('_', '-') .. ': [\'pattern1\', \'pattern2\']' + add_error(result, string.format('Missing required pattern type: "%s" (%s)', pattern_type:gsub('_', '-'), hint)) + return + end + + if pattern_type == 'user' then + if not is_string(patterns) then + add_error(result, 'User pattern must be a single string, not a list (e.g., user: "@([%w%-]+)")') + return + end + local valid, err = is_valid_lua_pattern(patterns) + if not valid then + add_error(result, string.format('Invalid Lua regex in user: %s (e.g., "@([%%w%%-%%%%]+)")', err)) + elseif not patterns:match('^%^?@') then + add_error(result, 'User pattern must start with "@" (e.g., "@([%w%-]+)")') + elseif count_captures(patterns) ~= 1 then + add_error(result, 'User pattern must have exactly one capture group, for the username (e.g., "@([%w%-]+)")') + end + return + end + + if not is_array(patterns) then + add_error( + result, + string.format( + 'Pattern type "%s" must be an array of patterns, got %s (e.g., [\'#(%%d+)\', \'owner/repo#(%%d+)\'])', + pattern_type:gsub('_', '-'), type(patterns)) + ) + return + end + + if #patterns == 0 then + add_warning(result, + string.format('Pattern type "%s" is empty (add at least one pattern)', pattern_type:gsub('_', '-'))) + return + end + + for i, pattern in ipairs(patterns) do + local valid, err = is_valid_lua_pattern(pattern) + if not valid then + add_error(result, string.format('Invalid Lua regex in %s[%d]: %s', pattern_type:gsub('_', '-'), i, err)) + else + local captures = count_captures(pattern) + if captures ~= 1 and captures ~= 2 then + local label = pattern_type:gsub('_', '-'):gsub('^%l', string.upper) + add_error(result, string.format('%s pattern must have one or two capture groups: %s', label, pattern)) + end + end + end +end + +--- Validate the patterns section of a platform configuration +--- @param patterns any The patterns object to validate +--- @param result ValidationResult The validation result to update +--- @return nil +local function validate_patterns_section(patterns, result) + if not patterns then + add_error(result, + 'Missing required field: "patterns" (add patterns section with: issue, merge-request, commit, user)') + return + end + + if not is_table(patterns) then + add_error(result, string.format('Field "patterns" must be a table, got %s', type(patterns))) + return + end + + for _, pattern_type in ipairs(REQUIRED_PATTERN_TYPES) do + validate_patterns(patterns[pattern_type], pattern_type, result) + end + + for key, _ in pairs(patterns) do + local found = false + for _, pattern_type in ipairs(REQUIRED_PATTERN_TYPES) do + if key == pattern_type then + found = true + break + end + end + if not found then + add_warning(result, string.format('Unknown pattern type: "%s" (not recognised)', key:gsub('_', '-'))) + end + end +end + +--- Validate the url-formats section of a platform configuration +--- @param url_formats any The url-formats object to validate +--- @param result ValidationResult The validation result to update +--- @return nil +local function validate_url_formats_section(url_formats, result) + if not url_formats then + add_error(result, + 'Missing required field: "url-formats" (add url-formats section with: issue, pull, merge-request, commit, user)') + return + end + + if not is_table(url_formats) then + add_error(result, string.format('Field "url-formats" must be a table, got %s', type(url_formats))) + return + end + + for _, format_type in ipairs(REQUIRED_URL_FORMAT_TYPES) do + local format = url_formats[format_type] + if not format then + local hint = format_type == 'issue' and '/{repo}/issues/{number}' or + format_type == 'pull' and '/{repo}/pull/{number}' or + format_type == 'merge_request' and '/{repo}/pull/{number}' or + format_type == 'commit' and '/{repo}/commit/{sha}' or + format_type == 'user' and '/{username}' or '/{path}' + add_error(result, string.format('Missing required URL format: "%s" (e.g., "%s")', format_type:gsub('_', '-'), hint)) + else + local valid, err = is_valid_url_format(format) + if not valid then + add_error(result, string.format('Invalid url-formats.%s: %s', format_type:gsub('_', '-'), err)) + end + end + end + + for key, _ in pairs(url_formats) do + local found = false + for _, format_type in ipairs(REQUIRED_URL_FORMAT_TYPES) do + if key == format_type then + found = true + break + end + end + if not found then + add_warning(result, string.format('Unknown URL format type: "%s" (not recognised)', key:gsub('_', '-'))) + end + end +end + +-- ============================================================================ +-- PUBLIC API +-- ============================================================================ + +--- Validate a complete platform configuration +--- Checks schema, types, patterns, and URLs +--- @param platform_name string The name of the platform being validated +--- @param config table The platform configuration to validate +--- @return ValidationResult +--- @usage +--- local result = platform_schema.validate_platform('github', config) +--- if not result.valid then +--- for _, err in ipairs(result.errors) do +--- print('ERROR: ' .. err) +--- end +--- end +function platform_schema.validate_platform(platform_name, config) + local result = create_validation_result() + + if not is_string(platform_name) or platform_name == '' then + add_error(result, 'Platform name must be a non-empty string') + return result + end + + if not is_table(config) then + add_error(result, string.format('Platform configuration must be a table, got %s', type(config))) + return result + end + + if not config.base_url then + add_error(result, 'Missing required field: "base-url" (e.g., https://github.com)') + else + local valid, err = is_valid_base_url(config.base_url) + if not valid then + add_error(result, string.format('Invalid base-url: %s (e.g., https://git.example.com)', err)) + end + end + + validate_patterns_section(config.patterns, result) + + validate_url_formats_section(config.url_formats, result) + + return result +end + +--- Validate all platforms in a configuration table +--- @param platforms table Table of platform configurations keyed by name +--- @return table Validation results for each platform +--- @usage +--- local results = platform_schema.validate_all_platforms(platforms_config) +function platform_schema.validate_all_platforms(platforms) + local results = {} + + if not is_table(platforms) then + results['__global__'] = create_validation_result() + add_error(results['__global__'], 'Platforms configuration must be a table') + return results + end + + for platform_name, config in pairs(platforms) do + results[platform_name] = platform_schema.validate_platform(platform_name, config) + end + + return results +end + +--- Format validation results as human-readable strings +--- @param result ValidationResult The validation result to format +--- @return string, string|nil Formatted message, and platform_name if provided +--- @usage +--- local msg = platform_schema.format_result(result) +--- print(msg) +function platform_schema.format_result(result) + local lines = {} + + if result.valid then + table.insert(lines, 'Validation passed.') + else + table.insert(lines, 'Validation failed with ' .. #result.errors .. ' error(s):') + for i, err in ipairs(result.errors) do + table.insert(lines, string.format(' [Error %d] %s', i, err)) + end + end + + if #result.warnings > 0 then + table.insert(lines, '') + table.insert(lines, #result.warnings .. ' warning(s):') + for i, warn in ipairs(result.warnings) do + table.insert(lines, string.format(' [Warning %d] %s', i, warn)) + end + end + + if #result.info > 0 then + table.insert(lines, '') + table.insert(lines, #result.info .. ' info message(s):') + for i, info in ipairs(result.info) do + table.insert(lines, string.format(' [Info %d] %s', i, info)) + end + end + + return table.concat(lines, '\n') +end + +--- Get a summary of validation errors and warnings +--- @param result ValidationResult The validation result +--- @return string Summary string +--- @usage +--- local summary = platform_schema.get_summary(result) +function platform_schema.get_summary(result) + return string.format( + 'Status: %s | Errors: %d | Warnings: %d', + result.valid and 'PASSED' or 'FAILED', + #result.errors, + #result.warnings + ) +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return platform_schema diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_modules/platforms.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_modules/platforms.lua new file mode 100644 index 0000000..1da58c6 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_modules/platforms.lua @@ -0,0 +1,323 @@ +--- Platform Configuration Module +--- @module "platforms" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local platforms_module = {} + +-- Load platform schema validation module +local platform_schema = require(quarto.utils.resolve_path('_modules/platform-schema.lua'):gsub('%.lua$', '')) + +-- ============================================================================ +-- CONFIGURATION STORAGE +-- ============================================================================ + +--- @type table Platform configurations cache +local platform_configs = nil + +--- @type table Custom platform configurations +local custom_platforms = {} + +--- @type table Validation results for last loaded platforms +local validation_results = {} + +-- ============================================================================ +-- HELPER FUNCTIONS +-- ============================================================================ + +--- Check if a value is empty or nil +--- @param val any The value to check +--- @return boolean True if the value is nil or empty, false otherwise +local function is_empty(val) + return val == nil or val == '' +end + +--- Convert YAML value to Lua table structure +--- @param yaml_value any The YAML value to convert +--- @return any The converted value +local function convert_yaml_value(yaml_value) + local yaml_type = pandoc.utils.type(yaml_value) + + if yaml_type == 'Inlines' or yaml_type == 'Blocks' then + return pandoc.utils.stringify(yaml_value) + elseif yaml_type == 'List' then + local result = {} + for i = 1, #yaml_value do + result[i] = convert_yaml_value(yaml_value[i]) + end + return result + elseif type(yaml_value) == 'table' then + local result = {} + for key, value in pairs(yaml_value) do + local converted_key = key:gsub('-', '_') + result[converted_key] = convert_yaml_value(value) + end + return result + else + return yaml_value + end +end + +-- ============================================================================ +-- CONFIGURATION LOADING +-- ============================================================================ + +--- Load platform configurations from YAML file +--- @param yaml_path string|nil Optional path to custom YAML file +--- @return table|nil The platform configurations or nil on error +--- @usage local configs = platforms_module.load_platforms('custom-platforms.yml') +local function load_platforms(yaml_path) + local config_path = yaml_path or quarto.utils.resolve_path('platforms.yml') + + local file = io.open(config_path, 'r') + if not file then + return nil + end + local content = file:read('*all') + file:close() + + local success, result = pcall(function() + local meta = pandoc.read('---\n' .. content .. '\n---', 'markdown').meta + if meta and meta.platforms then + return convert_yaml_value(meta.platforms) + end + return nil + end) + + if success and result then + return result + end + + return nil +end + +--- Initialise platform configurations +--- Loads platforms from YAML and validates them +--- @param yaml_path string|nil Optional path to custom YAML file +--- @return boolean, string|nil True if initialisation was successful, error message if failed +--- @usage local ok, err = platforms_module.initialise('custom-platforms.yml') +function platforms_module.initialise(yaml_path) + if platform_configs and not yaml_path then + return true, nil + end + + local loaded_configs = load_platforms(yaml_path) + + if not loaded_configs then + if not platform_configs then + platform_configs = {} + end + local msg = yaml_path and ('Failed to load platforms from ' .. yaml_path) or 'Failed to load built-in platforms' + return false, msg + end + + local validation_results_all = platform_schema.validate_all_platforms(loaded_configs) + local has_errors = false + local error_messages = {} + + for platform_name, result in pairs(validation_results_all) do + if not result.valid then + has_errors = true + local platform_errors = {} + for _, err in ipairs(result.errors) do + table.insert(platform_errors, ' - ' .. err) + end + table.insert(error_messages, platform_name .. ':\n' .. table.concat(platform_errors, '\n')) + end + end + + if has_errors then + local msg = table.concat(error_messages, '\n') + return false, msg + end + + if platform_configs and yaml_path then + for name, config in pairs(loaded_configs) do + custom_platforms[name] = config + validation_results[name] = validation_results_all[name] + end + else + platform_configs = loaded_configs + end + + return true, nil +end + +-- ============================================================================ +-- PUBLIC API +-- ============================================================================ + +--- Get platform configuration by name +--- @param platform_name string The platform name +--- @return table|nil The platform configuration or nil if not found +--- @usage local config = platforms_module.get_platform_config('github') +function platforms_module.get_platform_config(platform_name) + if not platform_configs then + platforms_module.initialise() + end + + local name_lower = platform_name:lower() + + if custom_platforms[name_lower] then + return custom_platforms[name_lower] + end + + if platform_configs and platform_configs[name_lower] then + return platform_configs[name_lower] + end + + return nil +end + +--- Get platform display name +--- Returns the display name from the platform configuration or a capitalised default +--- @param platform_name string The platform name +--- @return string The platform display name +--- @usage local name = platforms_module.get_platform_display_name('github') +function platforms_module.get_platform_display_name(platform_name) + local config = platforms_module.get_platform_config(platform_name) + if config and config.display_name then + return config.display_name + end + local name_str = tostring(platform_name) + return name_str:sub(1, 1):upper() .. name_str:sub(2) +end + +--- Get all available platform names +--- @return table List of available platform names +--- @usage local platforms = platforms_module.get_all_platform_names() +function platforms_module.get_all_platform_names() + if not platform_configs then + platforms_module.initialise() + end + + local names = {} + + if platform_configs then + for name, _ in pairs(platform_configs) do + table.insert(names, name) + end + end + + for name, _ in pairs(custom_platforms) do + if not platform_configs or not platform_configs[name] then + table.insert(names, name) + end + end + + table.sort(names) + return names +end + +--- Register a custom platform configuration +--- Validates the configuration against the schema before registration +--- @param platform_name string The platform name +--- @param config table The platform configuration +--- @return boolean, string|nil True if registration was successful, error message if failed +--- @usage local ok, err = platforms_module.register_custom_platform('forgejo', {...}) +function platforms_module.register_custom_platform(platform_name, config) + if not platform_name or not config then + return false, 'Platform name and configuration are required' + end + + local result = platform_schema.validate_platform(platform_name, config) + + if not result.valid then + local error_lines = {} + for _, err in ipairs(result.errors) do + table.insert(error_lines, ' - ' .. err) + end + local error_msg = 'Invalid platform configuration "' .. platform_name .. '":\n' .. table.concat(error_lines, '\n') + return false, error_msg + end + + local name_lower = platform_name:lower() + custom_platforms[name_lower] = config + validation_results[name_lower] = result + + return true, nil +end + +--- Load custom platform from YAML string +--- Parses YAML and registers the platform with full validation +--- @param yaml_string string The YAML string containing platform configuration +--- @param platform_name string The platform name to register +--- @return boolean, string|nil True if registration was successful, error message if failed +--- @usage local ok, err = platforms_module.load_custom_platform_from_yaml(yaml_str, 'forgejo') +function platforms_module.load_custom_platform_from_yaml(yaml_string, platform_name) + if is_empty(yaml_string) or is_empty(platform_name) then + return false, 'YAML string and platform name are required' + end + + local success, result = pcall(function() + local meta = pandoc.read('---\n' .. yaml_string .. '\n---', 'markdown').meta + if meta and meta.platforms and meta.platforms[platform_name] then + return convert_yaml_value(meta.platforms[platform_name]) + end + return nil + end) + + if not success then + return false, 'Failed to parse YAML: ' .. tostring(result) + end + + if not result then + return false, 'No platform configuration found for "' .. platform_name .. '" in YAML' + end + + return platforms_module.register_custom_platform(platform_name, result) +end + +--- Clear all custom platform configurations +--- @return nil +--- @usage platforms_module.clear_custom_platforms() +function platforms_module.clear_custom_platforms() + custom_platforms = {} +end + +--- Check if a platform is available +--- @param platform_name string The platform name +--- @return boolean True if the platform is available, false otherwise +--- @usage local available = platforms_module.is_platform_available('github') +function platforms_module.is_platform_available(platform_name) + local name_lower = platform_name:lower() + return platforms_module.get_platform_config(name_lower) ~= nil +end + +--- Get the validation result for a platform +--- @param platform_name string The platform name +--- @return table|nil The validation result or nil if not found +--- @usage local result = platforms_module.get_validation_result('forgejo') +function platforms_module.get_validation_result(platform_name) + local name_lower = platform_name:lower() + return validation_results[name_lower] +end + +--- Validate a platform configuration against the schema +--- Useful for testing custom platforms before registering them +--- @param platform_name string The platform name +--- @param config table The platform configuration to validate +--- @return table ValidationResult with valid, errors, warnings, and info fields +--- @usage +--- local result = platforms_module.validate_platform_config('forgejo', config) +--- if result.valid then print('OK') else print(table.concat(result.errors, ', ')) end +function platforms_module.validate_platform_config(platform_name, config) + return platform_schema.validate_platform(platform_name, config) +end + +--- Validate all platforms in a configuration table +--- Useful for validating entire custom platform files +--- @param platforms table Table of platform configurations +--- @return table ValidationResult for each platform +--- @usage +--- local results = platforms_module.validate_all_platforms(platforms_config) +function platforms_module.validate_all_platforms(platforms) + return platform_schema.validate_all_platforms(platforms) +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return platforms_module diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_modules/widget.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_modules/widget.lua new file mode 100644 index 0000000..55f52b3 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_modules/widget.lua @@ -0,0 +1,480 @@ +--- Gitlink Repository Navbar Widget +--- @module "widget" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @brief Injects the repository navbar widget (live stars/forks counts and a +--- links dropdown) into HTML output from the `extensions.gitlink.widget` +--- configuration and the per-platform `widget` sections of platforms.yml. + +local widget_module = {} + +local str = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/string.lua'):gsub('%.lua$', '')) +local log = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/logging.lua'):gsub('%.lua$', '')) +local html_mod = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/html.lua'):gsub('%.lua$', '')) + +--- @type string Version used for the injected HTML dependency +local WIDGET_DEPENDENCY_VERSION = '1.9.0' + +--- @type string The href of a navigation item that widget.js replaces +local WIDGET_PLACEHOLDER = '#gitlink-widget' + +--- @type table|nil Octicon SVG bodies by name (loaded once per render) +local octicons_cache = nil + +--- Load the embedded octicon set (octicons.json, generated by +--- tools/generate-octicons.py). Only the icons a page's menu actually uses +--- are embedded into the widget configuration, so the full set never reaches +--- the rendered page. +--- @param extension_name string The extension name (for warnings) +--- @return table Octicon SVG bodies by name +local function load_octicons(extension_name) + if octicons_cache then + return octicons_cache + end + octicons_cache = {} + local path = quarto.utils.resolve_path('octicons.json') + local file = io.open(path, 'r') + if not file then + log.log_warning( + extension_name, + "Missing 'octicons.json': widget icons fall back to Bootstrap names only. " .. + 'Regenerate it with tools/generate-octicons.py.' + ) + return octicons_cache + end + local content = file:read('*all') + file:close() + local ok, decoded = pcall(quarto.json.decode, content) + if ok and type(decoded) == 'table' then + octicons_cache = decoded + else + log.log_warning( + extension_name, + "Failed to parse 'octicons.json': widget icons fall back to Bootstrap names only." + ) + end + return octicons_cache +end + +--- Coerce a metadata value to a boolean with a default. +--- @param value any The raw metadata value (boolean, MetaValue, or nil) +--- @param default boolean The default when the value is absent +--- @return boolean The resolved boolean +local function to_boolean(value, default) + if value == nil then + return default + end + if type(value) == 'boolean' then + return value + end + return str.stringify(value):lower() == 'true' +end + +--- Tell whether a decoded metadata value holds a string that ends with the +--- widget placeholder, at any depth, as widget.js matches an href. This +--- covers an `href` key and the bare-string form of a navigation item. +--- @param value any A value decoded from JSON +--- @return boolean +local function holds_placeholder(value) + if type(value) == 'string' then + return value:sub(-#WIDGET_PLACEHOLDER) == WIDGET_PLACEHOLDER + end + if type(value) ~= 'table' then + return false + end + for _, child in pairs(value) do + if holds_placeholder(child) then + return true + end + end + return false +end + +--- Tell whether the website or book metadata has a widget placeholder +--- anywhere, as widget.js replaces every link to it on the page. +--- @param format_meta table|nil The project format.metadata table +--- @return boolean +local function has_placeholder(format_meta) + if type(format_meta) ~= 'table' then + return false + end + return holds_placeholder(format_meta['website']) or holds_placeholder(format_meta['book']) +end + +--- Percent-encode a string for use as a URL path segment. +--- @param value string The value to encode +--- @return string The encoded value +local function url_encode(value) + local encoded = value:gsub('[^%w%-%._~]', function(char) + return string.format('%%%02X', string.byte(char)) + end) + return encoded +end + +--- Substitute `{repo}`, `{repo-encoded}`, `{base-url}`, and `{username}` +--- placeholders in a template. Function replacements keep literal `%` in the +--- substituted values from being interpreted as gsub captures. +--- @param template string The template string +--- @param context table Fields: repo, base_url, username (all optional) +--- @return string The resolved string +local function resolve_template(template, context) + local resolved = template + resolved = resolved:gsub('{repo%-encoded}', function() return url_encode(context.repo or '') end) + resolved = resolved:gsub('{repo}', function() return context.repo or '' end) + resolved = resolved:gsub('{base%-url}', function() return context.base_url or '' end) + resolved = resolved:gsub('{username}', function() return context.username or '' end) + return resolved +end + +--- @type table Elements allowed in structured label parts +local ALLOWED_LABEL_TAGS = { + ['iconify-icon'] = true, + span = true, + em = true, + strong = true, + code = true, + sub = true, + sup = true, + i = true, +} + +--- @type table Attributes allowed on label elements +local ALLOWED_LABEL_ATTRIBUTES = { + class = true, + icon = true, + width = true, + height = true, + inline = true, + title = true, + role = true, + ['aria-label'] = true, + ['aria-hidden'] = true, +} + +--- Extract allowlisted elements and text from a raw HTML fragment into label +--- parts. Elements outside the allowlist are dropped and their surrounding +--- text kept, so widget.js can rebuild the label with createElement and +--- setAttribute without ever parsing HTML in the browser. +--- @param html string The raw HTML fragment (e.g. shortcode output) +--- @param parts table The accumulating list of label parts +local function raw_html_to_parts(html, parts) + local position = 1 + while true do + local start_pos, end_pos, tag, attr_text = html:find('<([%w-]+)(.-)>', position) + local before = html:sub(position, (start_pos or 0) - 1):gsub('<[^>]*>', '') + if before ~= '' then + table.insert(parts, { text = before }) + end + if not start_pos then + break + end + local tag_lower = tag:lower() + if ALLOWED_LABEL_TAGS[tag_lower] then + local attrs = {} + for name, value in attr_text:gmatch('([%w-]+)%s*=%s*"([^"]*)"') do + if ALLOWED_LABEL_ATTRIBUTES[name:lower()] then + attrs[name:lower()] = value + end + end + -- Valueless boolean attributes (e.g. iconify's `inline`). + local remainder = attr_text:gsub('([%w-]+)%s*=%s*"[^"]*"', '') + for name in remainder:gmatch('([%w-]+)') do + if ALLOWED_LABEL_ATTRIBUTES[name:lower()] and attrs[name:lower()] == nil then + attrs[name:lower()] = '' + end + end + table.insert(parts, { tag = tag_lower, attrs = attrs }) + end + position = end_pos + 1 + end +end + +--- Convert a metadata value to a structured label: either a plain string or +--- a list of parts ({text} or {tag, attrs}) preserving allowlisted shortcode +--- output (e.g. an iconify icon in an extra-link's `text`). +--- @param value any The metadata value +--- @return string|table The label string or parts list +local function meta_to_label(value) + if pandoc.utils.type(value) ~= 'Inlines' then + return str.stringify(value) + end + local parts = {} + local buffer = {} + local function flush() + if #buffer > 0 then + table.insert(parts, { text = table.concat(buffer) }) + buffer = {} + end + end + for _, inline in ipairs(value) do + if inline.t == 'RawInline' and (inline.format == 'html' or inline.format == 'html5') then + flush() + raw_html_to_parts(inline.text, parts) + elseif inline.t == 'Space' or inline.t == 'SoftBreak' then + table.insert(buffer, ' ') + else + table.insert(buffer, str.stringify(inline)) + end + end + flush() + return parts +end + +--- Whether the widget is enabled in the metadata (default false). +--- @param widget_meta table|nil The `extensions.gitlink.widget` metadata sub-table +--- @return boolean True when the widget should be injected +function widget_module.is_enabled(widget_meta) + if widget_meta == nil then + return false + end + return to_boolean(widget_meta['enabled'], false) +end + +--- Append a link entry to the list when its toggle is on and a path exists. +--- @param links table The accumulating list of link entries +--- @param toggles table|nil The `widget.links` metadata sub-table +--- @param key string The toggle key (kebab-case, as written in YAML) +--- @param default boolean Whether the link is included when the toggle is absent +--- @param entry table|nil The link entry ({label, href, icon}) or nil when unsupported +local function append_default_link(links, toggles, key, default, entry) + if not entry then + return + end + if to_boolean(toggles and toggles[key], default) then + table.insert(links, entry) + end +end + +--- Build a default link entry from a platform widget path template. +--- @param paths table|nil The platform `widget.paths` table (snake_case keys) +--- @param key string The path key (snake_case) +--- @param label string The link label +--- @param icon string The octicon key +--- @param context table The template context (repo, base_url) +--- @return table|nil The link entry or nil when the platform has no such path +local function platform_link(paths, key, label, icon, context) + local template = paths and paths[key] + if str.is_empty(template) then + return nil + end + return { + label = label, + href = context.base_url .. resolve_template(template, context), + icon = icon, + } +end + +--- Resolve the `extra-links` metadata list into link entries. +--- Relative hrefs (starting with '/') are resolved against the repository URL +--- so site configuration stays portable across forks. +--- @param extra_meta table|nil The `widget.extra-links` metadata list +--- @param repo_url string The repository URL +--- @param extension_name string The extension name (for warnings) +--- @return table The resolved link entries +local function resolve_extra_links(extra_meta, repo_url, extension_name) + local links = {} + if type(extra_meta) ~= 'table' then + return links + end + for _, item in ipairs(extra_meta) do + local label = item['text'] and meta_to_label(item['text']) + local href = item['href'] and str.stringify(item['href']) + if str.is_empty(item['text'] and str.stringify(item['text'])) or str.is_empty(href) then + log.log_warning( + extension_name, + "Ignoring 'widget.extra-links' entry without both 'text' and 'href'." + ) + elseif not (href:sub(1, 1) == '/' or href:match('^https?://')) then + log.log_warning( + extension_name, + "Ignoring 'widget.extra-links' entry '" .. str.stringify(item['text']) .. + "': 'href' must start with '/' or 'http(s)://'." + ) + else + if href:sub(1, 1) == '/' then + href = repo_url .. href + end + local entry = { label = label, href = href } + local icon = item['icon'] and str.stringify(item['icon']) + if not str.is_empty(icon) then + entry.icon = icon + end + table.insert(links, entry) + end + end + return links +end + +--- Concatenate link groups with dividers between non-empty groups. +--- @param groups table List of link-entry lists +--- @return table The flat link list with divider entries +local function join_link_groups(groups) + local links = {} + for _, group in ipairs(groups) do + if #group > 0 then + if #links > 0 then + table.insert(links, { divider = true }) + end + for _, entry in ipairs(group) do + table.insert(links, entry) + end + end + end + return links +end + +--- Build the widget configuration table serialised for widget.js. +--- @param spec table See `widget_module.setup` +--- @return table The JSON-ready configuration +local function build_config(spec) + local widget_meta = spec.widget_meta + local platform_widget = spec.platform_config.widget or {} + local paths = platform_widget.paths + local labels = platform_widget.labels or {} + local context = { repo = spec.repository_name, base_url = spec.base_url } + local repo_url = spec.base_url .. '/' .. spec.repository_name + local toggles = widget_meta['links'] + + local main_group = { + { label = 'Repository', href = repo_url, icon = 'repo' }, + } + append_default_link(main_group, toggles, 'issues', true, + platform_link(paths, 'issues', 'Issues', 'issue', context)) + append_default_link(main_group, toggles, 'pull-requests', true, + platform_link(paths, 'pull_requests', labels.pull_requests or 'Pull Requests', 'pull-request', context)) + append_default_link(main_group, toggles, 'releases', true, + platform_link(paths, 'releases', 'Releases', 'release', context)) + -- Discussions are off by default: not every repository has them enabled, + -- so a default link would often be dead. + append_default_link(main_group, toggles, 'discussions', false, + platform_link(paths, 'discussions', 'Discussions', 'discussion', context)) + + local action_group = {} + append_default_link(action_group, toggles, 'star', true, + platform_link(paths, 'star', 'Add a Star', 'star', context)) + append_default_link(action_group, toggles, 'fork', true, + platform_link(paths, 'fork', 'Create a Fork', 'fork', context)) + + local extra_group = resolve_extra_links(widget_meta['extra-links'], repo_url, spec.extension_name) + + local sponsor = widget_meta['sponsor'] and str.stringify(widget_meta['sponsor']) + if not str.is_empty(sponsor) then + local sponsor_template = platform_widget.sponsor_url + if str.is_empty(sponsor_template) then + log.log_warning( + spec.extension_name, + "Ignoring 'widget.sponsor': platform '" .. spec.platform .. "' defines no sponsor URL." + ) + else + context.username = sponsor + table.insert(extra_group, { + label = 'Sponsor', + href = resolve_template(sponsor_template, context), + icon = 'heart', + }) + end + end + + local icon = widget_meta['icon'] and str.stringify(widget_meta['icon']) + if str.is_empty(icon) then + icon = spec.platform == 'github' and 'mark-github' or 'git-branch' + end + + local config = { + cacheKey = 'gitlink-widget-' .. spec.platform .. '-' .. spec.repository_name, + menuLabel = spec.display_name .. ' repository menu', + icon = icon, + links = join_link_groups({ main_group, action_group, extra_group }), + } + + local api = platform_widget.api + if api and not str.is_empty(api.endpoint) then + -- Platform headers are "Name: value" strings in the YAML (map keys would + -- be case-mangled); parse them here so widget.js gets a ready object. + local headers = {} + local has_headers = false + for _, header in ipairs(api.headers or {}) do + local name, value = str.stringify(header):match('^([^:]+):%s*(.+)$') + if name then + headers[name] = value + has_headers = true + end + end + config.api = { + endpoint = resolve_template(api.endpoint, context), + starsField = api.stars_field, + forksField = api.forks_field, + headers = has_headers and headers or nil, + } + end + + -- Embed only the octicon bodies this menu uses; names not in the set are + -- left for widget.js to render as Bootstrap icons. + local octicons = load_octicons(spec.extension_name) + local used = {} + local function add_icon(name) + if name and octicons[name] then + used[name] = octicons[name] + end + end + add_icon(config.icon) + if config.api then + add_icon('star') + add_icon('fork') + end + for _, link in ipairs(config.links) do + add_icon(link.icon) + end + config.icons = used + + return config +end + +--- Inject the widget dependency and configuration into the document. +--- No-op outside HTML output. Skips injection when the repository name could +--- not be resolved, and logs a warning only when the website navigation has a +--- widget placeholder, as the widget would show nowhere else. +--- @param spec table Fields: extension_name (string), widget_meta (table), +--- platform (string), platform_config (table), base_url (string), +--- repository_name (string|nil), format_meta (table|nil), display_name (string) +--- @return nil +function widget_module.setup(spec) + if not quarto.doc.is_format('html:js') then + return + end + + if str.is_empty(spec.repository_name) then + if has_placeholder(spec.format_meta) then + log.log_warning( + spec.extension_name, + "Widget is enabled but no repository could be resolved. " .. + "Set 'extensions.gitlink.repository-name' (owner/repo) or add a git remote." + ) + end + return + end + + local stylesheets = { quarto.utils.resolve_path('widget.css') } + if to_boolean(spec.widget_meta['style-navbar-tools'], false) then + table.insert(stylesheets, quarto.utils.resolve_path('widget-navbar-tools.css')) + end + + html_mod.ensure_html_dependency({ + name = 'quarto-gitlink-widget', + version = WIDGET_DEPENDENCY_VERSION, + scripts = { quarto.utils.resolve_path('widget.js') }, + stylesheets = stylesheets, + }) + + local config = build_config(spec) + -- `' .. json .. '' + ) +end + +return widget_module diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_schema.yml b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_schema.yml new file mode 100644 index 0000000..0dbbbad --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_schema.yml @@ -0,0 +1,146 @@ +# Schema for the gitlink extension +# Describes options for IDE tooling and runtime validation. + +# Extension-level metadata options. +# Configured in _quarto.yml, _metadata.yml, or document front matter: +# extensions: +# gitlink: +# platform: "github" +# repository-name: "owner/repo" + +$schema: https://m.canouil.dev/quarto-wizard/assets/schema/v2/extension-schema.json + +options: + enabled: + type: boolean + default: true + description: "Whether the filter is enabled for the document, set to false in drafts or templates to opt out of link rewriting." + platform: + type: string + default: "github" + description: "Git hosting platform to use for link generation." + examples: + - github + - gitlab + - codeberg + - gitea + - bitbucket + repository-name: + type: string + description: "Repository in 'owner/repo' format, auto-detected from the git remote when not set." + base-url: + type: string + description: "Base URL for the Git hosting platform, defaulting to the platform's standard URL." + custom-platforms-file: + type: string + description: "Path to a YAML file defining custom platform configurations." + completion: + type: file + extensions: + - .yml + - .yaml + show-platform-badge: + type: boolean + default: true + description: "Whether to display a platform badge alongside generated links." + badge-position: + type: string + default: "after" + description: "Position of the platform badge relative to the link." + enum: + - after + - before + badge-background-colour: + type: string + default: "#c3c3c3" + description: "Background colour for the platform badge as a hex code of 3, 4, 6, or 8 digits, or a CSS named colour." + aliases: + - badge-background-color + completion: + type: color + badge-text-colour: + type: string + description: "Text colour for the platform badge as a hex code of 3, 4, 6, or 8 digits, or a CSS named colour. Defaults to black or white, whichever has more contrast with the background, on light and dark pages in HTML and on a white page in Typst." + aliases: + - badge-text-color + completion: + type: color + normalize-links: + type: boolean + default: true + description: "Whether to shorten platform URLs used as autolink text, so becomes #1." + fetch-titles: + type: boolean + default: false + description: "Whether to fetch the page title for autolinked platform URLs and use it as the link text, requiring network access via curl." + mentions: + type: array + description: "List of citation IDs to force-treat as Git hosting mentions even when a bibliography reference with the same id exists." + items: + type: string + widget: + type: object + description: "Repository widget showing live star/fork counts and a dropdown of repository links, replacing every navbar item, sidebar tool, or sidebar contents item with href '#gitlink-widget'. HTML websites only." + additionalProperties: false + properties: + enabled: + type: boolean + default: false + description: "Whether to inject the widget. Independent of the top-level 'enabled' option, so a site can run widget-only." + links: + type: object + description: "Toggles for the platform-derived default menu entries." + additionalProperties: false + properties: + issues: + type: boolean + default: true + description: "Whether to include the Issues link." + pull-requests: + type: boolean + default: true + description: "Whether to include the Pull Requests (or Merge Requests) link." + releases: + type: boolean + default: true + description: "Whether to include the Releases link." + discussions: + type: boolean + default: false + description: "Whether to include the Discussions link, on platforms that support discussions." + star: + type: boolean + default: true + description: "Whether to include the Add a Star link." + fork: + type: boolean + default: true + description: "Whether to include the Create a Fork link." + extra-links: + type: array + description: "Additional dropdown entries appended after the default set. Hrefs starting with '/' are resolved against the repository URL." + items: + type: object + additionalProperties: false + properties: + text: + type: string + required: true + description: "Link label." + href: + type: string + required: true + description: "Link target, absolute or relative to the repository URL when starting with '/'." + icon: + type: string + description: "Icon name from Quarto's bundled Bootstrap Icons (same names as navbar tools), or one of the widget's embedded octicons (see the README for the full list; embedded names win). For anything else, put a shortcode (e.g. iconify) in 'text' instead." + sponsor: + type: string + description: "Username for a Sponsor entry, on platforms that define a sponsor URL (GitHub)." + icon: + type: string + description: "Trigger icon override: a Bootstrap icon name (as bundled with Quarto) or one of the widget's embedded icons. Defaults to the platform mark." + style-navbar-tools: + type: boolean + default: false + description: "Whether to also apply the widget's bordered pill style to Quarto's navbar search button and colour-scheme toggle; sizing and spacing always match the widget." diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_snippets.json b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_snippets.json new file mode 100644 index 0000000..e5105b0 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_snippets.json @@ -0,0 +1,41 @@ +{ + "GitHub config": { + "prefix": "github-meta", + "body": [ + "filters:", + " - gitlink", + "extensions:", + " gitlink:", + " platform: github", + " repository-name: ${1:owner/repo}" + ], + "description": "Activate gitlink for GitHub references" + }, + "Navbar widget config": { + "prefix": "gitlink-widget-meta", + "body": [ + "filters:", + " - gitlink", + "extensions:", + " gitlink:", + " platform: ${1:github}", + " repository-name: ${2:owner/repo}", + " widget:", + " enabled: true" + ], + "description": "Activate the gitlink repository navbar widget" + }, + "GitLab config": { + "prefix": "gitlab-meta", + "body": [ + "filters:", + " - gitlink", + "extensions:", + " gitlink:", + " platform: gitlab", + " base-url: https://gitlab.com", + " repository-name: ${1:group/project}" + ], + "description": "Activate gitlink for GitLab references" + } +} diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/LICENSE b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/LICENSE new file mode 100644 index 0000000..4b53bb8 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mickaël Canouil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/colour.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/colour.lua new file mode 100644 index 0000000..f9032e9 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/colour.lua @@ -0,0 +1,368 @@ +--- MC Colour - Colour conversion utilities for Quarto Lua filters and shortcodes +--- @module "colour" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +-- ============================================================================ +-- CSS NAMED COLOURS +-- ============================================================================ + +--- CSS named colours lookup table. +--- Maps lowercase colour names to uppercase 6-character hex codes. +--- Contains all 148 standard CSS named colours. +--- @type table +local CSS_NAMED_COLOURS = { + aliceblue = '#F0F8FF', + antiquewhite = '#FAEBD7', + aqua = '#00FFFF', + aquamarine = '#7FFFD4', + azure = '#F0FFFF', + beige = '#F5F5DC', + bisque = '#FFE4C4', + black = '#000000', + blanchedalmond = '#FFEBCD', + blue = '#0000FF', + blueviolet = '#8A2BE2', + brown = '#A52A2A', + burlywood = '#DEB887', + cadetblue = '#5F9EA0', + chartreuse = '#7FFF00', + chocolate = '#D2691E', + coral = '#FF7F50', + cornflowerblue = '#6495ED', + cornsilk = '#FFF8DC', + crimson = '#DC143C', + cyan = '#00FFFF', + darkblue = '#00008B', + darkcyan = '#008B8B', + darkgoldenrod = '#B8860B', + darkgray = '#A9A9A9', + darkgreen = '#006400', + darkgrey = '#A9A9A9', + darkkhaki = '#BDB76B', + darkmagenta = '#8B008B', + darkolivegreen = '#556B2F', + darkorange = '#FF8C00', + darkorchid = '#9932CC', + darkred = '#8B0000', + darksalmon = '#E9967A', + darkseagreen = '#8FBC8F', + darkslateblue = '#483D8B', + darkslategray = '#2F4F4F', + darkslategrey = '#2F4F4F', + darkturquoise = '#00CED1', + darkviolet = '#9400D3', + deeppink = '#FF1493', + deepskyblue = '#00BFFF', + dimgray = '#696969', + dimgrey = '#696969', + dodgerblue = '#1E90FF', + firebrick = '#B22222', + floralwhite = '#FFFAF0', + forestgreen = '#228B22', + fuchsia = '#FF00FF', + gainsboro = '#DCDCDC', + ghostwhite = '#F8F8FF', + gold = '#FFD700', + goldenrod = '#DAA520', + gray = '#808080', + green = '#008000', + greenyellow = '#ADFF2F', + grey = '#808080', + honeydew = '#F0FFF0', + hotpink = '#FF69B4', + indianred = '#CD5C5C', + indigo = '#4B0082', + ivory = '#FFFFF0', + khaki = '#F0E68C', + lavender = '#E6E6FA', + lavenderblush = '#FFF0F5', + lawngreen = '#7CFC00', + lemonchiffon = '#FFFACD', + lightblue = '#ADD8E6', + lightcoral = '#F08080', + lightcyan = '#E0FFFF', + lightgoldenrodyellow = '#FAFAD2', + lightgray = '#D3D3D3', + lightgreen = '#90EE90', + lightgrey = '#D3D3D3', + lightpink = '#FFB6C1', + lightsalmon = '#FFA07A', + lightseagreen = '#20B2AA', + lightskyblue = '#87CEFA', + lightslategray = '#778899', + lightslategrey = '#778899', + lightsteelblue = '#B0C4DE', + lightyellow = '#FFFFE0', + lime = '#00FF00', + limegreen = '#32CD32', + linen = '#FAF0E6', + magenta = '#FF00FF', + maroon = '#800000', + mediumaquamarine = '#66CDAA', + mediumblue = '#0000CD', + mediumorchid = '#BA55D3', + mediumpurple = '#9370DB', + mediumseagreen = '#3CB371', + mediumslateblue = '#7B68EE', + mediumspringgreen = '#00FA9A', + mediumturquoise = '#48D1CC', + mediumvioletred = '#C71585', + midnightblue = '#191970', + mintcream = '#F5FFFA', + mistyrose = '#FFE4E1', + moccasin = '#FFE4B5', + navajowhite = '#FFDEAD', + navy = '#000080', + oldlace = '#FDF5E6', + olive = '#808000', + olivedrab = '#6B8E23', + orange = '#FFA500', + orangered = '#FF4500', + orchid = '#DA70D6', + palegoldenrod = '#EEE8AA', + palegreen = '#98FB98', + paleturquoise = '#AFEEEE', + palevioletred = '#DB7093', + papayawhip = '#FFEFD5', + peachpuff = '#FFDAB9', + peru = '#CD853F', + pink = '#FFC0CB', + plum = '#DDA0DD', + powderblue = '#B0E0E6', + purple = '#800080', + rebeccapurple = '#663399', + red = '#FF0000', + rosybrown = '#BC8F8F', + royalblue = '#4169E1', + saddlebrown = '#8B4513', + salmon = '#FA8072', + sandybrown = '#F4A460', + seagreen = '#2E8B57', + seashell = '#FFF5EE', + sienna = '#A0522D', + silver = '#C0C0C0', + skyblue = '#87CEEB', + slateblue = '#6A5ACD', + slategray = '#708090', + slategrey = '#708090', + snow = '#FFFAFA', + springgreen = '#00FF7F', + steelblue = '#4682B4', + tan = '#D2B48C', + teal = '#008080', + thistle = '#D8BFD8', + tomato = '#FF6347', + turquoise = '#40E0D0', + violet = '#EE82EE', + wheat = '#F5DEB3', + white = '#FFFFFF', + whitesmoke = '#F5F5F5', + yellow = '#FFFF00', + yellowgreen = '#9ACD32' +} + +--- Check if a value is a CSS named colour. +--- Performs case-insensitive matching against the 148 standard CSS named colours. +--- @param value string|nil The colour value to check +--- @return boolean True if the value is a recognised CSS named colour +--- @usage local result = M.is_named_colour('rebeccapurple') -- returns true +function M.is_named_colour(value) + if not value then return false end + return CSS_NAMED_COLOURS[value:lower()] ~= nil +end + +-- ============================================================================ +-- COLOUR CONVERSION UTILITIES +-- ============================================================================ + +--- Expand 3-character hex colour to 6-character format. +--- @param hex string Hex colour code (either #123 or #123456 format) +--- @return string 6-character hex colour code (e.g., #123 becomes #112233) +function M.expand_hex_colour(hex) + if string.len(hex) == 4 then + return (string.gsub(hex, '#(%x)(%x)(%x)', '#%1%1%2%2%3%3')) + end + return hex +end + +--- Convert RGB colour notation to HTML hex format. +--- @param rgb string RGB colour string in format "rgb(r, g, b)" +--- @return string HTML hex colour code in uppercase format (e.g., "#FF0000") +function M.RGB_to_HTML(rgb) + local r, g, b = rgb:match('rgb%((%d+)%s*,%s*(%d+)%s*,%s*(%d+)%s*%)') + r = tonumber(r) + g = tonumber(g) + b = tonumber(b) + return string.upper(string.format('#%02x%02x%02x', r, g, b)) +end + +--- Convert RGB percentage notation to HTML hex format. +--- @param rgb string RGB colour string in format "rgb(r%, g%, b%)" +--- @return string HTML hex colour code in uppercase format (e.g., "#FF0000") +function M.RGBPercent_to_HTML(rgb) + local r, g, b = rgb:match('rgb%((%d+)%s*%%%s*,%s*(%d+)%s*%%%s*,%s*(%d+)%s*%%%s*%)') + r = math.floor(tonumber(r) * 255 / 100 + 0.5) + g = math.floor(tonumber(g) * 255 / 100 + 0.5) + b = math.floor(tonumber(b) * 255 / 100 + 0.5) + return string.upper(string.format('#%02x%02x%02x', r, g, b)) +end + +--- Helper function to convert hue to RGB component. +--- @param p number +--- @param q number +--- @param t number +--- @return number RGB component value +function M.hue_to_rgb(p, q, t) + if t < 0 then t = t + 1 end + if t > 1 then t = t - 1 end + if t < 1 / 6 then return p + (q - p) * 6 * t end + if t < 1 / 2 then return q end + if t < 2 / 3 then return p + (q - p) * (2 / 3 - t) * 6 end + return p +end + +--- Convert HSL colour notation to HTML hex format. +--- @param hsl string HSL colour string in format "hsl(h, s%, l%)" +--- @return string HTML hex colour code in uppercase format (e.g., "#FF0000") +function M.HSL_to_HTML(hsl) + local h, s, l = hsl:match('hsl%((%d+)%s*,%s*(%d+)%%%s*,%s*(%d+)%%%s*%)') + h = tonumber(h) / 360 + s = tonumber(s) / 100 + l = tonumber(l) / 100 + + local r, g, b + if s == 0 then + r, g, b = l, l, l + else + local q = l < 0.5 and l * (1 + s) or l + s - l * s + local p = 2 * l - q + r = M.hue_to_rgb(p, q, h + 1 / 3) + g = M.hue_to_rgb(p, q, h) + b = M.hue_to_rgb(p, q, h - 1 / 3) + end + + r = math.floor(r * 255 + 0.5) + g = math.floor(g * 255 + 0.5) + b = math.floor(b * 255 + 0.5) + + return string.upper(string.format('#%02x%02x%02x', r, g, b)) +end + +--- Convert HWB colour notation to HTML hex format. +--- @param hwb string HWB colour string in format "hwb(h w% b%)" +--- @return string HTML hex colour code in uppercase format (e.g., "#FF0000") +function M.HWB_to_HTML(hwb) + local h, w, b = hwb:match('hwb%((%d+)%s+(%d+)%%%s+(%d+)%%%s*%)') + h = tonumber(h) + w = tonumber(w) / 100 + b = tonumber(b) / 100 + + local sum = w + b + if sum > 1 then + w = w / sum + b = b / sum + end + + h = h / 360 + + local r, g, b_colour + local q = 1 + local p = 0 + r = M.hue_to_rgb(p, q, h + 1 / 3) + g = M.hue_to_rgb(p, q, h) + b_colour = M.hue_to_rgb(p, q, h - 1 / 3) + + r = r * (1 - w - b) + w + g = g * (1 - w - b) + w + b_colour = b_colour * (1 - w - b) + w + + r = math.floor(r * 255 + 0.5) + g = math.floor(g * 255 + 0.5) + b_colour = math.floor(b_colour * 255 + 0.5) + + return string.upper(string.format('#%02x%02x%02x', r, g, b_colour)) +end + +--- Convert a CSS named colour to HTML hex format. +--- @param name string CSS colour name (case-insensitive) +--- @return string HTML hex colour code in uppercase format (e.g., "#FF0000") +function M.named_to_HTML(name) + local hex = CSS_NAMED_COLOURS[name:lower()] + if not hex then + error('Unknown CSS named colour: ' .. tostring(name)) + end + return hex +end + +--- Convert a colour code to HTML format based on its format type. +--- @param code string The colour code to convert +--- @param format string The colour format (e.g., "hwb", "hsl", "rgb", "rgb_percent", "hex", "named") +--- @return string HTML hex colour code +function M.to_html(code, format) + local converters = { + hwb = M.HWB_to_HTML, + hsl = M.HSL_to_HTML, + rgb = M.RGB_to_HTML, + rgb_percent = M.RGBPercent_to_HTML, + hex = M.expand_hex_colour, + hex3 = M.expand_hex_colour, + hex6 = M.expand_hex_colour, + named = M.named_to_HTML + } + + local converter = converters[format] + if converter then + return converter(code) + else + error('Unsupported colour format: ' .. format) + end +end + +-- ============================================================================ +-- COLOUR ATTRIBUTE UTILITIES +-- ============================================================================ + +--- Get colour value from attributes table, accepting both British and American spellings. +--- Checks for 'colour' first (British, primary), then falls back to 'color' (American). +--- @param attrs table Attributes table (kwargs or element.attributes) +--- @param default string|nil Default value if neither spelling is found +--- @return string|nil Colour value or default +--- @usage local colour = M.get_colour(kwargs, 'info') +function M.get_colour(attrs, default) + if attrs == nil then + return default + end + local value = attrs.colour or attrs.color + if value == nil then + return default + end + local str_value = pandoc.utils.stringify(value) + if str_value == '' then + return default + end + return str_value +end + +--- Check if a colour value is a custom colour (hex, rgb, hsl, hwb, etc.). +--- Used to determine whether to apply a semantic class or inline style. +--- Named CSS colours are not custom colours; they are standard/semantic values. +--- @param colour string|nil The colour value to check +--- @return boolean True if it's a custom colour value +--- @usage local is_custom = M.is_custom_colour('#ff6600') -- returns true +--- @usage local is_custom = M.is_custom_colour('red') -- returns false +function M.is_custom_colour(colour) + if not colour then return false end + if M.is_named_colour(colour) then return false end + local lower = colour:lower() + return lower:match('^#') or lower:match('^rgb') or lower:match('^hsl') or lower:match('^hwb') +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/git.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/git.lua new file mode 100644 index 0000000..d400d95 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/git.lua @@ -0,0 +1,50 @@ +--- MC Git - Git repository utilities for Quarto Lua filters and shortcodes +--- @module "git" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +-- ============================================================================ +-- GIT REPOSITORY UTILITIES +-- ============================================================================ + +--- Check if a string is empty or nil +--- @param s string|nil The string to check +--- @return boolean true if the string is nil or empty +local function is_empty(s) + return s == nil or s == '' +end + +--- Get repository name from git remote origin URL +--- Executes shell command to extract repository name from git remote +--- @return string|nil The repository name (e.g., "owner/repo") or nil if not found +--- @usage local repo = M.get_repository() +function M.get_repository() + local is_windows = package.config:sub(1, 1) == '\\' + local remote_repository_command + + if is_windows then + remote_repository_command = "(git remote get-url origin) -replace '.*[:/](.+?)(\\.git)?$', '$1'" + else + remote_repository_command = "git remote get-url origin 2>/dev/null | sed -E 's|.*[:/]([^/]+/[^/.]+)(\\.git)?$|\\1|'" + end + + local handle = io.popen(remote_repository_command) + if handle then + local git_repo = handle:read('*a'):gsub('%s+$', '') + handle:close() + if not is_empty(git_repo) then + return git_repo + end + end + + return nil +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/html.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/html.lua new file mode 100644 index 0000000..d947a3c --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/html.lua @@ -0,0 +1,108 @@ +--- MC HTML - HTML generation and dependency management for Quarto Lua filters and shortcodes +--- @module "html" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +--- Load a sibling module from the same directory as this file. +--- @param filename string The sibling module filename (e.g., 'string.lua') +--- @return table The loaded module +local function load_sibling(filename) + local name = filename:gsub('%.lua$', '') + if not (debug and debug.getinfo) then + -- q2 has no `debug` library, and its `require` looks in the directory of + -- the module being loaded first. + return require(name) + end + local source = debug.getinfo(1, 'S').source:sub(2) + local dir = source:match('(.*[/\\])') or '' + return require(dir .. name) +end + +--- Load string module for escape_html and escape_attribute +local str = load_sibling('string.lua') + +-- ============================================================================ +-- HTML RAW GENERATION UTILITIES +-- ============================================================================ + +--- Generates a raw HTML header element. +--- @param level integer The header level (e.g., 2 for

) +--- @param text string|nil The header text +--- @param id string The id attribute for the header +--- @param classes table List of classes for the header +--- @param attributes table|nil Additional HTML attributes +--- @return string Raw HTML string for the header +function M.raw_header(level, text, id, classes, attributes) + local attr_str = '' + if id and id ~= '' then + attr_str = attr_str .. ' id="' .. str.escape_attribute(id) .. '"' + end + if classes and #classes > 0 then + local escaped_classes = {} + for i, cls in ipairs(classes) do + escaped_classes[i] = str.escape_attribute(cls) + end + attr_str = attr_str .. ' class="' .. table.concat(escaped_classes, ' ') .. '"' + end + if attributes then + for k, v in pairs(attributes) do + attr_str = attr_str .. ' ' .. str.escape_attribute(k) .. '="' .. str.escape_attribute(v) .. '"' + end + end + return string.format('%s', level, attr_str, str.escape_html(text or ''), level) +end + +-- ============================================================================ +-- HTML DEPENDENCY UTILITIES +-- ============================================================================ + +--- Managed HTML dependency tracker +--- Tracks which dependencies have been added to prevent duplication +--- @type table +local dependency_tracker = {} + +--- Ensure HTML dependency is added only once per document. +--- Prevents duplicate dependency injection by tracking dependencies by name. +--- Returns true if dependency was added, false if already present. +--- +--- @param config table Dependency configuration with fields: name (required), version, scripts, stylesheets, head +--- @return boolean True if dependency was added, false if already added +--- @usage M.ensure_html_dependency({name = 'my-lib', version = '1.0.0', scripts = {'lib.js'}}) +function M.ensure_html_dependency(config) + if not config or not config.name then + error("HTML dependency configuration must include a 'name' field") + end + + --- @type string Unique key for this dependency + local dep_key = config.name + + -- Check if already added + if dependency_tracker[dep_key] then + return false + end + + -- Add the dependency + quarto.doc.add_html_dependency(config) + + -- Mark as added + dependency_tracker[dep_key] = true + return true +end + +--- Reset dependency tracker. +--- Useful for testing or when processing multiple independent documents. +--- In normal usage, this should not be called as dependencies persist per document. +--- +--- @return nil +function M.reset_dependencies() + dependency_tracker = {} +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/logging.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/logging.lua new file mode 100644 index 0000000..81fba55 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/logging.lua @@ -0,0 +1,61 @@ +--- MC Logging - Formatted log output for Quarto Lua filters and shortcodes +--- @module "logging" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +-- ============================================================================ +-- LOGGING UTILITIES +-- ============================================================================ + +--- Format and log an error message with extension prefix. +--- Provides standardised error messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "external", "lua-env") +--- @param message string The error message to display +--- @usage M.log_error("external", "Could not open file 'example.md'.") +function M.log_error(extension_name, message) + quarto.log.error('[' .. extension_name .. '] ' .. message) +end + +--- Format and log a warning message with extension prefix. +--- Provides standardised warning messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "external", "lua-env") +--- @param message string The warning message to display +--- @usage M.log_warning("lua-env", "No variable name provided.") +function M.log_warning(extension_name, message) + quarto.log.warning('[' .. extension_name .. '] ' .. message) +end + +--- Format and log an output message with extension prefix. +--- Provides standardised informational messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "lua-env") +--- @param message string The informational message to display +--- @usage M.log_output("lua-env", "Exported metadata to: output.json") +function M.log_output(extension_name, message) + quarto.log.output('[' .. extension_name .. '] ' .. message) +end + +--- Format and log a debug message with extension prefix. +--- Provides standardised debug messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "lua-env") +--- @param message string The debug message to display +--- @usage M.log_debug("lua-env", "Variable 'x' has value: 42") +function M.log_debug(extension_name, message) + quarto.log.debug('[' .. extension_name .. '] ' .. message) +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/metadata.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/metadata.lua new file mode 100644 index 0000000..fc5b9ac --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/metadata.lua @@ -0,0 +1,262 @@ +--- MC Metadata - Extension configuration and metadata access for Quarto Lua filters and shortcodes +--- @module "metadata" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +--- Load a sibling module from the same directory as this file. +--- @param filename string The sibling module filename (e.g., 'string.lua') +--- @return table The loaded module +local function load_sibling(filename) + local name = filename:gsub('%.lua$', '') + if not (debug and debug.getinfo) then + -- q2 has no `debug` library, and its `require` looks in the directory of + -- the module being loaded first. + return require(name) + end + local source = debug.getinfo(1, 'S').source:sub(2) + local dir = source:match('(.*[/\\])') or '' + return require(dir .. name) +end + +--- Load required modules +local str = load_sibling('string.lua') +local log = load_sibling('logging.lua') + +-- ============================================================================ +-- METADATA UTILITIES +-- ============================================================================ + +--- Get configuration from extensions.name namespace. +--- @param meta table Document metadata +--- @param extension_name string The extension name (e.g., "github", "iconify") +--- @return any The value/table or nil +--- @usage local value = M.get_extension_config(meta, 'section-outline') +function M.get_extension_config(meta, extension_name) + local config_ext = meta.extensions and meta.extensions[extension_name] + if not config_ext then return nil end + return config_ext +end + +--- Extract metadata value from document meta using nested structure. +--- Supports the extensions.{extension-name}.{key} pattern. +--- @param meta table The document metadata table +--- @param extension_name string The extension name (e.g., "github", "iconify") +--- @param key string The metadata key to retrieve +--- @return string|nil The metadata value as a string, or nil if not found +--- @usage local repo = M.get_metadata_value(meta, "github", "repository-name") +--- @note A boolean `false` is a value, not an absence, so the test is against +--- `nil` rather than truthiness. Stringifying it yields "false", which is what +--- a caller comparing against the string form already expects. +function M.get_metadata_value(meta, extension_name, key) + if meta['extensions'] and meta['extensions'][extension_name] + and meta['extensions'][extension_name][key] ~= nil then + return str.stringify(meta['extensions'][extension_name][key]) + end + return nil +end + +--- Check for deprecated top-level configuration and emit warning +--- @param meta table The document metadata table +--- @param extension_name string The extension name +--- @param key string|nil The configuration key being accessed (nil to check entire extension config) +--- @param deprecation_warning_shown boolean Flag to track if warning has been shown +--- @return any|nil The value from deprecated config, or nil if not found +--- @return boolean Updated deprecation warning flag +function M.check_deprecated_config(meta, extension_name, key, deprecation_warning_shown) + -- Handle array-based configuration (when key is nil) + if key == nil then + if not str.is_empty(meta[extension_name]) then + if not deprecation_warning_shown then + log.log_warning( + extension_name, + 'Top-level "' .. extension_name .. '" configuration is deprecated. ' .. + 'Please use:\n' .. + 'extensions:\n' .. + ' ' .. extension_name .. ':\n' .. + ' - (configuration array)' + ) + deprecation_warning_shown = true + end + return meta[extension_name], deprecation_warning_shown + end + return nil, deprecation_warning_shown + end + + -- Handle key-value configuration (original behaviour) + if not str.is_empty(meta[extension_name]) and not str.is_empty(meta[extension_name][key]) then + if not deprecation_warning_shown then + log.log_warning( + extension_name, + 'Top-level "' .. extension_name .. '" configuration is deprecated. ' .. + 'Please use:\n' .. + 'extensions:\n' .. + ' ' .. extension_name .. ':\n' .. + ' ' .. key .. ': value' + ) + deprecation_warning_shown = true + end + return str.stringify(meta[extension_name][key]), deprecation_warning_shown + end + return nil, deprecation_warning_shown +end + +-- ============================================================================ +-- ENHANCED METADATA/CONFIGURATION UTILITIES +-- ============================================================================ + +--- Get option value with fallback hierarchy: args -> extensions.{extension}.{key} -> defaults. +--- Provides a standardised way to read configuration values with multiple fallback levels. +--- Priority: 1. Named arguments (kwargs), 2. Document metadata, 3. Default values. +--- +--- @param spec table Configuration spec with fields: extension (string), key (string), args (table|nil), meta (table|nil), default (any|nil) +--- @return any The resolved option value (type depends on what's stored in config) +--- @usage local duration = M.get_option_with_fallbacks({extension = 'animate', key = 'duration', args = kwargs, meta = meta, default = '3s'}) +function M.get_option_with_fallbacks(spec) + -- Validate required fields + if not spec.extension or not spec.key then + error("Configuration spec must include 'extension' and 'key' fields") + end + + --- @type string The extension name + local extension = spec.extension + --- @type string The configuration key + local key = spec.key + --- @type table|nil Named arguments table + local args = spec.args + --- @type table|nil Document metadata + local meta = spec.meta + --- @type any Default value if not found elsewhere + local default = spec.default + + -- Priority 1: Check named arguments (kwargs) + if args and args[key] then + local arg_value = str.stringify(args[key]) + if not str.is_empty(arg_value) then + return arg_value + end + end + + -- Priority 2: Check metadata extensions.{extension}.{key} + if meta then + local meta_value = M.get_metadata_value(meta, extension, key) + if not str.is_empty(meta_value) then + return meta_value + end + end + + -- Priority 3: Return default value + return default +end + +--- Get multiple option values at once with fallback hierarchy. +--- Batch version of get_option_with_fallbacks for retrieving multiple configuration values. +--- Returns a table mapping each key to its resolved value. +--- +--- @param spec table Configuration spec with fields: extension (string), keys (table), args (table|nil), meta (table|nil), defaults (table|nil) +--- @return table Table mapping each key to its resolved value +--- @usage local opts = M.get_options({extension = 'animate', keys = {'duration', 'delay'}, args = kwargs, meta = meta, defaults = {duration = '3s', delay = '2s'}}) +function M.get_options(spec) + -- Validate required fields + if not spec.extension or not spec.keys then + error("Configuration spec must include 'extension' and 'keys' fields") + end + + --- @type table Result table + local result = {} + + --- @type table Default values table + local defaults = spec.defaults or {} + + -- Get each key using the single-option fallback logic + for _, key in ipairs(spec.keys) do + result[key] = M.get_option_with_fallbacks({ + extension = spec.extension, + key = key, + args = spec.args, + meta = spec.meta, + default = defaults[key] + }) + end + + return result +end + +-- ============================================================================ +-- PROJECT METADATA UTILITIES +-- ============================================================================ + +--- Get project-level metadata from Quarto via QUARTO_EXECUTE_INFO. +--- Reads and decodes the JSON file pointed to by the QUARTO_EXECUTE_INFO +--- environment variable and returns its format.metadata table, which holds +--- the website and book configuration that document metadata lacks. +--- The file is read on every call; pass the result on rather than calling twice. +--- @return table|nil format_meta The format.metadata table, or nil if unavailable +--- @return string|nil reason Why the file could not be used when +--- QUARTO_EXECUTE_INFO is set; nil when it is unset or empty. +--- The reasons are a stable API: callers can compare against them, and a +--- change to any of them is a breaking change. They are exactly +--- "file is unreadable", "file is empty", "invalid JSON" and +--- "no format.metadata table". +--- @return string|nil path The QUARTO_EXECUTE_INFO path, with every reason +--- @usage local format_meta, reason, path = M.get_project_metadata() +--- @note quarto-cli's decoder raises on bad JSON, while q2's (pandoc.json) +--- returns nil, so a nil result is also reported as invalid JSON. +function M.get_project_metadata() + local path = os.getenv("QUARTO_EXECUTE_INFO") + if str.is_empty(path) then return nil end + + local file = io.open(path, "r") + if not file then return nil, "file is unreadable", path end + + local content = file:read("*a") + file:close() + + if str.is_empty(content) then return nil, "file is empty", path end + + local ok, info = pcall(quarto.json.decode, content) + if not ok or info == nil then return nil, "invalid JSON", path end + + if type(info) ~= "table" or type(info["format"]) ~= "table" + or type(info["format"]["metadata"]) ~= "table" then + return nil, "no format.metadata table", path + end + + return info["format"]["metadata"] +end + +--- Get repo-url from Quarto project metadata via QUARTO_EXECUTE_INFO. +--- Extracts repo-url from website or book metadata, website first. +--- The QUARTO_EXECUTE_INFO file is read only when no table is given. +--- @param format_meta table|nil The project format.metadata table, as returned +--- by get_project_metadata; read from QUARTO_EXECUTE_INFO when nil +--- @return string|nil The repo-url value, or nil if not available +--- @usage local repo_url = M.get_project_repo_url(format_meta) +function M.get_project_repo_url(format_meta) + if format_meta == nil then + format_meta = M.get_project_metadata() + if not format_meta then return nil end + elseif type(format_meta) ~= "table" then + error("get_project_repo_url: format_meta must be a table or nil, got " .. type(format_meta), 2) + end + + local website = format_meta["website"] + local book = format_meta["book"] + local repo_url + if type(website) == "table" and website["repo-url"] then + repo_url = website["repo-url"] + elseif type(book) == "table" and book["repo-url"] then + repo_url = book["repo-url"] + end + + if str.is_empty(repo_url) then return nil end + return repo_url +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/paths.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/paths.lua new file mode 100644 index 0000000..3012c7b --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/paths.lua @@ -0,0 +1,112 @@ +--- MC Paths - Path resolution and file type checks for Quarto Lua filters and shortcodes +--- @module "paths" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +--- Load a sibling module from the same directory as this file. +--- @param filename string The sibling module filename (e.g., 'string.lua') +--- @return table The loaded module +local function load_sibling(filename) + local name = filename:gsub('%.lua$', '') + if not (debug and debug.getinfo) then + -- q2 has no `debug` library, and its `require` looks in the directory of + -- the module being loaded first. + return require(name) + end + local source = debug.getinfo(1, 'S').source:sub(2) + local dir = source:match('(.*[/\\])') or '' + return require(dir .. name) +end + +--- Load string module for is_empty +local str = load_sibling('string.lua') + +-- ============================================================================ +-- PATH UTILITIES +-- ============================================================================ + +--- Resolve a path relative to the project directory. +--- If the path starts with `/`, it is treated as relative to the project directory. +--- If `quarto.project.directory` is available, it is prepended to the path. +--- If `quarto.project.directory` is nil, the leading `/` is removed. +--- @param path string The path to resolve (may start with `/`) +--- @return string The resolved path +--- @usage local resolved = M.resolve_project_path("/config.yml") +--- @usage local resolved = M.resolve_project_path("config.yml") +function M.resolve_project_path(path) + if str.is_empty(path) then + return path + end + + if path:sub(1, 1) == '/' then + if quarto.project.directory then + return pandoc.path.join({ quarto.project.directory, path:sub(2) }) + else + return path:sub(2) + end + else + return path + end +end + +-- ============================================================================ +-- FILE TYPE CHECKS +-- ============================================================================ + +--- Check if URI has one of the specified extensions. +--- Performs case-insensitive extension matching by default. +--- Extensions can be provided with or without leading dot. +--- +--- @param uri string|nil File URI to check +--- @param extensions table Array of extensions (e.g., {".md", "qmd", ".txt"}) +--- @param case_sensitive boolean|nil Whether to match case-sensitively (default: false) +--- @return boolean True if URI ends with one of the extensions, false otherwise +--- @usage local is_md = M.has_extension("file.md", {".md", ".markdown"}) -- returns true +--- @usage local is_md = M.has_extension("file.MD", {".md"}, true) -- returns false (case-sensitive) +function M.has_extension(uri, extensions, case_sensitive) + if uri == nil or uri == '' or extensions == nil then + return false + end + + --- @type string URI for matching (lowercase if case-insensitive) + local match_uri = case_sensitive and uri or uri:lower() + + for _, ext in ipairs(extensions) do + --- @type string Extension to match (ensure it starts with dot) + local match_ext = ext + if not match_ext:match('^%.') then + match_ext = '.' .. match_ext + end + + --- @type string Extension for matching (lowercase if case-insensitive) + if not case_sensitive then + match_ext = match_ext:lower() + end + + -- Check if URI ends with this extension + if match_uri:match('%' .. match_ext .. '$') then + return true + end + end + + return false +end + +--- Check if URI is a markdown file. +--- Convenience function to check for markdown extensions: .md, .markdown, .qmd +--- +--- @param uri string|nil File URI to check +--- @return boolean True if markdown file, false otherwise +--- @usage local is_markdown = M.is_markdown("doc.qmd") -- returns true +function M.is_markdown(uri) + return M.has_extension(uri, { '.md', '.markdown', '.qmd' }) +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/schema-check.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/schema-check.lua new file mode 100644 index 0000000..35d25ae --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/schema-check.lua @@ -0,0 +1,636 @@ +--- MC Schema Check - Runtime schema checks for Quarto extensions +--- @module "schema-check" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- +--- Holds the wiring that every extension would otherwise copy: read the schema +--- once, check the document configuration against it, check one shortcode call +--- against it, and report what it finds through `logging`. +--- +--- The schema is `_schema.yml` beside the entry point that runs. An extension +--- whose entry points sit in a subdirectory names its own path instead, with +--- `'../_schema.yml'` for one level down. +--- +--- The validator arrives as an argument rather than through `require`. A +--- vendored copy of this module then knows nothing about where the validator +--- was vendored, so the two sources stay independent. The validator must +--- provide `load_schema`, `validate`, `validate_shortcode`, +--- `extract_meta_options`, `validate_attributes`, `validate_format` and +--- `validate_cells`. +--- +--- The last three are newer than the rest of that list, so a validator +--- vendored before one of them exists is reported rather than called. The +--- others are required outright and a validator without one of them raises. +--- +--- `validate_format` must also be the one that reads the top level of the +--- metadata, which is Quarto Wizard 3.6.0 or newer. An older one looks for the +--- format name as a metadata key, which a document never has, so it would +--- report nothing whatever the document wrote. +--- +--- Nothing here stops a render. A schema is configuration, and a fault in the +--- configuration must not remove the document. + +local M = {} + +--- Load a sibling module from the same directory as this file. +--- @param filename string The sibling module filename (e.g., 'string.lua') +--- @return table The loaded module +local function load_sibling(filename) + local name = filename:gsub('%.lua$', '') + if not (debug and debug.getinfo) then + -- q2 has no `debug` library, and its `require` looks in the directory of + -- the module being loaded first. + return require(name) + end + local source = debug.getinfo(1, 'S').source:sub(2) + local dir = source:match('(.*[/\\])') or '' + return require(dir .. name) +end + +--- Load required modules +local log = load_sibling('logging.lua') +local str = load_sibling('string.lua') + +-- ============================================================================ +-- SEVERITY +-- ============================================================================ + +--- The level each kind of finding is reported at. This is the only place a +--- severity is decided, so a later change to what an extension must correct is +--- a change to this table and nothing else. +--- +--- The current policy reports and never stops a render. A finding is a warning, +--- with two exceptions. An unreadable schema is an error, because no check runs +--- after it. A missing required argument is an error, because the shortcode +--- renders nothing without it, which is the one finding that changes the +--- document. +--- +--- A rejected document option keeps the error level it has today: it names a +--- value the extension cannot use, and the author has to correct it. A rejected +--- format option is the same finding about the same document, one section over, +--- so it takes the same level. +--- +--- A finding about an element's attribute is a warning instead. The attribute +--- stays on the element whatever the schema says, so the rendered output does +--- not change because of it, which is the reason a shortcode attribute is a +--- warning too. A cell option is the same: this module reports it and hands +--- the options back, and the cell renders whatever the finding says. +--- +--- `misuse` is the one kind that is not about the document. It reports a fault +--- in the extension calling this module, and it is an error because the caller +--- is handed no value and would otherwise carry on with nil. +--- @type table +local SEVERITY = { + schema = 'error', + option_error = 'error', + option_warning = 'warning', + call_error = 'warning', + call_warning = 'warning', + missing_argument = 'error', + misuse = 'error', + attribute_error = 'warning', + attribute_warning = 'warning', + format_error = 'error', + format_warning = 'warning', + cell_error = 'warning', + cell_warning = 'warning', +} + +--- The reporting function for each level. +--- @type table +local REPORTERS = { + error = log.log_error, + warning = log.log_warning, +} + +-- A level with no reporter is the mistake a change to `SEVERITY` makes, and it +-- is caught here, at load, rather than at the one render that reaches that kind +-- of finding. +for kind, level in pairs(SEVERITY) do + assert(REPORTERS[level] ~= nil, + string.format('schema-check: "%s" maps to the unknown level "%s"', kind, tostring(level))) +end + +-- ============================================================================ +-- PRIVATE HELPERS +-- ============================================================================ + +--- Read an attribute value with a surrounding quote pair removed. +--- Quarto's body parser strips those quotes before the value reaches the +--- shortcode, but the parser it uses for a text or attribute string hands the +--- raw token over instead, so `aria-hidden='true'` arrives as the five +--- character string `'true'`. Stripping here is what makes a quoted and an +--- unquoted value check as the same thing. +--- @param kwargs table Key-value options for the call +--- @param key string The attribute name to read +--- @return string +local function attr_value(kwargs, key) + --- @type string + local value = str.stringify(kwargs[key]) + --- @type string + local quote = value:sub(1, 1) + if #value > 1 and (quote == '"' or quote == "'") and value:sub(-1) == quote then + return value:sub(2, -2) + end + return value +end + +--- Copy a value, and every table inside it, all the way down. +--- The copy exists so that a caller writing into what it received cannot +--- change what every later reader of the same checker sees. +--- +--- The depth comes from the schema format, not from the schemas written so +--- far. The vocabulary allows `type: array` and `type: object`, and the +--- validator compiles a declared `default` by coercing it against that type +--- and returning it untouched once it already matches. Any option declaring a +--- collection type with a literal default therefore resolves to a Lua table, +--- at whatever depth the schema nests it, so a copy one level deep would hand +--- two callers the same inner table and leave the fault one level down. +--- +--- A table already copied on this walk is reused rather than copied again, +--- which keeps shared structure shared and makes a cycle terminate. A schema +--- file cannot express a cycle, because the validator's parser refuses anchors +--- and aliases, but the validator is injected and what it returns is not this +--- module's to assume. +--- @param value any The value to copy +--- @param seen table|nil Tables already copied on this walk +--- @return any +local function deep_copy(value, seen) + if type(value) ~= 'table' then + return value + end + seen = seen or {} + if seen[value] ~= nil then + return seen[value] + end + --- @type table + local copy = {} + seen[value] = copy + for key, item in pairs(value) do + copy[key] = deep_copy(item, seen) + end + return copy +end + +--- Flatten a call's named options to plain strings for the validator. +--- @param kwargs table Key-value options for the call +--- @return table +local function plain_kwargs(kwargs) + --- @type table + local plain = {} + for key in pairs(kwargs) do + plain[tostring(key)] = attr_value(kwargs, key) + end + return plain +end + +-- ============================================================================ +-- CHECKER +-- ============================================================================ + +--- @class Checker +--- @field validator table The validator the caller injected +--- @field extension string The extension name every message carries +--- @field schema table|nil The parsed schema, nil when it could not be read +--- @field defaults table The defaults the schema declares +--- @field resolved table|nil The three tables the configuration resolves to +--- @field options_checked boolean Whether the configuration was already checked +--- @field meta table|nil The metadata `options` was given, which `format` reads +--- @field formats table What each format checked so far resolved to +--- @field unavailable table Validator functions already reported missing +local Checker = {} +Checker.__index = Checker + +--- Report one finding at the level its kind is mapped to. +--- A kind with no entry in `SEVERITY` is a fault in this module, and it says so +--- rather than reporting the finding at a level nobody chose. It is reported +--- rather than raised, because a fault here must not remove a document. +--- @param kind string A key of `SEVERITY` +--- @param message string The message to report +--- @return nil +function Checker:_report(kind, message) + --- @type string|nil + local level = SEVERITY[kind] + if level == nil then + log.log_error(self.extension, string.format( + 'schema-check has no severity for "%s": %s', tostring(kind), message)) + return + end + REPORTERS[level](self.extension, message) +end + +--- Report a validator's errors and warnings under one kind's pair of levels. +--- @param kind string The prefix of a `_error` and `_warning` pair in `SEVERITY` +--- @param errors string[] The validator's errors +--- @param warnings string[] The validator's warnings +--- @return nil +function Checker:_report_findings(kind, errors, warnings) + for _, message in ipairs(errors) do + self:_report(kind .. '_error', message) + end + for _, message in ipairs(warnings) do + self:_report(kind .. '_warning', message) + end +end + +--- Whether the validator provides one function, reporting it once if not. +--- +--- `validate_attributes`, `validate_format` and `validate_cells` are newer than +--- the rest of the contract, so a validator vendored before one of them exists +--- satisfies everything else and still lacks them. Calling one raises, and a raise removes the +--- document, which is the one thing this module promises not to do. +--- +--- It is reported once for the render rather than once for each caller. A +--- filter reaches these for every element and every format it handles, and the +--- condition is a fact about the vendored pair, so the second message says +--- nothing the first did not. +--- @param name string The function the caller is about to use +--- @return boolean available +function Checker:_provides(name) + if type(self.validator[name]) == 'function' then + return true + end + if not self.unavailable[name] then + self.unavailable[name] = true + self:_report('misuse', string.format( + 'schema-check: the validator provides no `%s`, so nothing was checked with it', name)) + end + return false +end + +--- Check the document configuration and return what it resolves to. The check +--- runs once, so an extension can ask on every shortcode without repeating the +--- messages. +--- +--- The defaults come first, because that is what most extensions want, and +--- because the schema is the one place they are written. They come from a +--- second pass over an empty table, which yields the declared defaults alone. +--- +--- The second return holds three tables, because they answer different +--- questions: +--- provided what the document actually set, which is the only way to tell +--- a deliberate `false` or `0` from an absent key, +--- merged the same values with coercion and defaults applied, +--- defaults the schema defaults on their own. +--- It is nil when there is nothing to resolve against, so an extension can +--- tell an unreadable schema from a document that set nothing. +--- +--- The defaults are a fresh copy on every call, so a caller may treat them as +--- its own. The tables inside the second return are the checker's, and every +--- later reader of the same checker sees them, so they must not be written to. +--- +--- Only the first call reads `meta`. A later call returns what the first one +--- resolved, whatever it is handed. A caller that needs a second document +--- checked builds a second checker. +--- +--- The argument cannot be read on a later call without losing the single check +--- this function promises. Quarto hands a shortcode a new metadata table on +--- every call, and the content is the same each time. A checker that read it +--- again repeats every finding once per shortcode. +--- @param meta table Document metadata, read on the first call only +--- @return table defaults A copy of the defaults, empty when there is no schema +--- @return table|nil resolved {provided, merged, defaults}, nil when there is no schema +function Checker:options(meta) + if self.options_checked then + return deep_copy(self.defaults), self.resolved + end + self.options_checked = true + -- Kept for `format`, which reads the same document. Quarto merges the options + -- of the selected format into the top level of this table, so the format + -- check has nowhere else to read them from. + self.meta = meta + + --- @type table|nil + local loaded = self.schema + -- The validator is injected from an independent source, so the shape of what + -- it returns is not this module's to assume. `call` makes the same allowance + -- for `shortcodes` one field over. + if loaded == nil or next(loaded.options or {}) == nil then + return deep_copy(self.defaults), self.resolved + end + + --- @type table + local provided = self.validator.extract_meta_options(meta, self.extension) + local valid, errors, warnings, merged = self.validator.validate(provided, loaded.options) + + for _, message in ipairs(warnings) do + self:_report('option_warning', message) + end + if not valid then + for _, message in ipairs(errors) do + self:_report('option_error', message) + end + end + + local _, _, _, defaults = self.validator.validate({}, loaded.options, { unknown = 'ignore' }) + self.defaults = defaults or {} + self.resolved = { provided = provided, merged = merged, defaults = self.defaults } + + return deep_copy(self.defaults), self.resolved +end + +--- Read what one option resolves to, after `options` has run. +--- +--- This is the value the schema decides, not the text the document holds. An +--- extension that reads the metadata itself has to decide what counts as true, +--- and each one that did decided something different, so `enabled: no` turned +--- one filter off and left another on. Here the schema is the only answer: the +--- validator coerces the written value toward the declared type, and a key the +--- document never set resolves to its declared default. +--- +--- It answers nil when there is no schema, which the checker has already +--- reported once. A key the schema does not declare answers nil as well, +--- because a schema that omits an option is the author's statement that the +--- extension does not have it. +--- @param key string The option name, as the schema declares it +--- @return any value The resolved value, nil when there is nothing to resolve +function Checker:option(key) + if type(key) ~= 'string' then + self:_report('misuse', string.format( + 'schema-check: the key given to `option` must be a string, got %s', type(key))) + return nil + end + if not self.options_checked then + self:_report('misuse', string.format( + 'schema-check: `option("%s")` was called before `options`', key)) + return nil + end + if self.resolved == nil then + return nil + end + return self.resolved.merged[key] +end + +--- Check one element's attributes against the `attributes` section, and return +--- what they resolve to. +--- +--- The section declares a map of groups. A group is named after the element it +--- describes, such as `Header` or `CodeBlock`, or after the class the extension +--- gives it, such as `modal`. `_any` is the group every element takes, and it +--- is additive rather than an alternative: quarto-revealjs-tabset declares +--- `panel-tabset` for a tabset's own attributes and `_any` for one that any +--- slide can carry, and an element can meet both. +--- +--- So both apply, and the named group runs last, over what `_any` resolved. The +--- validator hands an undeclared attribute straight back, so chaining the two +--- passes is the whole of the merge and there is no rule here about which group +--- wins. The one that declares the attribute decides it. +--- +--- This reports only. The attribute stays on the element whatever the schema +--- says, so nothing about the rendered output changes, which is why a finding +--- here is a warning as it is for a shortcode attribute. +--- The attributes may arrive as Pandoc's `AttributeList` rather than as a +--- table, which is what an element filter holds, so nothing here assumes a +--- plain table. The validator reads them with `pairs`, which both answer. +--- @param attributes table The element's attributes +--- @param group string|nil The element's own group, nil when it has none +--- @return table|nil resolved The attributes with the schema applied +function Checker:attributes(attributes, group) + if group ~= nil and type(group) ~= 'string' then + self:_report('misuse', string.format( + 'schema-check: the group given to `attributes` must be a string, got %s', type(group))) + return nil + end + + attributes = attributes or {} + + --- @type table|nil + local loaded = self.schema + -- The validator is injected from an independent source, so the shape of what + -- it returns is not this module's to assume, as `options` and `call` allow + -- for `options` and `shortcodes`. + if loaded == nil or next(loaded.attributes or {}) == nil then + return attributes + end + + if not self:_provides('validate_attributes') then + return attributes + end + + --- @type table + local resolved = attributes + -- `_any` is the group every element takes, so a caller that names it has + -- already asked for the only pass there is. Running the list would report + -- each of its findings twice for every element handed over. + local groups = (group == nil or group == '_any') and { '_any' } or { '_any', group } + for _, name in ipairs(groups) do + local _, errors, warnings, merged = + self.validator.validate_attributes(resolved, name, loaded) + self:_report_findings('attribute', errors, warnings) + resolved = merged + end + return resolved +end + +--- Check one code cell's comment-pipe options against the `cells` section, and +--- return what they resolve to. +--- +--- The section is keyed by cell language, so an extension that handles `typst` +--- cells checks those cells and nothing else. Checking them under `attributes` +--- would also match a `{.typst}` block that only displays code, which carries +--- the same class and none of the same options. +--- +--- The validator matches the language and leaves Quarto's own options, such as +--- `label` and `echo`, alone, because they share the same `#|` and `//|` +--- lines. A language the schema does not declare hands the options back. +--- +--- This reports only, as `attributes` does. The cell renders whatever the +--- finding says, so a finding here is a warning. +--- @param options table The cell's comment-pipe options +--- @param language string The cell language, as the schema declares it +--- @return table|nil resolved The options with the schema applied +function Checker:cells(options, language) + if type(language) ~= 'string' then + self:_report('misuse', string.format( + 'schema-check: the language given to `cells` must be a string, got %s', type(language))) + return nil + end + + options = options or {} + + --- @type table|nil + local loaded = self.schema + if loaded == nil or next(loaded.cells or {}) == nil then + return options + end + if not self:_provides('validate_cells') then + return options + end + + local _, errors, warnings, merged = + self.validator.validate_cells(options, language, loaded) + self:_report_findings('cell', errors, warnings) + return merged +end + +--- Check one output format's options against the `formats` section, and return +--- what they resolve to. +--- +--- Quarto merges the options of the selected format into the top level of the +--- document metadata, and the format name is never a key there. So this reads +--- the metadata `options` was given, and it must be called after `options`. +--- +--- The extension names its own format, as it names an attribute group. A format +--- name such as `letter-pdf` is a name the extension contributes, and nothing +--- here can work out which of several declared formats a render selected. +--- +--- The answer is kept, so a filter that asks again in the same render gets the +--- same table and the findings are reported once. +--- @param name string The format name, as the schema declares it +--- @return table|nil resolved The format's options, nil when there is nothing to resolve +function Checker:format(name) + if type(name) ~= 'string' then + self:_report('misuse', string.format( + 'schema-check: the name given to `format` must be a string, got %s', type(name))) + return nil + end + if not self.options_checked then + self:_report('misuse', string.format( + 'schema-check: `format("%s")` was called before `options`', name)) + return nil + end + if self.formats[name] ~= nil then + return self.formats[name] + end + + --- @type table|nil + local loaded = self.schema + if loaded == nil or next(loaded.formats or {}) == nil then + return nil + end + if not self:_provides('validate_format') then + return nil + end + + local _, errors, warnings, merged = + self.validator.validate_format(self.meta, name, loaded) + self:_report_findings('format', errors, warnings) + + self.formats[name] = merged + return merged +end + +--- Check one shortcode call against its entry in the schema. +--- This reports only. Nothing about the rendered output changes, so an +--- unrecognised attribute is surfaced rather than dropped. +--- @param name string Shortcode name +--- @param args table Positional arguments +--- @param kwargs table Key-value options for the call +--- @return nil +function Checker:call(name, args, kwargs) + --- @type table|nil + local loaded = self.schema + if loaded == nil then return end + + --- @type table|nil + local entry = loaded.shortcodes and loaded.shortcodes[name] + if entry == nil then return end + + args = args or {} + kwargs = kwargs or {} + + --- @type table + local positional = {} + for index, value in ipairs(args) do + positional[index] = str.stringify(value) + end + + local _, errors, warnings = self.validator.validate_shortcode( + name, positional, plain_kwargs(kwargs), entry) + + for _, message in ipairs(warnings) do + self:_report('call_warning', message) + end + + -- A required argument that is absent is read here rather than out of the + -- validator's findings, because the validator reports a nested argument + -- fault under one `arguments` entry, which cannot tell a missing argument + -- from a malformed one. + --- @type table + local missing = {} + for index, argument in ipairs(entry.arguments or {}) do + if argument.required == true and str.is_empty(positional[index]) then + missing[#missing + 1] = argument + end + end + + if #missing > 0 then + -- The only message about the missing argument: the schema's own `required` + -- wording says the same thing, and reporting both would state one fault + -- twice at two severities. Attribute warnings above still stand, and every + -- other finding about this call is secondary to there being no output. + for _, argument in ipairs(missing) do + --- The example comes from the schema, so that each shortcode carries its + --- own rather than this message naming one shortcode for all of them. + --- @type string + local advice = '' + local example = type(argument.examples) == 'table' and argument.examples[1] or nil + if example ~= nil then + advice = string.format(' For example: {{< %s %s >}}.', name, tostring(example)) + end + self:_report('missing_argument', string.format( + 'The "%s" shortcode needs its "%s" argument.%s', name, argument.name, advice)) + end + return + end + + for _, message in ipairs(errors) do + self:_report('call_error', message) + end +end + +-- ============================================================================ +-- PUBLIC API +-- ============================================================================ + +--- Build a checker for one extension, reading its schema once. +--- A schema that cannot be read is reported, and the checker it returns does +--- nothing: `options` gives an empty table and `call` gives no message. +--- +--- The path is resolved with `quarto.utils.resolve_path`, which answers +--- relative to the entry point that is running rather than to the extension +--- directory. An entry point in a subdirectory therefore has to say where the +--- schema is, and the checker it builds belongs at file scope, so that the +--- schema is read once for the render and not once for each call. +--- @param validator table The validator, with `load_schema`, `validate`, +--- `validate_shortcode`, `extract_meta_options`, `validate_attributes`, +--- `validate_format` and `validate_cells` +--- @param extension_name string The extension name every message carries +--- @param schema_path string|nil The schema to read, relative to the entry +--- point that is running. Defaults to `_schema.yml`. +--- @return Checker +--- @usage local checker = M.new(validator, 'iconify', '../_schema.yml') +function M.new(validator, extension_name, schema_path) + --- @type Checker + local checker = setmetatable({ + validator = validator, + extension = extension_name, + schema = nil, + defaults = {}, + resolved = nil, + options_checked = false, + meta = nil, + formats = {}, + unavailable = {}, + }, Checker) + + -- The default is chosen here rather than in the signature, so a caller that + -- passes two arguments reads the same file it read before this argument + -- existed. + local loaded, err = validator.load_schema( + quarto.utils.resolve_path(schema_path or '_schema.yml')) + if err then + checker:_report('schema', err) + else + checker.schema = loaded + end + + return checker +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/string.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/string.lua new file mode 100644 index 0000000..e551066 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-lua-modules/string.lua @@ -0,0 +1,376 @@ +--- MC String - String manipulation and escaping for Quarto Lua filters and shortcodes +--- @module "string" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +local M = {} + +-- ============================================================================ +-- STRING UTILITIES +-- ============================================================================ + +--- Pandoc utility function for converting values to strings +--- @type function +M.stringify = pandoc.utils.stringify + +--- Check if a string is empty or nil. +--- Utility function to determine if a value is empty or nil, +--- which is useful for parameter validation throughout the module. +--- @param s string|nil|table The value to check for emptiness +--- @return boolean True if the value is nil or empty, false otherwise +--- @usage local result = M.is_empty("") -- returns true +--- @usage local result = M.is_empty(nil) -- returns true +--- @usage local result = M.is_empty("hello") -- returns false +function M.is_empty(s) + return s == nil or s == '' +end + +--- Escape special pattern characters in a string for Lua pattern matching +--- @param s string The string to escape +--- @return string The escaped string +--- @usage local escaped = M.escape_pattern("user/repo#123") +function M.escape_pattern(s) + local escaped = s:gsub('([%^%$%(%)%%%.%[%]%*%+%-%?])', '%%%1') + return escaped +end + +--- Split a string by a separator +--- @param str string The string to split +--- @param sep string The separator pattern +--- @return table Array of string fields +--- @usage local parts = M.split("a.b.c", ".") +function M.split(str, sep) + local fields = {} + local pattern = string.format('([^%s]+)', sep) + str:gsub(pattern, function(c) fields[#fields + 1] = c end) + return fields +end + +--- Trim leading and trailing whitespace from a string +--- @param str string The string to trim +--- @return string The trimmed string +--- @usage local trimmed = M.trim(" hello world ") -- returns "hello world" +function M.trim(str) + if str == nil then return '' end + return str:match('^%s*(.-)%s*$') +end + +--- Convert any value to a string, handling Pandoc objects and empty values. +--- Returns nil for empty or nil values, otherwise returns a string representation. +--- @param val any The value to convert +--- @return string|nil The string value or nil if empty +--- @usage local str = M.to_string(kwargs.value) +function M.to_string(val) + if not val then return nil end + if type(val) == 'string' then + return val ~= '' and val or nil + end + -- Handle Pandoc objects + if pandoc and pandoc.utils and pandoc.utils.stringify then + local str = pandoc.utils.stringify(val) + return str ~= '' and str or nil + end + local str = tostring(val) + return str ~= '' and str or nil +end + +--- Strip one layer of surrounding bracket or punctuation characters. +--- Handles balanced pairs: () [] {} "" '' `` «» +--- Handles trailing-only punctuation: , . ; : ! ? +--- @param text string The input text +--- @return string prefix Characters stripped from the start (may be empty) +--- @return string inner The inner text after stripping +--- @return string suffix Characters stripped from the end (may be empty) +function M.strip_surrounding(text) + if not text or #text < 2 then + return '', text or '', '' + end + + local balanced = { + ['('] = ')', ['['] = ']', ['{'] = '}', + ['"'] = '"', ["'"] = "'", ['`'] = '`', + } + -- UTF-8 guillemets + local first_two = text:sub(1, 2) + local last_two = text:sub(-2) + if first_two == '\xC2\xAB' and last_two == '\xC2\xBB' then + return first_two, text:sub(3, -3), last_two + end + + local first = text:sub(1, 1) + local last = text:sub(-1) + + if balanced[first] and last == balanced[first] then + return first, text:sub(2, -2), last + end + + local trailing = { + [','] = true, ['.'] = true, [';'] = true, + [':'] = true, ['!'] = true, ['?'] = true, + } + if trailing[last] then + return '', text:sub(1, -2), last + end + + return '', text, '' +end + +--- Peel unbalanced surrounding brackets and trailing punctuation from a token. +--- Unlike `strip_surrounding`, this does not require a balanced pair: it removes +--- any run of leading opening-bracket characters and any run of trailing +--- closing-bracket or punctuation characters. This handles bracket groups that +--- Pandoc split across whitespace, e.g. "(#2," and "#3)" from "(#2, #3)". +--- Leading set: ( [ { " ' ` and the 2-byte UTF-8 « (\xC2\xAB). +--- Trailing set: ) ] } " ' ` , . ; : ! ? and the 2-byte UTF-8 » (\xC2\xBB). +--- @param text string The input text +--- @return string prefix Characters peeled from the start (may be empty) +--- @return string inner The inner text after peeling +--- @return string suffix Characters peeled from the end (may be empty) +function M.strip_edges(text) + if not text or text == '' then + return '', text or '', '' + end + + local leading = { + ['('] = true, ['['] = true, ['{'] = true, + ['"'] = true, ["'"] = true, ['`'] = true, + } + local trailing = { + [')'] = true, [']'] = true, ['}'] = true, + ['"'] = true, ["'"] = true, ['`'] = true, + [','] = true, ['.'] = true, [';'] = true, + [':'] = true, ['!'] = true, ['?'] = true, + } + + local first = 1 + local last = #text + local prefix = '' + local suffix = '' + + while first <= last do + if text:sub(first, first + 1) == '\xC2\xAB' then + prefix = prefix .. '\xC2\xAB' + first = first + 2 + elseif leading[text:sub(first, first)] then + prefix = prefix .. text:sub(first, first) + first = first + 1 + else + break + end + end + + while last >= first do + -- The two-byte window can only match a real «/» pair: the leading loop + -- never leaves \xC2 at first - 1 (openers are ASCII or the \xAB of a peeled + -- «), so the guillemet check cannot straddle the already-peeled prefix. + if last >= 2 and text:sub(last - 1, last) == '\xC2\xBB' then + suffix = '\xC2\xBB' .. suffix + last = last - 2 + elseif trailing[text:sub(last, last)] then + suffix = text:sub(last, last) .. suffix + last = last - 1 + else + break + end + end + + return prefix, text:sub(first, last), suffix +end + +--- Find a balanced bracket pair anywhere in the text and split around it. +--- Walks the text from `start_pos` looking for an opening bracket whose matching +--- closing bracket appears later in the string. Returns the text split into +--- a prefix (up to and including the opening bracket), the inner content, and +--- a suffix (closing bracket and everything after). +--- Supports the same bracket pairs as `strip_surrounding`: +--- () [] {} "" '' `` and the 2-byte UTF-8 guillemets «». +--- @param text string The input text +--- @param start_pos integer|nil Byte position to start searching from (default 1) +--- @return string|nil prefix Text up to and including the opening bracket +--- @return string|nil content Non-empty text between the brackets +--- @return string|nil suffix Closing bracket and trailing text +--- @return integer|nil open_pos Byte position of the opening bracket +function M.find_bracketed_content(text, start_pos) + if not text or #text < 2 then + return nil, nil, nil, nil + end + start_pos = start_pos or 1 + + local balanced = { + ['('] = ')', ['['] = ']', ['{'] = '}', + ['"'] = '"', ["'"] = "'", ['`'] = '`', + } + + local i = start_pos + while i <= #text do + -- UTF-8 guillemet «…» + if text:sub(i, i + 1) == '\xC2\xAB' then + local close_pos = text:find('\xC2\xBB', i + 2, true) + if close_pos and close_pos > i + 2 then + return text:sub(1, i + 1), text:sub(i + 2, close_pos - 1), text:sub(close_pos), i + end + i = i + 2 + else + local c = text:sub(i, i) + local close_char = balanced[c] + if close_char then + local close_pos = text:find(close_char, i + 1, true) + if close_pos and close_pos > i + 1 then + return text:sub(1, i), text:sub(i + 1, close_pos - 1), text:sub(close_pos), i + end + end + i = i + 1 + end + end + + return nil, nil, nil, nil +end + +-- ============================================================================ +-- ESCAPE UTILITIES +-- ============================================================================ + +--- Escape special LaTeX characters in text. +--- @param text string The text to escape +--- @return string The escaped text safe for LaTeX +function M.escape_latex(text) + text = string.gsub(text, '\\', '\\textbackslash{}') + text = string.gsub(text, '%{', '\\{') + text = string.gsub(text, '%}', '\\}') + text = string.gsub(text, '%$', '\\$') + text = string.gsub(text, '%&', '\\&') + text = string.gsub(text, '%%', '\\%%') + text = string.gsub(text, '%#', '\\#') + text = string.gsub(text, '%^', '\\textasciicircum{}') + text = string.gsub(text, '%_', '\\_') + text = string.gsub(text, '~', '\\textasciitilde{}') + return text +end + +--- Escape special Typst characters in text. +--- @param text string The text to escape +--- @return string The escaped text safe for Typst +function M.escape_typst(text) + text = string.gsub(text, '%#', '\\#') + return text +end + +--- Escape characters for Typst string literals (inside `"..."`). +--- Handles backslash, double quote, newline, carriage return, and tab. +--- @param text string The text to escape +--- @return string The escaped text safe for Typst string literals +function M.escape_typst_string(text) + return (text + :gsub('\\', '\\\\') + :gsub('"', '\\"') + :gsub('\n', '\\n') + :gsub('\r', '\\r') + :gsub('\t', '\\t')) +end + +--- Escape characters for JavaScript string literals (inside `"..."` or `'...'`). +--- Handles backslash, both quote styles, newlines, carriage returns, tabs, +--- form feeds, and the `` block. +--- @param text string|nil The text to escape +--- @return string The escaped text safe for JavaScript string literals +--- @usage local safe = M.escape_js_string([[a "b" ]]) +function M.escape_js_string(text) + if text == nil then return '' end + if type(text) ~= 'string' then text = tostring(text) end + return (text + :gsub('\\', '\\\\') + :gsub('"', '\\"') + :gsub("'", "\\'") + :gsub('\n', '\\n') + :gsub('\r', '\\r') + :gsub('\t', '\\t') + :gsub('\f', '\\f') + :gsub(', ", and ' to prevent XSS and ensure valid HTML. +--- @param text string The text to escape +--- @return string Escaped text safe for use in HTML +--- @usage local escaped = M.escape_html('Hello ') +function M.escape_html(text) + if text == nil then return '' end + if type(text) ~= 'string' then text = tostring(text) end + local result = text + :gsub('&', '&') + :gsub('<', '<') + :gsub('>', '>') + :gsub('"', '"') + :gsub("'", ''') + return result +end + +--- Escape special HTML attribute characters. +--- Escapes characters that could break attribute values. +--- @param value string The attribute value to escape +--- @return string Escaped value safe for use in HTML attributes +--- @usage local escaped = M.escape_attribute('Hello "World"') +function M.escape_attribute(value) + if value == nil then return '' end + if type(value) ~= 'string' then value = tostring(value) end + local result = value + :gsub('&', '&') + :gsub('"', '"') + :gsub('<', '<') + :gsub('>', '>') + return result +end + +--- Escape text for different formats. +--- @param text string The text to escape +--- @param format string The format to escape for (e.g., "latex", "typst", "lua") +--- @return string The escaped text +function M.escape_text(text, format) + local escape_functions = { + latex = M.escape_latex, + typst = M.escape_typst, + lua = M.escape_lua_pattern + } + + local escape = escape_functions[format] + if escape then + return escape(text) + else + error('Unsupported escape format: ' .. format) + end +end + +--- Converts a string to a valid HTML id by lowercasing and replacing spaces. +--- @param text string The text to convert +--- @return string The HTML id +function M.ascii_id(text) + local id = text:lower():gsub('[^a-z0-9 ]', ''):gsub(' +', '-') + return id +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-wizard/LICENSE b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-wizard/LICENSE new file mode 100644 index 0000000..4b53bb8 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-wizard/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mickaël Canouil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-wizard/schema.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-wizard/schema.lua new file mode 100644 index 0000000..bab8676 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/_vendor/quarto-wizard/schema.lua @@ -0,0 +1,2612 @@ +--- MC Schema - Reference validator for Quarto extension schemas (`_schema.yml`) +--- @module "schema" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @version 2.3.0 +--- +--- Implements the v2 extension schema vocabulary published at +--- . +--- The v1 dual-key vocabulary (`min`, `max`, `enum-case-insensitive`, +--- `element-attributes`, `pattern-exact`) is not accepted. +--- +--- The module is standalone: it requires no sibling module, and it reaches +--- `pandoc` and `quarto` only through the `M._env` table, so it can be +--- exercised outside a Quarto render with stubs in place. + +local M = {} + +--- Meta-schema this module implements. +M.SCHEMA_VERSION = 'https://m.canouil.dev/quarto-wizard/assets/schema/v2/extension-schema.json' + +-- ============================================================================ +-- ENVIRONMENT SEAM +-- ============================================================================ + +--- Indirection over the two host globals, resolved at call time so a test +--- harness can install stubs before the first call. +local _env = {} + +--- Render a Pandoc value as plain text. +--- @param value any Pandoc value +--- @return string Plain text +function _env.stringify(value) + return pandoc.utils.stringify(value) +end + +--- Report the Pandoc type of a value. +--- @param value any Pandoc value +--- @return string Pandoc type name +function _env.pandoc_type(value) + return pandoc.utils.type(value) +end + +--- Emit a warning through the host logger. +--- @param message string Message to emit +--- @return nil +function _env.warn(message) + if quarto and quarto.log and quarto.log.warning then + quarto.log.warning(message) + else + io.stderr:write(message .. '\n') + end +end + +--- Emit an error through the host logger. +--- @param message string Message to emit +--- @return nil +function _env.report_error(message) + if quarto and quarto.log and quarto.log.error then + quarto.log.error(message) + else + io.stderr:write(message .. '\n') + end +end + +M._env = _env + +-- ============================================================================ +-- CONSTANTS +-- ============================================================================ + +--- Maximum nesting depth accepted when converting a parsed schema. +local MAX_DEPTH = 16 + +--- Maximum number of Lua patterns one regex may expand into. +local MAX_PATTERN_BRANCHES = 64 + +--- Descriptor keywords holding a numeric bound. +local NUMERIC_KEYWORDS = { + 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', + 'minLength', 'maxLength', 'minItems', 'maxItems', +} + +--- Every keyword the v2 field descriptor may carry, mapped to whether this +--- module enforces it. Annotations are carried through untouched for tooling. +M.KEYWORDS = { + type = true, + required = true, + default = true, + const = true, + enum = true, + enumCaseInsensitive = true, + pattern = true, + minimum = true, + maximum = true, + exclusiveMinimum = true, + exclusiveMaximum = true, + multipleOf = true, + minLength = true, + maxLength = true, + minItems = true, + maxItems = true, + uniqueItems = true, + items = true, + properties = true, + additionalProperties = true, + propertyNames = true, + dependentRequired = true, + aliases = true, + deprecated = true, + name = true, + description = false, + title = false, + examples = false, + format = false, + completion = false, + contentEncoding = false, + contentMediaType = false, +} + +-- ============================================================================ +-- PRIVATE HELPERS +-- ============================================================================ + +--- Report whether a table is a dense array. +--- @param value any Value to inspect +--- @return boolean True when the table has only keys 1..n +local function _is_array(value) + if type(value) ~= 'table' then + return false + end + local count = 0 + for _ in pairs(value) do + count = count + 1 + end + return count == #value +end + +--- Read a key from a map, accepting the hyphen and underscore spellings. +--- Keys are never rewritten, so a schema keeps the names its author wrote. +--- @param map table|nil Map to read +--- @param key string|nil Key to read +--- @return any value The stored value, or nil +--- @return string|nil found_key The spelling that matched +local function _lookup(map, key) + if type(map) ~= 'table' or type(key) ~= 'string' then + return nil, nil + end + if map[key] ~= nil then + return map[key], key + end + local underscored = (key:gsub('%-', '_')) + if underscored ~= key and map[underscored] ~= nil then + return map[underscored], underscored + end + local hyphenated = (key:gsub('_', '-')) + if hyphenated ~= key and map[hyphenated] ~= nil then + return map[hyphenated], hyphenated + end + return nil, nil +end + +--- Render any value for an error message, including mixed-type arrays. +--- Replaces `table.concat`, which throws on a boolean entry. +--- @param value any Value to render +--- @return string Readable representation +local function _format_value(value) + local kind = type(value) + if kind == 'string' then + if value == '' then + return '""' + end + return value + elseif kind == 'number' or kind == 'boolean' then + return tostring(value) + elseif kind == 'nil' then + return 'null' + elseif kind == 'table' then + if _is_array(value) then + local parts = {} + for index = 1, #value do + parts[index] = _format_value(value[index]) + end + return '[' .. table.concat(parts, ', ') .. ']' + end + return '{object}' + end + return tostring(value) +end + +--- Render a list of values as a comma-separated string. +--- @param values table Array of values +--- @return string Readable list +local function _format_list(values) + local parts = {} + for index = 1, #values do + parts[index] = _format_value(values[index]) + end + return table.concat(parts, ', ') +end + +--- Normalise a type spec to an array of type names. +--- @param type_spec string|table|nil Declared type +--- @return table|nil Array of type names, or nil when no type is declared +local function _type_names(type_spec) + if type_spec == nil then + return nil + end + if type(type_spec) == 'table' then + return type_spec + end + return { type_spec } +end + +--- Render a type spec for an error message. +--- @param type_spec string|table Declared type +--- @return string Readable type +local function _format_type(type_spec) + if type(type_spec) == 'table' then + return table.concat(type_spec, ' | ') + end + return tostring(type_spec) +end + +--- Report the type name of a value, distinguishing arrays from objects. +--- @param value any Value to inspect +--- @return string Type name +local function _actual_type(value) + local kind = type(value) + if kind == 'table' then + return _is_array(value) and 'array' or 'object' + end + if kind == 'nil' then + return 'null' + end + return kind +end + +--- Report whether a value satisfies a single type name. +--- @param value any Value to check +--- @param name string Type name +--- @return boolean True when the value matches +local function _matches_type(value, name) + if name == 'content' then + return value ~= nil + end + if name == 'null' then + return value == nil + end + if name == 'integer' then + return type(value) == 'number' + and value == value + and value ~= math.huge + and value ~= -math.huge + and value == math.floor(value) + end + if name == 'number' then + return type(value) == 'number' + end + if name == 'boolean' then + return type(value) == 'boolean' + end + if name == 'string' then + return type(value) == 'string' + end + if name == 'array' then + return type(value) == 'table' and _is_array(value) + end + if name == 'object' then + -- An array is not an object. An empty table is both, and Lua cannot tell + -- the two apart, so it satisfies either. + return type(value) == 'table' and (next(value) == nil or not _is_array(value)) + end + return false +end + +--- Report whether a value satisfies any declared type. +--- @param value any Value to check +--- @param type_spec string|table|nil Declared type +--- @return boolean True when the value matches, or no type is declared +local function _matches_type_spec(value, type_spec) + local names = _type_names(type_spec) + if names == nil then + return true + end + for _, name in ipairs(names) do + if _matches_type(value, name) then + return true + end + end + return false +end + +--- Coerce a string toward a single scalar type. +--- Pandoc renders every YAML number as a string, so the incoming value and +--- the schema's own literals both need this before any comparison. +--- @param value any Value to coerce +--- @param name string Target type name +--- @return any coerced Coerced value, or nil +--- @return boolean ok True when the coercion applied +local function _coerce_scalar(value, name) + if type(value) ~= 'string' then + return nil, false + end + if name == 'number' then + local number = tonumber(value) + if number then + return number, true + end + elseif name == 'integer' then + local number = tonumber(value) + if number and number == math.floor(number) then + return number, true + end + elseif name == 'boolean' then + -- Only `true` and `false`, which is what YAML 1.2 calls a boolean and what + -- Pandoc reads. `yes` and `no` are booleans in YAML 1.1 alone, and Pandoc + -- hands them over as strings, so accepting them here made this validator + -- disagree with the YAML the documents are written in. + -- + -- The coercion is still needed for the words that do name a boolean, + -- because an attribute and a shortcode argument always arrive as strings. + local lowered = value:lower() + if lowered == 'true' then + return true, true + end + if lowered == 'false' then + return false, true + end + end + return nil, false +end + +--- Coerce a value toward its declared type. +--- A value already matching one declared type is returned untouched, so +--- `type: [boolean, string]` keeps the string "fenced" a string. +--- @param value any Value to coerce +--- @param type_spec string|table|nil Declared type +--- @return any Coerced value +local function _coerce(value, type_spec) + local names = _type_names(type_spec) + if names == nil or value == nil then + return value + end + for _, name in ipairs(names) do + if _matches_type(value, name) then + return value + end + end + for _, name in ipairs(names) do + local coerced, ok = _coerce_scalar(value, name) + if ok then + return coerced + end + end + return value +end + +-- ============================================================================ +-- PATTERN COMPILATION +-- ============================================================================ + +--- Lua replacements for JS shorthand classes, outside a character class. +local ESCAPE_OUTSIDE = { + d = '%d', D = '%D', w = '[%w_]', W = '[^%w_]', s = '%s', S = '%S', +} + +--- Lua replacements for JS shorthand classes, inside a character class. +--- `\W` has no in-class form, because the underscore cannot be excluded there. +local ESCAPE_INSIDE = { + d = '%d', D = '%D', w = '%w_', s = '%s', S = '%S', +} + +--- Escapes with a real Lua equivalent. Anything else alphanumeric is refused, +--- because compiling it to the bare letter would accept the wrong values. +local CONTROL_ESCAPES = { + n = '\n', + t = '\t', + r = '\r', + f = '\f', + v = '\v', +} + +--- Characters Lua treats as magic outside a character class. +local LUA_MAGIC = '^$()%.[]*+-?' + +--- Escape one literal character for use in a Lua pattern. +--- @param char string Single character +--- @return string Escaped character +local function _escape_literal(char) + if LUA_MAGIC:find(char, 1, true) then + return '%' .. char + end + return char +end + +--- Compile a JS regular expression into a list of equivalent Lua patterns. +--- Alternation is expanded into one pattern per branch, which is how Lua, +--- lacking alternation, can honour the commonest JSON Schema idiom. +--- Anything that cannot be expressed is reported rather than accepted. +--- @param regex string JS regular expression +--- @return table|nil branches Array of Lua patterns, or nil on failure +--- @return string|nil reason Why compilation failed +local function _compile_pattern(regex) + local position = 1 + local length = #regex + local anchor_start = false + local anchor_end = false + local has_top_level_alternation = false + local parse_alternation + + --- Combine a set of prefixes with a set of continuations. + local function cross(prefixes, continuations) + local out = {} + for _, prefix in ipairs(prefixes) do + for _, continuation in ipairs(continuations) do + if #out >= MAX_PATTERN_BRANCHES then + return nil, 'alternation expands past ' .. MAX_PATTERN_BRANCHES .. ' branches' + end + out[#out + 1] = prefix .. continuation + end + end + return out + end + + --- Parse a bracketed character class starting at the current position. + local function parse_class() + local out = { '[' } + position = position + 1 + if regex:sub(position, position) == '^' then + out[#out + 1] = '^' + position = position + 1 + end + if regex:sub(position, position) == ']' then + out[#out + 1] = '%]' + position = position + 1 + end + while position <= length do + local char = regex:sub(position, position) + if char == ']' then + position = position + 1 + out[#out + 1] = ']' + return table.concat(out) + elseif char == '\\' then + local next_char = regex:sub(position + 1, position + 1) + if next_char == '' then + return nil, 'trailing backslash' + end + if next_char == 'W' then + return nil, 'unsupported "\\W" inside a character class' + end + local mapped = ESCAPE_INSIDE[next_char] + if mapped then + out[#out + 1] = mapped + elseif next_char:match('[1-9]') then + return nil, 'unsupported backreference "\\' .. next_char .. '"' + elseif next_char == '%' then + out[#out + 1] = '%%' + elseif CONTROL_ESCAPES[next_char] then + out[#out + 1] = CONTROL_ESCAPES[next_char] + elseif next_char:match('%w') then + return nil, 'unsupported escape "\\' .. next_char .. '"' + else + out[#out + 1] = '%' .. next_char + end + position = position + 2 + elseif char == '%' then + out[#out + 1] = '%%' + position = position + 1 + else + out[#out + 1] = char + position = position + 1 + end + end + return nil, 'unterminated character class' + end + + --- Read a trailing quantifier, rejecting the forms Lua cannot express. + local function parse_quantifier() + local char = regex:sub(position, position) + if char ~= '*' and char ~= '+' and char ~= '?' then + return '' + end + if regex:sub(position + 1, position + 1) == '?' then + return nil, 'unsupported non-greedy quantifier "' .. char .. '?"' + end + position = position + 1 + return char + end + + --- Parse a single atom and return the branches it contributes. + local function parse_atom(depth, at_sequence_start) + local char = regex:sub(position, position) + + if char == '(' then + if regex:sub(position + 1, position + 1) == '?' then + local marker = regex:sub(position + 2, position + 2) + if marker == '=' or marker == '!' or marker == '<' then + return nil, 'unsupported lookahead or lookbehind' + end + if marker ~= ':' then + return nil, 'unsupported group modifier "(?' .. marker .. '"' + end + position = position + 3 + else + position = position + 1 + end + local branches, reason = parse_alternation(depth + 1) + if not branches then + return nil, reason + end + if regex:sub(position, position) ~= ')' then + return nil, 'unterminated group' + end + position = position + 1 + local quantifier = regex:sub(position, position) + if quantifier == '*' or quantifier == '+' or quantifier == '?' then + return nil, 'unsupported quantifier applied to a group' + end + return branches + end + + if char == '[' then + local class, reason = parse_class() + if not class then + return nil, reason + end + local quantifier, quantifier_reason = parse_quantifier() + if not quantifier then + return nil, quantifier_reason + end + return { class .. quantifier } + end + + if char == '\\' then + local next_char = regex:sub(position + 1, position + 1) + if next_char == '' then + return nil, 'trailing backslash' + end + if next_char:match('[1-9]') then + return nil, 'unsupported backreference "\\' .. next_char .. '"' + end + position = position + 2 + local mapped = ESCAPE_OUTSIDE[next_char] + local atom + if mapped then + atom = mapped + elseif CONTROL_ESCAPES[next_char] then + atom = CONTROL_ESCAPES[next_char] + elseif next_char:match('%w') then + return nil, 'unsupported escape "\\' .. next_char .. '"' + else + atom = _escape_literal(next_char) + end + local quantifier, quantifier_reason = parse_quantifier() + if not quantifier then + return nil, quantifier_reason + end + return { atom .. quantifier } + end + + if char == '^' then + if depth ~= 0 or not at_sequence_start or position ~= 1 then + return nil, 'unsupported anchor "^" away from the start of the pattern' + end + position = position + 1 + anchor_start = true + return { '' } + end + + if char == '$' then + if depth ~= 0 or position ~= length then + return nil, 'unsupported anchor "$" away from the end of the pattern' + end + position = position + 1 + anchor_end = true + return { '' } + end + + if char == '{' then + return nil, 'unsupported counted quantifier "{n,m}"' + end + + if char == '*' or char == '+' or char == '?' then + return nil, 'quantifier "' .. char .. '" with nothing to repeat' + end + + if char == '.' then + position = position + 1 + local quantifier, quantifier_reason = parse_quantifier() + if not quantifier then + return nil, quantifier_reason + end + return { '.' .. quantifier } + end + + position = position + 1 + local atom = _escape_literal(char) + local quantifier, quantifier_reason = parse_quantifier() + if not quantifier then + return nil, quantifier_reason + end + return { atom .. quantifier } + end + + --- Parse one alternative: a run of atoms up to `|`, `)` or the end. + local function parse_sequence(depth) + local branches = { '' } + local first = true + while position <= length do + local char = regex:sub(position, position) + if char == '|' or char == ')' then + break + end + local atom_branches, reason = parse_atom(depth, first) + if not atom_branches then + return nil, reason + end + first = false + local crossed, cross_reason = cross(branches, atom_branches) + if not crossed then + return nil, cross_reason + end + branches = crossed + end + return branches + end + + parse_alternation = function(depth) + local all = {} + local iterations = 0 + while true do + iterations = iterations + 1 + local branches, reason = parse_sequence(depth) + if not branches then + return nil, reason + end + for _, branch in ipairs(branches) do + if #all >= MAX_PATTERN_BRANCHES then + return nil, 'alternation expands past ' .. MAX_PATTERN_BRANCHES .. ' branches' + end + all[#all + 1] = branch + end + if regex:sub(position, position) == '|' then + position = position + 1 + else + break + end + end + if depth == 0 and iterations > 1 then + has_top_level_alternation = true + end + return all + end + + local branches, reason = parse_alternation(0) + if not branches then + return nil, reason + end + if position <= length then + return nil, 'unbalanced ")" in pattern' + end + + -- The anchors are collected for the expression as a whole, so applying them + -- to multiple top-level branches would anchor branches the author did not anchor. + if has_top_level_alternation and (anchor_start or anchor_end) then + return nil, 'unsupported anchor in a top-level alternation' + end + + local prefix = anchor_start and '^' or '' + local suffix = anchor_end and '$' or '' + local out = {} + for index, branch in ipairs(branches) do + out[index] = prefix .. branch .. suffix + end + return out +end + +M._compile_pattern = _compile_pattern + +--- Report whether a string satisfies any compiled branch. +--- @param value string Value to test +--- @param branches table Array of Lua patterns +--- @return boolean True when a branch matches +local function _pattern_matches(value, branches) + for _, branch in ipairs(branches) do + local ok, matched = pcall(string.match, value, branch) + if ok and matched ~= nil then + return true + end + end + return false +end + +-- ============================================================================ +-- PANDOC VALUE CONVERSION +-- ============================================================================ + +--- Convert a Pandoc metadata value to a native Lua value. +--- Pandoc's Lua objects carry no `t` field, so the kind has to come from +--- `pandoc.utils.type`; testing `value.t` silently turns every string option +--- into a list of its inline elements. +--- @param value any Pandoc metadata value +--- @return any Native Lua value +local function _convert_pandoc_value(value) + local kind = _env.pandoc_type(value) + + -- A bare key, `null` and `~` all arrive as an empty Pandoc `string`, which + -- is how Pandoc collapses YAML null. An explicit `""` arrives as an empty + -- `Inlines` instead, so the two are told apart before either is unwrapped. + if kind == 'string' and value == '' then + return nil + end + + if type(value) ~= 'table' then + return value + end + + if kind == 'Inlines' or kind == 'Blocks' or kind == 'Inline' or kind == 'Block' then + return _env.stringify(value) + end + + if kind == 'List' then + -- A null element is absent, the same rule this module applies to a null + -- key. Writing it into a running count rather than the source index + -- keeps the result a proper sequence instead of leaving a gap. + local result = {} + for index = 1, #value do + local converted = _convert_pandoc_value(value[index]) + if converted ~= nil then + result[#result + 1] = converted + end + end + return result + end + + if _is_array(value) and #value > 0 then + -- A null element is absent, the same rule this module applies to a null + -- key. Writing it into a running count rather than the source index + -- keeps the result a proper sequence instead of leaving a gap. + local result = {} + for index = 1, #value do + local converted = _convert_pandoc_value(value[index]) + if converted ~= nil then + result[#result + 1] = converted + end + end + return result + end + + local result = {} + for key, item in pairs(value) do + result[tostring(key)] = _convert_pandoc_value(item) + end + return result +end + +-- ============================================================================ +-- YAML PARSING +-- ============================================================================ + +--- Pandoc parses YAML string scalars as Markdown, which rewrites a schema in +--- place: `^[a-z]+$` arrives as `+$`, and `^\d+\.?\d*m?s$` arrives as +--- `^.??s$`. The schema file is therefore parsed here instead. +--- +--- The subset covers what an extension schema needs: block maps and +--- sequences, plain and quoted scalars, folded and literal block scalars, +--- flow sequences and flow maps (including ones spanning several lines), and +--- `#` comments. Anchors, aliases, tags, multiple documents and complex keys +--- are not supported and are reported rather than misread. +--- +--- Every function here reports a failure by returning a message rather than +--- by calling `error`. Quarto replaces `error` with a logger that returns +--- instead of unwinding, so a module that trusts `error` to stop execution +--- carries on with invalid state and fails later somewhere unrelated. +--- +--- One deliberate deviation from YAML: a block scalar is always stripped of +--- its trailing newline unless it is chomped with `+`. Schema text is +--- annotation, and a trailing newline there is noise rather than meaning. + +--- Build a parse error message carrying the offending line number. +--- @param line_number number Line number +--- @param message string Explanation +--- @return string Formatted message +local function _yaml_error(line_number, message) + return string.format('line %d: %s', line_number, message) +end + +--- Remove a trailing `#` comment, respecting quoted spans. +--- @param text string Line content +--- @return string Content without its comment +local function _strip_comment(text) + local out = {} + local quote = nil + local index = 1 + while index <= #text do + local char = text:sub(index, index) + if quote then + out[#out + 1] = char + if char == '\\' and quote == '"' then + index = index + 1 + out[#out + 1] = text:sub(index, index) + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + out[#out + 1] = char + elseif char == '#' and (index == 1 or text:sub(index - 1, index - 1):match('%s')) then + break + else + out[#out + 1] = char + end + index = index + 1 + end + return (table.concat(out):gsub('%s+$', '')) +end + +--- Trim leading and trailing whitespace. +--- @param text string Text to trim +--- @return string Trimmed text +local function _trim(text) + return (text:gsub('^%s+', ''):gsub('%s+$', '')) +end + +--- Parse a scalar, giving numbers and booleans their real Lua types. +--- @param text string Scalar text +--- @param line_number number Line number for errors +--- @return any value Parsed value, nil for an explicit null +--- @return string|nil err +local function _parse_scalar(text, line_number) + text = _trim(text) + + if text == '' or text == '~' or text == 'null' or text == 'Null' or text == 'NULL' then + return nil, nil + end + + local first = text:sub(1, 1) + + if first == '"' then + if #text < 2 or text:sub(-1) ~= '"' then + return nil, _yaml_error(line_number, 'unterminated double-quoted scalar') + end + return (text:sub(2, -2):gsub('\\(.)', function(escaped) + if escaped == 'n' then return '\n' end + if escaped == 't' then return '\t' end + if escaped == 'r' then return '\r' end + return escaped + end)), nil + end + + if first == "'" then + if #text < 2 or text:sub(-1) ~= "'" then + return nil, _yaml_error(line_number, 'unterminated single-quoted scalar') + end + return (text:sub(2, -2):gsub("''", "'")), nil + end + + if first == '&' or first == '*' or first == '!' then + return nil, _yaml_error(line_number, 'anchors, aliases and tags are not supported') + end + + if text == 'true' or text == 'True' or text == 'TRUE' or text == 'yes' or text == 'Yes' then + return true, nil + end + if text == 'false' or text == 'False' or text == 'FALSE' or text == 'no' or text == 'No' then + return false, nil + end + + local number = tonumber(text) + if number ~= nil and text:match('^[%-+]?[%d%.eE%+%-]+$') then + return number, nil + end + + return text, nil +end + +--- Split a flow collection body on its top-level commas. +--- @param body string Text between the brackets +--- @return table Array of raw item strings +local function _split_flow(body) + local items = {} + local depth = 0 + local quote = nil + local start = 1 + local index = 1 + while index <= #body do + local char = body:sub(index, index) + if quote then + if char == '\\' and quote == '"' then + index = index + 1 + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + elseif char == '[' or char == '{' then + depth = depth + 1 + elseif char == ']' or char == '}' then + depth = depth - 1 + elseif char == ',' and depth == 0 then + items[#items + 1] = body:sub(start, index - 1) + start = index + 1 + end + index = index + 1 + end + local tail = body:sub(start) + if tail:match('%S') then + items[#items + 1] = tail + end + return items +end + +--- Strip the quotes from a mapping key. +--- @param key string Raw key text +--- @param line_number number Line number for errors +--- @return string|nil key +--- @return string|nil err +local function _unquote_key(key, line_number) + key = _trim(key) + local first = key:sub(1, 1) + if first == '"' or first == "'" then + local parsed, err = _parse_scalar(key, line_number) + if err then + return nil, err + end + return tostring(parsed), nil + end + return key, nil +end + +local _parse_flow + +--- Parse a value written in flow style, or a plain scalar. +--- @param text string Value text +--- @param line_number number Line number for errors +--- @param depth number|nil Current nesting depth +--- @return any value +--- @return string|nil err +_parse_flow = function(text, line_number, depth) + depth = depth or 0 + if depth > MAX_DEPTH then + return nil, _yaml_error(line_number, 'value nests deeper than ' .. MAX_DEPTH .. ' levels') + end + + text = _trim(text) + local first = text:sub(1, 1) + + if first == '[' then + if text:sub(-1) ~= ']' then + return nil, _yaml_error(line_number, 'unterminated flow sequence') + end + local out = {} + for index, item in ipairs(_split_flow(text:sub(2, -2))) do + local value, err = _parse_flow(item, line_number, depth + 1) + if err then + return nil, err + end + out[index] = value + end + return out, nil + end + + if first == '{' then + if text:sub(-1) ~= '}' then + return nil, _yaml_error(line_number, 'unterminated flow mapping') + end + local out = {} + for _, item in ipairs(_split_flow(text:sub(2, -2))) do + local key, rest = item:match('^%s*(.-)%s*:%s*(.*)$') + if not key or key == '' then + return nil, _yaml_error(line_number, 'expected "key: value" inside a flow mapping') + end + local name, key_err = _unquote_key(key, line_number) + if key_err then + return nil, key_err + end + local value, err = _parse_flow(rest, line_number, depth + 1) + if err then + return nil, err + end + out[name] = value + end + return out, nil + end + + return _parse_scalar(text, line_number) +end + +--- Split a mapping line into its key and the rest of the line. +--- The separator is the first unquoted colon followed by a space or the end +--- of the line, so a URL value such as `https://...` is not split. +--- @param text string Line content +--- @return string|nil key +--- @return string|nil rest +local function _split_key(text) + local quote = nil + local index = 1 + while index <= #text do + local char = text:sub(index, index) + if quote then + if char == '\\' and quote == '"' then + index = index + 1 + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + elseif char == ':' then + local following = text:sub(index + 1, index + 1) + if following == '' or following == ' ' then + return text:sub(1, index - 1), _trim(text:sub(index + 2)) + end + end + index = index + 1 + end + return nil, nil +end + +--- Break the source into classified lines. +--- @param source string YAML text +--- @return table|nil lines Array of {raw, indent, number, blank, comment} +--- @return string|nil err +local function _scan_lines(source) + local lines = {} + local number = 0 + for raw in (source .. '\n'):gmatch('([^\n]*)\n') do + number = number + 1 + if raw:find('\t') then + return nil, _yaml_error(number, 'tabs are not valid YAML indentation') + end + local indent = #(raw:match('^ *') or '') + local body = raw:sub(indent + 1) + lines[#lines + 1] = { + raw = raw, + indent = indent, + number = number, + blank = body == '', + comment = body:sub(1, 1) == '#', + } + end + return lines, nil +end + +--- Advance past blank and comment lines. +--- @param lines table Scanned lines +--- @param index number Starting index +--- @return number Index of the next significant line +local function _skip_insignificant(lines, index) + while index <= #lines and (lines[index].blank or lines[index].comment) do + index = index + 1 + end + return index +end + +--- Read the comment-stripped content of a line. +--- @param line table Scanned line +--- @return string Content +local function _content(line) + return _strip_comment(line.raw:sub(line.indent + 1)) +end + +--- Track bracket depth across one line, ignoring quoted spans. +--- @param text string Line content +--- @param depth number Depth carried in +--- @param quote string|nil Open quote carried in +--- @return number depth +--- @return string|nil quote +local function _scan_flow_depth(text, depth, quote) + local index = 1 + while index <= #text do + local char = text:sub(index, index) + if quote then + if char == '\\' and quote == '"' then + index = index + 1 + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + elseif char == '[' or char == '{' then + depth = depth + 1 + elseif char == ']' or char == '}' then + depth = depth - 1 + end + index = index + 1 + end + return depth, quote +end + +--- Collect a flow collection that may run across several lines. +--- @param lines table Scanned lines +--- @param start number Index of the line holding the opening bracket +--- @param initial string Flow text already read from that line +--- @param line_number number Line number for errors +--- @return string|nil text The whole flow collection on one line +--- @return number next_index Index of the first line after it +--- @return string|nil err +local function _gather_flow(lines, start, initial, line_number) + local pieces = { initial } + local depth, quote = _scan_flow_depth(initial, 0, nil) + local index = start + + while depth > 0 do + index = index + 1 + if index > #lines then + return nil, index, _yaml_error(line_number, 'unterminated flow collection') + end + local content = _content(lines[index]) + pieces[#pieces + 1] = content + depth, quote = _scan_flow_depth(content, depth, quote) + end + + return table.concat(pieces, ' '), index + 1, nil +end + +--- Read a folded or literal block scalar. +--- @param lines table Scanned lines +--- @param start number First candidate line +--- @param parent_indent number Indentation of the owning key +--- @param style string Block style, such as '>', '>-' or '|' +--- @return string text +--- @return number next_index +local function _read_block_scalar(lines, start, parent_indent, style) + local kind = style:sub(1, 1) + local chomp = style:sub(2, 2) + + local collected = {} + local block_indent = nil + local index = start + + while index <= #lines do + local line = lines[index] + if line.blank then + collected[#collected + 1] = '' + index = index + 1 + elseif line.indent > parent_indent then + block_indent = block_indent or line.indent + collected[#collected + 1] = line.raw:sub(block_indent + 1) + index = index + 1 + else + break + end + end + + while #collected > 0 and collected[#collected] == '' do + table.remove(collected) + end + + local text + if kind == '>' then + local parts = {} + local buffer = {} + for _, line in ipairs(collected) do + if line == '' then + if #buffer > 0 then + parts[#parts + 1] = table.concat(buffer, ' ') + buffer = {} + end + parts[#parts + 1] = '\n' + else + buffer[#buffer + 1] = (line:gsub('%s+$', '')) + end + end + if #buffer > 0 then + parts[#parts + 1] = table.concat(buffer, ' ') + end + text = table.concat(parts) + else + text = table.concat(collected, '\n') + end + + if chomp == '+' then + text = text .. '\n' + end + + return text, index +end + +local _parse_block +local _parse_sequence + +--- The order in which each parsed mapping declared its keys. +--- Lua tables have no key order, but a schema is authored in a meaningful one, +--- and a generator that turns a schema into documentation needs it. The order +--- is kept beside the data rather than inside it, so it cannot be mistaken for +--- a key of the mapping. Weak keys let a discarded mapping take its order with +--- it. +local _key_order = setmetatable({}, { __mode = 'k' }) + +--- Parse a block mapping at a known indentation. +--- @return table|nil map +--- @return number next_index +--- @return string|nil err +local function _parse_map(lines, start, indent, depth) + local map = {} + local order = {} + local index = start + + while index <= #lines do + local line = lines[index] + if line.blank or line.comment then + index = index + 1 + elseif line.indent < indent then + break + else + if line.indent > indent then + return nil, index, _yaml_error(line.number, 'unexpected indentation inside a mapping') + end + + local content = _content(line) + if content == '' then + index = index + 1 + else + if content == '---' or content == '...' then + return nil, index, + _yaml_error(line.number, 'multiple YAML documents are not supported') + end + + if content:sub(1, 1) == '-' then + return nil, index, + _yaml_error(line.number, 'a sequence item cannot appear inside a mapping here') + end + + local raw_key, rest = _split_key(content) + if raw_key == nil or _trim(raw_key) == '' then + return nil, index, _yaml_error(line.number, 'expected "key: value"') + end + + local key, key_err = _unquote_key(raw_key, line.number) + if key_err then + return nil, index, key_err + end + if map[key] == nil then + order[#order + 1] = key + end + index = index + 1 + + -- An explicit indentation indicator (`|2`, `>2-`) is reported rather + -- than misread: without this the whole block is stored as the string + -- "|2" and its body is then parsed as further mapping lines. + if rest:match('^[|>]%d') or rest:match('^[|>][+%-]%d') then + return nil, index, _yaml_error( + line.number, + 'a block scalar indentation indicator is not supported: ' .. rest + ) + end + + local block_style = rest:match('^([|>][+%-]?)$') + if block_style then + local text, next_index = _read_block_scalar(lines, index, indent, block_style) + map[key] = text + index = next_index + elseif rest == '' then + local value, next_index, err = _parse_block(lines, index, indent, depth + 1) + if err then + return nil, next_index, err + end + + -- A block sequence may sit at the column of its own key, which + -- `_parse_block` declines because its guard wants a deeper line. + -- Nothing was consumed in that case, so read the sequence here. + if value == nil and next_index == index then + local peek = _skip_insignificant(lines, index) + if peek <= #lines and lines[peek].indent == indent then + local ahead = _content(lines[peek]) + if ahead == '-' or ahead:sub(1, 2) == '- ' then + value, next_index, err = _parse_sequence(lines, peek, indent, depth + 1) + if err then + return nil, next_index, err + end + end + end + end + + map[key] = value + index = next_index + elseif rest:sub(1, 1) == '[' or rest:sub(1, 1) == '{' then + local text, next_index, err = _gather_flow(lines, index - 1, rest, line.number) + if err then + return nil, next_index, err + end + local value, value_err = _parse_flow(text, line.number, depth) + if value_err then + return nil, next_index, value_err + end + map[key] = value + index = next_index + else + local value, err = _parse_flow(rest, line.number, depth) + if err then + return nil, index, err + end + map[key] = value + end + end + end + end + + _key_order[map] = order + return map, index, nil +end + +--- Parse a block sequence at a known indentation. +--- @return table|nil sequence +--- @return number next_index +--- @return string|nil err +_parse_sequence = function(lines, start, indent, depth) + local sequence = {} + local index = start + + while index <= #lines do + local line = lines[index] + if line.blank or line.comment then + index = index + 1 + elseif line.indent < indent then + break + else + if line.indent > indent then + return nil, index, _yaml_error(line.number, 'unexpected indentation inside a sequence') + end + + local content = _content(line) + local rest = content:match('^%-%s*(.*)$') + if rest == nil then + break + end + + if rest == '' then + local value, next_index, err = _parse_block(lines, index + 1, indent, depth + 1) + if err then + return nil, next_index, err + end + -- A Lua array cannot hold an interior nil, so an empty item would be + -- dropped and every later item would shift down one index, moving the + -- element that `minItems` and an `items` path such as `preload[2]` + -- refer to. Report it instead. + if value == nil then + return nil, index, _yaml_error(line.number, 'an empty sequence item is not supported') + end + sequence[#sequence + 1] = value + index = next_index + elseif _split_key(rest) == nil then + -- A plain or flow scalar item, such as `- password` or `- [a, b]`. + if rest:sub(1, 1) == '[' or rest:sub(1, 1) == '{' then + local text, next_index, err = _gather_flow(lines, index, rest, line.number) + if err then + return nil, next_index, err + end + local value, value_err = _parse_flow(text, line.number, depth) + if value_err then + return nil, next_index, value_err + end + sequence[#sequence + 1] = value + index = next_index + else + local value, err = _parse_flow(rest, line.number, depth) + if err then + return nil, index, err + end + sequence[#sequence + 1] = value + index = index + 1 + end + else + -- The remainder of the dash line is the first line of the item, and it + -- sits at the column where that remainder begins. + local offset = #content - #rest + local synthetic = { + raw = string.rep(' ', indent + offset) .. rest, + indent = indent + offset, + number = line.number, + blank = false, + comment = false, + } + local saved = lines[index] + lines[index] = synthetic + local value, next_index, err = _parse_block(lines, index, indent, depth + 1) + lines[index] = saved + if err then + return nil, next_index, err + end + sequence[#sequence + 1] = value + index = next_index + end + end + end + + return sequence, index, nil +end + +--- Parse whichever block collection begins at `start`. +--- @param lines table Scanned lines +--- @param start number Index to start from +--- @param parent_indent number|nil Indentation that the block must exceed +--- @param depth number Current nesting depth +--- @return any value +--- @return number next_index +--- @return string|nil err +_parse_block = function(lines, start, parent_indent, depth) + depth = depth or 0 + local index = _skip_insignificant(lines, start) + if index > #lines then + return nil, index, nil + end + + local line = lines[index] + if parent_indent ~= nil and line.indent <= parent_indent then + return nil, start, nil + end + + if depth > MAX_DEPTH then + return nil, index, _yaml_error(line.number, 'schema nests deeper than ' .. MAX_DEPTH .. ' levels') + end + + local indent = line.indent + local content = _content(line) + + if content:sub(1, 1) == '[' or content:sub(1, 1) == '{' then + local text, next_index, err = _gather_flow(lines, index, content, line.number) + if err then + return nil, next_index, err + end + local value, value_err = _parse_flow(text, line.number, depth) + if value_err then + return nil, next_index, value_err + end + return value, next_index, nil + end + + if content == '-' or content:sub(1, 2) == '- ' then + return _parse_sequence(lines, index, indent, depth) + end + return _parse_map(lines, index, indent, depth) +end + +--- Parse YAML text into a native Lua table. +--- @param source string YAML text +--- @return table|nil tree +--- @return string|nil err +local function _parse_yaml_text(source) + local lines, scan_err = _scan_lines(source) + if scan_err then + return nil, scan_err + end + + -- A single leading `---` opens one document, which is valid and common. + -- A later one opens a second document, which `_parse_map` reports. Testing + -- the raw text instead would also reject a `---` inside a block scalar. + local first = _skip_insignificant(lines, 1) + if first <= #lines and _content(lines[first]) == '---' then + lines[first].blank = true + end + + local value, _, err = _parse_block(lines, 1, nil, 0) + if err then + return nil, err + end + if value == nil then + return {}, nil + end + if type(value) ~= 'table' or (_is_array(value) and #value > 0) then + return nil, 'the file must contain a mapping at its top level' + end + return value, nil +end + +M._parse_yaml_text = _parse_yaml_text + +--- Read and parse a YAML schema file. +--- @param filename string Path to the schema file +--- @return table|nil tree +--- @return string|nil err +local function _parse_yaml_file(filename) + local handle = io.open(filename, 'r') + if not handle then + return nil, string.format('Could not open schema file: %s', filename) + end + + local content = handle:read('*a') + handle:close() + + local tree, err = _parse_yaml_text(content) + if err then + return nil, string.format('Could not parse schema file %s: %s', filename, err) + end + return tree, nil +end + + +-- ============================================================================ +-- DESCRIPTOR COMPILATION +-- ============================================================================ + +local _compiled_cache = setmetatable({}, { __mode = 'k' }) + +local _compile + +--- Compile a field descriptor: coerce its literals against its own declared +--- type, compile its patterns, and recurse into nested descriptors. +--- Returns a new table, so the caller's schema is never mutated. +--- @param spec table Raw field descriptor +--- @return table Compiled descriptor +_compile = function(spec) + if type(spec) ~= 'table' then + return spec + end + + local cached = _compiled_cache[spec] + if cached then + return cached + end + + local out = {} + for key, value in pairs(spec) do + out[key] = value + end + + local type_spec = out.type + + if out.default ~= nil then + out.default = _coerce(out.default, type_spec) + end + if out.const ~= nil then + out.const = _coerce(out.const, type_spec) + end + if type(out.enum) == 'table' then + local values = {} + for index = 1, #out.enum do + values[index] = _coerce(out.enum[index], type_spec) + end + out.enum = values + end + + for _, keyword in ipairs(NUMERIC_KEYWORDS) do + if out[keyword] ~= nil then + out[keyword] = tonumber(out[keyword]) or out[keyword] + end + end + + if type(out.required) == 'string' then + out.required = out.required:lower() == 'true' or out.required:lower() == 'yes' + end + if type(out.uniqueItems) == 'string' then + out.uniqueItems = out.uniqueItems:lower() == 'true' + end + if type(out.enumCaseInsensitive) == 'string' then + out.enumCaseInsensitive = out.enumCaseInsensitive:lower() == 'true' + end + if out.additionalProperties == 'false' then + out.additionalProperties = false + elseif out.additionalProperties == 'true' then + out.additionalProperties = true + end + + if type(out.pattern) == 'string' then + out._pattern, out._pattern_error = _compile_pattern(out.pattern) + end + if type(out.propertyNames) == 'string' then + out._property_names, out._property_names_error = _compile_pattern(out.propertyNames) + end + + if type(out.items) == 'table' then + out.items = _compile(out.items) + end + if type(out.properties) == 'table' then + local properties = {} + for key, value in pairs(out.properties) do + properties[key] = _compile(value) + end + out.properties = properties + end + if type(out.additionalProperties) == 'table' then + out.additionalProperties = _compile(out.additionalProperties) + end + + _compiled_cache[spec] = out + return out +end + +-- ============================================================================ +-- FINDINGS +-- ============================================================================ + +--- Record a finding against a path. +--- @param context table Validation context +--- @param severity string 'error' or 'warning' +--- @param path string Dotted path of the offending value +--- @param keyword string Keyword that produced the finding +--- @param message string Human-readable explanation +--- @return nil +local function _report(context, severity, path, keyword, message) + table.insert(context.findings, { + path = path, + keyword = keyword, + message = message, + severity = severity, + }) +end + +--- Split findings into the string arrays the public API returns. +--- @param findings table Array of findings +--- @return table errors Array of error strings +--- @return table warnings Array of warning strings +local function _split_findings(findings) + local errors = {} + local warnings = {} + for _, finding in ipairs(findings) do + local line = finding.path and (finding.path .. ': ' .. finding.message) or finding.message + if finding.severity == 'warning' then + table.insert(warnings, line) + else + table.insert(errors, line) + end + end + return errors, warnings +end + +-- ============================================================================ +-- KEYWORD CHECKS +-- ============================================================================ + +local _validate_value +local _validate_map + +--- Check `enum`, honouring `enumCaseInsensitive`. +local function _check_enum(value, spec, path, context) + if type(spec.enum) ~= 'table' then + return + end + local case_insensitive = spec.enumCaseInsensitive == true + for _, allowed in ipairs(spec.enum) do + if value == allowed then + return + end + if case_insensitive and type(value) == 'string' and type(allowed) == 'string' + and value:lower() == allowed:lower() then + return + end + end + _report(context, 'error', path, 'enum', string.format( + 'must be one of: %s, got %s.', _format_list(spec.enum), _format_value(value) + )) +end + +--- Check `pattern`, reporting a pattern the module cannot compile. +local function _check_pattern(value, spec, path, context) + if spec.pattern == nil or type(value) ~= 'string' then + return + end + if spec._pattern == nil then + _report(context, 'error', path, 'pattern', string.format( + 'schema declares a pattern this validator cannot compile (%s): %s.', + spec._pattern_error or 'unsupported construct', spec.pattern + )) + return + end + if not _pattern_matches(value, spec._pattern) then + _report(context, 'error', path, 'pattern', string.format( + 'does not match required pattern: %s.', spec.pattern + )) + end +end + +--- Check the numeric bounds. +local function _check_numeric(value, spec, path, context) + if type(value) ~= 'number' then + return + end + if type(spec.minimum) == 'number' and value < spec.minimum then + _report(context, 'error', path, 'minimum', string.format( + 'must be at least %s, got %s.', _format_value(spec.minimum), _format_value(value) + )) + end + if type(spec.maximum) == 'number' and value > spec.maximum then + _report(context, 'error', path, 'maximum', string.format( + 'must be at most %s, got %s.', _format_value(spec.maximum), _format_value(value) + )) + end + if type(spec.exclusiveMinimum) == 'number' and value <= spec.exclusiveMinimum then + _report(context, 'error', path, 'exclusiveMinimum', string.format( + 'must be greater than %s, got %s.', _format_value(spec.exclusiveMinimum), _format_value(value) + )) + end + if type(spec.exclusiveMaximum) == 'number' and value >= spec.exclusiveMaximum then + _report(context, 'error', path, 'exclusiveMaximum', string.format( + 'must be less than %s, got %s.', _format_value(spec.exclusiveMaximum), _format_value(value) + )) + end + if type(spec.multipleOf) == 'number' and spec.multipleOf ~= 0 then + local quotient = value / spec.multipleOf + if quotient ~= math.floor(quotient) then + _report(context, 'error', path, 'multipleOf', string.format( + 'must be a multiple of %s, got %s.', _format_value(spec.multipleOf), _format_value(value) + )) + end + end +end + +--- Check string length. +local function _check_string_length(value, spec, path, context) + if type(value) ~= 'string' then + return + end + -- Count characters rather than bytes, so a multi-byte glyph counts as one. + local length = utf8 and utf8.len(value) or nil + if length == nil then + length = #value + end + if type(spec.minLength) == 'number' and length < spec.minLength then + _report(context, 'error', path, 'minLength', string.format( + 'must be at least %d characters, got %d.', spec.minLength, length + )) + end + if type(spec.maxLength) == 'number' and length > spec.maxLength then + _report(context, 'error', path, 'maxLength', string.format( + 'must be at most %d characters, got %d.', spec.maxLength, length + )) + end +end + +--- Check array length and uniqueness. +local function _check_array(value, spec, path, context) + if type(value) ~= 'table' then + return + end + local length = #value + if type(spec.minItems) == 'number' and length < spec.minItems then + _report(context, 'error', path, 'minItems', string.format( + 'must have at least %d items, got %d.', spec.minItems, length + )) + end + if type(spec.maxItems) == 'number' and length > spec.maxItems then + _report(context, 'error', path, 'maxItems', string.format( + 'must have at most %d items, got %d.', spec.maxItems, length + )) + end + if spec.uniqueItems == true then + local seen = {} + for index = 1, length do + local element = value[index] + local rendered = _format_value(element) + -- The key carries the type so 1 and "1" are different items. A string + -- keys on its own text rather than on the rendering, because + -- `_format_value` renders an empty string as `""`, which would make it + -- collide with the two-character string `""`. The message carries only + -- the rendering, because the key is not the author's text. + local raw = type(element) == 'string' and element or rendered + local key = type(element) .. '\0' .. raw + if seen[key] then + _report(context, 'error', path, 'uniqueItems', string.format( + 'must not repeat items, but %s appears more than once.', rendered + )) + break + end + seen[key] = true + end + end +end + +--- Validate array elements against `items`. +local function _check_items(value, spec, path, context) + if type(spec.items) ~= 'table' or type(value) ~= 'table' then + return + end + for index = 1, #value do + local element = _coerce(value[index], spec.items.type) + value[index] = element + if element ~= nil then + _validate_value(element, spec.items, string.format('%s[%d]', path, index), context) + end + end +end + +--- Validate object members against `properties` and its companions. +local function _check_object(value, spec, path, context) + if type(value) ~= 'table' then + return + end + + if spec._property_names ~= nil or spec._property_names_error ~= nil then + if spec._property_names == nil then + _report(context, 'error', path, 'propertyNames', string.format( + 'schema declares a propertyNames pattern this validator cannot compile (%s): %s.', + spec._property_names_error or 'unsupported construct', spec.propertyNames + )) + else + for key in pairs(value) do + if type(key) == 'string' and not _pattern_matches(key, spec._property_names) then + _report(context, 'error', path .. '.' .. key, 'propertyNames', string.format( + 'key does not match required pattern: %s.', spec.propertyNames + )) + end + end + end + end + + local defaulted = {} + + if type(spec.properties) == 'table' then + local sub, filled = _validate_map(value, spec.properties, path, context, { + unknown = spec.additionalProperties == false and 'warn' or 'ignore', + additional = type(spec.additionalProperties) == 'table' and spec.additionalProperties or nil, + }) + + -- `_validate_map` builds a new table, so its defaults, coercion and alias + -- resolution have to be written back into the value the parent holds. + -- Without this a nested `properties` contributes nothing to `merged`. + local stale = {} + for key in pairs(value) do + if sub[key] == nil then + stale[#stale + 1] = key + end + end + for _, key in ipairs(stale) do + value[key] = nil + end + for key, member in pairs(sub) do + value[key] = member + end + defaulted = filled + elseif type(spec.additionalProperties) == 'table' then + for key, member in pairs(value) do + local coerced = _coerce(member, spec.additionalProperties.type) + value[key] = coerced + if coerced ~= nil then + _validate_value(coerced, spec.additionalProperties, path .. '.' .. tostring(key), context) + end + end + end + + -- Runs after the properties block, which writes the resolved members back + -- into `value`. Before that, a dependent supplied under an alias is not yet + -- there to be found. A `default` is an annotation and does not change the + -- instance, so a key that only holds one neither triggers a requirement nor + -- satisfies it. + if type(spec.dependentRequired) == 'table' then + for key, dependents in pairs(spec.dependentRequired) do + local trigger, trigger_key = _lookup(value, key) + if trigger ~= nil and not defaulted[trigger_key] and type(dependents) == 'table' then + for _, dependent in ipairs(dependents) do + local supplied, dependent_key = _lookup(value, dependent) + if supplied == nil or defaulted[dependent_key] then + _report(context, 'error', path, 'dependentRequired', string.format( + 'requires "%s" when "%s" is present.', dependent, key + )) + end + end + end + end + end +end + +--- Run every keyword check against one value. +--- A failed type check short-circuits, so one mistake yields one message. +--- @param value any Value to validate +--- @param spec table Compiled descriptor +--- @param path string Dotted path of the value +--- @param context table Validation context +--- @return nil +_validate_value = function(value, spec, path, context) + local names = _type_names(spec.type) + + if names then + for _, name in ipairs(names) do + if name == 'content' then + return + end + end + end + + if not _matches_type_spec(value, spec.type) then + _report(context, 'error', path, 'type', string.format( + 'must be of type "%s", got "%s".', _format_type(spec.type), _actual_type(value) + )) + return + end + + if spec.const ~= nil and value ~= spec.const then + _report(context, 'error', path, 'const', string.format( + 'must be %s, got %s.', _format_value(spec.const), _format_value(value) + )) + end + + _check_enum(value, spec, path, context) + _check_pattern(value, spec, path, context) + _check_numeric(value, spec, path, context) + _check_string_length(value, spec, path, context) + _check_array(value, spec, path, context) + _check_items(value, spec, path, context) + _check_object(value, spec, path, context) +end + +-- ============================================================================ +-- MAP VALIDATION +-- ============================================================================ + +--- Build the deprecation warning for a descriptor, and forward its value. +--- @param field string Field name +--- @param spec table Compiled descriptor +--- @param value any Current value +--- @param merged table Working values +--- @return string message Warning text +--- @return boolean cleared True when the deprecated key was removed +local function _apply_deprecation(field, spec, value, merged) + local deprecated = spec.deprecated + + if type(deprecated) == 'string' then + return string.format('option "%s" is deprecated. %s', field, deprecated), false + end + + if type(deprecated) ~= 'table' then + return string.format('option "%s" is deprecated.', field), false + end + + local message + if deprecated.since then + message = string.format('option "%s" is deprecated since %s.', field, deprecated.since) + else + message = string.format('option "%s" is deprecated.', field) + end + if deprecated.message then + message = message .. ' ' .. deprecated.message + end + + -- A v1 schema writes `replace-with`, which this vocabulary does not accept. + -- Say so, rather than dropping the forwarding without a word. + local unrecognised = {} + for key in pairs(deprecated) do + if key ~= 'since' and key ~= 'message' and key ~= 'replaceWith' then + unrecognised[#unrecognised + 1] = key + end + end + if #unrecognised > 0 then + table.sort(unrecognised) + message = message .. string.format( + ' The deprecation declares %s, which this vocabulary does not accept; use "replaceWith".', + table.concat(unrecognised, ', ') + ) + end + + local cleared = false + if deprecated.replaceWith then + local replacement = deprecated.replaceWith + message = message .. string.format(' Use "%s" instead.', replacement) + if _lookup(merged, replacement) == nil then + merged[replacement] = value + end + merged[field] = nil + cleared = true + end + + return message, cleared +end + +--- Record that a descriptor accepts a spelling, and report a contested one. +--- +--- Two descriptors can name the same spelling, when one declares as an alias +--- what another declares as its own name, or when two of them declare the same +--- alias. `pairs` yields descriptors in no particular order, so which field a +--- contested spelling resolves to is not decidable from the schema. It is +--- reported rather than resolved in silence, because the schema is what is +--- wrong and only its author can settle it. +--- +--- @param sources table Map of spelling to declared name +--- @param spelling string The spelling being declared +--- @param field string The declared name that accepts it +--- @param base_path string|nil Path prefix for findings +--- @param context table Validation context +local function _declare_spelling(sources, spelling, field, base_path, context) + -- The owner is read through `_lookup`, like every other name comparison in + -- this module, so a contest between the hyphen and the underscore spelling + -- of one name is found as well. Those sit under different keys but reach the + -- same document key at merge time, so they contest just as directly. + local owner = _lookup(sources, spelling) + if owner ~= nil and owner ~= field then + _report(context, 'warning', base_path and (base_path .. '.' .. spelling) or spelling, + 'aliases', string.format( + 'is declared by both "%s" and "%s"; which one accepts it is not defined.', + owner, field)) + end + -- Stored under the literal spelling either way. The map is also the set of + -- keys a descriptor claimed, and a contested spelling is still claimed: the + -- schema is ambiguous, which the warning says, and treating the key as + -- undeclared on top of that would report it as unknown as well. + sources[spelling] = sources[spelling] or field +end + + +--- Validate a map of values against a map of field descriptors. +--- @param values table Values to validate +--- @param descriptors table Field descriptor map +--- @param base_path string|nil Path prefix for findings +--- @param context table Validation context +--- @param options table|nil {unknown = 'warn'|'error'|'ignore', additional = descriptor} +--- @return table merged Values with aliases, coercion and defaults applied +--- @return table defaulted Set of field names whose value came from `default` +--- @return table sources Map of every spelling the schema accepts to the +--- declared name it resolves to. A reader that has to answer a question +--- about a name the caller wrote asks this rather than walking the +--- descriptors again, because the merge has already decided the answer. +--- It doubles as the set of keys a descriptor claimed, which is what the +--- unknown key pass below reads. +_validate_map = function(values, descriptors, base_path, context, options) + options = options or {} + local unknown_policy = options.unknown or 'ignore' + + local merged = {} + for key, value in pairs(values) do + merged[key] = value + end + + local defaulted = {} + local sources = {} + local fields = {} + + -- Three passes, because `pairs` yields descriptors in no particular order. + -- Aliases must land before deprecation reads a value, and a `replaceWith` + -- target must be populated before its own descriptor is validated. + for field, raw_spec in pairs(descriptors) do + local spec = _compile(raw_spec) + fields[#fields + 1] = { + name = field, + spec = spec, + path = base_path and (base_path .. '.' .. field) or field, + } + -- Two writes with different weight. Declaring a spelling fills an empty + -- slot only, while claiming the value under one overwrites whatever was + -- there. The value lands under whichever descriptor claimed it, so the + -- claim is what the map has to follow: a declaration that outranked it + -- would send a reader to a field the merge has already emptied. + _declare_spelling(sources, field, field, base_path, context) + + -- A value found under an alias, or under the other spelling, moves to the + -- name the schema declares. The key it came from is removed, so `merged` + -- never carries the same value twice, once coerced and once raw. + -- The declared name is read first, so it wins over any alias. + local supplied_key + local found, found_key = _lookup(merged, field) + if found ~= nil then + merged[field] = found + sources[found_key] = field + supplied_key = found_key + if found_key ~= field then + merged[found_key] = nil + end + end + + if type(spec.aliases) == 'table' then + for _, alias in ipairs(spec.aliases) do + _declare_spelling(sources, alias, field, base_path, context) + local aliased, alias_key = _lookup(merged, alias) + if aliased ~= nil then + sources[alias_key] = field + if merged[field] == nil then + merged[field] = aliased + supplied_key = alias_key + elseif alias_key ~= field then + -- Two spellings were supplied. The first one read wins, and the + -- other is reported rather than left behind unvalidated. Both + -- names come from the document, never from the schema. + _report(context, 'warning', + base_path and (base_path .. '.' .. field) or field, + 'aliases', + string.format('was given as both "%s" and "%s"; "%s" was used.', + supplied_key, alias_key, supplied_key)) + end + if alias_key ~= field then + merged[alias_key] = nil + end + end + end + end + end + + for _, entry in ipairs(fields) do + local value = merged[entry.name] + if entry.spec.deprecated and value ~= nil then + local message, cleared = _apply_deprecation(entry.name, entry.spec, value, merged) + _report(context, 'warning', entry.path, 'deprecated', message) + entry.cleared = cleared + end + end + + for _, entry in ipairs(fields) do + local field, spec, path = entry.name, entry.spec, entry.path + local value + + if entry.cleared then + value = nil + else + value = _coerce(merged[field], spec.type) + merged[field] = value + + if value == nil and spec.default ~= nil then + value = spec.default + merged[field] = value + defaulted[field] = true + end + end + + -- An empty string is a value the author wrote. Only a missing key is + -- absent, so `minLength`, `const` and `enum` can be tested against ''. + if spec.required == true and value == nil then + _report(context, 'error', path, 'required', 'is required but was not provided.') + elseif value ~= nil then + _validate_value(value, spec, path, context) + end + end + + if unknown_policy ~= 'ignore' or options.additional then + for key, value in pairs(values) do + if sources[key] == nil then + local path = base_path and (base_path .. '.' .. tostring(key)) or tostring(key) + if options.additional then + local coerced = _coerce(value, options.additional.type) + merged[key] = coerced + if coerced ~= nil then + _validate_value(coerced, options.additional, path, context) + end + elseif unknown_policy == 'error' then + _report(context, 'error', path, 'additionalProperties', 'is not a recognised key.') + else + _report(context, 'warning', path, 'additionalProperties', 'is not a recognised key and was ignored.') + end + end + end + end + + return merged, defaulted, sources +end + +--- Start a validation run. +--- @return table Fresh validation context +local function _new_context() + return { findings = {} } +end + +--- Close a validation run and shape the public return values. +--- @param context table Validation context +--- @param merged table Merged values +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged +--- @return table findings +local function _finish(context, merged) + local errors, warnings = _split_findings(context.findings) + return #errors == 0, errors, warnings, merged, context.findings +end + +-- ============================================================================ +-- PUBLIC API +-- ============================================================================ + +--- Load and parse a schema file. +--- Sections absent from the file default to an empty table. +--- +--- A failure is returned, not raised. Quarto replaces `error` with a logger +--- that returns instead of unwinding, so raising here would let a caller +--- carry on with a nil schema and fail later somewhere unrelated. +--- +--- @param filename string|nil Path to the schema file (defaults to '_schema.yml') +--- @return table|nil schema Schema with {['$schema'], options, shortcodes, formats, projects, attributes, classes, cells} +--- @return string|nil err Why the schema could not be read +--- @usage local schema, err = M.load_schema('_schema.yml') +function M.load_schema(filename) + local raw, err = _parse_yaml_file(filename or '_schema.yml') + if err then + return nil, err + end + + return { + ['$schema'] = raw['$schema'], + options = raw.options or {}, + shortcodes = raw.shortcodes or {}, + formats = raw.formats or {}, + projects = raw.projects or {}, + attributes = raw.attributes or {}, + classes = raw.classes or {}, + cells = raw.cells or {}, + }, nil +end + +--- List the keys of a parsed mapping in the order the schema declared them. +--- A schema is authored in a meaningful order, and a generator that turns one +--- into documentation needs that order rather than an arbitrary one. +--- Falls back to sorted keys for a table this module did not parse. +--- @param map table A mapping from a loaded schema +--- @return table Array of keys +--- @usage for _, name in ipairs(schema.key_order(loaded.options)) do +function M.key_order(map) + if type(map) ~= 'table' then + return {} + end + + local order = _key_order[map] + if order ~= nil then + local copy = {} + for index, key in ipairs(order) do + copy[index] = key + end + return copy + end + + local keys = {} + for key in pairs(map) do + keys[#keys + 1] = key + end + table.sort(keys) + return keys +end + +--- Validate a map of values against a map of field descriptors. +--- @param values table Values to validate +--- @param descriptors table Field descriptor map +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid True when no error was reported +--- @return table errors Array of error strings +--- @return table warnings Array of warning strings +--- @return table merged Values with aliases, coercion and defaults applied +--- @return table findings Array of structured findings +--- @usage local valid, errors, warnings, merged = M.validate(options, schema.options) +function M.validate(values, descriptors, options) + options = options or {} + local context = _new_context() + local merged = _validate_map(values or {}, descriptors or {}, nil, context, { + unknown = options.unknown or 'warn', + }) + return _finish(context, merged) +end + +--- Validate positional shortcode arguments against an argument descriptor array. +--- Each descriptor should carry a `name`; the merged table is keyed by name. +--- @param args table Array of positional argument values +--- @param argument_specs table Array of argument descriptors +--- @param options table|nil {unknown = 'warn'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged Map keyed by argument name +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_arguments(args, specs) +function M.validate_arguments(args, argument_specs, options) + options = options or {} + args = args or {} + argument_specs = argument_specs or {} + + local context = _new_context() + local merged = {} + + for index, raw_spec in ipairs(argument_specs) do + local spec = _compile(raw_spec) + local name = spec.name or tostring(index) + local path = string.format('argument %d ("%s")', index, name) + + local value = _coerce(args[index], spec.type) + if value == nil and spec.default ~= nil then + value = spec.default + end + merged[name] = value + + if spec.required == true and value == nil then + _report(context, 'error', path, 'required', 'is required but was not provided.') + elseif value ~= nil then + _validate_value(value, spec, path, context) + end + end + + if (options.unknown or 'warn') ~= 'ignore' and #args > #argument_specs then + _report(context, 'warning', nil, 'arguments', string.format( + 'Received %d arguments but only %d are defined. The extra arguments were ignored.', + #args, #argument_specs + )) + end + + return _finish(context, merged) +end + +--- Validate one shortcode call against its schema entry. +--- Covers the positional `arguments`, the named `attributes`, and the +--- parent-level `required` array of attribute names. +--- @param name string Shortcode name, used as the path prefix +--- @param args table Array of positional argument values +--- @param kwargs table Map of named attribute values +--- @param entry table Shortcode entry from `schema.shortcodes[name]` +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged {arguments = ..., attributes = ...} +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_shortcode('iconify', args, kwargs, schema.shortcodes.iconify) +function M.validate_shortcode(name, args, kwargs, entry, options) + options = options or {} + entry = entry or {} + + local context = _new_context() + local merged = { arguments = {}, attributes = {} } + + if type(entry.arguments) == 'table' then + local _, errors, warnings, arguments = M.validate_arguments(args or {}, entry.arguments, options) + merged.arguments = arguments + for _, message in ipairs(errors) do + _report(context, 'error', nil, 'arguments', name .. ' ' .. message) + end + for _, message in ipairs(warnings) do + _report(context, 'warning', nil, 'arguments', name .. ' ' .. message) + end + end + + local declared = type(entry.attributes) == 'table' + local spellings + if declared then + local attributes, _, resolved = _validate_map(kwargs or {}, entry.attributes, name, context, { + unknown = options.unknown or 'warn', + }) + merged.attributes = attributes + spellings = resolved + end + + if type(entry.required) == 'table' then + -- `merged.attributes` is filled only when the entry declares `attributes`, + -- and it is authoritative when it is: it keeps every unclaimed key and it + -- drops a key that a deprecation cleared. The vocabulary allows `required` + -- on its own, so fall back to what the caller supplied only when the + -- entry declares no `attributes`. A name that names an alias is not + -- missing either: `_validate_map` moves its value to the field that + -- declares the alias and clears the alias spelling, the same way it + -- clears a deprecated key, so look for the value under that field. The + -- merge reports which declared name each spelling resolves to, so that + -- is one lookup rather than a second walk over the descriptors. It goes + -- through `_lookup`, the same as every other name comparison in this + -- module, so a hyphen in one spelling and an underscore in the other + -- still match. + -- + -- This asks what the shortcode receives, not what the author wrote, so + -- a `default` present in `merged.attributes` satisfies a name here. + -- That is the opposite of `dependentRequired` above, which asks what + -- the author wrote and treats a default as an annotation nobody + -- supplied. Neither reading is a bug: they answer different questions + -- about the same instance. + local function _required_satisfied(required) + local field = (spellings and _lookup(spellings, required)) or required + if _lookup(merged.attributes, field) ~= nil then + return true + end + if not declared then + return _lookup(kwargs or {}, required) ~= nil + end + return false + end + + for _, required in ipairs(entry.required) do + if not _required_satisfied(required) then + _report(context, 'error', name .. '.' .. required, 'required', + 'is required but was not provided.') + end + end + end + + return _finish(context, merged) +end + +--- Validate Pandoc element attributes against one group of the `attributes` +--- section. Undeclared attributes are ignored by default, because a Pandoc +--- element legitimately carries attributes from Quarto and other filters. +--- @param attributes table Map of attribute values +--- @param group string Group key, such as a class, identifier or element name +--- @param schema table Loaded schema +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_attributes(el.attributes, 'callout-note', schema) +function M.validate_attributes(attributes, group, schema, options) + options = options or {} + local descriptors = schema and schema.attributes and _lookup(schema.attributes, group) or nil + + local context = _new_context() + if descriptors == nil then + return _finish(context, attributes or {}) + end + + local merged = _validate_map(attributes or {}, descriptors, group, context, { + unknown = options.unknown or 'ignore', + }) + return _finish(context, merged) +end + +--- Validate the comment-pipe options of one code cell against the `cells` +--- section. The language is matched case-insensitively, because Quarto treats +--- `{Typst}` and `{typst}` as the same cell language. Undeclared options are +--- ignored by default, because a cell legitimately carries options from Quarto +--- and from other filters. +--- @param options table Map of option values, as read from the comment-pipe lines +--- @param language string Cell language, such as 'typst' +--- @param schema table Loaded schema +--- @param opts table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_cells(cell_options, 'typst', schema) +function M.validate_cells(options, language, schema, opts) + opts = opts or {} + local entry + if schema and type(schema.cells) == 'table' and type(language) == 'string' then + entry = _lookup(schema.cells, language) + if entry == nil then + local wanted = language:lower() + for key, candidate in pairs(schema.cells) do + if type(key) == 'string' and key:lower() == wanted then + entry = candidate + break + end + end + end + end + + local context = _new_context() + if type(entry) ~= 'table' or type(entry.options) ~= 'table' then + return _finish(context, options or {}) + end + + local merged = _validate_map(options or {}, entry.options, language, context, { + unknown = opts.unknown or 'ignore', + }) + return _finish(context, merged) +end + +--- Validate the options of one output format against the `formats` section. +--- Quarto merges the options of the selected format into the top level of the +--- metadata, so the values are read from there. The format name is never a key. +--- A value is collected only when a descriptor declares its name, which keeps +--- the document's own keys, such as `title`, out of the merge. The `unknown` +--- option therefore has nothing to report here, and is kept so that the entry +--- points keep one signature. +--- @param meta table Document metadata +--- @param format string Format name, such as 'html' or 'typst' +--- @param schema table Loaded schema +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return boolean valid +--- @return table errors +--- @return table warnings +--- @return table merged +--- @return table findings +--- @usage local valid, errors, warnings, merged = M.validate_format(meta, 'typst', schema) +function M.validate_format(meta, format, schema, options) + options = options or {} + local descriptors = schema and schema.formats and _lookup(schema.formats, format) or nil + + local context = _new_context() + if descriptors == nil then + return _finish(context, {}) + end + + -- Each value is stored under the spelling the document wrote, never under the + -- declared name. `_validate_map` is what moves an alias or the other spelling + -- to the declared name, and it needs the original key to say which two + -- spellings a document supplied for one field. + -- + -- `_lookup` returns the first spelling it finds, so a document that wrote one + -- name in both spellings loses the other. Every other entry point reports the + -- loser through the `unknown` option, which has nothing to report here, so the + -- collection reports it in the same words `_validate_map` uses. + local values = {} + for field, raw_spec in pairs(descriptors) do + local spec = _compile(raw_spec) + + -- One entry per name, and not one per spelling. `_lookup` reads both + -- spellings of whichever entry is kept, so an alias that is only the other + -- spelling of a name already listed adds nothing. Probing it as well would + -- collect the two spellings under separate keys, and one mistake would then + -- be reported three times, once by each probe and once by `_validate_map`. + local names = { field } + local listed = { [(field:gsub('_', '-'))] = true } + if type(spec) == 'table' and type(spec.aliases) == 'table' then + for _, alias in ipairs(spec.aliases) do + -- A schema file is not read against the meta-schema, so an unquoted + -- `no` reaches here as a boolean. `_lookup` skips a key that is not a + -- string everywhere else, and so does this. + if type(alias) == 'string' then + local normalised = (alias:gsub('_', '-')) + if not listed[normalised] then + listed[normalised] = true + names[#names + 1] = alias + end + end + end + end + + for _, name in ipairs(names) do + local value, found_key = _lookup(meta, name) + if value ~= nil then + values[found_key] = _convert_pandoc_value(value) + + for _, other in ipairs({ (name:gsub('%-', '_')), (name:gsub('_', '-')) }) do + if other ~= found_key and meta[other] ~= nil then + _report(context, 'warning', format .. '.' .. field, 'aliases', + string.format('was given as both "%s" and "%s"; "%s" was used.', + found_key, other, found_key)) + end + end + end + end + end + + local merged = _validate_map(values, descriptors, format, context, { + unknown = options.unknown or 'ignore', + }) + return _finish(context, merged) +end + +--- Extract an extension's options from document metadata. +--- Reads `meta.extensions[extension_name]` and converts Pandoc values to +--- native Lua values, keeping every key exactly as the document wrote it. +--- @param meta table Document metadata +--- @param extension_name string Extension name +--- @return table Map of option values +--- @usage local options = M.extract_meta_options(meta, 'iconify') +function M.extract_meta_options(meta, extension_name) + local extension_meta = meta and meta['extensions'] and meta['extensions'][extension_name] + if not extension_meta then + return {} + end + + local options = {} + for key, value in pairs(extension_meta) do + options[tostring(key)] = _convert_pandoc_value(value) + end + return options +end + +--- Load a schema, validate an extension's options, and log the outcome. +--- A schema that cannot be loaded is reported and the render continues with +--- no validated options, rather than aborting on a configuration file. +--- @param meta table Document metadata +--- @param extension_name string Extension name +--- @param schema_path string Path to the schema file, resolved by the caller +--- @param options table|nil {unknown = 'warn'|'error'|'ignore'} +--- @return table merged Validated options with defaults applied +--- @usage local options = M.validate_options(meta, 'iconify', quarto.utils.resolve_path('_schema.yml')) +function M.validate_options(meta, extension_name, schema_path, options) + local schema, err = M.load_schema(schema_path) + if err then + _env.report_error(string.format('Extension "%s": %s', extension_name, err)) + return {} + end + + if not schema.options or next(schema.options) == nil then + return {} + end + + local values = M.extract_meta_options(meta, extension_name) + local valid, errors, warnings, merged = M.validate(values, schema.options, options) + + if #warnings > 0 then + _env.warn(M.format_warnings(warnings, extension_name)) + end + if not valid then + _env.report_error(M.format_errors(errors, extension_name)) + end + + return merged +end + +--- Format error messages as one readable block. +--- @param errors table Array of error strings +--- @param extension_name string|nil Extension name for context +--- @return string Formatted message, or an empty string +--- @usage local message = M.format_errors(errors, 'iconify') +function M.format_errors(errors, extension_name) + if #errors == 0 then + return '' + end + + local prefix = extension_name and ('Extension "' .. extension_name .. '": ') or '' + local lines = {} + for index, message in ipairs(errors) do + table.insert(lines, ' ' .. index .. '. ' .. message) + end + + return prefix .. 'Configuration validation failed:\n' .. table.concat(lines, '\n') +end + +--- Format warning messages as one readable block. +--- @param warnings table Array of warning strings +--- @param extension_name string|nil Extension name for context +--- @return string Formatted message, or an empty string +--- @usage local message = M.format_warnings(warnings, 'iconify') +function M.format_warnings(warnings, extension_name) + if #warnings == 0 then + return '' + end + + local prefix = extension_name and ('Extension "' .. extension_name .. '": ') or '' + local lines = {} + for index, message in ipairs(warnings) do + table.insert(lines, ' ' .. index .. '. ' .. message) + end + + return prefix .. 'Configuration warnings:\n' .. table.concat(lines, '\n') +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/gitlink.css b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/gitlink.css new file mode 100644 index 0000000..0feade8 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/gitlink.css @@ -0,0 +1,6 @@ +.gitlink-badge { + font-size: 0.45em; + font-weight: 500; + vertical-align: super; + margin-left: 0.15em; +} diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/gitlink.lua b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/gitlink.lua new file mode 100644 index 0000000..b842744 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/gitlink.lua @@ -0,0 +1,1281 @@ +--- @module "gitlink" +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +--- Extension name constant +local EXTENSION_NAME = 'gitlink' + +--- Load modules +local str = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/string.lua'):gsub('%.lua$', '')) +local log = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/logging.lua'):gsub('%.lua$', '')) +local meta_mod = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/metadata.lua'):gsub('%.lua$', '')) +local html_mod = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/html.lua'):gsub('%.lua$', '')) +local paths = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/paths.lua'):gsub('%.lua$', '')) +local git = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/git.lua'):gsub('%.lua$', '')) +local bitbucket = require(quarto.utils.resolve_path('_modules/bitbucket.lua'):gsub('%.lua$', '')) +local platforms = require(quarto.utils.resolve_path('_modules/platforms.lua'):gsub('%.lua$', '')) +local colour = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/colour.lua'):gsub('%.lua$', '')) +local widget = require(quarto.utils.resolve_path('_modules/widget.lua'):gsub('%.lua$', '')) +local schema = require(quarto.utils.resolve_path('_vendor/quarto-wizard/schema.lua'):gsub('%.lua$', '')) +local check = require(quarto.utils.resolve_path('_vendor/quarto-lua-modules/schema-check.lua'):gsub('%.lua$', '')) + +--- Checks the document configuration against `_schema.yml` and reports what the +--- extension cannot use. +--- The validator is injected rather than required by the checker, so the two +--- vendored sources stay independent of each other. +--- The schema is read once here, at file scope, rather than once per document. +--- An unreadable schema is reported and never stops a render, because a fault +--- in the configuration must not remove the document. +local checker = check.new(schema, EXTENSION_NAME) + +--- @type string The platform type (github, gitlab, codeberg, gitea, bitbucket) +local platform = 'github' + +--- @type string|nil The repository name (e.g., "owner/repo") +local repository_name = nil + +--- @type string The base URL for the Git hosting platform +local base_url = 'https://github.com' + +--- @type table Set of reference IDs from the document +local references_ids_set = {} + +--- @type table Set of citation IDs forced to be treated as mentions +local force_mentions_set = {} + +--- @type boolean Whether the filter is enabled for this document +local is_enabled = true + +--- @type boolean Whether to show visible platform badges +local show_platform_badge = true + +--- @type string Badge position: "after" or "before" +local badge_position = 'after' + +--- @type string Badge background colour (hex or colour name) +local badge_background_colour = '#c3c3c3' + +--- @type string|nil Badge text colour (hex or colour name), nil to pick black or white +local badge_text_colour = nil + +--- @type boolean Whether this document has already warned about a faint badge background +local badge_contrast_warned = false + +--- @type boolean Whether to shorten link text matching platform URLs +local normalize_links = true + +--- @type boolean Whether to fetch issue/PR/commit titles for link text +local fetch_titles = false + +--- @type table Cache of fetched titles by URL +local title_cache = {} + +--- @type table Cached platform configurations by name (per render); false means "looked up and not found" +local platform_config_cache = {} + +--- @type table|nil Cached list of all known platform names (per render) +local all_platform_names_cache = nil + +--- @type integer Full length of a git commit SHA +local COMMIT_SHA_FULL_LENGTH = 40 + +--- @type integer Short length for displaying commit SHA +local COMMIT_SHA_SHORT_LENGTH = 7 + +--- @type integer Minimum length for a valid git commit SHA +local COMMIT_SHA_MIN_LENGTH = 7 + +--- @type string Class Quarto puts on the markdown-pipeline envelope elements +local MARKDOWN_ENVELOPE_CLASS = 'quarto-markdown-envelope-contents' + +--- @type table Digit counts of a valid hex colour: #rgb, #rgba, #rrggbb, #rrggbbaa +local HEX_COLOUR_DIGIT_COUNTS = { [3] = true, [4] = true, [6] = true, [8] = true } + +--- @type number WCAG AA minimum contrast ratio for normal text +local WCAG_AA_CONTRAST = 4.5 + +--- @type number[] Page colours behind an HTML badge: white for a light theme, black for a dark one +local HTML_PAGES = { 1, 0 } + +--- @type number[] Page colour behind a Typst badge: white +local TYPST_PAGES = { 1 } + +--- Check whether a value is a hex colour with 3, 4, 6, or 8 digits after the #. +--- @param value string The candidate colour value +--- @return boolean True if the value is a hex colour +local function is_hex_colour(value) + return value:match('^#%x+$') ~= nil and HEX_COLOUR_DIGIT_COUNTS[#value - 1] == true +end + +--- Validate a colour value as a hex code or CSS named colour. +--- Returns the original value if valid, or nil if invalid. +--- A value that is not a string returns nil without a warning, because the +--- schema check has already reported its type. +--- @param value any The candidate colour value +--- @param option_label string The metadata option name (for warnings) +--- @return string|nil The validated colour value, or nil if invalid +local function validate_colour(value, option_label) + if type(value) ~= 'string' or str.is_empty(value) then + return nil + end + local s = value --[[@as string]] + if is_hex_colour(s) or colour.is_named_colour(s) then + return s + end + log.log_warning( + EXTENSION_NAME, + "Ignoring invalid '" .. option_label .. "' value '" .. s .. + "': expected a hex colour (e.g. '#c3c3c3') or a CSS named colour." + ) + return nil +end + +--- Convert a validated colour value to its hex form for Typst rgb(). +--- Hex codes are returned unchanged; CSS named colours are resolved via +--- the colour module. Assumes the value has already been validated. +--- @param value string The colour value (hex code or CSS named colour) +--- @return string The hex form +local function colour_to_hex(value) + if is_hex_colour(value) then + return value + end + return colour.named_to_HTML(value) +end + +--- Split a validated colour value into its channels, each from 0 to 1. +--- @param value string The colour value (hex code or CSS named colour) +--- @return number[] The red, green, and blue channels +--- @return number The alpha channel, 1 when the colour has no alpha digits +local function colour_channels(value) + local digits = colour_to_hex(value):sub(2) + if #digits <= 4 then + digits = digits:gsub('%x', '%0%0') + end + local channels = {} + for i = 1, 3 do + channels[i] = tonumber(digits:sub(2 * i - 1, 2 * i), 16) / 255 + end + local alpha = #digits == 8 and tonumber(digits:sub(7, 8), 16) / 255 or 1 + return channels, alpha +end + +--- Compute the WCAG relative luminance of sRGB channels. +--- @param channels number[] The red, green, and blue channels, each from 0 to 1 +--- @return number The relative luminance, from 0 (black) to 1 (white) +local function relative_luminance(channels) + local luminance = 0 + local weights = { 0.2126, 0.7152, 0.0722 } + for i, weight in ipairs(weights) do + local channel = channels[i] + if channel <= 0.04045 then + channel = channel / 12.92 + else + channel = ((channel + 0.055) / 1.055) ^ 2.4 + end + luminance = luminance + weight * channel + end + return luminance +end + +--- Compute the WCAG contrast ratio between two relative luminances. +--- @param a number A relative luminance +--- @param b number Another relative luminance +--- @return number The contrast ratio, from 1 to 21 +local function contrast_ratio(a, b) + return (math.max(a, b) + 0.05) / (math.min(a, b) + 0.05) +end + +--- Pick black or white text for a background colour. +--- A translucent background is blended over each page colour it can sit on. +--- The text colour with the higher worst-case contrast wins. +--- @param background string The background colour (hex code or CSS named colour) +--- @param pages number[] The grey levels of the possible pages, from 0 (black) to 1 (white) +--- @return string '#000000' or '#ffffff' +--- @return number The worst-case contrast ratio of that text colour +local function contrasting_text_colour(background, pages) + local channels, alpha = colour_channels(background) + local worst_black, worst_white = math.huge, math.huge + for _, page in ipairs(pages) do + local blended = {} + for i, channel in ipairs(channels) do + blended[i] = channel * alpha + page * (1 - alpha) + end + local luminance = relative_luminance(blended) + worst_black = math.min(worst_black, contrast_ratio(luminance, 0)) + worst_white = math.min(worst_white, contrast_ratio(luminance, 1)) + end + local text, worst = '#000000', worst_black + if worst_white > worst_black then + text, worst = '#ffffff', worst_white + end + return text, worst +end + +--- Resolve the badge text colour for the pages of the current output format. +--- Returns the configured text colour when there is one. +--- Otherwise picks black or white, and warns once per document when neither +--- reaches WCAG AA on every page. +--- @param pages number[] The grey levels of the possible pages, from 0 (black) to 1 (white) +--- @return string The text colour (hex code or CSS named colour) +local function resolve_badge_text_colour(pages) + if badge_text_colour then + return badge_text_colour + end + local text, worst = contrasting_text_colour(badge_background_colour, pages) + if worst < WCAG_AA_CONTRAST and not badge_contrast_warned then + badge_contrast_warned = true + log.log_warning( + EXTENSION_NAME, + "'badge-background-colour' value '" .. badge_background_colour .. "' is too transparent for readable text " .. + "(" .. string.format('%.1f', worst) .. ':1 at worst, below ' .. WCAG_AA_CONTRAST .. ':1). ' .. + "Use a more opaque colour or a valid 'badge-text-colour'." + ) + end + return text +end + +--- Reset module-level state to defaults. +--- Quarto can render multiple documents in one process, so module-level state +--- from a previous document must be cleared at the start of each Meta pass. +--- `references_ids_set` is rebuilt by `get_references` in its own pass. +local function reset_state() + platform = 'github' + repository_name = nil + base_url = 'https://github.com' + force_mentions_set = {} + is_enabled = true + show_platform_badge = true + badge_position = 'after' + badge_background_colour = '#c3c3c3' + badge_text_colour = nil + badge_contrast_warned = false + normalize_links = true + fetch_titles = false + title_cache = {} + platform_config_cache = {} + all_platform_names_cache = nil + if platforms.clear_custom_platforms then + platforms.clear_custom_platforms() + end + -- Dependency tracking is per document; without a reset, dependencies added + -- for a previous document in the same process would be skipped here. + html_mod.reset_dependencies() +end + +--- Get the cached list of all known platform names. +--- @return table List of platform names +local function get_all_platform_names() + if not all_platform_names_cache then + all_platform_names_cache = platforms.get_all_platform_names() + end + return all_platform_names_cache +end + +--- Get platform configuration (cached per render). +--- Memoises lookups against `platform_config_cache` to avoid repeated calls +--- to the platforms module for every Str element in the document. +--- The cache stores `false` for "looked up and not found" so repeated misses +--- skip the underlying lookup. +--- @param platform_name string The platform name +--- @return table|nil The platform configuration or nil if not found +local function get_platform_config(platform_name) + if not platform_name then + return nil + end + local key = platform_name:lower() + local cached = platform_config_cache[key] + if cached ~= nil then + return cached or nil + end + local config = platforms.get_platform_config(key) + platform_config_cache[key] = config or false + return config +end + +--- Parse a full repository URL to extract platform, base-url, and owner/repo. +--- Matches the URL against all known platform base URLs. +--- @param url string The full repository URL (e.g., "https://github.com/owner/repo") +--- @return string|nil platform_name The matched platform name +--- @return string|nil matched_base_url The base URL portion +--- @return string|nil repo_path The owner/repo portion +local function parse_repo_url(url) + local all_names = get_all_platform_names() + for _, name in ipairs(all_names) do + local config = get_platform_config(name) + if config and config.base_url then + local escaped = str.escape_pattern(config.base_url) + local repo_path = url:match('^' .. escaped .. '/(.+)$') + if repo_path then + repo_path = repo_path:match('^([^%?#]+)') or repo_path + repo_path = repo_path:gsub('%.git$', ''):gsub('/$', '') + if not str.is_empty(repo_path) then + return name, config.base_url, repo_path + end + end + end + end + return nil, nil, nil +end + +--- Create a link with platform label +--- @param text string|nil The link text +--- @param uri string|nil The URI +--- @param platform_name string|nil The platform name +--- @return pandoc.Link|pandoc.Span|nil A Pandoc Link element with platform label or Span containing link and badge +local function create_platform_link(text, uri, platform_name) + if str.is_empty(uri) or str.is_empty(text) or str.is_empty(platform_name) then + return nil + end + + local platform_label = platforms.get_platform_display_name(platform_name --[[@as string]]) + + local link_content = { pandoc.Str(text --[[@as string]]) } + local link_attr = pandoc.Attr('', {}, {}) + + if quarto.doc.is_format("html:js") or quarto.doc.is_format("html") then + link_attr = pandoc.Attr('', {}, { title = platform_label }) + local link = pandoc.Link(link_content, uri --[[@as string]], '', link_attr) + + if show_platform_badge then + local css_path = quarto.utils.resolve_path("gitlink.css") + html_mod.ensure_html_dependency({ + name = 'quarto-gitlink', + version = '1.0.0', + stylesheets = { css_path } + }) + + local badge_attr = pandoc.Attr( + '', + { 'gitlink-badge', 'badge' }, + { + title = platform_label, + ['aria-label'] = platform_label .. ' platform', + style = 'background-color: ' .. badge_background_colour .. + '; color: ' .. resolve_badge_text_colour(HTML_PAGES) .. ';' + } + ) + local badge = pandoc.Span({ pandoc.Str(platform_label) }, badge_attr) + + local inlines = {} + if badge_position == "before" then + inlines = { badge, pandoc.Space(), link } + else + inlines = { link, badge } + end + + return pandoc.Span(inlines) + else + return link + end + elseif quarto.doc.is_format("typst") then + local link = pandoc.Link(link_content, uri --[[@as string]], '', link_attr) + + if show_platform_badge then + -- Typst rgb() only accepts hex strings, so convert any CSS-named colour + -- (already validated at Meta time) to its hex equivalent. + local bg_hex = colour_to_hex(badge_background_colour) + local text_hex = colour_to_hex(resolve_badge_text_colour(TYPST_PAGES)) + local badge_raw = '#box(fill: rgb("' .. + bg_hex .. + '"), inset: 2pt, outset: 0pt, radius: 3pt, baseline: -0.3em, text(size: 0.45em, fill: rgb("' .. + text_hex .. '"), [' .. platform_label .. ']))' + local badge = pandoc.RawInline('typst', ' ' .. badge_raw) + + local inlines = {} + if badge_position == "before" then + inlines = { badge, pandoc.Space(), link } + else + inlines = { link, badge } + end + + return pandoc.Span(inlines) + else + return link + end + else + table.insert(link_content, pandoc.Space()) + table.insert(link_content, pandoc.Str("(" .. platform_label .. ")")) + return pandoc.Link(link_content, uri --[[@as string]], '', link_attr) + end +end + +--- Get repository name from metadata or git remote. +--- This function extracts the repository name either from document metadata +--- or by querying the git remote origin URL. +--- @param meta table The document metadata table. +--- @return table The metadata table (unchanged). +local function get_repository(meta) + -- Reset module-level state at the start of every document so a previous + -- render in a batch does not bleed into this one. + reset_state() + + -- After the reset and before the first option read, so that every option this + -- pass goes on to read has already been reported on. + checker:options(meta) + + -- Allow opt-out at the document level for drafts, templates, or any + -- document where automatic link rewriting is undesirable. The navbar + -- widget is gated independently so a site can run widget-only with + -- `enabled: false` and `widget.enabled: true`. + local extensions_meta = meta and meta['extensions'] + local gitlink_meta = extensions_meta and extensions_meta['gitlink'] + local widget_meta = gitlink_meta and gitlink_meta['widget'] + local widget_enabled = widget.is_enabled(widget_meta) + -- The schema decides each of these, so `enabled: no` turns the filter off. + -- Reading the document itself treated every spelling but `false` as true, + -- and treated every spelling but `true` as false one flag over. + is_enabled = checker:option('enabled') ~= false + if not is_enabled and not widget_enabled then + return meta + end + + local meta_platform = meta_mod.get_metadata_value(meta, 'gitlink', 'platform') + local meta_base_url = meta_mod.get_metadata_value(meta, 'gitlink', 'base-url') + local meta_repository = meta_mod.get_metadata_value(meta, 'gitlink', 'repository-name') + local meta_custom_platforms = meta_mod.get_metadata_value(meta, 'gitlink', 'custom-platforms-file') + + if not str.is_empty(meta_custom_platforms) then + local original_path = meta_custom_platforms --[[@as string]] + local custom_file_path = paths.resolve_project_path(original_path) + local ok, err = platforms.initialise(custom_file_path) + if not ok then + log.log_error( + EXTENSION_NAME, + "Failed to load custom platforms from '" .. original_path .. "':\n" .. + (err or 'unknown error') .. '\nUsing the built-in platforms only.' + ) + meta_custom_platforms = nil + end + end + if str.is_empty(meta_custom_platforms) then + local ok, err = platforms.initialise() + if not ok then + log.log_error(EXTENSION_NAME, "Failed to load built-in platforms:\n" .. (err or 'unknown error')) + return meta + end + end + + -- Parse repo-url from project metadata (website/book). That configuration + -- is not in the document metadata, so read it once and share it with the + -- widget. + local project_meta, project_meta_error, project_meta_path = meta_mod.get_project_metadata() + if project_meta_error then + log.log_warning( + EXTENSION_NAME, + "Could not read the project metadata (" .. project_meta_error .. ") from '" .. + project_meta_path .. "'. " .. + "'website.repo-url' and 'book.repo-url' are ignored, " .. + "and the widget placeholder check is skipped." + ) + end + local project_repo_url = project_meta and meta_mod.get_project_repo_url(project_meta) + local parsed_platform, parsed_base_url, parsed_repo_name = nil, nil, nil + if project_repo_url then + parsed_platform, parsed_base_url, parsed_repo_name = parse_repo_url(project_repo_url) + if not parsed_platform then + log.log_warning( + EXTENSION_NAME, + "Could not match project repo-url '" .. project_repo_url .. + "' to any known platform. Falling back to default resolution." + ) + end + end + + -- Resolve platform: explicit metadata > repo-url detection > default 'github' + if not str.is_empty(meta_platform) then + platform = (meta_platform --[[@as string]]):lower() + elseif parsed_platform then + platform = parsed_platform + else + platform = 'github' + end + + local config = get_platform_config(platform) + if not config then + local available_platforms = table.concat(get_all_platform_names(), ', ') + log.log_error( + EXTENSION_NAME, + "Unsupported platform: '" .. platform .. + "'. Supported platforms are: " .. available_platforms .. '.' + ) + return meta + end + + -- Resolve base-url: explicit metadata > repo-url detection > platform default + if not str.is_empty(meta_base_url) then + base_url = meta_base_url --[[@as string]] + elseif parsed_base_url then + base_url = parsed_base_url + else + base_url = config.base_url + end + + -- Resolve repository-name: explicit metadata > repo-url path > git remote + if not str.is_empty(meta_repository) then + repository_name = meta_repository + elseif parsed_repo_name then + repository_name = parsed_repo_name + else + repository_name = git.get_repository() + end + + show_platform_badge = checker:option('show-platform-badge') ~= false + + -- Read through the schema, which applies the defaults and resolves the + -- `-color` spelling aliases. + badge_position = checker:option('badge-position') or badge_position + badge_background_colour = validate_colour(checker:option('badge-background-colour'), 'badge-background-colour') + or badge_background_colour + badge_text_colour = validate_colour(checker:option('badge-text-colour'), 'badge-text-colour') + + normalize_links = checker:option('normalize-links') ~= false + + fetch_titles = checker:option('fetch-titles') == true + + -- Read the optional `mentions` list (citation IDs to force-treat as mentions). + -- Direct table access because get_metadata_value flattens lists via stringify. + local mentions_meta = gitlink_meta and gitlink_meta['mentions'] + if mentions_meta then + if type(mentions_meta) == 'table' then + for _, mention in ipairs(mentions_meta) do + local id = str.stringify(mention) + if not str.is_empty(id) then + force_mentions_set[id] = true + end + end + else + local id = str.stringify(mentions_meta) + if not str.is_empty(id) then + force_mentions_set[id] = true + end + end + end + + if widget_enabled then + widget.setup({ + extension_name = EXTENSION_NAME, + widget_meta = widget_meta, + platform = platform, + platform_config = config, + base_url = base_url, + repository_name = repository_name, + format_meta = project_meta, + display_name = platforms.get_platform_display_name(platform), + }) + end + + return meta +end + +--- Turn a Typst bibliography entry into a path that Pandoc can read. +--- Quarto stores it as raw Typst, which `str.stringify` drops, and Typst reads +--- a leading `/` from the project root. +--- Remove when https://github.com/quarto-dev/quarto-cli/issues/11552 and +--- https://github.com/quarto-dev/quarto-cli/issues/12553 are fixed. +--- @param entry pandoc.MetaValue A bibliography entry +--- @return string The bibliography path +local function typst_bibliography_path(entry) + if pandoc.utils.type(entry) == 'Inlines' then + entry = entry:walk({ + RawInline = function(raw) return pandoc.Str(raw.text) end + }) + end + return paths.resolve_project_path(str.stringify(entry)) +end + +--- Extract and store reference IDs from the document +--- This function collects all reference IDs from the document to distinguish +--- between actual citations and Git hosting mentions +--- @param doc pandoc.Pandoc The Pandoc document +--- @return pandoc.Pandoc The document (unchanged) +local function get_references(doc) + references_ids_set = {} + if not is_enabled then + return doc + end + local source = doc + local bibliography = doc.meta.bibliography + if bibliography and quarto.doc.is_format('typst') then + local meta = {} + for key, value in pairs(doc.meta) do + meta[key] = value + end + local entries = pandoc.utils.type(bibliography) == 'List' and bibliography or { bibliography } + meta.bibliography = pandoc.List.map(entries, typst_bibliography_path) + source = pandoc.Pandoc(doc.blocks, meta) + end + for _, reference in ipairs(pandoc.utils.references(source)) do + if reference.id then + references_ids_set[reference.id] = true + end + end + return doc +end + +--- Remove the anchors a custom pattern may already carry. +--- @param pattern string A `user`, `issue`, `merge-request` or `commit` pattern of a platform +--- @return string The pattern without a leading `^` or a trailing unescaped `$` +local function strip_anchors(pattern) + local stripped = pattern:gsub('^%^', '') + if stripped:sub(-1) == '$' and stripped:sub(-2, -2) ~= '%' then + stripped = stripped:sub(1, -2) + end + return stripped +end + +--- Match a whole `@username` against the `user` pattern of a platform. +--- A partial match does not count, and the username must end with a letter or digit. +--- @param config table The platform configuration +--- @param text string The text to match, such as `@mcanouil` +--- @return string|nil The username, or nil if the text is not a valid mention +local function match_username(config, text) + if not config.patterns.user then + return nil + end + local username = text:match('^' .. strip_anchors(config.patterns.user) .. '$') + if username and username:match('%w$') then + return username + end + return nil +end + +--- Find the username a citation id stands for. +--- An id listed in `gitlink.mentions` is used as is; an id in the bibliography is not a mention. +--- @param config table The platform configuration +--- @param id string The citation id +--- @return string|nil The username, or nil if the citation is not a mention +local function mention_username(config, id) + if force_mentions_set[id] then + return id + end + if references_ids_set[id] then + return nil + end + return match_username(config, '@' .. id) +end + +--- Build the inlines for one mention: prefix, link, suffix. +--- @param config table The platform configuration +--- @param citation pandoc.Citation The citation +--- @param username string The username +--- @return pandoc.Inlines The prefix, the mention link, and the suffix +local function mention_inlines(config, citation, username) + local uri = base_url .. config.url_formats.user:gsub('{username}', username) + local inlines = pandoc.Inlines({}) + if #citation.prefix > 0 then + inlines:extend(citation.prefix) + inlines:insert(pandoc.Space()) + end + inlines:insert(create_platform_link('@' .. username, uri, platform)) + local suffix = citation.suffix + if #suffix > 0 then + local first = suffix[1] + local hugs_link = first.t == 'Space' or (first.t == 'Str' and first.text:match('^[,;:.%)%]]') ~= nil) + if not hugs_link then + inlines:insert(pandoc.Space()) + end + inlines:extend(suffix) + end + return inlines +end + +--- Process Git hosting mentions in citations. +--- Distinguishes between actual bibliography citations and Git hosting @mentions. +--- When the citation id appears in `gitlink.mentions`, the citation is forced +--- to be treated as a mention even if a reference with that id exists. +--- A citation with several ids becomes links only when every id is a mention. +--- @param cite pandoc.Cite The citation element +--- @return pandoc.Cite|pandoc.Inlines The original citation or the mention inlines +local function process_mentions(cite) + if not is_enabled then + return cite + end + local config = get_platform_config(platform) + if not config then + return cite + end + local inlines = pandoc.Inlines({}) + for i, citation in ipairs(cite.citations) do + local username = mention_username(config, citation.id) + if not username then + return cite + end + if i > 1 then + inlines:extend({ pandoc.Str(';'), pandoc.Space() }) + end + inlines:extend(mention_inlines(config, citation, username)) + end + return inlines +end + + +--- Match a whole token against a list of issue or merge request patterns. +--- One capture is a number in the configured repository; two captures are a repository and a number. +--- @param patterns table|nil The patterns, tried in order +--- @param text string The token text +--- @param separator string The character shown between repository and number (`#` or `!`) +--- @return string|nil repo The repository +--- @return string|nil number The issue or merge request number +--- @return string|nil short_link The link text +local function match_numbered_reference(patterns, text, separator) + for _, pattern in ipairs(patterns or {}) do + local first, second = text:match('^' .. strip_anchors(pattern) .. '$') + if second then + return first, second, first .. separator .. second + elseif first then + return repository_name, first, separator .. first + end + end + return nil, nil, nil +end + +--- Match a whole token against the commit patterns of a platform. +--- One capture is a SHA in the configured repository. +--- Two captures are a repository (`owner/repo`) or a user (their fork), then a SHA. +--- @param config table The platform configuration +--- @param text string The token text +--- @return string|nil repo The repository +--- @return string|nil sha The commit SHA +--- @return string|nil short_link The link text +local function match_commit_reference(config, text) + for _, pattern in ipairs(config.patterns.commit) do + local first, second = text:match('^' .. strip_anchors(pattern) .. '$') + local sha = second or first + local valid_sha = sha and sha:match('^%x+$') + and #sha >= COMMIT_SHA_MIN_LENGTH and #sha <= COMMIT_SHA_FULL_LENGTH + if valid_sha then + local short_sha = sha:sub(1, COMMIT_SHA_SHORT_LENGTH) + if not second then + return repository_name, sha, short_sha + elseif first:find('/', 1, true) then + return first, sha, first .. '@' .. short_sha + else + local user = match_username(config, '@' .. first) + local repo_part = repository_name and repository_name:match('/(.+)') + if user and repo_part then + return user .. '/' .. repo_part, sha, user .. '@' .. short_sha + end + end + end + end + return nil, nil, nil +end + +--- Process issues and merge requests +--- @param elem pandoc.Str The string element to process +--- @param current_platform string The current platform name +--- @param current_base_url string The current base URL +--- @return pandoc.Link|nil A link or nil if no valid pattern found +--- @return string|nil The platform name used for this match +--- @return string|nil The base URL used for this match +local function process_issues_and_mrs(elem, current_platform, current_base_url) + local config = get_platform_config(current_platform) + if not config then + return nil, nil, nil + end + + local text = elem.text + local repo = nil + local number = nil + local ref_type = nil + local short_link = nil + local matched_platform = current_platform + local matched_base_url = current_base_url + + repo, number, short_link = match_numbered_reference(config.patterns.issue, text, '#') + if number then + ref_type = 'issue' + else + repo, number, short_link = match_numbered_reference(config.patterns.merge_request, text, '!') + if number then + ref_type = 'merge_request' + end + end + + if not number then + local all_platform_names = get_all_platform_names() + for _, platform_name in ipairs(all_platform_names) do + local platform_config = get_platform_config(platform_name) + if platform_config then + local platform_base_url = platform_config.base_url + local escaped_platform_url = str.escape_pattern(platform_base_url) + local url_pattern_issue = '^' .. escaped_platform_url .. '/([^/]+/[^/]+)/%-?/?issues?/(%d+)' + local url_pattern_mr = '^' .. escaped_platform_url .. '/([^/]+/[^/]+)/%-?/?merge[_%-]requests/(%d+)' + local url_pattern_pull_requests = '^' .. escaped_platform_url .. '/([^/]+/[^/]+)/%-?/?pull%-requests/(%d+)' + local url_pattern_pull = '^' .. escaped_platform_url .. '/([^/]+/[^/]+)/%-?/?pulls?/(%d+)' + + if text:match(url_pattern_issue) then + repo, number = text:match(url_pattern_issue) + ref_type = 'issue' + if repo == repository_name then + short_link = '#' .. number + else + short_link = repo .. '#' .. number + end + matched_platform = platform_name + matched_base_url = platform_base_url + config = platform_config + break + elseif text:match(url_pattern_mr) then + repo, number = text:match(url_pattern_mr) + ref_type = 'merge_request' + if repo == repository_name then + short_link = '!' .. number + else + short_link = repo .. '!' .. number + end + matched_platform = platform_name + matched_base_url = platform_base_url + config = platform_config + break + elseif text:match(url_pattern_pull_requests) then + repo, number = text:match(url_pattern_pull_requests) + ref_type = 'pull' + if repo == repository_name then + short_link = '#' .. number + else + short_link = repo .. '#' .. number + end + matched_platform = platform_name + matched_base_url = platform_base_url + config = platform_config + break + elseif text:match(url_pattern_pull) then + repo, number = text:match(url_pattern_pull) + ref_type = 'pull' + if repo == repository_name then + short_link = '#' .. number + else + short_link = repo .. '#' .. number + end + matched_platform = platform_name + matched_base_url = platform_base_url + config = platform_config + break + end + end + end + end + + if number and repo and ref_type then + local url_format + if ref_type == "issue" then + url_format = config.url_formats.issue + elseif ref_type == "merge_request" then + url_format = config.url_formats.merge_request + elseif ref_type == "pull" then + url_format = config.url_formats.pull + end + + if url_format then + local uri = matched_base_url .. url_format:gsub("{repo}", repo):gsub("{number}", number) + return create_platform_link(short_link, uri, matched_platform), matched_platform, matched_base_url + end + end + + return nil, nil, nil +end + +--- Process user/organisation references +--- @param elem pandoc.Str The string element to process +--- @param current_platform string The current platform name +--- @return pandoc.Link|nil A user link or nil if no valid pattern found +--- @return string|nil The platform name used for this match +--- @return string|nil The base URL used for this match +local function process_users(elem, current_platform) + local config = get_platform_config(current_platform) + if not config then + return nil, nil, nil + end + + local text = elem.text + + local all_platform_names = get_all_platform_names() + for _, platform_name in ipairs(all_platform_names) do + local platform_config = get_platform_config(platform_name) + if platform_config then + local platform_base_url = platform_config.base_url + local escaped_platform_url = str.escape_pattern(platform_base_url) + local path = text:match('^' .. escaped_platform_url .. '/([^/]+)$') + local username = path and match_username(platform_config, '@' .. path) + if username then + local url_format = platform_config.url_formats.user + local uri = platform_base_url .. url_format:gsub('{username}', username) + return create_platform_link('@' .. username, uri, platform_name), platform_name, platform_base_url + end + end + end + + return nil, nil, nil +end + +--- Process commit references +--- @param elem pandoc.Str The string element to process +--- @param current_platform string The current platform name +--- @param current_base_url string The current base URL +--- @return pandoc.Link|nil A commit link or nil if no valid pattern found +--- @return string|nil The platform name used for this match +--- @return string|nil The base URL used for this match +local function process_commits(elem, current_platform, current_base_url) + local config = get_platform_config(current_platform) + if not config then + return nil, nil, nil + end + + local text = elem.text + local repo = nil + local commit_sha = nil + local short_link = nil + local matched_platform = current_platform + local matched_base_url = current_base_url + + repo, commit_sha, short_link = match_commit_reference(config, text) + + if not commit_sha then + local all_platform_names = get_all_platform_names() + for _, platform_name in ipairs(all_platform_names) do + local platform_config = get_platform_config(platform_name) + if platform_config then + local platform_base_url = platform_config.base_url + local escaped_platform_url = str.escape_pattern(platform_base_url) + local url_pattern = '^' .. escaped_platform_url .. '/([^/]+/[^/]+)/%-?/?commits?/(%x+)$' + if text:match(url_pattern) then + local r, sha = text:match(url_pattern) + if sha:len() >= COMMIT_SHA_MIN_LENGTH and sha:len() <= COMMIT_SHA_FULL_LENGTH then + repo = r + commit_sha = sha + if repo == repository_name then + short_link = commit_sha:sub(1, COMMIT_SHA_SHORT_LENGTH) + else + short_link = repo .. '@' .. commit_sha:sub(1, COMMIT_SHA_SHORT_LENGTH) + end + matched_platform = platform_name + matched_base_url = platform_base_url + config = platform_config + break + end + end + end + end + end + + if commit_sha and repo + and commit_sha:len() >= COMMIT_SHA_MIN_LENGTH + and commit_sha:len() <= COMMIT_SHA_FULL_LENGTH then + local url_format = config.url_formats.commit + local uri = matched_base_url .. url_format:gsub("{repo}", repo):gsub("{sha}", commit_sha) + return create_platform_link(short_link, uri, matched_platform), matched_platform, matched_base_url + end + + return nil, nil, nil +end + +--- Try the issue/MR, commit, and user matchers on a single token's text. +--- @param text string The token text to match +--- @return pandoc.Link|pandoc.Span|nil A link, a badge span, or nil +local function match_single(text) + local elem = pandoc.Str(text) + return process_issues_and_mrs(elem, platform, base_url) + or process_commits(elem, platform, base_url) + or process_users(elem, platform) +end + +--- Match a token's text as a single reference or a comma-separated group. +--- First tries a whole-text match. If that fails and the text is a +--- comma-separated group (two or more segments), matches every segment; the +--- group is recognised only when *all* segments are valid references, so mixed +--- text such as "1,000" or "#1,note" is left untouched. Comma separators are +--- preserved as literal `Str` inlines. +--- @param text string The token text (already stripped of surrounding brackets) +--- @return pandoc.Link|pandoc.Span|pandoc.List|nil A link, a badge span, a list of inlines, or nil +local function match_reference_group(text) + local link = match_single(text) + if link then + return link + end + + if not text:find(",", 1, true) then + return nil + end + + local links = {} + local count = 0 + for segment in (text .. ","):gmatch("([^,]*),") do + if segment == "" then + return nil + end + local seg_link = match_single(segment) + if not seg_link then + return nil + end + count = count + 1 + links[count] = seg_link + end + + if count < 2 then + return nil + end + + local result = pandoc.List({}) + for i = 1, count do + if i > 1 then + result:insert(pandoc.Str(",")) + end + result:insert(links[i]) + end + return result +end + +--- Main Git hosting processing function +--- Attempts to convert string elements into Git hosting links by trying different patterns +--- @param elem pandoc.Str The string element to process +--- @return pandoc.Str|pandoc.Link|pandoc.List The original element, a link, or a list of inlines +local function process_gitlink(elem) + if not is_enabled then + return elem + end + if not platform or not base_url or str.is_empty(platform) then + return elem + end + -- When link normalisation is disabled, leave bare URL tokens alone. + -- Pandoc represents the visible text of an autolink as a Str inside the + -- Link element, so skipping URL-shaped tokens preserves the URL text. + if not normalize_links then + local t = elem.text + if t and (t:sub(1, 7) == 'http://' or t:sub(1, 8) == 'https://') then + return elem + end + end + + -- Fast path: match the raw text directly, including bare comma-separated + -- groups such as "#2,#3". + local link = match_reference_group(elem.text) + if link then + return link + end + + -- Slow path: peel unbalanced surrounding brackets and trailing punctuation + -- and retry. This also handles bracket groups that Pandoc split across + -- whitespace, e.g. "(#2," and "#3)" from "(#2, #3)", and single-token groups + -- such as "(#2,#3)". + local prefix, inner, suffix = str.strip_edges(elem.text) + if prefix ~= "" or suffix ~= "" then + if inner ~= "" then + link = match_reference_group(inner) + if link then + local result = pandoc.List({}) + if prefix ~= "" then + result:insert(pandoc.Str(prefix)) + end + if pandoc.utils.type(link) == "List" then + result:extend(link) + else + result:insert(link) + end + if suffix ~= "" then + result:insert(pandoc.Str(suffix)) + end + return result + end + end + end + + -- Embedded path: scan for a bracket pair anywhere inside the token and + -- retry the matchers on the bracket content. Handles cases where the + -- bracket is surrounded by additional text or punctuation, e.g. + -- "something(#1)", "(#1).", ".(#1).", "(#1)something", "something(#1,#2)". + local search_pos = 1 + while true do + local b_prefix, b_content, b_suffix, open_pos = str.find_bracketed_content(elem.text, search_pos) + if not b_content then + break + end + + local content_link = match_reference_group(b_content) + if content_link then + local result = pandoc.List({}) + if b_prefix ~= "" then + result:insert(pandoc.Str(b_prefix)) + end + if pandoc.utils.type(content_link) == "List" then + result:extend(content_link) + else + result:insert(content_link) + end + if b_suffix ~= "" then + result:insert(pandoc.Str(b_suffix)) + end + return result + end + + search_pos = open_pos + 1 + end + + return elem +end + +--- Process inline elements for Bitbucket multi-word patterns +--- @param elem table Block element containing inline content +--- @return table The modified element +local function process_inlines(elem) + if not is_enabled then + return elem + end + if elem.content and platform == "bitbucket" then + elem.content = bitbucket.process_inlines(elem.content, base_url, repository_name, create_platform_link) + end + return elem +end + +--- Fetch the HTML of a URL via curl (best-effort, cached per render). +--- Returns nil when fetching is disabled, the URL cannot be reached, or curl +--- is not available. Failures log a warning but never abort the render. +--- @param uri string The URL to fetch +--- @return string|nil The page title, or nil if unavailable +local function fetch_title_for(uri) + if not fetch_titles then + return nil + end + local cached = title_cache[uri] + if cached ~= nil then + return cached or nil + end + if uri:find('"', 1, true) or uri:find("'", 1, true) then + title_cache[uri] = false + return nil + end + local handle = io.popen( + 'curl -fsSL --max-time 5 -A "quarto-gitlink" "' .. uri .. '" 2>/dev/null', 'r' + ) + if not handle then + log.log_warning(EXTENSION_NAME, "Title fetch unavailable (could not start curl).") + title_cache[uri] = false + return nil + end + local body = handle:read('*a') or '' + handle:close() + local raw_title = body:match('<title[^>]*>(.-)') + if not raw_title or raw_title == '' then + title_cache[uri] = false + return nil + end + local decoded = raw_title + :gsub('&', '&') + :gsub('<', '<') + :gsub('>', '>') + :gsub('"', '"') + :gsub(''', "'") + local trimmed = str.trim(decoded) + if trimmed == '' then + title_cache[uri] = false + return nil + end + title_cache[uri] = trimmed + return trimmed +end + +--- Process Link elements to shorten platform URLs used as link text. +--- When `gitlink.normalize-links` is true (the default), an autolink whose text +--- equals its target (e.g. ``) is +--- unwrapped so the later `Str` pass converts the bare URL to its platform-style +--- form (e.g. `#1`). Unwrapping (rather than returning the converted link here) +--- avoids a doubly nested link, because the `Str` pass also descends into link +--- content and would re-process the shortened text. +--- When `gitlink.fetch-titles` is true, an autolink whose target points at a +--- known platform URL is given a title-derived link text (issue/PR/commit +--- title) instead of the URL when the fetch succeeds. +--- @param elem pandoc.Link The link element to process +--- @return pandoc.Link|pandoc.Str The original link, a retitled link, or the unwrapped URL +local function process_link(elem) + if not is_enabled or not normalize_links then + return elem + end + + local link_text = str.stringify(elem.content) + local link_target = elem.target + + if link_text == link_target then + if fetch_titles then + local title = fetch_title_for(link_target) + if title then + return pandoc.Link({ pandoc.Str(title) }, link_target, '', elem.attr) + end + end + + -- Only unwrap when the URL is a recognised platform reference; otherwise the + -- autolink is left untouched so ordinary URLs keep their link. + -- `process_gitlink` returns its argument unchanged when nothing matches, so + -- an identity check reliably detects a conversion. `pandoc.utils.type` + -- cannot be used here: it reports "Inline" for both `Str` and `Link`. + local temp_str = pandoc.Str(link_text) + if process_gitlink(temp_str) ~= temp_str then + return temp_str + end + end + + return elem +end + +--- Skip the content of an inline markdown-pipeline entry. +--- Quarto renders navigation hrefs and social metadata values as hidden inline +--- snippets in such a span, then reads the rendered fragment back with +--- `innerText` and puts the result in an attribute. A converted reference would +--- add its badge text to that value. +--- The same envelope also carries values that Quarto inserts with `innerHTML`, +--- such as a navbar or sidebar title, a navigation entry text, an `about` link +--- text, and a next or previous page title. A reference in those no longer +--- converts. The two kinds cannot be told apart, because a navigation entry +--- registers its text and its href under one identifier prefix, so the whole +--- envelope is skipped. +--- A `Div` with the same class is a block entry (page footer, margin and body +--- header and footer, announcement). Quarto inserts those with `innerHTML` as +--- well, and they are a separate envelope, so only spans are skipped. +--- @param span pandoc.Span The span element to inspect +--- @return pandoc.Span span The unchanged span +--- @return boolean|nil descend False to stop traversal of the subtree +local function skip_markdown_envelope(span) + if span.classes:includes(MARKDOWN_ENVELOPE_CLASS) then + return span, false + end + return span +end + +--- Convert a string element and stop traversal of the result. +--- Top-down traversal descends into a returned element, so without this a +--- created link would have its own text converted a second time. +--- @param elem pandoc.Str The string element to process +--- @return pandoc.Str|pandoc.Link|pandoc.List The result of `process_gitlink` +--- @return boolean descend Always false +local function process_gitlink_topdown(elem) + return process_gitlink(elem), false +end + +--- Turn element handlers into a top-down pass that skips envelope spans. +--- Each pass needs its own prune point, because the passes that create links +--- stay separate walks: `process_link` unwraps an autolink into a `Str` that the +--- later `Str` pass has to convert. +--- @param handlers table The element handlers for the pass +--- @return table The filter table for the pass +local function envelope_safe_pass(handlers) + handlers.traverse = 'topdown' + handlers.Span = skip_markdown_envelope + return handlers +end + +--- Pandoc filter configuration +--- Defines the order of filter execution: +--- 1. Get repository information from metadata +--- 2. Extract references from the document, once `enabled` is known +--- 3. Process inline containers for Bitbucket multi-word patterns +--- 4. Process link elements to shorten URLs used as link text +--- 5. Process string elements for Git hosting patterns +--- 6. Process citations for Git hosting mentions +return { + { Meta = get_repository }, + { Pandoc = get_references }, + { Plain = process_inlines, Para = process_inlines }, + envelope_safe_pass({ Link = process_link }), + envelope_safe_pass({ Str = process_gitlink_topdown }), + envelope_safe_pass({ Cite = process_mentions }) +} diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/octicons.json b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/octicons.json new file mode 100644 index 0000000..f0ed331 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/octicons.json @@ -0,0 +1 @@ +{"accessibility":[{"d":"M9.923 5.302c.063.063.122.129.178.198H14A.75.75 0 0 1 14 7h-3.3l.578 5.163.362 2.997a.75.75 0 0 1-1.49.18L9.868 13H6.132l-.282 2.34a.75.75 0 0 1-1.49-.18l.362-2.997L5.3 7H2a.75.75 0 0 1 0-1.5h3.9a2.54 2.54 0 0 1 .176-.198 3 3 0 1 1 3.847 0ZM9.2 7.073h-.001a1.206 1.206 0 0 0-2.398 0L6.305 11.5h3.39ZM9.5 3a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 3Z"}],"accessibility-inset":[{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM3.75 6a.75.75 0 0 0 0 1.5h2.409a1.9 1.9 0 0 0-.025.151l-.307 2.508-.323 3.011a.75.75 0 0 0 1.492.16l.25-2.33h1.509l.25 2.33a.748.748 0 0 0 1.199.552.75.75 0 0 0 .291-.712l-.32-3-.309-2.519a1.9 1.9 0 0 0-.025-.151h2.409a.75.75 0 0 0 0-1.5h-8.5ZM8 6a2 2 0 1 0 .001-3.999A2 2 0 0 0 8 6Z"}],"agent":[{"d":"M14.5 8.9v-.052A2.956 2.956 0 0 0 11.542 5.9a.815.815 0 0 1-.751-.501l-.145-.348A3.496 3.496 0 0 0 7.421 2.9h-.206a3.754 3.754 0 0 0-3.736 4.118l.011.121a.822.822 0 0 1-.619.879A1.81 1.81 0 0 0 1.5 9.773v.14c0 1.097.89 1.987 1.987 1.987H4.5a.75.75 0 0 1 0 1.5H3.487A3.487 3.487 0 0 1 0 9.913v-.14C0 8.449.785 7.274 1.963 6.75A5.253 5.253 0 0 1 7.215 1.4h.206a4.992 4.992 0 0 1 4.586 3.024A4.455 4.455 0 0 1 16 8.848V8.9a.75.75 0 0 1-1.5 0Z"},{"d":"m8.38 7.67 2.25 2.25a.749.749 0 0 1 0 1.061L8.38 13.23a.749.749 0 1 1-1.06-1.06l1.719-1.72L7.32 8.731A.75.75 0 0 1 8.38 7.67ZM15 13.45h-3a.75.75 0 0 1 0-1.5h3a.75.75 0 0 1 0 1.5Z"}],"ai-model":[{"d":"M10.628 7.25a2.25 2.25 0 1 1 0 1.5H8.622a2.25 2.25 0 0 1-2.513 1.466L5.03 12.124a2.25 2.25 0 1 1-1.262-.814l1.035-1.832A2.245 2.245 0 0 1 4.25 8c0-.566.209-1.082.553-1.478L3.768 4.69a2.25 2.25 0 1 1 1.262-.814l1.079 1.908A2.25 2.25 0 0 1 8.622 7.25ZM2.5 2.5a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm4 4.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm6.25 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm-9.5 5.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"}],"alert":[{"d":"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"}],"alert-fill":[{"d":"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575ZM8 5a.75.75 0 0 0-.75.75v2.5a.75.75 0 0 0 1.5 0v-2.5A.75.75 0 0 0 8 5Zm1 6a1 1 0 1 0-2 0 1 1 0 0 0 2 0Z"}],"apps":[{"d":"M1.5 3.25c0-.966.784-1.75 1.75-1.75h2.5c.966 0 1.75.784 1.75 1.75v2.5A1.75 1.75 0 0 1 5.75 7.5h-2.5A1.75 1.75 0 0 1 1.5 5.75Zm7 0c0-.966.784-1.75 1.75-1.75h2.5c.966 0 1.75.784 1.75 1.75v2.5a1.75 1.75 0 0 1-1.75 1.75h-2.5A1.75 1.75 0 0 1 8.5 5.75Zm-7 7c0-.966.784-1.75 1.75-1.75h2.5c.966 0 1.75.784 1.75 1.75v2.5a1.75 1.75 0 0 1-1.75 1.75h-2.5a1.75 1.75 0 0 1-1.75-1.75Zm7 0c0-.966.784-1.75 1.75-1.75h2.5c.966 0 1.75.784 1.75 1.75v2.5a1.75 1.75 0 0 1-1.75 1.75h-2.5a1.75 1.75 0 0 1-1.75-1.75ZM3.25 3a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h2.5A.25.25 0 0 0 6 5.75v-2.5A.25.25 0 0 0 5.75 3Zm7 0a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h2.5a.25.25 0 0 0 .25-.25v-2.5a.25.25 0 0 0-.25-.25Zm-7 7a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h2.5a.25.25 0 0 0 .25-.25v-2.5a.25.25 0 0 0-.25-.25Zm7 0a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h2.5a.25.25 0 0 0 .25-.25v-2.5a.25.25 0 0 0-.25-.25Z"}],"archive":[{"d":"M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v1.5A1.75 1.75 0 0 1 14.25 6H1.75A1.75 1.75 0 0 1 0 4.25ZM1.75 7a.75.75 0 0 1 .75.75v5.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-5.5a.75.75 0 0 1 1.5 0v5.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25v-5.5A.75.75 0 0 1 1.75 7Zm0-4.5a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-1.5a.25.25 0 0 0-.25-.25ZM6.25 8h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1 0-1.5Z"}],"arrow-both":[{"d":"M3.72 3.72a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L2.56 7h10.88l-2.22-2.22a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018l3.5 3.5a.75.75 0 0 1 0 1.06l-3.5 3.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l2.22-2.22H2.56l2.22 2.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-3.5-3.5a.75.75 0 0 1 0-1.06Z"}],"arrow-down":[{"d":"M13.03 8.22a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L3.47 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018l2.97 2.97V3.75a.75.75 0 0 1 1.5 0v7.44l2.97-2.97a.75.75 0 0 1 1.06 0Z"}],"arrow-down-left":[{"d":"M11.78 4.22a.75.75 0 0 1 0 1.06l-5.26 5.26h4.2a.75.75 0 0 1 0 1.5H4.71a.75.75 0 0 1-.75-.75V5.28a.75.75 0 0 1 1.5 0v4.2l5.26-5.26a.75.75 0 0 1 1.06 0Z"}],"arrow-down-right":[{"d":"M4.22 4.179a.75.75 0 0 1 1.06 0l5.26 5.26v-4.2a.75.75 0 0 1 1.5 0v6.01a.75.75 0 0 1-.75.75H5.28a.75.75 0 0 1 0-1.5h4.2L4.22 5.24a.75.75 0 0 1 0-1.06Z"}],"arrow-left":[{"d":"M7.78 12.53a.75.75 0 0 1-1.06 0L2.47 8.28a.75.75 0 0 1 0-1.06l4.25-4.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L4.81 7h7.44a.75.75 0 0 1 0 1.5H4.81l2.97 2.97a.75.75 0 0 1 0 1.06Z"}],"arrow-right":[{"d":"M8.22 2.97a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l2.97-2.97H3.75a.75.75 0 0 1 0-1.5h7.44L8.22 4.03a.75.75 0 0 1 0-1.06Z"}],"arrow-switch":[{"d":"M5.22 14.78a.75.75 0 0 0 1.06-1.06L4.56 12h8.69a.75.75 0 0 0 0-1.5H4.56l1.72-1.72a.75.75 0 0 0-1.06-1.06l-3 3a.75.75 0 0 0 0 1.06l3 3Zm5.56-6.5a.75.75 0 1 1-1.06-1.06l1.72-1.72H2.75a.75.75 0 0 1 0-1.5h8.69L9.72 2.28a.75.75 0 0 1 1.06-1.06l3 3a.75.75 0 0 1 0 1.06l-3 3Z"}],"arrow-up":[{"d":"M3.47 7.78a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0l4.25 4.25a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018L9 4.81v7.44a.75.75 0 0 1-1.5 0V4.81L4.53 7.78a.75.75 0 0 1-1.06 0Z"}],"arrow-up-left":[{"d":"M3.96 4.75A.75.75 0 0 1 4.71 4h6.01a.75.75 0 0 1 0 1.5h-4.2l5.26 5.26a.75.75 0 0 1-1.06 1.061l-5.26-5.26v4.2a.75.75 0 0 1-1.5 0Z"}],"arrow-up-right":[{"d":"M4.53 4.75A.75.75 0 0 1 5.28 4h6.01a.75.75 0 0 1 .75.75v6.01a.75.75 0 0 1-1.5 0v-4.2l-5.26 5.261a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L9.48 5.5h-4.2a.75.75 0 0 1-.75-.75Z"}],"beaker":[{"d":"M5 5.782V2.5h-.25a.75.75 0 0 1 0-1.5h6.5a.75.75 0 0 1 0 1.5H11v3.282l3.666 5.76C15.619 13.04 14.543 15 12.767 15H3.233c-1.776 0-2.852-1.96-1.899-3.458Zm-2.4 6.565a.75.75 0 0 0 .633 1.153h9.534a.75.75 0 0 0 .633-1.153L12.225 10.5h-8.45ZM9.5 2.5h-3V6c0 .143-.04.283-.117.403L4.73 9h6.54L9.617 6.403A.746.746 0 0 1 9.5 6Z"}],"bell":[{"d":"M8 16a2 2 0 0 0 1.985-1.75c.017-.137-.097-.25-.235-.25h-3.5c-.138 0-.252.113-.235.25A2 2 0 0 0 8 16ZM3 5a5 5 0 0 1 10 0v2.947c0 .05.015.098.042.139l1.703 2.555A1.519 1.519 0 0 1 13.482 13H2.518a1.516 1.516 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947Zm5-3.5A3.5 3.5 0 0 0 4.5 5v2.947c0 .346-.102.683-.294.97l-1.703 2.556a.017.017 0 0 0-.003.01l.001.006c0 .002.002.004.004.006l.006.004.007.001h10.964l.007-.001.006-.004.004-.006.001-.007a.017.017 0 0 0-.003-.01l-1.703-2.554a1.745 1.745 0 0 1-.294-.97V5A3.5 3.5 0 0 0 8 1.5Z"}],"bell-fill":[{"d":"M8 16c.9 0 1.7-.6 1.9-1.5.1-.3-.1-.5-.4-.5h-3c-.3 0-.5.2-.4.5.2.9 1 1.5 1.9 1.5ZM3 5c0-2.8 2.2-5 5-5s5 2.2 5 5v3l1.7 2.6c.2.2.3.5.3.8 0 .8-.7 1.5-1.5 1.5h-11c-.8.1-1.5-.6-1.5-1.4 0-.3.1-.6.3-.8L3 8.1V5Z"}],"bell-slash":[{"d":"m4.182 4.31.016.011 10.104 7.316.013.01 1.375.996a.75.75 0 1 1-.88 1.214L13.626 13H2.518a1.516 1.516 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947V5.305L.31 3.357a.75.75 0 1 1 .88-1.214Zm7.373 7.19L4.5 6.391v1.556c0 .346-.102.683-.294.97l-1.703 2.556a.017.017 0 0 0-.003.01c0 .005.002.009.005.012l.006.004.007.001ZM8 1.5c-.997 0-1.895.416-2.534 1.086A.75.75 0 1 1 4.38 1.55 5 5 0 0 1 13 5v2.373a.75.75 0 0 1-1.5 0V5A3.5 3.5 0 0 0 8 1.5ZM8 16a2 2 0 0 1-1.985-1.75c-.017-.137.097-.25.235-.25h3.5c.138 0 .252.113.235.25A2 2 0 0 1 8 16Z"}],"blocked":[{"d":"M4.467.22a.749.749 0 0 1 .53-.22h6.006c.199 0 .389.079.53.22l4.247 4.247c.141.14.22.331.22.53v6.006a.749.749 0 0 1-.22.53l-4.247 4.247a.749.749 0 0 1-.53.22H4.997a.749.749 0 0 1-.53-.22L.22 11.533a.749.749 0 0 1-.22-.53V4.997c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.308v5.384L5.308 14.5h5.384l3.808-3.808V5.308L10.692 1.5ZM4 7.75A.75.75 0 0 1 4.75 7h6.5a.75.75 0 0 1 0 1.5h-6.5A.75.75 0 0 1 4 7.75Z"}],"bold":[{"d":"M4 2h4.5a3.501 3.501 0 0 1 2.852 5.53A3.499 3.499 0 0 1 9.5 14H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1Zm1 7v3h4.5a1.5 1.5 0 0 0 0-3Zm3.5-2a1.5 1.5 0 0 0 0-3H5v3Z"}],"book":[{"d":"M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.757a3.75 3.75 0 0 1 1.994.574ZM8.755 4.75l-.004 7.322a3.752 3.752 0 0 1 1.992-.572H14.5v-9h-3.495a2.25 2.25 0 0 0-2.25 2.25Z"}],"book-locked":[{"d":"M12 6a3 3 0 0 1 3 3v1.168c.591.281 1 .884 1 1.582v2.5A1.75 1.75 0 0 1 14.25 16h-4.5A1.75 1.75 0 0 1 8 14.25v-2.5c0-.698.409-1.301 1-1.582V9a3 3 0 0 1 3-3Zm0 1.5A1.5 1.5 0 0 0 10.5 9v1h3V9A1.5 1.5 0 0 0 12 7.5Z"},{"d":"M5.003 1c1.227 0 2.317.59 3.001 1.501A3.746 3.746 0 0 1 11.005 1h4.245a.75.75 0 0 1 .75.75V5.5a.75.75 0 0 1-1.5 0v-3h-3.495c-1.21 0-2.204.956-2.255 2.153V6.5a.75.75 0 0 1-1.5 0V4.69A2.249 2.249 0 0 0 5.003 2.5H1.5v9h3.758c.612 0 1.208.15 1.74.429l.005.001a.75.75 0 0 1-.705 1.324l-.001-.001v.002A2.245 2.245 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75V1.75A.75.75 0 0 1 .75 1h4.253Z"}],"bookmark":[{"d":"M3 2.75C3 1.784 3.784 1 4.75 1h6.5c.966 0 1.75.784 1.75 1.75v11.5a.75.75 0 0 1-1.227.579L8 11.722l-3.773 3.107A.751.751 0 0 1 3 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.91l3.023-2.489a.75.75 0 0 1 .954 0l3.023 2.49V2.75a.25.25 0 0 0-.25-.25Z"}],"bookmark-filled":[{"d":"M3 2.75C3 1.784 3.784 1 4.75 1h6.5c.966 0 1.75.784 1.75 1.75v11.5a.75.75 0 0 1-1.227.579L8 11.722l-3.773 3.107A.75.75 0 0 1 3 14.25V2.75Z"}],"bookmark-slash":[{"d":"M1.19 1.143 4.182 3.31l.014.01 8.486 6.145.014.01 2.994 2.168a.75.75 0 1 1-.88 1.214L13 11.547v2.703a.75.75 0 0 1-1.206.596L8 11.944l-3.794 2.902A.75.75 0 0 1 3 14.25V4.305L.31 2.357a.75.75 0 1 1 .88-1.214ZM4.5 5.39v7.341l3.044-2.328a.75.75 0 0 1 .912 0l3.044 2.328V10.46ZM5.865 1h5.385c.966 0 1.75.784 1.75 1.75v3.624a.75.75 0 0 1-1.5 0V2.75a.25.25 0 0 0-.25-.25H5.865a.75.75 0 0 1 0-1.5Z"}],"bookmark-slash-fill":[{"d":"M1.19 1.143a.75.75 0 1 0-.88 1.215L3 4.305v9.945a.75.75 0 0 0 1.206.596L8 11.944l3.794 2.902A.75.75 0 0 0 13 14.25v-2.703l1.81 1.31a.75.75 0 1 0 .88-1.214l-2.994-2.168a1.09 1.09 0 0 0-.014-.01L4.196 3.32a.712.712 0 0 0-.014-.01L1.19 1.143Zm2.934.203A.5.5 0 0 1 4.6 1h7.233C12.478 1 13 1.522 13 2.167v5.05a.5.5 0 0 1-.793.405l-7.9-5.717a.5.5 0 0 1-.183-.559Z"}],"boolean-off":[{"d":"M13.25 1c.966 0 1.75.784 1.75 1.75v10.5c0 .464-.184.909-.513 1.237A1.746 1.746 0 0 1 13.25 15H2.75c-.464 0-.909-.184-1.237-.513A1.746 1.746 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1h10.5ZM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25H2.75Z"},{"d":"M9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"}],"boolean-on":[{"d":"M13.25 1c.966 0 1.75.784 1.75 1.75v10.5c0 .464-.184.909-.513 1.237A1.746 1.746 0 0 1 13.25 15H2.75c-.464 0-.909-.184-1.237-.513A1.746 1.746 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1h10.5ZM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25H2.75Z"},{"d":"M8.75 5.75a.75.75 0 0 0-1.5 0v4.5a.75.75 0 0 0 1.5 0v-4.5Z"}],"briefcase":[{"d":"M6.75 0h2.5C10.216 0 11 .784 11 1.75V3h3.25c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25v-8.5C0 3.784.784 3 1.75 3H5V1.75C5 .784 5.784 0 6.75 0ZM3.5 9.5a3.49 3.49 0 0 1-2-.627v4.377c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V8.873a3.49 3.49 0 0 1-2 .627Zm-1.75-5a.25.25 0 0 0-.25.25V6a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V4.75a.25.25 0 0 0-.25-.25H1.75ZM9.5 3V1.75a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25V3Z"}],"broadcast":[{"d":"M8.75 8.582v5.668a.75.75 0 0 1-1.5 0V8.582a1.75 1.75 0 1 1 1.5 0Zm3.983-7.125a.75.75 0 0 1 1.06.026A7.976 7.976 0 0 1 16 7c0 2.139-.84 4.083-2.207 5.517a.75.75 0 1 1-1.086-1.034A6.474 6.474 0 0 0 14.5 7a6.474 6.474 0 0 0-1.793-4.483.75.75 0 0 1 .026-1.06Zm-9.466 0c.3.286.312.76.026 1.06A6.474 6.474 0 0 0 1.5 7a6.47 6.47 0 0 0 1.793 4.483.75.75 0 0 1-1.086 1.034A7.973 7.973 0 0 1 0 7c0-2.139.84-4.083 2.207-5.517a.75.75 0 0 1 1.06-.026Zm8.556 2.321A4.988 4.988 0 0 1 13 7a4.988 4.988 0 0 1-1.177 3.222.75.75 0 1 1-1.146-.967A3.487 3.487 0 0 0 11.5 7c0-.86-.309-1.645-.823-2.255a.75.75 0 0 1 1.146-.967Zm-6.492.958A3.48 3.48 0 0 0 4.5 7a3.48 3.48 0 0 0 .823 2.255.75.75 0 0 1-1.146.967A4.981 4.981 0 0 1 3 7a4.982 4.982 0 0 1 1.188-3.236.75.75 0 1 1 1.143.972Z"}],"browser":[{"d":"M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25ZM14.5 6h-13v7.25c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25Zm-6-3.5v2h6V2.75a.25.25 0 0 0-.25-.25ZM5 2.5v2h2v-2Zm-3.25 0a.25.25 0 0 0-.25.25V4.5h2v-2Z"}],"bug":[{"d":"M4.72.22a.75.75 0 0 1 1.06 0l1 .999a3.488 3.488 0 0 1 2.441 0l.999-1a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.775.776c.616.63.995 1.493.995 2.444v.327c0 .1-.009.197-.025.292.408.14.764.392 1.029.722l1.968-.787a.75.75 0 0 1 .556 1.392L13 7.258V9h2.25a.75.75 0 0 1 0 1.5H13v.5c0 .409-.049.806-.141 1.186l2.17.868a.75.75 0 0 1-.557 1.392l-2.184-.873A4.997 4.997 0 0 1 8 16a4.997 4.997 0 0 1-4.288-2.427l-2.183.873a.75.75 0 0 1-.558-1.392l2.17-.868A5.036 5.036 0 0 1 3 11v-.5H.75a.75.75 0 0 1 0-1.5H3V7.258L.971 6.446a.75.75 0 0 1 .558-1.392l1.967.787c.265-.33.62-.583 1.03-.722a1.677 1.677 0 0 1-.026-.292V4.5c0-.951.38-1.814.995-2.444L4.72 1.28a.75.75 0 0 1 0-1.06Zm.53 6.28a.75.75 0 0 0-.75.75V11a3.5 3.5 0 1 0 7 0V7.25a.75.75 0 0 0-.75-.75ZM6.173 5h3.654A.172.172 0 0 0 10 4.827V4.5a2 2 0 1 0-4 0v.327c0 .096.077.173.173.173Z"}],"cache":[{"d":"M2.5 5.724V8c0 .248.238.7 1.169 1.159.874.43 2.144.745 3.62.822a.75.75 0 1 1-.078 1.498c-1.622-.085-3.102-.432-4.204-.975a5.565 5.565 0 0 1-.507-.28V12.5c0 .133.058.318.282.551.227.237.591.483 1.101.707 1.015.447 2.47.742 4.117.742.406 0 .802-.018 1.183-.052a.751.751 0 1 1 .134 1.494C8.89 15.98 8.45 16 8 16c-1.805 0-3.475-.32-4.721-.869-.623-.274-1.173-.619-1.579-1.041-.408-.425-.7-.964-.7-1.59v-9c0-.626.292-1.165.7-1.591.406-.42.956-.766 1.579-1.04C4.525.32 6.195 0 8 0c1.806 0 3.476.32 4.721.869.623.274 1.173.619 1.579 1.041.408.425.7.964.7 1.59 0 .626-.292 1.165-.7 1.591-.406.42-.956.766-1.578 1.04C11.475 6.68 9.805 7 8 7c-1.805 0-3.475-.32-4.721-.869a6.15 6.15 0 0 1-.779-.407Zm0-2.224c0 .133.058.318.282.551.227.237.591.483 1.101.707C4.898 5.205 6.353 5.5 8 5.5c1.646 0 3.101-.295 4.118-.742.508-.224.873-.471 1.1-.708.224-.232.282-.417.282-.55 0-.133-.058-.318-.282-.551-.227-.237-.591-.483-1.101-.707C11.102 1.795 9.647 1.5 8 1.5c-1.646 0-3.101.295-4.118.742-.508.224-.873.471-1.1.708-.224.232-.282.417-.282.55Z"},{"d":"M14.49 7.582a.375.375 0 0 0-.66-.313l-3.625 4.625a.375.375 0 0 0 .295.606h2.127l-.619 2.922a.375.375 0 0 0 .666.304l3.125-4.125A.375.375 0 0 0 15.5 11h-1.778l.769-3.418Z"}],"calendar":[{"d":"M4.75 0a.75.75 0 0 1 .75.75V2h5V.75a.75.75 0 0 1 1.5 0V2h1.25c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25V3.75C1 2.784 1.784 2 2.75 2H4V.75A.75.75 0 0 1 4.75 0ZM2.5 7.5v6.75c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V7.5Zm10.75-4H2.75a.25.25 0 0 0-.25.25V6h11V3.75a.25.25 0 0 0-.25-.25Z"}],"check":[{"d":"M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"}],"check-circle":[{"d":"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm1.5 0a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm10.28-1.72-4.5 4.5a.75.75 0 0 1-1.06 0l-2-2a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018l1.47 1.47 3.97-3.97a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"}],"check-circle-fill":[{"d":"M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm3.78-9.72a.751.751 0 0 0-.018-1.042.751.751 0 0 0-1.042-.018L6.75 9.19 5.28 7.72a.751.751 0 0 0-1.042.018.751.751 0 0 0-.018 1.042l2 2a.75.75 0 0 0 1.06 0Z"}],"checkbox":[{"d":"M2.75 1h10.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1ZM2.5 2.75v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25H2.75a.25.25 0 0 0-.25.25Zm9.28 3.53-4.5 4.5a.75.75 0 0 1-1.06 0l-2-2a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018l1.47 1.47 3.97-3.97a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"}],"checkbox-fill":[{"d":"M2.75 1h10.5c.967 0 1.75.783 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25V2.75C1 1.783 1.783 1 2.75 1Zm9.03 5.28a.749.749 0 1 0-1.06-1.06L6.75 9.189 5.28 7.72a.749.749 0 1 0-1.06 1.06l2 2a.749.749 0 0 0 1.06 0Z"}],"checklist":[{"d":"M2.5 1.75v11.5c0 .138.112.25.25.25h3.17a.75.75 0 0 1 0 1.5H2.75A1.75 1.75 0 0 1 1 13.25V1.75C1 .784 1.784 0 2.75 0h8.5C12.216 0 13 .784 13 1.75v7.736a.75.75 0 0 1-1.5 0V1.75a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13.274 9.537v-.001l-4.557 4.45a.75.75 0 0 1-1.055-.008l-1.943-1.95a.75.75 0 0 1 1.062-1.058l1.419 1.425 4.026-3.932a.75.75 0 1 1 1.048 1.074ZM4.75 4h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM4 7.75A.75.75 0 0 1 4.75 7h2a.75.75 0 0 1 0 1.5h-2A.75.75 0 0 1 4 7.75Z"}],"chevron-down":[{"d":"M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z"}],"chevron-left":[{"d":"M9.78 12.78a.75.75 0 0 1-1.06 0L4.47 8.53a.75.75 0 0 1 0-1.06l4.25-4.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L6.06 8l3.72 3.72a.75.75 0 0 1 0 1.06Z"}],"chevron-right":[{"d":"M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z"}],"chevron-up":[{"d":"M3.22 10.53a.749.749 0 0 1 0-1.06l4.25-4.25a.749.749 0 0 1 1.06 0l4.25 4.25a.749.749 0 1 1-1.06 1.06L8 6.811 4.28 10.53a.749.749 0 0 1-1.06 0Z"}],"circle":[{"d":"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Z"}],"circle-slash":[{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM3.965 13.096a6.5 6.5 0 0 0 9.131-9.131ZM1.5 8a6.474 6.474 0 0 0 1.404 4.035l9.131-9.131A6.499 6.499 0 0 0 1.5 8Z"}],"clock":[{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7-3.25v2.992l2.028.812a.75.75 0 0 1-.557 1.392l-2.5-1A.751.751 0 0 1 7 8.25v-3.5a.75.75 0 0 1 1.5 0Z"}],"clock-fill":[{"d":"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8.575-3.25a.825.825 0 1 0-1.65 0v3.5c0 .337.205.64.519.766l2.5 1a.825.825 0 0 0 .612-1.532l-1.981-.793Z"}],"cloud":[{"d":"M2 7.25A5.225 5.225 0 0 1 7.25 2a5.222 5.222 0 0 1 4.767 3.029A4.472 4.472 0 0 1 16 9.5c0 2.505-1.995 4.5-4.5 4.5h-8A3.474 3.474 0 0 1 0 10.5c0-1.41.809-2.614 2.001-3.17Zm1.54.482a.75.75 0 0 1-.556.832c-.86.22-1.484.987-1.484 1.936 0 1.124.876 2 2 2h8c1.676 0 3-1.324 3-3s-1.324-3-3-3a.75.75 0 0 1-.709-.504A3.72 3.72 0 0 0 7.25 3.5C5.16 3.5 3.5 5.16 3.5 7.25c.002.146.014.292.035.436l.004.036.001.008Z"}],"cloud-offline":[{"d":"M7.25 2c-.69 0-1.351.13-1.957.371a.75.75 0 1 0 .554 1.394c.43-.17.903-.265 1.403-.265a3.72 3.72 0 0 1 3.541 2.496.75.75 0 0 0 .709.504c1.676 0 3 1.324 3 3a3 3 0 0 1-.681 1.92.75.75 0 0 0 1.156.955A4.496 4.496 0 0 0 16 9.5a4.472 4.472 0 0 0-3.983-4.471A5.222 5.222 0 0 0 7.25 2ZM.72 1.72a.75.75 0 0 1 1.06 0l2.311 2.31c.03.025.056.052.08.08l8.531 8.532.035.034 2.043 2.044a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-1.8-1.799a4.54 4.54 0 0 1-.42.019h-8A3.474 3.474 0 0 1 0 10.5c0-1.41.809-2.614 2.001-3.17a5.218 5.218 0 0 1 .646-2.622L.72 2.78a.75.75 0 0 1 0-1.06ZM3.5 7.25c.004.161.018.322.041.481a.75.75 0 0 1-.557.833c-.86.22-1.484.986-1.484 1.936 0 1.124.876 2 2 2h6.94L3.771 5.832A3.788 3.788 0 0 0 3.5 7.25Z"}],"code":[{"d":"m11.28 3.22 4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L13.94 8l-3.72-3.72a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215Zm-6.56 0a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L2.06 8l3.72 3.72a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L.47 8.53a.75.75 0 0 1 0-1.06Z"}],"code-of-conduct":[{"d":"M8.048 2.241c.964-.709 2.079-1.238 3.325-1.241a4.616 4.616 0 0 1 3.282 1.355c.41.408.757.86.996 1.428.238.568.348 1.206.347 1.968 0 2.193-1.505 4.254-3.081 5.862-1.496 1.526-3.213 2.796-4.249 3.563l-.22.163a.749.749 0 0 1-.895 0l-.221-.163c-1.036-.767-2.753-2.037-4.249-3.563C1.51 10.008.007 7.952.002 5.762a4.614 4.614 0 0 1 1.353-3.407C3.123.585 6.223.537 8.048 2.24Zm-1.153.983c-1.25-1.033-3.321-.967-4.48.191a3.115 3.115 0 0 0-.913 2.335c0 1.556 1.109 3.24 2.652 4.813C5.463 11.898 6.96 13.032 8 13.805c.353-.262.758-.565 1.191-.905l-1.326-1.223a.75.75 0 0 1 1.018-1.102l1.48 1.366c.328-.281.659-.577.984-.887L9.99 9.802a.75.75 0 1 1 1.019-1.103l1.384 1.28c.295-.329.566-.661.81-.995L12.92 8.7l-1.167-1.168c-.674-.671-1.78-.664-2.474.03-.268.269-.538.537-.802.797-.893.882-2.319.843-3.185-.032-.346-.35-.693-.697-1.043-1.047a.75.75 0 0 1-.04-1.016c.162-.191.336-.401.52-.623.62-.748 1.356-1.637 2.166-2.417Zm7.112 4.442c.313-.65.491-1.293.491-1.916v-.001c0-.614-.088-1.045-.23-1.385-.143-.339-.357-.633-.673-.949a3.111 3.111 0 0 0-2.218-.915c-1.092.003-2.165.627-3.226 1.602-.823.755-1.554 1.637-2.228 2.45l-.127.154.562.566a.755.755 0 0 0 1.066.02l.794-.79c1.258-1.258 3.312-1.31 4.594-.032.396.394.792.791 1.173 1.173Z"}],"code-review":[{"d":"M1.75 1h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 13H8.061l-2.574 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25v-8.5C0 1.784.784 1 1.75 1ZM1.5 2.75v8.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25Zm5.28 1.72a.75.75 0 0 1 0 1.06L5.31 7l1.47 1.47a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-2-2a.75.75 0 0 1 0-1.06l2-2a.75.75 0 0 1 1.06 0Zm2.44 0a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L10.69 7 9.22 5.53a.75.75 0 0 1 0-1.06Z"}],"code-square":[{"d":"M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"}],"codescan":[{"d":"M8.47 4.97a.75.75 0 0 0 0 1.06L9.94 7.5 8.47 8.97a.75.75 0 1 0 1.06 1.06l2-2a.75.75 0 0 0 0-1.06l-2-2a.75.75 0 0 0-1.06 0ZM6.53 6.03a.75.75 0 0 0-1.06-1.06l-2 2a.75.75 0 0 0 0 1.06l2 2a.75.75 0 1 0 1.06-1.06L5.06 7.5l1.47-1.47Z"},{"d":"M12.246 13.307a7.501 7.501 0 1 1 1.06-1.06l2.474 2.473a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM1.5 7.5a6.002 6.002 0 0 0 3.608 5.504 6.002 6.002 0 0 0 6.486-1.117.748.748 0 0 1 .292-.293A6 6 0 1 0 1.5 7.5Z"}],"codescan-checkmark":[{"d":"M10.28 6.28a.75.75 0 1 0-1.06-1.06L6.25 8.19l-.97-.97a.75.75 0 0 0-1.06 1.06l1.5 1.5a.75.75 0 0 0 1.06 0l3.5-3.5Z"},{"d":"M7.5 15a7.5 7.5 0 1 1 5.807-2.754l2.473 2.474a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2.474-2.473A7.472 7.472 0 0 1 7.5 15Zm0-13.5a6 6 0 1 0 4.094 10.386.748.748 0 0 1 .293-.292 6.002 6.002 0 0 0 1.117-6.486A6.002 6.002 0 0 0 7.5 1.5Z"}],"codespaces":[{"d":"M0 11.25c0-.966.784-1.75 1.75-1.75h12.5c.966 0 1.75.784 1.75 1.75v3A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm2-9.5C2 .784 2.784 0 3.75 0h8.5C13.216 0 14 .784 14 1.75v5a1.75 1.75 0 0 1-1.75 1.75h-8.5A1.75 1.75 0 0 1 2 6.75Zm1.75-.25a.25.25 0 0 0-.25.25v5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-5a.25.25 0 0 0-.25-.25Zm-2 9.5a.25.25 0 0 0-.25.25v3c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-3a.25.25 0 0 0-.25-.25Z"},{"d":"M7 12.75a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Zm-4 0a.75.75 0 0 1 .75-.75h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1-.75-.75Z"}],"columns":[{"d":"M2.75 0h2.5C6.216 0 7 .784 7 1.75v12.5A1.75 1.75 0 0 1 5.25 16h-2.5A1.75 1.75 0 0 1 1 14.25V1.75C1 .784 1.784 0 2.75 0Zm8 0h2.5C14.216 0 15 .784 15 1.75v12.5A1.75 1.75 0 0 1 13.25 16h-2.5A1.75 1.75 0 0 1 9 14.25V1.75C9 .784 9.784 0 10.75 0ZM2.5 1.75v12.5c0 .138.112.25.25.25h2.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25Zm8 0v12.5c0 .138.112.25.25.25h2.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25Z"}],"command-palette":[{"d":"m6.354 8.04-4.773 4.773a.75.75 0 1 0 1.061 1.06L7.945 8.57a.75.75 0 0 0 0-1.06L2.642 2.206a.75.75 0 0 0-1.06 1.061L6.353 8.04ZM8.75 11.5a.75.75 0 0 0 0 1.5h5.5a.75.75 0 0 0 0-1.5h-5.5Z"}],"comment":[{"d":"M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"}],"comment-ai":[{"d":"M7.75 1a.75.75 0 0 1 0 1.5h-5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2c.199 0 .39.079.53.22.141.14.22.331.22.53v2.19l2.72-2.72a.747.747 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-2a.75.75 0 0 1 1.5 0v2c0 .464-.184.909-.513 1.237A1.746 1.746 0 0 1 13.25 12H9.06l-2.573 2.573A1.457 1.457 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25v-7.5C1 1.784 1.784 1 2.75 1h5Zm4.519-.837a.248.248 0 0 1 .466 0l.238.648a3.726 3.726 0 0 0 2.218 2.219l.649.238a.249.249 0 0 1 0 .467l-.649.238a3.725 3.725 0 0 0-2.218 2.218l-.238.649a.248.248 0 0 1-.466 0l-.239-.649a3.725 3.725 0 0 0-2.218-2.218l-.649-.238a.249.249 0 0 1 0-.467l.649-.238A3.726 3.726 0 0 0 12.03.811l.239-.648Z"}],"comment-discussion":[{"d":"M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z"}],"comment-locked":[{"d":"M12 6a3 3 0 0 1 3 3v1.168c.591.281 1 .884 1 1.582v2.5A1.75 1.75 0 0 1 14.25 16h-4.5A1.75 1.75 0 0 1 8 14.25v-2.5c0-.698.409-1.301 1-1.582V9a3 3 0 0 1 3-3Zm0 1.5A1.5 1.5 0 0 0 10.5 9v1h3V9A1.5 1.5 0 0 0 12 7.5Z"},{"d":"M10.25 1A1.75 1.75 0 0 1 12 2.75v1.5a.75.75 0 0 1-1.5 0v-1.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25v5.5a.25.25 0 0 0 .25.25h1c.199 0 .39.079.53.22.141.14.22.331.22.53v2.19l2.72-2.72a.747.747 0 0 1 .53-.22h.35a.75.75 0 0 1 0 1.5h-.039l-2.574 2.573A1.457 1.457 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5A1.75 1.75 0 0 1 1.75 1h8.5Zm4 2c.966 0 1.75.784 1.75 1.75v.75c0-.061-.003-.115-.006-.164A.75.75 0 0 1 14.5 5.25v-.5a.25.25 0 0 0-.25-.25h-.5a.747.747 0 0 1-.53-.22.747.747 0 0 1 0-1.06.747.747 0 0 1 .53-.22h.5Z"}],"compose":[{"d":"m14.515.456.965.965a1.555 1.555 0 0 1 0 2.2L9.745 9.355a1.549 1.549 0 0 1-.672.396l-2.89.826a.67.67 0 0 1-.828-.474.66.66 0 0 1 .004-.35l.825-2.89c.073-.254.209-.486.396-.673L12.315.456c.144-.145.316-.259.505-.337a1.538 1.538 0 0 1 1.19 0c.189.078.361.192.505.337Zm-3.322 3.008-3.67 3.669a.214.214 0 0 0-.057.096L6.97 8.965l1.736-.496a.218.218 0 0 0 .096-.056l3.67-3.67Zm2.065-2.066L12.135 2.52l1.28 1.28 1.122-1.122a.216.216 0 0 0 .065-.157.216.216 0 0 0-.065-.157l-.965-.966a.216.216 0 0 0-.157-.065.226.226 0 0 0-.157.065Z"},{"d":"M0 14.25V2.75A1.75 1.75 0 0 1 1.75 1H7a.75.75 0 0 1 0 1.5H1.75a.25.25 0 0 0-.25.25v11.5a.25.25 0 0 0 .25.25h11.5a.25.25 0 0 0 .25-.25V8.5a.75.75 0 0 1 1.5 0v5.75c0 .464-.184.909-.513 1.237A1.746 1.746 0 0 1 13.25 16H1.75A1.75 1.75 0 0 1 0 14.25Z"}],"container":[{"d":"m10.41.24 4.711 2.774c.544.316.878.897.879 1.526v5.01a1.77 1.77 0 0 1-.88 1.53l-7.753 4.521-.002.001a1.769 1.769 0 0 1-1.774 0H5.59L.873 12.85A1.761 1.761 0 0 1 0 11.327V6.292c0-.304.078-.598.22-.855l.004-.005.01-.019c.15-.262.369-.486.64-.643L8.641.239a1.752 1.752 0 0 1 1.765 0l.002.001ZM9.397 1.534l-7.17 4.182 4.116 2.388a.27.27 0 0 0 .269 0l7.152-4.148-4.115-2.422a.252.252 0 0 0-.252 0Zm-7.768 10.02 4.1 2.393V9.474a1.807 1.807 0 0 1-.138-.072L1.5 7.029v4.298c0 .095.05.181.129.227Zm8.6.642 1.521-.887v-4.45l-1.521.882ZM7.365 9.402h.001c-.044.026-.09.049-.136.071v4.472l1.5-.875V8.61Zm5.885 1.032 1.115-.65h.002a.267.267 0 0 0 .133-.232V5.264l-1.25.725Z"}],"copilot":[{"d":"M7.998 15.035c-4.562 0-7.873-2.914-7.998-3.749V9.338c.085-.628.677-1.686 1.588-2.065.013-.07.024-.143.036-.218.029-.183.06-.384.126-.612-.201-.508-.254-1.084-.254-1.656 0-.87.128-1.769.693-2.484.579-.733 1.494-1.124 2.724-1.261 1.206-.134 2.262.034 2.944.765.05.053.096.108.139.165.044-.057.094-.112.143-.165.682-.731 1.738-.899 2.944-.765 1.23.137 2.145.528 2.724 1.261.566.715.693 1.614.693 2.484 0 .572-.053 1.148-.254 1.656.066.228.098.429.126.612.012.076.024.148.037.218.924.385 1.522 1.471 1.591 2.095v1.872c0 .766-3.351 3.795-8.002 3.795Zm0-1.485c2.28 0 4.584-1.11 5.002-1.433V7.862l-.023-.116c-.49.21-1.075.291-1.727.291-1.146 0-2.059-.327-2.71-.991A3.222 3.222 0 0 1 8 6.303a3.24 3.24 0 0 1-.544.743c-.65.664-1.563.991-2.71.991-.652 0-1.236-.081-1.727-.291l-.023.116v4.255c.419.323 2.722 1.433 5.002 1.433ZM6.762 2.83c-.193-.206-.637-.413-1.682-.297-1.019.113-1.479.404-1.713.7-.247.312-.369.789-.369 1.554 0 .793.129 1.171.308 1.371.162.181.519.379 1.442.379.853 0 1.339-.235 1.638-.54.315-.322.527-.827.617-1.553.117-.935-.037-1.395-.241-1.614Zm4.155-.297c-1.044-.116-1.488.091-1.681.297-.204.219-.359.679-.242 1.614.091.726.303 1.231.618 1.553.299.305.784.54 1.638.54.922 0 1.28-.198 1.442-.379.179-.2.308-.578.308-1.371 0-.765-.123-1.242-.37-1.554-.233-.296-.693-.587-1.713-.7Z"},{"d":"M6.25 9.037a.75.75 0 0 1 .75.75v1.501a.75.75 0 0 1-1.5 0V9.787a.75.75 0 0 1 .75-.75Zm4.25.75v1.501a.75.75 0 0 1-1.5 0V9.787a.75.75 0 0 1 1.5 0Z"}],"copilot-error":[{"d":"M16 11.24c0 .112-.072.274-.21.467L13 9.688V7.862l-.023-.116c-.49.21-1.075.291-1.727.291-.198 0-.388-.009-.571-.029L6.833 5.226a4.01 4.01 0 0 0 .17-.782c.117-.935-.037-1.395-.241-1.614-.193-.206-.637-.413-1.682-.297-.683.076-1.115.231-1.395.415l-1.257-.91c.579-.564 1.413-.877 2.485-.996 1.206-.134 2.262.034 2.944.765.05.053.096.108.139.165.044-.057.094-.112.143-.165.682-.731 1.738-.899 2.944-.765 1.23.137 2.145.528 2.724 1.261.566.715.693 1.614.693 2.484 0 .572-.053 1.148-.254 1.656.066.228.098.429.126.612.012.076.024.148.037.218.924.385 1.522 1.471 1.591 2.095Zm-5.083-8.707c-1.044-.116-1.488.091-1.681.297-.204.219-.359.679-.242 1.614.091.726.303 1.231.618 1.553.299.305.784.54 1.638.54.922 0 1.28-.198 1.442-.379.179-.2.308-.578.308-1.371 0-.765-.123-1.242-.37-1.554-.233-.296-.693-.587-1.713-.7Zm2.511 11.074c-1.393.776-3.272 1.428-5.43 1.428-4.562 0-7.873-2.914-7.998-3.749V9.338c.085-.628.677-1.686 1.588-2.065.013-.07.024-.143.036-.218.029-.183.06-.384.126-.612-.18-.455-.241-.963-.252-1.475L.31 4.107A.747.747 0 0 1 0 3.509V3.49a.748.748 0 0 1 .625-.73c.156-.026.306.047.435.139l14.667 10.578a.592.592 0 0 1 .227.264.752.752 0 0 1 .046.249v.022a.75.75 0 0 1-1.19.596Zm-1.367-.991L5.635 7.964a5.128 5.128 0 0 1-.889.073c-.652 0-1.236-.081-1.727-.291l-.023.116v4.255c.419.323 2.722 1.433 5.002 1.433 1.539 0 3.089-.505 4.063-.934Z"}],"copilot-warning":[{"d":"M8.498 14.811a4.53 4.53 0 0 1-1.161-1.337 4.476 4.476 0 0 1-.587-2.224 4.496 4.496 0 0 1 4.5-4.5 4.5 4.5 0 0 1 4.5 4.5 4.5 4.5 0 0 1-7.252 3.561ZM10.5 8.75V11a.75.75 0 0 0 1.5 0V8.75a.75.75 0 1 0-1.5 0Zm.75 5.75a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"},{"d":"m14.354 6.114-.05-.029a5.949 5.949 0 0 0-1.351-.589c.03-.19.047-.422.047-.709 0-.765-.123-1.242-.37-1.554-.233-.296-.693-.587-1.713-.7-1.044-.116-1.488.091-1.681.297-.204.219-.359.679-.242 1.614.058.462.165.834.316 1.127A6.025 6.025 0 0 0 6.369 7.76c-.472.185-1.015.277-1.623.277-.652 0-1.236-.081-1.727-.291l-.023.116v4.255c.265.205 1.285.725 2.577 1.079a5.937 5.937 0 0 0 .939 1.736C2.733 14.407.111 12.027 0 11.286V9.338c.085-.628.677-1.686 1.588-2.065.013-.07.024-.143.036-.218.029-.183.06-.384.126-.612-.201-.508-.254-1.084-.254-1.656 0-.87.128-1.769.693-2.484.579-.733 1.494-1.124 2.724-1.261 1.206-.134 2.262.034 2.944.765.05.053.096.108.139.165.044-.057.094-.112.143-.165.682-.731 1.738-.899 2.944-.765 1.23.137 2.145.528 2.724 1.261.566.715.693 1.614.693 2.484 0 .452-.033.906-.146 1.327ZM6.762 2.83c-.193-.206-.637-.413-1.682-.297-1.019.113-1.479.404-1.713.7-.247.312-.369.789-.369 1.554 0 .793.129 1.171.308 1.371.162.181.519.379 1.442.379.853 0 1.339-.235 1.638-.54.315-.322.527-.827.617-1.553.117-.935-.037-1.395-.241-1.614Z"}],"copy":[{"d":"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"},{"d":"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"}],"cpu":[{"d":"M6.5.75V2h3V.75a.75.75 0 0 1 1.5 0V2h1.25c.966 0 1.75.784 1.75 1.75V5h1.25a.75.75 0 0 1 0 1.5H14v3h1.25a.75.75 0 0 1 0 1.5H14v1.25A1.75 1.75 0 0 1 12.25 14H11v1.25a.75.75 0 0 1-1.5 0V14h-3v1.25a.75.75 0 0 1-1.5 0V14H3.75A1.75 1.75 0 0 1 2 12.25V11H.75a.75.75 0 0 1 0-1.5H2v-3H.75a.75.75 0 0 1 0-1.5H2V3.75C2 2.784 2.784 2 3.75 2H5V.75a.75.75 0 0 1 1.5 0Zm5.75 11.75a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25ZM5.75 5h4.5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-4.5A.75.75 0 0 1 5.75 5Zm.75 4.5h3v-3h-3Z"}],"credit-card":[{"d":"M10.75 9a.75.75 0 0 0 0 1.5h1.5a.75.75 0 0 0 0-1.5h-1.5Z"},{"d":"M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25ZM14.5 6.5h-13v5.75c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25Zm0-2.75a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25V5h13Z"}],"cross-reference":[{"d":"M2.75 3.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 13H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 14.543V13H2.75A1.75 1.75 0 0 1 1 11.25v-7.5C1 2.784 1.784 2 2.75 2h5.5a.75.75 0 0 1 0 1.5ZM16 1.25v4.146a.25.25 0 0 1-.427.177L14.03 4.03l-3.75 3.75a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l3.75-3.75-1.543-1.543A.25.25 0 0 1 11.604 1h4.146a.25.25 0 0 1 .25.25Z"}],"crosshairs":[{"d":"M14 8A6 6 0 1 1 2 8a6 6 0 0 1 12 0Zm-1.5 0a4.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0Z"},{"d":"M5 7.25a.75.75 0 0 1 0 1.5H1a.75.75 0 0 1 0-1.5Zm3-7a.75.75 0 0 1 .75.75v4a.75.75 0 0 1-1.5 0V1A.75.75 0 0 1 8 .25Zm7 7a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1 0-1.5Zm-7 3a.75.75 0 0 1 .75.75v4a.75.75 0 0 1-1.5 0v-4a.75.75 0 0 1 .75-.75Z"}],"dash":[{"d":"M2 7.75A.75.75 0 0 1 2.75 7h10a.75.75 0 0 1 0 1.5h-10A.75.75 0 0 1 2 7.75Z"}],"database":[{"d":"M1 3.5c0-.626.292-1.165.7-1.59.406-.422.956-.767 1.579-1.041C4.525.32 6.195 0 8 0c1.805 0 3.475.32 4.722.869.622.274 1.172.62 1.578 1.04.408.426.7.965.7 1.591v9c0 .626-.292 1.165-.7 1.59-.406.422-.956.767-1.579 1.041C11.476 15.68 9.806 16 8 16c-1.805 0-3.475-.32-4.721-.869-.623-.274-1.173-.62-1.579-1.04-.408-.426-.7-.965-.7-1.591Zm1.5 0c0 .133.058.318.282.551.227.237.591.483 1.101.707C4.898 5.205 6.353 5.5 8 5.5c1.646 0 3.101-.295 4.118-.742.508-.224.873-.471 1.1-.708.224-.232.282-.417.282-.55 0-.133-.058-.318-.282-.551-.227-.237-.591-.483-1.101-.707C11.102 1.795 9.647 1.5 8 1.5c-1.646 0-3.101.295-4.118.742-.508.224-.873.471-1.1.708-.224.232-.282.417-.282.55Zm0 4.5c0 .133.058.318.282.551.227.237.591.483 1.101.707C4.898 9.705 6.353 10 8 10c1.646 0 3.101-.295 4.118-.742.508-.224.873-.471 1.1-.708.224-.232.282-.417.282-.55V5.724c-.241.15-.503.286-.778.407C11.475 6.68 9.805 7 8 7c-1.805 0-3.475-.32-4.721-.869a6.15 6.15 0 0 1-.779-.407Zm0 2.225V12.5c0 .133.058.318.282.55.227.237.592.484 1.1.708 1.016.447 2.471.742 4.118.742 1.647 0 3.102-.295 4.117-.742.51-.224.874-.47 1.101-.707.224-.233.282-.418.282-.551v-2.275c-.241.15-.503.285-.778.406-1.247.549-2.917.869-4.722.869-1.805 0-3.475-.32-4.721-.869a6.327 6.327 0 0 1-.779-.406Z"}],"dependabot":[{"d":"M5.75 7.5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5a.75.75 0 0 1 .75-.75Zm5.25.75a.75.75 0 0 0-1.5 0v1.5a.75.75 0 0 0 1.5 0v-1.5Z"},{"d":"M6.25 0h2A.75.75 0 0 1 9 .75V3.5h3.25a2.25 2.25 0 0 1 2.25 2.25V8h.75a.75.75 0 0 1 0 1.5h-.75v2.75a2.25 2.25 0 0 1-2.25 2.25h-8.5a2.25 2.25 0 0 1-2.25-2.25V9.5H.75a.75.75 0 0 1 0-1.5h.75V5.75A2.25 2.25 0 0 1 3.75 3.5H7.5v-2H6.25a.75.75 0 0 1 0-1.5ZM3 5.75v6.5c0 .414.336.75.75.75h8.5a.75.75 0 0 0 .75-.75v-6.5a.75.75 0 0 0-.75-.75h-8.5a.75.75 0 0 0-.75.75Z"}],"desktop-download":[{"d":"m4.927 5.427 2.896 2.896a.25.25 0 0 0 .354 0l2.896-2.896A.25.25 0 0 0 10.896 5H8.75V.75a.75.75 0 1 0-1.5 0V5H5.104a.25.25 0 0 0-.177.427Z"},{"d":"M1.573 2.573a.25.25 0 0 0-.073.177v7.5a.25.25 0 0 0 .25.25h12.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25h-3a.75.75 0 1 1 0-1.5h3A1.75 1.75 0 0 1 16 2.75v7.5A1.75 1.75 0 0 1 14.25 12h-3.727c.099 1.041.52 1.872 1.292 2.757A.75.75 0 0 1 11.25 16h-6.5a.75.75 0 0 1-.565-1.243c.772-.885 1.192-1.716 1.292-2.757H1.75A1.75 1.75 0 0 1 0 10.25v-7.5A1.75 1.75 0 0 1 1.75 1h3a.75.75 0 0 1 0 1.5h-3a.25.25 0 0 0-.177.073ZM6.982 12a5.72 5.72 0 0 1-.765 2.5h3.566a5.72 5.72 0 0 1-.765-2.5H6.982Z"}],"device-camera":[{"d":"M15 3c.55 0 1 .45 1 1v9c0 .55-.45 1-1 1H1c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1 0-.55.45-1 1-1h4c.55 0 1 .45 1 1Zm-4.5 9c1.94 0 3.5-1.56 3.5-3.5S12.44 5 10.5 5 7 6.56 7 8.5 8.56 12 10.5 12ZM13 8.5c0 1.38-1.13 2.5-2.5 2.5S8 9.87 8 8.5 9.13 6 10.5 6 13 7.13 13 8.5ZM6 5V4H2v1Z"}],"device-camera-video":[{"d":"M16 3.75v8.5a.75.75 0 0 1-1.136.643L11 10.575v.675A1.75 1.75 0 0 1 9.25 13h-7.5A1.75 1.75 0 0 1 0 11.25v-6.5C0 3.784.784 3 1.75 3h7.5c.966 0 1.75.784 1.75 1.75v.675l3.864-2.318A.75.75 0 0 1 16 3.75Zm-6.5 1a.25.25 0 0 0-.25-.25h-7.5a.25.25 0 0 0-.25.25v6.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-6.5ZM11 8.825l3.5 2.1v-5.85l-3.5 2.1Z"}],"device-desktop":[{"d":"M14.25 1c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 14.25 12h-3.727c.099 1.041.52 1.872 1.292 2.757A.752.752 0 0 1 11.25 16h-6.5a.75.75 0 0 1-.565-1.243c.772-.885 1.192-1.716 1.292-2.757H1.75A1.75 1.75 0 0 1 0 10.25v-7.5C0 1.784.784 1 1.75 1ZM1.75 2.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25ZM9.018 12H6.982a5.72 5.72 0 0 1-.765 2.5h3.566a5.72 5.72 0 0 1-.765-2.5Z"}],"device-mobile":[{"d":"M3.75 0h8.5C13.216 0 14 .784 14 1.75v12.5A1.75 1.75 0 0 1 12.25 16h-8.5A1.75 1.75 0 0 1 2 14.25V1.75C2 .784 2.784 0 3.75 0ZM3.5 1.75v12.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25ZM8 13a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"}],"devices":[{"d":"M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75V5a.75.75 0 0 1-1.5 0V2.75a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25H7A.75.75 0 0 1 7 12h-.268a5.712 5.712 0 0 1-.765 2.5H7A.75.75 0 0 1 7 16H4.5a.75.75 0 0 1-.565-1.243c.772-.885 1.193-1.716 1.292-2.757H1.75A1.75 1.75 0 0 1 0 10.25v-7.5Z"},{"d":"M10.75 7h3.5c.967 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 16h-3.5A1.75 1.75 0 0 1 9 14.25v-5.5C9 7.784 9.783 7 10.75 7Zm-.25 1.75v5.5c0 .138.112.25.25.25h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-3.5a.25.25 0 0 0-.25.25Z"}],"diamond":[{"d":"M.527 9.237a1.75 1.75 0 0 1 0-2.474L6.777.512a1.75 1.75 0 0 1 2.475 0l6.251 6.25a1.75 1.75 0 0 1 0 2.475l-6.25 6.251a1.75 1.75 0 0 1-2.475 0L.527 9.238Zm1.06-1.414a.25.25 0 0 0 0 .354l6.251 6.25a.25.25 0 0 0 .354 0l6.25-6.25a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.354 0l-6.25 6.25Z"}],"dice":[{"d":"M13.25 1c.966 0 1.75.784 1.75 1.75v10.5c0 .464-.184.909-.513 1.237A1.746 1.746 0 0 1 13.25 15H2.75c-.464 0-.909-.184-1.237-.513A1.746 1.746 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1h10.5ZM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25H2.75Z"},{"d":"M5 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm6-6a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM8 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"}],"diff":[{"d":"M8.75 1.75V5H12a.75.75 0 0 1 0 1.5H8.75v3.25a.75.75 0 0 1-1.5 0V6.5H4A.75.75 0 0 1 4 5h3.25V1.75a.75.75 0 0 1 1.5 0ZM4 13h8a.75.75 0 0 1 0 1.5H4A.75.75 0 0 1 4 13Z"}],"diff-added":[{"d":"M2.75 1h10.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1Zm10.5 1.5H2.75a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25ZM8 4a.75.75 0 0 1 .75.75v2.5h2.5a.75.75 0 0 1 0 1.5h-2.5v2.5a.75.75 0 0 1-1.5 0v-2.5h-2.5a.75.75 0 0 1 0-1.5h2.5v-2.5A.75.75 0 0 1 8 4Z"}],"diff-ignored":[{"d":"M13.25 1c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1ZM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25Zm8.53 3.28-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"}],"diff-modified":[{"d":"M13.25 1c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1ZM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z"}],"diff-removed":[{"d":"M13.25 1c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1ZM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25Zm8.5 6.25h-6.5a.75.75 0 0 1 0-1.5h6.5a.75.75 0 0 1 0 1.5Z"}],"diff-renamed":[{"d":"M13.25 1c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1ZM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25Zm9.03 6.03-3.25 3.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l1.97-1.97H4.75a.75.75 0 0 1 0-1.5h4.69L7.47 5.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018l3.25 3.25a.75.75 0 0 1 0 1.06Z"}],"discussion":[{"d":"M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z"}],"discussion-closed":[{"d":"M0 2.75C0 1.783.784 1 1.75 1h8.5c.967 0 1.75.783 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.457 1.457 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25Zm1.75-.25a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.189L6.22 8.72a.747.747 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25Zm12.5 2h-.5a.75.75 0 0 1 0-1.5h.5c.967 0 1.75.783 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.457 1.457 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 1 1 1.06-1.06l2.22 2.219V11.25a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25Zm-5.47.28-3 3a.747.747 0 0 1-1.06 0l-1.5-1.5a.749.749 0 1 1 1.06-1.06l.97.969L7.72 3.72a.749.749 0 1 1 1.06 1.06Z"}],"discussion-duplicate":[{"d":"M0 2.75C0 1.783.784 1 1.75 1h8.5c.967 0 1.75.783 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.457 1.457 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25Zm1.75-.25a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.189L6.22 8.72a.747.747 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25Zm12.5 2h-.5a.75.75 0 0 1 0-1.5h.5c.967 0 1.75.783 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.457 1.457 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 1 1 1.06-1.06l2.22 2.219V11.25a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25Zm-6.282.03L5.03 7.468a.749.749 0 1 1-1.06-1.061L6.907 3.47a.75.75 0 0 1 1.061 1.06Z"}],"discussion-outdated":[{"d":"M0 2.75C0 1.783.784 1 1.75 1h8.5c.967 0 1.75.783 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.457 1.457 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25Zm1.75-.25a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.189L6.22 8.72a.747.747 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25Zm12.5 2h-.5a.75.75 0 0 1 0-1.5h.5c.967 0 1.75.783 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.457 1.457 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 1 1 1.06-1.06l2.22 2.219V11.25a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25ZM6.5 4v1.492l.466.187.036.015.812.375a.75.75 0 1 1-.628 1.362l-.795-.367-.92-.368A.75.75 0 0 1 5 6V4a.75.75 0 0 1 1.5 0Z"}],"dot":[{"d":"M4 8a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Z"}],"dot-fill":[{"d":"M8 4a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z"}],"download":[{"d":"M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"},{"d":"M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"}],"duplicate":[{"d":"M10.5 3a.75.75 0 0 1 .75.75v1h1a.75.75 0 0 1 0 1.5h-1v1a.75.75 0 0 1-1.5 0v-1h-1a.75.75 0 0 1 0-1.5h1v-1A.75.75 0 0 1 10.5 3Z"},{"d":"M6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25v-7.5C5 .784 5.784 0 6.75 0ZM6.5 1.75v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25h-7.5a.25.25 0 0 0-.25.25Z"},{"d":"M1.75 5A1.75 1.75 0 0 0 0 6.75v7.5C0 15.216.784 16 1.75 16h7.5A1.75 1.75 0 0 0 11 14.25v-1.5a.75.75 0 0 0-1.5 0v1.5a.25.25 0 0 1-.25.25h-7.5a.25.25 0 0 1-.25-.25v-7.5a.25.25 0 0 1 .25-.25h1.5a.75.75 0 0 0 0-1.5h-1.5Z"}],"ellipsis":[{"d":"M0 5.75C0 4.784.784 4 1.75 4h12.5c.966 0 1.75.784 1.75 1.75v4.5A1.75 1.75 0 0 1 14.25 12H1.75A1.75 1.75 0 0 1 0 10.25ZM12 7a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM7 8a1 1 0 1 0 2 0 1 1 0 0 0-2 0ZM4 7a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"}],"exclamation":[{"d":"M8 11a2 2 0 1 1 .001 3.999A2 2 0 0 1 8 11ZM8 1a1.5 1.5 0 0 1 1.5 1.5v6a1.5 1.5 0 0 1-3 0v-6A1.5 1.5 0 0 1 8 1Z"}],"eye":[{"d":"M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.677 1.367-1.931 2.637-3.022C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z"}],"eye-closed":[{"d":"M.143 2.31a.75.75 0 0 1 1.047-.167l14.5 10.5a.75.75 0 1 1-.88 1.214l-2.248-1.628C11.346 13.19 9.792 14 8 14c-1.981 0-3.67-.992-4.933-2.078C1.797 10.832.88 9.577.43 8.9a1.619 1.619 0 0 1 0-1.797c.353-.533.995-1.42 1.868-2.305L.31 3.357A.75.75 0 0 1 .143 2.31Zm1.536 5.622A.12.12 0 0 0 1.657 8c0 .021.006.045.022.068.412.621 1.242 1.75 2.366 2.717C5.175 11.758 6.527 12.5 8 12.5c1.195 0 2.31-.488 3.29-1.191L9.063 9.695A2 2 0 0 1 6.058 7.52L3.529 5.688a14.207 14.207 0 0 0-1.85 2.244ZM8 3.5c-.516 0-1.017.09-1.499.251a.75.75 0 1 1-.473-1.423A6.207 6.207 0 0 1 8 2c1.981 0 3.67.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.11.166-.248.365-.41.587a.75.75 0 1 1-1.21-.887c.148-.201.272-.382.371-.53a.119.119 0 0 0 0-.137c-.412-.621-1.242-1.75-2.366-2.717C10.825 4.242 9.473 3.5 8 3.5Z"}],"feed-discussion":[{"d":"M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16ZM4 5v5a1 1 0 0 0 1 1h1v1.5a.5.5 0 0 0 .854.354L8.707 11H11a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1Z"}],"feed-forked":[{"d":"M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16ZM6 6.928a1.75 1.75 0 1 0-1 0V7.5A1.5 1.5 0 0 0 6.5 9h1v1.072a1.75 1.75 0 1 0 1 0V9h1A1.5 1.5 0 0 0 11 7.5v-.572a1.75 1.75 0 1 0-1 0V7.5a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5Z"}],"feed-heart":[{"d":"M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm2.33-11.5c-1.22 0-1.83.5-2.323 1.136C7.513 5 6.903 4.5 5.682 4.5c-1.028 0-2.169.784-2.169 2.5 0 1.499 1.493 3.433 3.246 4.517.52.321.89.479 1.248.484.357-.005.728-.163 1.247-.484C11.007 10.433 12.5 8.5 12.5 7c0-1.716-1.14-2.5-2.17-2.5Z"}],"feed-issue-closed":[{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm3.457 6.957a.999.999 0 1 0-1.414-1.414L7.25 8.336 5.957 7.043a.999.999 0 1 0-1.414 1.414l2 2a.999.999 0 0 0 1.414 0Z"}],"feed-issue-draft":[{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM3.802 7.334a.75.75 0 0 0-1.482-.233 5.8 5.8 0 0 0 0 1.798.749.749 0 1 0 1.482-.233 4.296 4.296 0 0 1 0-1.332ZM8 2.25a5.8 5.8 0 0 0-.899.07.749.749 0 1 0 .233 1.482 4.296 4.296 0 0 1 1.332 0 .75.75 0 0 0 .233-1.482A5.8 5.8 0 0 0 8 2.25Zm-.666 9.948a.75.75 0 0 0-.233 1.482 5.8 5.8 0 0 0 1.798 0 .749.749 0 1 0-.233-1.482 4.296 4.296 0 0 1-1.332 0Zm6.346-5.097a.749.749 0 1 0-1.482.233 4.296 4.296 0 0 1 0 1.332.75.75 0 0 0 1.482.233 5.8 5.8 0 0 0 0-1.798Zm-8.178-2.54a.75.75 0 0 0-.882-1.213A5.77 5.77 0 0 0 3.348 4.62a.749.749 0 1 0 1.213.882c.263-.361.58-.678.941-.941Zm-.941 5.937a.75.75 0 0 0-1.213.882 5.77 5.77 0 0 0 1.272 1.272.749.749 0 1 0 .882-1.213 4.285 4.285 0 0 1-.941-.941Zm6.819-7.15a.749.749 0 1 0-.882 1.213c.36.263.679.58.941.941a.75.75 0 0 0 1.213-.882 5.77 5.77 0 0 0-1.272-1.272Zm1.272 8.032a.749.749 0 1 0-1.213-.882c-.262.36-.581.679-.941.941a.75.75 0 0 0 .882 1.213 5.77 5.77 0 0 0 1.272-1.272Z"}],"feed-issue-open":[{"d":"M3.75 8a4.25 4.25 0 1 1 8.5 0 4.25 4.25 0 0 1-8.5 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z"},{"d":"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-5.75a5.75 5.75 0 1 0 0 11.5 5.75 5.75 0 1 0 0-11.5Z"}],"feed-issue-reopen":[{"d":"M.5 8a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm3.427-4.323a.25.25 0 0 0-.427.177V6c0 .138.112.25.25.25h2.146a.25.25 0 0 0 .177-.427l-.524-.524a4.003 4.003 0 0 1 6.862 1.858.75.75 0 0 0 1.467-.314 5.502 5.502 0 0 0-9.39-2.605Zm9.573 8.469V10a.25.25 0 0 0-.25-.25h-2.146a.25.25 0 0 0-.177.427l.524.524a4.002 4.002 0 0 1-6.862-1.858.75.75 0 0 0-1.467.314 5.502 5.502 0 0 0 9.39 2.605l.561.561a.25.25 0 0 0 .427-.177ZM10 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 10 8Z"}],"feed-merged":[{"d":"M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm.25-11.25A1.75 1.75 0 1 0 6 6.428v3.144a1.75 1.75 0 1 0 1 0V8.236A2.99 2.99 0 0 0 9 9h.571a1.75 1.75 0 1 0 0-1H9a2 2 0 0 1-1.957-1.586A1.75 1.75 0 0 0 8.25 4.75Z"}],"feed-person":[{"d":"M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm.847-8.145a2.502 2.502 0 1 0-1.694 0C5.471 8.261 4 9.775 4 11c0 .395.145.995 1 .995h6c.855 0 1-.6 1-.995 0-1.224-1.47-2.74-3.153-3.145Z"}],"feed-plus":[{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm1.062 4.312a1 1 0 1 0-2 0v2.75h-2.75a1 1 0 0 0 0 2h2.75v2.75a1 1 0 1 0 2 0v-2.75h2.75a1 1 0 1 0 0-2h-2.75Z"}],"feed-public":[{"d":"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm4.5.25v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1H7v-1.5a1.5 1.5 0 0 1 2.443-1.167.75.75 0 0 0 .943-1.166A3 3 0 0 0 5.5 5.75v1.5a1 1 0 0 0-1 1Z"}],"feed-pull-request-closed":[{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM5.5 12.25A1.75 1.75 0 0 0 6 8.822V6.928A1.752 1.752 0 0 0 5.5 3.5 1.75 1.75 0 0 0 5 6.928v1.894a1.752 1.752 0 0 0 .5 3.428Zm5-5a.5.5 0 0 0-.5.5v1.072a1.752 1.752 0 0 0 .5 3.428 1.75 1.75 0 0 0 .5-3.428V7.75a.5.5 0 0 0-.5-.5Zm1.255-2.763a.5.5 0 0 0-.707-.707l-.53.531-.531-.531a.5.5 0 0 0-.707.707l.531.531-.531.53a.5.5 0 0 0 .707.707l.531-.53.53.53a.5.5 0 0 0 .707-.707l-.53-.53Z"}],"feed-pull-request-draft":[{"d":"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm7.25 2.5c0-.793-.527-1.462-1.25-1.678V6.928A1.752 1.752 0 0 0 5.5 3.5 1.75 1.75 0 0 0 5 6.928v1.894a1.752 1.752 0 0 0 .5 3.428 1.75 1.75 0 0 0 1.75-1.75Zm3.25 1.75a1.75 1.75 0 1 0 .001-3.499 1.75 1.75 0 0 0-.001 3.499Zm0-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm.75-3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"}],"feed-pull-request-open":[{"d":"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm6.75 2.5c0-.793-.527-1.462-1.25-1.678V6.928A1.752 1.752 0 0 0 5 3.5a1.75 1.75 0 0 0-.5 3.428v1.894A1.752 1.752 0 0 0 5 12.25a1.75 1.75 0 0 0 1.75-1.75Zm3.25-5h.25a.5.5 0 0 1 .5.5v2.822a1.752 1.752 0 0 0 .5 3.428 1.75 1.75 0 0 0 .5-3.428V6a1.5 1.5 0 0 0-1.5-1.5H10V3.129a.25.25 0 0 0-.427-.177L7.702 4.823a.25.25 0 0 0 0 .354l1.871 1.871A.25.25 0 0 0 10 6.871Z"}],"feed-repo":[{"d":"M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16ZM5.5 4A1.5 1.5 0 0 0 4 5.5v5c0 .828.5 1.5 1 1.5v-1a1 1 0 0 1 1-1h5v1h-1v1h1.5a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.5-.5Zm.5 7.25v2.514a.25.25 0 0 0 .426.178l.898-.888a.25.25 0 0 1 .352 0l.898.888A.25.25 0 0 0 9 13.764V11H6.25a.25.25 0 0 0-.25.25Z"}],"feed-rocket":[{"d":"M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm3.031-12a4.38 4.38 0 0 0-3.097 1.283l-.23.229c-.156.157-.308.32-.452.49H5.65a.876.876 0 0 0-.746.417l-.856 1.388a.377.377 0 0 0 .21.556l1.552.477 1.35 1.35.478 1.553a.374.374 0 0 0 .555.21l1.389-.855a.876.876 0 0 0 .416-.746V8.747c.17-.144.333-.295.49-.452l.23-.23A4.379 4.379 0 0 0 12 4.969v-.093A.876.876 0 0 0 11.124 4Zm-5.107 7.144h-.001a.809.809 0 0 0-1.33-.881c-.395.394-.564 1.258-.62 1.62a.12.12 0 0 0 .035.108.12.12 0 0 0 .108.035c.362-.056 1.226-.225 1.62-.619a.803.803 0 0 0 .188-.263Z"}],"feed-star":[{"d":"M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm.252-12.932a.476.476 0 0 0-.682.195l-1.2 2.432-2.684.39a.477.477 0 0 0-.266.816l1.944 1.892-.46 2.674a.479.479 0 0 0 .694.504L8 10.709l2.4 1.261a.478.478 0 0 0 .694-.504l-.458-2.673L12.578 6.9a.479.479 0 0 0-.265-.815l-2.685-.39-1.2-2.432a.473.473 0 0 0-.176-.195Z"}],"feed-tag":[{"d":"M7.22 6.5a.72.72 0 1 1-1.44 0 .72.72 0 0 1 1.44 0Z"},{"d":"M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16ZM4 5v3.38c.001.397.159.778.44 1.059l3.211 3.213a1.202 1.202 0 0 0 1.698 0l3.303-3.303a1.202 1.202 0 0 0 0-1.698L9.439 4.44A1.5 1.5 0 0 0 8.379 4H5a1 1 0 0 0-1 1Z"}],"feed-trophy":[{"d":"M11 5h1v1.146a1 1 0 0 1-.629.928L11 7.223V5ZM5 7.223l-.371-.149A1 1 0 0 1 4 6.146V5h1v2.223Z"},{"d":"M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16ZM3 5v1.146a2 2 0 0 0 1.257 1.858l.865.346a3.005 3.005 0 0 0 2.294 2.093C7.22 11.404 6.658 12 5.502 12H5.5a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1c-1.158 0-1.72-.595-1.916-1.557a3.005 3.005 0 0 0 2.294-2.094l.865-.346A2 2 0 0 0 13 6.146V5a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1Z"}],"file":[{"d":"M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"}],"file-added":[{"d":"M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073Zm4.48 3.758a.75.75 0 0 1 .755.745l.01 1.497h1.497a.75.75 0 0 1 0 1.5H9v1.507a.75.75 0 0 1-1.5 0V9.005l-1.502.01a.75.75 0 0 1-.01-1.5l1.507-.01-.01-1.492a.75.75 0 0 1 .745-.755Z"}],"file-badge":[{"d":"M2.75 1.5a.25.25 0 0 0-.25.25v11.5c0 .138.112.25.25.25h3.5a.75.75 0 0 1 0 1.5h-3.5A1.75 1.75 0 0 1 1 13.25V1.75C1 .784 1.784 0 2.75 0h8a1.75 1.75 0 0 1 1.508.862.75.75 0 1 1-1.289.768.25.25 0 0 0-.219-.13h-8Z"},{"d":"M8 7a3.999 3.999 0 0 1 7.605-1.733 4 4 0 0 1-1.115 4.863l.995 4.973a.75.75 0 0 1-.991.852l-2.409-.876a.248.248 0 0 0-.17 0l-2.409.876a.75.75 0 0 1-.991-.852l.994-4.973A3.994 3.994 0 0 1 8 7Zm4-2.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Zm0 6.5c-.373 0-.745-.051-1.104-.154l-.649 3.243 1.155-.42c.386-.14.81-.14 1.196 0l1.155.42-.649-3.243A4.004 4.004 0 0 1 12 11Z"}],"file-binary":[{"d":"M4 1.75C4 .784 4.784 0 5.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0 1 14.25 15h-9a.75.75 0 0 1 0-1.5h9a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 10 4.25V1.5H5.75a.25.25 0 0 0-.25.25v2a.75.75 0 0 1-1.5 0Zm-4 6C0 6.784.784 6 1.75 6h1.5C4.216 6 5 6.784 5 7.75v2.5A1.75 1.75 0 0 1 3.25 12h-1.5A1.75 1.75 0 0 1 0 10.25ZM6.75 6h1.5a.75.75 0 0 1 .75.75v3.75h.75a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h.75v-3h-.75a.75.75 0 0 1 0-1.5Zm-5 1.5a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h1.5a.25.25 0 0 0 .25-.25v-2.5a.25.25 0 0 0-.25-.25Zm9.75-5.938V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"}],"file-check":[{"d":"M10.336 0c.464 0 .91.184 1.237.513l2.914 2.914c.33.328.513.773.513 1.237v3.587c0 .199-.079.39-.22.53a.747.747 0 0 1-1.06 0 .747.747 0 0 1-.22-.53V6h-2.75c-.464 0-.909-.184-1.237-.513A1.746 1.746 0 0 1 9 4.25V1.5H3.75a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25H7c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06A.747.747 0 0 1 7 16H3.75c-.464 0-.909-.184-1.237-.513A1.746 1.746 0 0 1 2 14.25V1.75C2 .784 2.784 0 3.75 0Zm.164 4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"},{"d":"M15.259 10a.754.754 0 0 1 .686.472.746.746 0 0 1-.171.815l-4.557 4.45a.752.752 0 0 1-1.055-.01L8.22 13.778a.754.754 0 0 1 .04-1.02.748.748 0 0 1 1.02-.038l1.42 1.425 4.025-3.932a.753.753 0 0 1 .534-.213Z"}],"file-code":[{"d":"M4 1.75C4 .784 4.784 0 5.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0 1 14.25 15h-9a.75.75 0 0 1 0-1.5h9a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 10 4.25V1.5H5.75a.25.25 0 0 0-.25.25v2.5a.75.75 0 0 1-1.5 0Zm1.72 4.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l1.47-1.47-1.47-1.47a.75.75 0 0 1 0-1.06ZM3.28 7.78 1.81 9.25l1.47 1.47a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Zm8.22-6.218V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"}],"file-diff":[{"d":"M1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073ZM8 3.25a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0V7h-1.5a.75.75 0 0 1 0-1.5h1.5V4A.75.75 0 0 1 8 3.25Zm-3 8a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z"}],"file-directory":[{"d":"M0 2.75C0 1.784.784 1 1.75 1H5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1h6.75c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25H7.5c-.55 0-1.07-.26-1.4-.7l-.9-1.2a.25.25 0 0 0-.2-.1Z"}],"file-directory-fill":[{"d":"M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"}],"file-directory-open-fill":[{"d":"M.513 1.513A1.75 1.75 0 0 1 1.75 1h3.5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1H13a1 1 0 0 1 1 1v.5H2.75a.75.75 0 0 0 0 1.5h11.978a1 1 0 0 1 .994 1.117L15 13.25A1.75 1.75 0 0 1 13.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75c0-.464.184-.91.513-1.237Z"}],"file-directory-symlink":[{"d":"M0 2.75C0 1.784.784 1 1.75 1H5a1.75 1.75 0 0 1 1.4.7l.9 1.2a.25.25 0 0 0 .2.1h6.75c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 15H5.375a.75.75 0 0 1 0-1.5h8.875a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25H7.5a1.75 1.75 0 0 1-1.4-.7l-.9-1.2a.25.25 0 0 0-.2-.1H1.75a.25.25 0 0 0-.25.25v3a.75.75 0 0 1-1.5 0v-3Z"},{"d":"M1.5 12.237a2.25 2.25 0 0 1 2.262-2.249L4 9.989v1.938c0 .218.26.331.42.183l2.883-2.677a.25.25 0 0 0 0-.366L4.42 6.39a.25.25 0 0 0-.42.183v1.916l-.229-.001A3.75 3.75 0 0 0 0 12.237v1.013a.75.75 0 0 0 1.5 0v-1.013Z"}],"file-media":[{"d":"M16 13.25A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75ZM1.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h.94l.03-.03 6.077-6.078a1.75 1.75 0 0 1 2.412-.06L14.5 10.31V2.75a.25.25 0 0 0-.25-.25Zm12.5 11a.25.25 0 0 0 .25-.25v-.917l-4.298-3.889a.25.25 0 0 0-.344.009L4.81 13.5ZM7 6a2 2 0 1 1-3.999.001A2 2 0 0 1 7 6ZM5.5 6a.5.5 0 1 0-1 0 .5.5 0 0 0 1 0Z"}],"file-moved":[{"d":"M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-3.5a.75.75 0 0 1 0-1.5h3.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073H3.75a.25.25 0 0 0-.25.25v6.5a.75.75 0 0 1-1.5 0v-6.5Z"},{"d":"m5.427 15.573 3.146-3.146a.25.25 0 0 0 0-.354L5.427 8.927A.25.25 0 0 0 5 9.104V11.5H.75a.75.75 0 0 0 0 1.5H5v2.396c0 .223.27.335.427.177Z"}],"file-removed":[{"d":"M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073Zm4.5 6h2.242a.75.75 0 0 1 0 1.5h-2.24l-2.254.015a.75.75 0 0 1-.01-1.5Z"}],"file-submodule":[{"d":"M0 2.75C0 1.784.784 1 1.75 1H5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1h6.75c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm9.42 9.36 2.883-2.677a.25.25 0 0 0 0-.366L9.42 6.39a.249.249 0 0 0-.42.183V8.5H4.75a.75.75 0 0 0 0 1.5H9v1.927c0 .218.26.331.42.183Z"}],"file-symlink-file":[{"d":"M2 1.75C2 .784 2.784 0 3.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0 1 12.25 15h-7a.75.75 0 0 1 0-1.5h7a.25.25 0 0 0 .25-.25V6H9.75A1.75 1.75 0 0 1 8 4.25V1.5H3.75a.25.25 0 0 0-.25.25V4.5a.75.75 0 0 1-1.5 0Zm-.5 10.487v1.013a.75.75 0 0 1-1.5 0v-1.012a3.748 3.748 0 0 1 3.77-3.749L4 8.49V6.573a.25.25 0 0 1 .42-.183l2.883 2.678a.25.25 0 0 1 0 .366L4.42 12.111a.25.25 0 0 1-.42-.183V9.99l-.238-.003a2.25 2.25 0 0 0-2.262 2.25Zm8-10.675V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"}],"file-zip":[{"d":"M3.5 1.75v11.5c0 .09.048.173.126.217a.75.75 0 0 1-.752 1.298A1.748 1.748 0 0 1 2 13.25V1.75C2 .784 2.784 0 3.75 0h5.586c.464 0 .909.185 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0 1 12.25 15h-.5a.75.75 0 0 1 0-1.5h.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177L9.513 1.573a.25.25 0 0 0-.177-.073H7.25a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5h-3a.25.25 0 0 0-.25.25Zm3.75 8.75h.5c.966 0 1.75.784 1.75 1.75v3a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1-.75-.75v-3c0-.966.784-1.75 1.75-1.75ZM6 5.25a.75.75 0 0 1 .75-.75h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 6 5.25Zm.75 2.25h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM8 6.75A.75.75 0 0 1 8.75 6h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 8 6.75ZM8.75 3h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM8 9.75A.75.75 0 0 1 8.75 9h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 8 9.75Zm-1 2.5v2.25h1v-2.25a.25.25 0 0 0-.25-.25h-.5a.25.25 0 0 0-.25.25Z"}],"filter":[{"d":"M.75 3h14.5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1 0-1.5ZM3 7.75A.75.75 0 0 1 3.75 7h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 3 7.75Zm3 4a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"}],"filter-remove":[{"d":"M14.384 4.57a.752.752 0 0 0 1.061 0 .752.752 0 0 0 0-1.061l-.884-.884.883-.884A.75.75 0 0 0 14.384.68l-.884.884-.884-.884a.75.75 0 0 0-1.06 1.061l.883.884-.883.884a.75.75 0 0 0 1.06 1.061l.884-.884.884.884ZM.75 3a.75.75 0 0 0 0 1.5H9A.75.75 0 0 0 9 3H.75ZM3 7.75A.75.75 0 0 1 3.75 7h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 3 7.75Zm3 4a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"}],"fiscal-host":[{"d":"M10 8a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"},{"d":"M4 9.25h-.75a.75.75 0 0 1 0-1.5H4v-1.5h-.75a.75.75 0 0 1 0-1.5H4V3.5a1 1 0 0 1 1-1h7.5a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1ZM5.5 4v.793a.75.75 0 0 1 0 1.414v1.586a.75.75 0 0 1 0 1.414V10H12V4Z"},{"d":"M12.75 14.25V14h-9.5v.25a.75.75 0 0 1-1.5 0V14A1.75 1.75 0 0 1 0 12.25V1.75C0 .784.784 0 1.75 0h12.5C15.217 0 16 .784 16 1.75v10.5A1.75 1.75 0 0 1 14.25 14v.25a.75.75 0 0 1-1.5 0ZM1.75 1.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Z"}],"flag":[{"d":"M15 10.2108V2.22083C15 2.14083 14.98 2.07083 14.95 1.99083C14.95 1.97083 14.95 1.95083 14.94 1.93083C14.92 1.88083 14.88 1.84083 14.85 1.80083C14.82 1.76083 14.81 1.72083 14.78 1.69083C14.76 1.67083 14.74 1.67083 14.72 1.65083C14.66 1.60083 14.6 1.56083 14.53 1.53083C11.75 0.420826 9.67 1.28083 7.84 2.03083C6.11 2.75083 4.6 3.36083 2.5 2.60083V1.97083C2.5 1.56083 2.16 1.22083 1.75 1.22083C1.34 1.22083 1 1.56083 1 1.97083V14.4708C1 14.8808 1.34 15.2208 1.75 15.2208C2.16 15.2208 2.5 14.8808 2.5 14.4708V4.18083C3.16 4.37083 3.78 4.45083 4.37 4.45083C5.91 4.45083 7.22 3.91083 8.42 3.41083C10.08 2.72083 11.54 2.14083 13.51 2.76083V9.19083C11.23 8.63083 9.45 9.36083 7.85 10.0308C6.6 10.5508 5.44 11.0308 4.06 10.9508C3.68 10.9208 3.29 11.2408 3.26 11.6508C3.23 12.0608 3.55 12.4208 3.96 12.4508C4.1 12.4508 4.25 12.4608 4.38 12.4608C5.9 12.4608 7.18 11.9308 8.42 11.4208C10.21 10.6808 11.76 10.0408 13.98 10.9208C14.04 10.9408 14.1 10.9508 14.17 10.9508C14.2 10.9508 14.23 10.9708 14.26 10.9708C14.49 10.9708 14.71 10.8508 14.85 10.6608C14.89 10.6108 14.93 10.5708 14.95 10.5108C14.97 10.4608 14.97 10.4108 14.98 10.3608C14.98 10.3208 15.01 10.2808 15.01 10.2308L15 10.2108Z"}],"flame":[{"d":"M9.533.753V.752c.217 2.385 1.463 3.626 2.653 4.81C13.37 6.74 14.498 7.863 14.498 10c0 3.5-3 6-6.5 6S1.5 13.512 1.5 10c0-1.298.536-2.56 1.425-3.286.376-.308.862 0 1.035.454C4.46 8.487 5.581 8.419 6 8c.282-.282.341-.811-.003-1.5C4.34 3.187 7.035.75 8.77.146c.39-.137.726.194.763.607ZM7.998 14.5c2.832 0 5-1.98 5-4.5 0-1.463-.68-2.19-1.879-3.383l-.036-.037c-1.013-1.008-2.3-2.29-2.834-4.434-.322.256-.63.579-.864.953-.432.696-.621 1.58-.046 2.73.473.947.67 2.284-.278 3.232-.61.61-1.545.84-2.403.633a2.79 2.79 0 0 1-1.436-.874A3.198 3.198 0 0 0 3 10c0 2.53 2.164 4.5 4.998 4.5Z"}],"flowchart":[{"d":"M9.25 5c.199 0 .39-.079.53-.22a.747.747 0 0 0 0-1.06.747.747 0 0 0-.53-.22V5Zm-2-1.5a.747.747 0 0 0-.53.22.747.747 0 0 0 0 1.06c.14.141.331.22.53.22V3.5Zm7 8.75.53.53c.3-.3.3-.77 0-1.06l-.53.53Zm-1.47-2.53a.748.748 0 0 0-1.244.23.76.76 0 0 0 .184.83l.53-.53.53-.53Zm-1.06 4a.748.748 0 0 0 .23 1.244.76.76 0 0 0 .83-.184l-.53-.53-.53-.53Zm1.53-.72c.199 0 .39-.079.53-.22a.747.747 0 0 0 0-1.06.747.747 0 0 0-.53-.22V13Zm-6-1.5a.747.747 0 0 0-.53.22.747.747 0 0 0 0 1.06c.14.141.331.22.53.22v-1.5Zm3.53-4.72.53-.53-1.06-1.06-.53.53.53.53.53.53ZM5.72 9.72l-.53.53 1.06 1.06.53-.53-.53-.53-.53-.53Zm3.53-5.47V3.5h-2V5h2v-.75Zm5 8 .53-.53-2-2-.53.53-.53.53 2 2 .53-.53Zm0 0-.53-.53-2 2 .53.53.53.53 2-2-.53-.53Zm-1 0v-.75h-6V13h6v-.75Zm-3-6-.53-.53-4 4 .53.53.53.53 4-4-.53-.53ZM3.25 2v.75h2v-1.5h-2V2Zm2 0v.75c.28 0 .5.22.5.5h1.5a2 2 0 0 0-2-2V2ZM6.5 3.25h-.75v2h1.5v-2H6.5Zm0 2h-.75c0 .133-.053.26-.146.354a.504.504 0 0 1-.354.146v1.5a2 2 0 0 0 2-2H6.5ZM5.25 6.5v-.75h-2v1.5h2V6.5Zm-2 0v-.75a.504.504 0 0 1-.354-.146.504.504 0 0 1-.146-.354h-1.5c0 1.1.9 2 2 2V6.5ZM2 5.25h.75v-2h-1.5v2H2Zm0-2h.75c0-.28.22-.5.5-.5v-1.5a2 2 0 0 0-2 2H2ZM11.25 2v.75h2v-1.5h-2V2Zm2 0v.75c.28 0 .5.22.5.5h1.5a2 2 0 0 0-2-2V2Zm1.25 1.25h-.75v2h1.5v-2h-.75Zm0 2h-.75c0 .133-.053.26-.146.354a.504.504 0 0 1-.354.146v1.5a2 2 0 0 0 2-2h-.75ZM13.25 6.5v-.75h-2v1.5h2V6.5Zm-2 0v-.75a.504.504 0 0 1-.354-.146.504.504 0 0 1-.146-.354h-1.5c0 1.1.9 2 2 2V6.5ZM10 5.25h.75v-2h-1.5v2H10Zm0-2h.75c0-.28.22-.5.5-.5v-1.5a2 2 0 0 0-2 2H10ZM3.25 10v.75h2v-1.5h-2V10Zm2 0v.75c.28 0 .5.22.5.5h1.5a2 2 0 0 0-2-2V10Zm1.25 1.25h-.75v2h1.5v-2H6.5Zm0 2h-.75c0 .133-.053.26-.146.354a.504.504 0 0 1-.354.146v1.5a2 2 0 0 0 2-2H6.5ZM5.25 14.5v-.75h-2v1.5h2v-.75Zm-2 0v-.75a.504.504 0 0 1-.354-.146.504.504 0 0 1-.146-.354h-1.5c0 1.1.9 2 2 2v-.75ZM2 13.25h.75v-2h-1.5v2H2Zm0-2h.75c0-.28.22-.5.5-.5v-1.5a2 2 0 0 0-2 2H2Z"}],"focus-center":[{"d":"M2.75 2.5a.25.25 0 0 0-.25.25v2.5a.75.75 0 0 1-1.5 0v-2.5C1 1.784 1.784 1 2.75 1h2.5a.75.75 0 0 1 0 1.5h-2.5ZM10 1.75a.75.75 0 0 1 .75-.75h2.5c.966 0 1.75.784 1.75 1.75v2.5a.75.75 0 0 1-1.5 0v-2.5a.25.25 0 0 0-.25-.25h-2.5a.75.75 0 0 1-.75-.75ZM1.75 10a.75.75 0 0 1 .75.75v2.5c0 .138.112.25.25.25h2.5a.75.75 0 0 1 0 1.5h-2.5A1.75 1.75 0 0 1 1 13.25v-2.5a.75.75 0 0 1 .75-.75Zm12.5 0a.75.75 0 0 1 .75.75v2.5A1.75 1.75 0 0 1 13.25 15h-2.5a.75.75 0 0 1 0-1.5h2.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 .75-.75ZM8 10a2 2 0 1 0 .001-3.999A2 2 0 0 0 8 10Z"},{"d":"M8 10a2 2 0 1 0 .001-3.999A2 2 0 0 0 8 10Z"}],"fold":[{"d":"M10.896 2H8.75V.75a.75.75 0 0 0-1.5 0V2H5.104a.25.25 0 0 0-.177.427l2.896 2.896a.25.25 0 0 0 .354 0l2.896-2.896A.25.25 0 0 0 10.896 2ZM8.75 15.25a.75.75 0 0 1-1.5 0V14H5.104a.25.25 0 0 1-.177-.427l2.896-2.896a.25.25 0 0 1 .354 0l2.896 2.896a.25.25 0 0 1-.177.427H8.75v1.25Zm-6.5-6.5a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM6 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 6 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM12 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 12 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5Z"}],"fold-down":[{"d":"m8.177 14.323 2.896-2.896a.25.25 0 0 0-.177-.427H8.75V7.764a.75.75 0 1 0-1.5 0V11H5.104a.25.25 0 0 0-.177.427l2.896 2.896a.25.25 0 0 0 .354 0ZM2.25 5a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM6 4.25a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5a.75.75 0 0 1 .75.75ZM8.25 5a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM12 4.25a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5a.75.75 0 0 1 .75.75Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5Z"}],"fold-up":[{"d":"M7.823 1.677 4.927 4.573A.25.25 0 0 0 5.104 5H7.25v3.236a.75.75 0 1 0 1.5 0V5h2.146a.25.25 0 0 0 .177-.427L8.177 1.677a.25.25 0 0 0-.354 0ZM13.75 11a.75.75 0 0 0 0 1.5h.5a.75.75 0 0 0 0-1.5h-.5Zm-3.75.75a.75.75 0 0 1 .75-.75h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1-.75-.75ZM7.75 11a.75.75 0 0 0 0 1.5h.5a.75.75 0 0 0 0-1.5h-.5ZM4 11.75a.75.75 0 0 1 .75-.75h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1-.75-.75ZM1.75 11a.75.75 0 0 0 0 1.5h.5a.75.75 0 0 0 0-1.5h-.5Z"}],"fork":[{"d":"M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"}],"gear":[{"d":"M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z"}],"gift":[{"d":"M2 2.75A2.75 2.75 0 0 1 4.75 0c.983 0 1.873.42 2.57 1.232.268.318.497.668.68 1.042.183-.375.411-.725.68-1.044C9.376.42 10.266 0 11.25 0a2.75 2.75 0 0 1 2.45 4h.55c.966 0 1.75.784 1.75 1.75v2c0 .698-.409 1.301-1 1.582v4.918A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25V9.332C.409 9.05 0 8.448 0 7.75v-2C0 4.784.784 4 1.75 4h.55c-.192-.375-.3-.8-.3-1.25ZM7.25 9.5H2.5v4.75c0 .138.112.25.25.25h4.5Zm1.5 0v5h4.5a.25.25 0 0 0 .25-.25V9.5Zm0-4V8h5.5a.25.25 0 0 0 .25-.25v-2a.25.25 0 0 0-.25-.25Zm-7 0a.25.25 0 0 0-.25.25v2c0 .138.112.25.25.25h5.5V5.5h-5.5Zm3-4a1.25 1.25 0 0 0 0 2.5h2.309c-.233-.818-.542-1.401-.878-1.793-.43-.502-.915-.707-1.431-.707ZM8.941 4h2.309a1.25 1.25 0 0 0 0-2.5c-.516 0-1 .205-1.43.707-.337.392-.646.975-.879 1.793Z"}],"git-branch":[{"d":"M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"}],"git-branch-check":[{"d":"M15.26 10a.741.741 0 0 1 .414.133.75.75 0 0 1 .1 1.154l-4.557 4.45a.753.753 0 0 1-1.055-.008l-1.943-1.95a.755.755 0 0 1 .024-1.038.753.753 0 0 1 1.038-.022l1.42 1.427 4.026-3.933A.752.752 0 0 1 15.26 10Zm-3.51-9a2.252 2.252 0 0 1 1.942 3.389 2.252 2.252 0 0 1-1.192.983V6A2.5 2.5 0 0 1 10 8.5H6a.997.997 0 0 0-1 1v1.128a2.256 2.256 0 0 1 1.469 2.503A2.252 2.252 0 1 1 3.5 10.628V5.372a2.255 2.255 0 0 1-1.469-2.503A2.252 2.252 0 1 1 5 5.372v1.836A2.493 2.493 0 0 1 6 7h4a.997.997 0 0 0 1-1v-.628A2.252 2.252 0 0 1 11.75 1Zm-7.5 1.5a.747.747 0 0 0-.53.22.747.747 0 0 0 0 1.06.747.747 0 0 0 1.06 0 .747.747 0 0 0 0-1.06.747.747 0 0 0-.53-.22Zm0 9.5a.747.747 0 0 0-.53.22.747.747 0 0 0 0 1.06.747.747 0 0 0 1.06 0 .747.747 0 0 0 0-1.06.747.747 0 0 0-.53-.22Zm7.5-9.5a.747.747 0 0 0-.53.22.747.747 0 0 0 0 1.06.747.747 0 0 0 1.06 0 .747.747 0 0 0 0-1.06.747.747 0 0 0-.53-.22Z"}],"git-commit":[{"d":"M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"}],"git-compare":[{"d":"M9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM6 12v-1.646a.25.25 0 0 1 .427-.177l2.396 2.396a.25.25 0 0 1 0 .354l-2.396 2.396A.25.25 0 0 1 6 15.146V13.5H5A2.5 2.5 0 0 1 2.5 11V5.372a2.25 2.25 0 1 1 1.5 0V11a1 1 0 0 0 1 1ZM4 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0ZM12.75 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"}],"git-merge":[{"d":"M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z"}],"git-merge-queue":[{"d":"M3.75 4.5a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM3 7.75a.75.75 0 0 1 1.5 0v2.878a2.251 2.251 0 1 1-1.5 0Zm.75 5.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm5-7.75a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm5.75 2.5a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-1.5 0a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"}],"git-pull-request":[{"d":"M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z"}],"git-pull-request-closed":[{"d":"M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75Zm-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-.97-.97-.97.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"}],"git-pull-request-draft":[{"d":"M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 14a2.25 2.25 0 1 1 0-4.5 2.25 2.25 0 0 1 0 4.5ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM14 7.5a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm0-4.25a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Z"}],"git-pull-request-locked":[{"d":"M12 6a3 3 0 0 1 3 3v1.169c.591.281 1 .883 1 1.581v2.5A1.75 1.75 0 0 1 14.25 16h-4.5A1.75 1.75 0 0 1 8 14.25v-2.5c0-.698.409-1.3 1-1.581V9a3 3 0 0 1 3-3Zm0 1.5A1.5 1.5 0 0 0 10.5 9v1h3V9A1.5 1.5 0 0 0 12 7.5ZM3.25 1A2.25 2.25 0 0 1 4 5.372v5.257a2.25 2.25 0 1 1-1.5 0V5.372A2.252 2.252 0 0 1 3.25 1Zm0 1.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM10 .854a.25.25 0 0 0-.427-.177L7.177 3.073a.251.251 0 0 0 0 .355l2.396 2.395A.25.25 0 0 0 10 5.646V.854Z"},{"d":"M11.997 2.708A2.492 2.492 0 0 0 11 2.5h-1V4h1c.5 0 .891 0 .956.597a.735.735 0 0 0 .746.674.75.75 0 0 0 .746-.674c0-.097 0-.147-.066-.356 0 0-.041-.122-.073-.198a2.242 2.242 0 0 0-.209-.393 2.004 2.004 0 0 0-.327-.412l-.039-.036a2.78 2.78 0 0 0-.127-.114c-.01-.009-.019-.018-.03-.026a1.977 1.977 0 0 0-.172-.129l-.035-.023a3.087 3.087 0 0 0-.156-.095l-.047-.025a2.656 2.656 0 0 0-.17-.082Z"}],"globe":[{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM5.78 8.75a9.64 9.64 0 0 0 1.363 4.177c.255.426.542.832.857 1.215.245-.296.551-.705.857-1.215A9.64 9.64 0 0 0 10.22 8.75Zm4.44-1.5a9.64 9.64 0 0 0-1.363-4.177c-.307-.51-.612-.919-.857-1.215a9.927 9.927 0 0 0-.857 1.215A9.64 9.64 0 0 0 5.78 7.25Zm-5.944 1.5H1.543a6.507 6.507 0 0 0 4.666 5.5c-.123-.181-.24-.365-.352-.552-.715-1.192-1.437-2.874-1.581-4.948Zm-2.733-1.5h2.733c.144-2.074.866-3.756 1.58-4.948.12-.197.237-.381.353-.552a6.507 6.507 0 0 0-4.666 5.5Zm10.181 1.5c-.144 2.074-.866 3.756-1.58 4.948-.12.197-.237.381-.353.552a6.507 6.507 0 0 0 4.666-5.5Zm2.733-1.5a6.507 6.507 0 0 0-4.666-5.5c.123.181.24.365.353.552.714 1.192 1.436 2.874 1.58 4.948Z"}],"goal":[{"d":"M13.637 2.363h-.001l1.676.335c.09.018.164.084.19.173a.25.25 0 0 1-.062.249l-1.373 1.374a.876.876 0 0 1-.619.256H12.31L9.45 7.611A1.5 1.5 0 1 1 6.5 8a1.501 1.501 0 0 1 1.889-1.449l2.861-2.862V2.552c0-.232.092-.455.256-.619L12.88.559a.25.25 0 0 1 .249-.062c.089.026.155.1.173.19Z"},{"d":"M2 8a6 6 0 1 0 11.769-1.656.751.751 0 1 1 1.442-.413 7.502 7.502 0 0 1-12.513 7.371A7.501 7.501 0 0 1 10.069.789a.75.75 0 0 1-.413 1.442A6.001 6.001 0 0 0 2 8Z"},{"d":"M5 8a3.002 3.002 0 0 0 4.699 2.476 3 3 0 0 0 1.28-2.827.748.748 0 0 1 1.045-.782.75.75 0 0 1 .445.61A4.5 4.5 0 1 1 8.516 3.53a.75.75 0 1 1-.17 1.49A3 3 0 0 0 5 8Z"}],"grabber":[{"d":"M10 13a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm0-4a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm-4 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm5-9a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"}],"graph":[{"d":"M1.5 1.75V13.5h13.75a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1-.75-.75V1.75a.75.75 0 0 1 1.5 0Zm14.28 2.53-5.25 5.25a.75.75 0 0 1-1.06 0L7 7.06 4.28 9.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.25-3.25a.75.75 0 0 1 1.06 0L10 7.94l4.72-4.72a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"}],"graph-bar-horizontal":[{"d":"M15.25 15H.75q-.311 0-.53-.22-.22-.219-.22-.53 0-.311.22-.53.219-.22.53-.22h14.5q.311 0 .53.22.22.219.22.53 0 .311-.22.53-.219.22-.53.22Z"},{"d":"M2.25 7h2.5a.25.25 0 0 1 .25.25v6.5a.25.25 0 0 1-.25.25h-2.5a.25.25 0 0 1-.25-.25v-6.5A.25.25 0 0 1 2.25 7Zm4-4h2.5a.25.25 0 0 1 .25.25v10.5a.25.25 0 0 1-.25.25h-2.5a.25.25 0 0 1-.25-.25V3.25A.25.25 0 0 1 6.25 3Zm4 6h2.5a.25.25 0 0 1 .25.25v4.5a.25.25 0 0 1-.25.25h-2.5a.25.25 0 0 1-.25-.25v-4.5a.25.25 0 0 1 .25-.25Z"}],"graph-bar-vertical":[{"d":"M1 15.25V.75q0-.311.22-.53.219-.22.53-.22.311 0 .53.22.22.219.22.53v14.5q0 .311-.22.53-.219.22-.53.22-.311 0-.53-.22-.22-.219-.22-.53Z"},{"d":"M9 3.25v2.5a.25.25 0 0 1-.25.25h-6.5A.25.25 0 0 1 2 5.75v-2.5A.25.25 0 0 1 2.25 3h6.5a.25.25 0 0 1 .25.25Zm4 4v2.5a.25.25 0 0 1-.25.25H2.25A.25.25 0 0 1 2 9.75v-2.5A.25.25 0 0 1 2.25 7h10.5a.25.25 0 0 1 .25.25Zm-6 4v2.5a.25.25 0 0 1-.25.25h-4.5a.25.25 0 0 1-.25-.25v-2.5a.25.25 0 0 1 .25-.25h4.5a.25.25 0 0 1 .25.25Z"}],"graph-stacked-area":[{"d":"M10.548 11.513a.775.775 0 0 1-1.096 0L7 9.06 2.56 13.5H14.5V7.56ZM14.72 1.22a.75.75 0 1 1 1.06 1.06l-5.232 5.233a.775.775 0 0 1-1.096 0L7 5.06l-5.72 5.72A.75.75 0 1 1 .22 9.72l6.232-6.233.059-.052a.775.775 0 0 1 1.037.052L10 5.94ZM16 14.225a.776.776 0 0 1-.775.775H.81a.775.775 0 0 1-.548-1.323l6.19-6.19.058-.052a.775.775 0 0 1 1.037.052L10 9.94l4.677-4.676.096-.082A.775.775 0 0 1 16 5.81Z"}],"hash":[{"d":"M6.368 1.01a.75.75 0 0 1 .623.859L6.57 4.5h3.98l.46-2.868a.75.75 0 0 1 1.48.237L12.07 4.5h2.18a.75.75 0 0 1 0 1.5h-2.42l-.64 4h2.56a.75.75 0 0 1 0 1.5h-2.8l-.46 2.869a.75.75 0 0 1-1.48-.237l.42-2.632H5.45l-.46 2.869a.75.75 0 0 1-1.48-.237l.42-2.632H1.75a.75.75 0 0 1 0-1.5h2.42l.64-4H2.25a.75.75 0 0 1 0-1.5h2.8l.46-2.868a.75.75 0 0 1 .858-.622ZM9.67 10l.64-4H6.33l-.64 4Z"}],"heading":[{"d":"M3.75 2a.75.75 0 0 1 .75.75V7h7V2.75a.75.75 0 0 1 1.5 0v10.5a.75.75 0 0 1-1.5 0V8.5h-7v4.75a.75.75 0 0 1-1.5 0V2.75A.75.75 0 0 1 3.75 2Z"}],"heart":[{"d":"m8 14.25.345.666a.75.75 0 0 1-.69 0l-.008-.004-.018-.01a7.152 7.152 0 0 1-.31-.17 22.055 22.055 0 0 1-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.066 22.066 0 0 1-3.744 2.584l-.018.01-.006.003h-.002ZM4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.58 20.58 0 0 0 8 13.393a20.58 20.58 0 0 0 3.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.749.749 0 0 1-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5Z"}],"heart-fill":[{"d":"M7.655 14.916v-.001h-.002l-.006-.003-.018-.01a22.066 22.066 0 0 1-3.744-2.584C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.044 5.231-3.886 6.818a22.094 22.094 0 0 1-3.433 2.414 7.152 7.152 0 0 1-.31.17l-.018.01-.008.004a.75.75 0 0 1-.69 0Z"}],"history":[{"d":"m.427 1.927 1.215 1.215a8.002 8.002 0 1 1-1.6 5.685.75.75 0 1 1 1.493-.154 6.5 6.5 0 1 0 1.18-4.458l1.358 1.358A.25.25 0 0 1 3.896 6H.25A.25.25 0 0 1 0 5.75V2.104a.25.25 0 0 1 .427-.177ZM7.75 4a.75.75 0 0 1 .75.75v2.992l2.028.812a.75.75 0 0 1-.557 1.392l-2.5-1A.751.751 0 0 1 7 8.25v-3.5A.75.75 0 0 1 7.75 4Z"}],"home":[{"d":"M6.906.664a1.749 1.749 0 0 1 2.187 0l5.25 4.2c.415.332.657.835.657 1.367v7.019A1.75 1.75 0 0 1 13.25 15h-3.5a.75.75 0 0 1-.75-.75V9H7v5.25a.75.75 0 0 1-.75.75h-3.5A1.75 1.75 0 0 1 1 13.25V6.23c0-.531.242-1.034.657-1.366l5.25-4.2Zm1.25 1.171a.25.25 0 0 0-.312 0l-5.25 4.2a.25.25 0 0 0-.094.196v7.019c0 .138.112.25.25.25H5.5V8.25a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 .75.75v5.25h2.75a.25.25 0 0 0 .25-.25V6.23a.25.25 0 0 0-.094-.195Z"}],"home-fill":[{"d":"M6.906.384a1.75 1.75 0 0 1 2.187 0l5.25 4.2c.415.332.657.835.657 1.367v7.019a1.75 1.75 0 0 1-1.75 1.75h-2.5a.75.75 0 0 1-.75-.75V8.72H6v5.25a.75.75 0 0 1-.75.75h-2.5A1.75 1.75 0 0 1 1 12.97V5.95c0-.531.242-1.034.657-1.366l5.249-4.2Z"}],"horizontal-rule":[{"d":"M0 7.75A.75.75 0 0 1 .75 7h14.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 7.75Z"}],"hourglass":[{"d":"M2.75 1h10.5a.75.75 0 0 1 0 1.5h-.75v1.25a4.75 4.75 0 0 1-1.9 3.8l-.333.25a.25.25 0 0 0 0 .4l.333.25a4.75 4.75 0 0 1 1.9 3.8v1.25h.75a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5h.75v-1.25a4.75 4.75 0 0 1 1.9-3.8l.333-.25a.25.25 0 0 0 0-.4L5.4 7.55a4.75 4.75 0 0 1-1.9-3.8V2.5h-.75a.75.75 0 0 1 0-1.5ZM11 2.5H5v1.25c0 1.023.482 1.986 1.3 2.6l.333.25c.934.7.934 2.1 0 2.8l-.333.25a3.251 3.251 0 0 0-1.3 2.6v1.25h6v-1.25a3.251 3.251 0 0 0-1.3-2.6l-.333-.25a1.748 1.748 0 0 1 0-2.8l.333-.25a3.251 3.251 0 0 0 1.3-2.6Z"}],"hubot":[{"d":"M0 8a8 8 0 0 1 16 0v5.25a.75.75 0 0 1-1.5 0V8a6.5 6.5 0 1 0-13 0v5.25a.75.75 0 0 1-1.5 0Zm3-1.25C3 5.784 3.784 5 4.75 5h6.5c.966 0 1.75.784 1.75 1.75v1.5A1.75 1.75 0 0 1 11.25 10h-6.5A1.75 1.75 0 0 1 3 8.25Zm1.47-.53a.75.75 0 0 0 0 1.06l1.5 1.5a.75.75 0 0 0 1.06 0L8 7.81l.97.97a.75.75 0 0 0 1.06 0l1.5-1.5a.749.749 0 0 0-.326-1.275.749.749 0 0 0-.734.215l-.97.97-.97-.97a.75.75 0 0 0-1.06 0l-.97.97-.97-.97a.75.75 0 0 0-1.06 0Zm1.03 6.03a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"}],"id-badge":[{"d":"M3 7.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-3Zm10 .25a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1 0-1.5h4.5a.75.75 0 0 1 .75.75ZM10.25 11a.75.75 0 0 0 0-1.5h-2.5a.75.75 0 0 0 0 1.5h2.5Z"},{"d":"M7.25 0h1.5c.966 0 1.75.784 1.75 1.75V3h3.75c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25v-8.5C0 3.784.784 3 1.75 3H5.5V1.75C5.5.784 6.284 0 7.25 0Zm3.232 4.5A1.75 1.75 0 0 1 8.75 6h-1.5a1.75 1.75 0 0 1-1.732-1.5H1.75a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25ZM7 1.75v2.5c0 .138.112.25.25.25h1.5A.25.25 0 0 0 9 4.25v-2.5a.25.25 0 0 0-.25-.25h-1.5a.25.25 0 0 0-.25.25Z"}],"image":[{"d":"M16 13.25A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75ZM1.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h.94l.03-.03 6.077-6.078a1.75 1.75 0 0 1 2.412-.06L14.5 10.31V2.75a.25.25 0 0 0-.25-.25Zm12.5 11a.25.25 0 0 0 .25-.25v-.917l-4.298-3.889a.25.25 0 0 0-.344.009L4.81 13.5ZM7 6a2 2 0 1 1-3.999.001A2 2 0 0 1 7 6ZM5.5 6a.5.5 0 1 0-1 0 .5.5 0 0 0 1 0Z"}],"inbox":[{"d":"M2.8 2.06A1.75 1.75 0 0 1 4.41 1h7.18c.7 0 1.333.417 1.61 1.06l2.74 6.395c.04.093.06.194.06.295v4.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25v-4.5c0-.101.02-.202.06-.295Zm1.61.44a.25.25 0 0 0-.23.152L1.887 8H4.75a.75.75 0 0 1 .6.3L6.625 10h2.75l1.275-1.7a.75.75 0 0 1 .6-.3h2.863L11.82 2.652a.25.25 0 0 0-.23-.152Zm10.09 7h-2.875l-1.275 1.7a.75.75 0 0 1-.6.3h-3.5a.75.75 0 0 1-.6-.3L4.375 9.5H1.5v3.75c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25Z"}],"inbox-fill":[{"d":"M2.8 2.06A1.75 1.75 0 0 1 4.41 1h7.18c.7 0 1.333.417 1.61 1.06l2.74 6.395a.75.75 0 0 1 .06.295v4.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25v-4.5a.75.75 0 0 1 .06-.295L2.8 2.06Zm1.61.44a.25.25 0 0 0-.23.152L1.887 8H4.75a.75.75 0 0 1 .6.3L6.625 10h2.75l1.275-1.7a.75.75 0 0 1 .6-.3h2.863L11.82 2.652a.25.25 0 0 0-.23-.152H4.41Z"}],"infinity":[{"d":"M8 6.984c.59-.533 1.204-1.066 1.825-1.493.797-.548 1.7-.991 2.675-.991C14.414 4.5 16 6.086 16 8s-1.586 3.5-3.5 3.5c-.975 0-1.878-.444-2.675-.991-.621-.427-1.235-.96-1.825-1.493-.59.533-1.204 1.066-1.825 1.493-.797.547-1.7.991-2.675.991C1.586 11.5 0 9.914 0 8s1.586-3.5 3.5-3.5c.975 0 1.878.443 2.675.991.621.427 1.235.96 1.825 1.493ZM9.114 8c.536.483 1.052.922 1.56 1.273.704.483 1.3.727 1.826.727 1.086 0 2-.914 2-2 0-1.086-.914-2-2-2-.525 0-1.122.244-1.825.727-.51.35-1.025.79-1.561 1.273ZM3.5 6c-1.086 0-2 .914-2 2 0 1.086.914 2 2 2 .525 0 1.122-.244 1.825-.727.51-.35 1.025-.79 1.561-1.273-.536-.483-1.052-.922-1.56-1.273C4.621 6.244 4.025 6 3.5 6Z"}],"info":[{"d":"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"}],"issue":[{"d":"M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"},{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"}],"issue-closed":[{"d":"M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z"},{"d":"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z"}],"issue-draft":[{"d":"M14.307 11.655a.75.75 0 0 1 .165 1.048 8.05 8.05 0 0 1-1.769 1.77.75.75 0 0 1-.883-1.214 6.552 6.552 0 0 0 1.44-1.439.75.75 0 0 1 1.047-.165Zm-2.652-9.962a.75.75 0 0 1 1.048-.165 8.05 8.05 0 0 1 1.77 1.769.75.75 0 0 1-1.214.883 6.552 6.552 0 0 0-1.439-1.44.75.75 0 0 1-.165-1.047ZM6.749.097a8.074 8.074 0 0 1 2.502 0 .75.75 0 1 1-.233 1.482 6.558 6.558 0 0 0-2.036 0A.751.751 0 0 1 6.749.097ZM.955 6.125a.75.75 0 0 1 .624.857 6.558 6.558 0 0 0 0 2.036.75.75 0 1 1-1.482.233 8.074 8.074 0 0 1 0-2.502.75.75 0 0 1 .858-.624Zm14.09 0a.75.75 0 0 1 .858.624c.13.829.13 1.673 0 2.502a.75.75 0 1 1-1.482-.233 6.558 6.558 0 0 0 0-2.036.75.75 0 0 1 .624-.857Zm-8.92 8.92a.75.75 0 0 1 .857-.624 6.558 6.558 0 0 0 2.036 0 .75.75 0 1 1 .233 1.482c-.829.13-1.673.13-2.502 0a.75.75 0 0 1-.624-.858Zm-4.432-3.39a.75.75 0 0 1 1.048.165 6.552 6.552 0 0 0 1.439 1.44.751.751 0 0 1-.883 1.212 8.05 8.05 0 0 1-1.77-1.769.75.75 0 0 1 .166-1.048Zm2.652-9.962A.75.75 0 0 1 4.18 2.74a6.556 6.556 0 0 0-1.44 1.44.751.751 0 0 1-1.212-.883 8.05 8.05 0 0 1 1.769-1.77.75.75 0 0 1 1.048.166Z"}],"issue-locked":[{"d":"M12.001 6a3 3 0 0 1 3 3v1.168c.591.281 1 .884 1 1.582v2.5a1.75 1.75 0 0 1-1.75 1.75h-4.5a1.75 1.75 0 0 1-1.75-1.75v-2.5c0-.698.409-1.301 1-1.582V9a3 3 0 0 1 3-3Zm0 1.5a1.5 1.5 0 0 0-1.5 1.5v1h3V9a1.5 1.5 0 0 0-1.5-1.5Z"},{"d":"M5.095.546a8.004 8.004 0 0 1 3.847-.49l.259.035a8.007 8.007 0 0 1 3.58 1.494l.207.16a7.997 7.997 0 0 1 2.148 2.639c.187.369-.005.807-.391.959-.386.152-.817-.04-1.013-.406a6.516 6.516 0 0 0-1.242-1.635l-.11-.105-.052-.046a5.601 5.601 0 0 0-.226-.193l-.049-.04-.042-.031a5.773 5.773 0 0 0-.249-.187l-.082-.057a6.275 6.275 0 0 0-.683-.411l-.028-.014a6.448 6.448 0 0 0-1.146-.458l-.039-.011a6.667 6.667 0 0 0-.376-.095l-.018-.005a6.137 6.137 0 0 0-.409-.075l-.003-.001h-.003l-.015-.002a7.553 7.553 0 0 0-.479-.051 5.582 5.582 0 0 0-.26-.015l-.155-.004L8 1.5c-.056 0-.112.003-.168.004-.054.001-.108.001-.162.004a6.436 6.436 0 0 0-.37.029l-.069.009c-.11.013-.218.029-.325.047l-.079.014a7.31 7.31 0 0 0-.383.082c-.27.066-.533.15-.788.249l-.016.005a6.598 6.598 0 0 0-1.096.553l-.083.053a6.974 6.974 0 0 0-.288.197l-.022.017a6.438 6.438 0 0 0-.609.509l-.064.061c-.082.079-.161.16-.238.243l-.038.039a7.194 7.194 0 0 0-.254.296l-.015.019c-.012.014-.022.029-.033.044a5.793 5.793 0 0 0-.188.249c-.018.025-.037.05-.054.076a6.468 6.468 0 0 0-.89 1.854l-.014.048a7.116 7.116 0 0 0-.084.327l-.02.089a6.429 6.429 0 0 0-.145 1.159l-.003.129L1.5 8c0 .066.003.131.005.196l.003.102a6.898 6.898 0 0 0 .034.434c.014.123.032.245.052.366l.007.034c.148.84.456 1.625.893 2.321l.034.052c.058.09.118.179.18.266l.054.076c.077.104.156.207.239.306l.024.029c.075.089.153.175.232.259l.073.077c.064.065.128.13.195.193.029.028.058.057.088.084a6.601 6.601 0 0 0 .299.259c.062.049.124.098.187.145l.072.052.146.104a6.46 6.46 0 0 0 1.929.904c.399.112.68.492.615.901-.065.409-.45.691-.851.588a8.005 8.005 0 0 1-3.041-1.528l-.202-.169A8.009 8.009 0 0 1 .059 7.03l.036-.259a8.007 8.007 0 0 1 1.507-3.574l.161-.207A8.005 8.005 0 0 1 5.095.546Z"},{"d":"M8.001 6.5c.259 0 .511.068.733.192A3.984 3.984 0 0 0 8.001 9v.5a1.503 1.503 0 0 1-1.5-1.5 1.503 1.503 0 0 1 1.5-1.5Z"}],"issue-opened":[{"d":"M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"},{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"}],"issue-reopened":[{"d":"M5.029 2.217a6.5 6.5 0 0 1 9.437 5.11.75.75 0 1 0 1.492-.154 8 8 0 0 0-14.315-4.03L.427 1.927A.25.25 0 0 0 0 2.104V5.75A.25.25 0 0 0 .25 6h3.646a.25.25 0 0 0 .177-.427L2.715 4.215a6.491 6.491 0 0 1 2.314-1.998ZM1.262 8.169a.75.75 0 0 0-1.22.658 8.001 8.001 0 0 0 14.315 4.03l1.216 1.216a.25.25 0 0 0 .427-.177V10.25a.25.25 0 0 0-.25-.25h-3.646a.25.25 0 0 0-.177.427l1.358 1.358a6.501 6.501 0 0 1-11.751-3.11.75.75 0 0 0-.272-.506Z"},{"d":"M9.06 9.06a1.5 1.5 0 1 1-2.12-2.12 1.5 1.5 0 0 1 2.12 2.12Z"}],"issue-tracked-by":[{"d":"M1.5 8a6.5 6.5 0 0 1 13 0A.75.75 0 0 0 16 8a8 8 0 1 0-8 8 .75.75 0 0 0 0-1.5A6.5 6.5 0 0 1 1.5 8Z"},{"d":"M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm3.573 5.823-2.896-2.896a.25.25 0 0 1 0-.354l2.896-2.896a.25.25 0 0 1 .427.177V11.5h3.25a.75.75 0 0 1 0 1.5H12v2.146a.25.25 0 0 1-.427.177Z"}],"issue-tracks":[{"d":"M1.5 8a6.5 6.5 0 0 1 13 0A.75.75 0 0 0 16 8a8 8 0 1 0-8 8 .75.75 0 0 0 0-1.5A6.5 6.5 0 0 1 1.5 8Z"},{"d":"M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm1.5 1.75a.75.75 0 0 1 .75-.75h5a.75.75 0 0 1 0 1.5h-5a.75.75 0 0 1-.75-.75Zm2.75 2.25a.75.75 0 0 0 0 1.5h3a.75.75 0 0 0 0-1.5h-3Z"}],"italic":[{"d":"M6 2.75A.75.75 0 0 1 6.75 2h6.5a.75.75 0 0 1 0 1.5h-2.505l-3.858 9H9.25a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.505l3.858-9H6.75A.75.75 0 0 1 6 2.75Z"}],"iterations":[{"d":"M2.5 7.25a4.75 4.75 0 0 1 9.5 0 .75.75 0 0 0 1.5 0 6.25 6.25 0 1 0-6.25 6.25H12v2.146c0 .223.27.335.427.177l2.896-2.896a.25.25 0 0 0 0-.354l-2.896-2.896a.25.25 0 0 0-.427.177V12H7.25A4.75 4.75 0 0 1 2.5 7.25Z"}],"kebab-horizontal":[{"d":"M8 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM1.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm13 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"}],"key":[{"d":"M10.5 0a5.499 5.499 0 1 1-1.288 10.848l-.932.932a.749.749 0 0 1-.53.22H7v.75a.749.749 0 0 1-.22.53l-.5.5a.749.749 0 0 1-.53.22H5v.75a.749.749 0 0 1-.22.53l-.5.5a.749.749 0 0 1-.53.22h-2A1.75 1.75 0 0 1 0 14.25v-2c0-.199.079-.389.22-.53l4.932-4.932A5.5 5.5 0 0 1 10.5 0Zm-4 5.5c-.001.431.069.86.205 1.269a.75.75 0 0 1-.181.768L1.5 12.56v1.69c0 .138.112.25.25.25h1.69l.06-.06v-1.19a.75.75 0 0 1 .75-.75h1.19l.06-.06v-1.19a.75.75 0 0 1 .75-.75h1.19l1.023-1.025a.75.75 0 0 1 .768-.18A4 4 0 1 0 6.5 5.5ZM11 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"}],"key-asterisk":[{"d":"M0 2.75A2.75 2.75 0 0 1 2.75 0h10.5A2.75 2.75 0 0 1 16 2.75v10.5A2.75 2.75 0 0 1 13.25 16H2.75A2.75 2.75 0 0 1 0 13.25ZM2.75 1.5c-.69 0-1.25.56-1.25 1.25v10.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25V2.75c0-.69-.56-1.25-1.25-1.25Z"},{"d":"M8 4a.75.75 0 0 1 .75.75V6.7l1.69-.975a.75.75 0 0 1 .75 1.3L9.5 8l1.69.976a.75.75 0 0 1-.75 1.298L8.75 9.3v1.951a.75.75 0 0 1-1.5 0V9.299l-1.69.976a.75.75 0 0 1-.75-1.3L6.5 8l-1.69-.975a.75.75 0 0 1 .75-1.3l1.69.976V4.75A.75.75 0 0 1 8 4Z"}],"law":[{"d":"M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"}],"light-bulb":[{"d":"M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"}],"link":[{"d":"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"}],"link-external":[{"d":"M3.75 2h3.5a.75.75 0 0 1 0 1.5h-3.5a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-3.5a.75.75 0 0 1 1.5 0v3.5A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-8.5C2 2.784 2.784 2 3.75 2Zm6.854-1h4.146a.25.25 0 0 1 .25.25v4.146a.25.25 0 0 1-.427.177L13.03 4.03 9.28 7.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.75-3.75-1.543-1.543A.25.25 0 0 1 10.604 1Z"}],"list-ordered":[{"d":"M5 3.25a.75.75 0 0 1 .75-.75h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 5 3.25Zm0 5a.75.75 0 0 1 .75-.75h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 5 8.25Zm0 5a.75.75 0 0 1 .75-.75h8.5a.75.75 0 0 1 0 1.5h-8.5a.75.75 0 0 1-.75-.75ZM.924 10.32a.5.5 0 0 1-.851-.525l.001-.001.001-.002.002-.004.007-.011c.097-.144.215-.273.348-.384.228-.19.588-.392 1.068-.392.468 0 .858.181 1.126.484.259.294.377.673.377 1.038 0 .987-.686 1.495-1.156 1.845l-.047.035c-.303.225-.522.4-.654.597h1.357a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5c0-1.005.692-1.52 1.167-1.875l.035-.025c.531-.396.8-.625.8-1.078a.57.57 0 0 0-.128-.376C1.806 10.068 1.695 10 1.5 10a.658.658 0 0 0-.429.163.835.835 0 0 0-.144.153ZM2.003 2.5V6h.503a.5.5 0 0 1 0 1H.5a.5.5 0 0 1 0-1h.503V3.308l-.28.14a.5.5 0 0 1-.446-.895l1.003-.5a.5.5 0 0 1 .723.447Z"}],"list-unordered":[{"d":"M5.75 2.5h8.5a.75.75 0 0 1 0 1.5h-8.5a.75.75 0 0 1 0-1.5Zm0 5h8.5a.75.75 0 0 1 0 1.5h-8.5a.75.75 0 0 1 0-1.5Zm0 5h8.5a.75.75 0 0 1 0 1.5h-8.5a.75.75 0 0 1 0-1.5ZM2 14a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-6a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM2 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"}],"location":[{"d":"m12.596 11.596-3.535 3.536a1.5 1.5 0 0 1-2.122 0l-3.535-3.536a6.5 6.5 0 1 1 9.192-9.193 6.5 6.5 0 0 1 0 9.193Zm-1.06-8.132v-.001a5 5 0 1 0-7.072 7.072L8 14.07l3.536-3.534a5 5 0 0 0 0-7.072ZM8 9a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 9Z"}],"lock":[{"d":"M4 4a4 4 0 0 1 8 0v2h.25c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25v-5.5C2 6.784 2.784 6 3.75 6H4Zm8.25 3.5h-8.5a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25ZM10.5 6V4a2.5 2.5 0 1 0-5 0v2Z"}],"lockup-github":[{"d":"M24.606 2.058c2.796 0 4.485 1.194 5.024 3.204l-2.504.597c-.306-1.092-1.18-1.69-2.52-1.69-1.995 0-3.072 1.311-3.072 3.801s1.048 3.83 3.014 3.83c1.656 0 2.7-.93 2.861-2.534h-3.065V7.17h5.592v1.326c0 3.48-2.024 5.417-5.432 5.417-3.378 0-5.504-2.316-5.504-5.942s2.17-5.912 5.606-5.912m28.554 8.372c0 1.005.45 1.559 1.31 1.559.99 0 1.747-.918 1.747-2.17V5.51h2.316v8.183h-2.316V12.31c-.436.888-1.485 1.529-2.606 1.529-1.791 0-2.767-.99-2.767-2.796V5.509h2.315z"},{"d":"M62.055 7.01c.471-.99 1.535-1.69 2.607-1.69 2.233 0 3.412 1.544 3.334 4.267.079 2.694-1.145 4.252-3.334 4.252-1.116 0-2.136-.641-2.621-1.558v1.412H59.74V2.277h2.315zm1.791.247c-.955 0-1.713.932-1.79 2.213v.131c.077 1.325.835 2.301 1.79 2.301 1.2 0 1.855-.888 1.777-2.315.078-1.442-.577-2.33-1.777-2.33","fill-rule":"evenodd"},{"d":"M33.11 13.693h-2.3V5.51h2.3zm4.636-8.183h1.703v1.922h-1.703v3.713c0 .451.203.626.655.626h1.048v1.922h-1.878c-1.53 0-2.126-.655-2.126-2.039V7.432h-1.456V5.509h1.456V3.88l2.3-.54zm5.15-3.233v4.63h4.34v-4.63h2.475v11.416h-2.476V9.121h-4.34v4.572H40.42V2.277zM31.96 2a1.35 1.35 0 0 1 1.369 1.369 1.35 1.35 0 0 1-1.369 1.369 1.35 1.35 0 0 1-1.369-1.37A1.35 1.35 0 0 1 31.96 2M6.766 11.328c-2.063-.25-3.516-1.734-3.516-3.656 0-.781.281-1.625.75-2.188-.203-.515-.172-1.609.063-2.062.625-.078 1.468.25 1.968.703.594-.187 1.219-.281 1.985-.281.765 0 1.39.094 1.953.265.484-.437 1.344-.765 1.969-.687.218.422.25 1.515.046 2.047.5.593.766 1.39.766 2.203 0 1.922-1.453 3.375-3.547 3.64.531.344.89 1.094.89 1.954v1.625c0 .468.391.734.86.547C13.781 14.359 16 11.53 16 8.03 16 3.61 12.406 0 7.984 0 3.563 0 0 3.61 0 8.031a7.88 7.88 0 0 0 5.172 7.422c.422.156.828-.125.828-.547v-1.25c-.219.094-.5.156-.75.156-1.031 0-1.64-.562-2.078-1.609-.172-.422-.36-.672-.719-.719-.187-.015-.25-.093-.25-.187 0-.188.313-.328.625-.328.453 0 .844.281 1.25.86.313.452.64.655 1.031.655s.641-.14 1-.5c.266-.265.47-.5.657-.656"}],"log":[{"d":"M5 8.25a.75.75 0 0 1 .75-.75h4a.75.75 0 0 1 0 1.5h-4A.75.75 0 0 1 5 8.25ZM4 10.5A.75.75 0 0 0 4 12h4a.75.75 0 0 0 0-1.5H4Z"},{"d":"M13-.005c1.654 0 3 1.328 3 3 0 .982-.338 1.933-.783 2.818-.443.879-1.028 1.758-1.582 2.588l-.011.017c-.568.853-1.104 1.659-1.501 2.446-.398.789-.623 1.494-.623 2.136a1.5 1.5 0 1 0 2.333-1.248.75.75 0 0 1 .834-1.246A3 3 0 0 1 13 16H3a3 3 0 0 1-3-3c0-1.582.891-3.135 1.777-4.506.209-.322.418-.637.623-.946.473-.709.923-1.386 1.287-2.048H2.51c-.576 0-1.381-.133-1.907-.783A2.68 2.68 0 0 1 0 2.995a3 3 0 0 1 3-3Zm0 1.5a1.5 1.5 0 0 0-1.5 1.5c0 .476.223.834.667 1.132A.75.75 0 0 1 11.75 5.5H5.368c-.467 1.003-1.141 2.015-1.773 2.963-.192.289-.381.571-.558.845C2.13 10.711 1.5 11.916 1.5 13A1.5 1.5 0 0 0 3 14.5h7.401A2.989 2.989 0 0 1 10 13c0-.979.338-1.928.784-2.812.441-.874 1.023-1.748 1.575-2.576l.017-.026c.568-.853 1.103-1.658 1.501-2.448.398-.79.623-1.497.623-2.143 0-.838-.669-1.5-1.5-1.5Zm-10 0a1.5 1.5 0 0 0-1.5 1.5c0 .321.1.569.27.778.097.12.325.227.74.227h7.674A2.737 2.737 0 0 1 10 2.995c0-.546.146-1.059.401-1.5Z"}],"logo-gist":[{"d":"M4.7 8.73v-1h3.52v5.69c-.78.37-1.95.64-3.59.64C1.11 14.06 0 11.37 0 8.03 0 4.69 1.13 2 4.63 2c1.62 0 2.64.33 3.28.66v1.05c-1.22-.5-2-.73-3.28-.73-2.57 0-3.48 2.21-3.48 5.06 0 2.85.91 5.05 3.47 5.05.89 0 1.98-.07 2.53-.34V8.73Zm10.98.69h.03c2.22.2 2.75.95 2.75 2.23 0 1.21-.76 2.41-3.14 2.41-.75 0-1.83-.19-2.33-.39v-.94c.47.17 1.22.36 2.33.36 1.62 0 2.06-.69 2.06-1.42 0-.71-.22-1.21-1.77-1.34-2.26-.2-2.73-1-2.73-2.08 0-1.11.72-2.31 2.92-2.31.73 0 1.56.09 2.25.39v.94c-.61-.2-1.22-.36-2.27-.36-1.55 0-1.88.57-1.88 1.34 0 .69.28 1.04 1.78 1.17Zm8.58-3.33v.85h-2.42v4.87c0 .95.53 1.34 1.5 1.34.2 0 .42 0 .61-.03v.89c-.17.03-.5.05-.69.05-1.31 0-2.5-.6-2.5-2.13v-5H19.2v-.48l1.56-.44V3.9l1.08-.31v2.5h2.42Zm-13.17-.03v6.41c0 .54.19.7.67.7v.89c-1.14 0-1.72-.47-1.72-1.72V6.06h1.05Zm.25-2.33c0 .44-.34.78-.78.78a.76.76 0 0 1-.77-.78c0-.44.32-.78.77-.78s.78.34.78.78Z"}],"logo-github":[{"d":"M5.606 2.058c2.796 0 4.485 1.194 5.024 3.204l-2.505.597c-.305-1.092-1.179-1.69-2.519-1.69-1.995 0-3.072 1.311-3.072 3.801s1.048 3.83 3.014 3.83c1.656 0 2.7-.93 2.861-2.534H5.344V7.17h5.592v1.326c0 3.48-2.024 5.417-5.432 5.417C2.126 13.912 0 11.596 0 7.97s2.17-5.912 5.606-5.912m28.554 8.373c0 1.005.45 1.559 1.31 1.559.99 0 1.747-.918 1.747-2.17V5.51h2.316v8.183h-2.316V12.31c-.436.888-1.485 1.529-2.606 1.529-1.791 0-2.767-.99-2.767-2.796V5.509h2.315z"},{"d":"M43.055 7.01c.471-.99 1.535-1.69 2.607-1.69 2.233 0 3.413 1.544 3.334 4.267.078 2.694-1.145 4.252-3.334 4.252-1.116 0-2.136-.641-2.621-1.558v1.412H40.74V2.277h2.315zm1.791.247c-.955 0-1.713.932-1.79 2.213v.131c.077 1.325.835 2.301 1.79 2.301 1.2 0 1.855-.888 1.777-2.315.078-1.442-.577-2.33-1.777-2.33","fill-rule":"evenodd"},{"d":"M14.11 13.693h-2.3V5.51h2.3zm4.636-8.183h1.703v1.922h-1.703v3.713c0 .451.204.626.655.626h1.048v1.922h-1.878c-1.53 0-2.126-.655-2.126-2.039V7.432h-1.456V5.509h1.456V3.88l2.3-.54zm5.15-3.233v4.63h4.34v-4.63h2.475v11.416h-2.476V9.121h-4.34v4.572H21.42V2.277zM12.96 2a1.35 1.35 0 0 1 1.369 1.369 1.35 1.35 0 0 1-1.369 1.369 1.35 1.35 0 0 1-1.369-1.37A1.35 1.35 0 0 1 12.96 2"}],"loop":[{"d":"M1.896 4.559a6.25 6.25 0 0 1 8.839 0 .75.75 0 0 1-1.06 1.061 4.75 4.75 0 1 0 0 6.717L13.03 8.98l-1.553-1.554A.25.25 0 0 1 11.654 7h4.096a.25.25 0 0 1 .25.25v4.096a.25.25 0 0 1-.427.177l-1.482-1.482-3.356 3.356a6.25 6.25 0 0 1-8.839-8.838Z"}],"mail":[{"d":"M1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25v-8.5C0 2.784.784 2 1.75 2ZM1.5 12.251c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V5.809L8.38 9.397a.75.75 0 0 1-.76 0L1.5 5.809v6.442Zm13-8.181v-.32a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25v.32L8 7.88Z"}],"mark-github":[{"d":"M6.766 11.328c-2.063-.25-3.516-1.734-3.516-3.656 0-.781.281-1.625.75-2.188-.203-.515-.172-1.609.063-2.062.625-.078 1.468.25 1.968.703.594-.187 1.219-.281 1.985-.281.765 0 1.39.094 1.953.265.484-.437 1.344-.765 1.969-.687.218.422.25 1.515.046 2.047.5.593.766 1.39.766 2.203 0 1.922-1.453 3.375-3.547 3.64.531.344.89 1.094.89 1.954v1.625c0 .468.391.734.86.547C13.781 14.359 16 11.53 16 8.03 16 3.61 12.406 0 7.984 0 3.563 0 0 3.61 0 8.031a7.88 7.88 0 0 0 5.172 7.422c.422.156.828-.125.828-.547v-1.25c-.219.094-.5.156-.75.156-1.031 0-1.64-.562-2.078-1.609-.172-.422-.36-.672-.719-.719-.187-.015-.25-.093-.25-.187 0-.188.313-.328.625-.328.453 0 .844.281 1.25.86.313.452.64.655 1.031.655s.641-.14 1-.5c.266-.265.47-.5.657-.656"}],"markdown":[{"d":"M14.85 3c.63 0 1.15.52 1.14 1.15v7.7c0 .63-.51 1.15-1.15 1.15H1.15C.52 13 0 12.48 0 11.84V4.15C0 3.52.52 3 1.15 3ZM9 11V5H7L5.5 7 4 5H2v6h2V8l1.5 1.92L7 8v3Zm2.99.5L14.5 8H13V5h-2v3H9.5Z"}],"marketplace":[{"d":"M2 3.75C2 2.784 2.784 2 3.75 2h.5C5.216 2 6 2.784 6 3.75v1.5c0 .078-.005.155-.015.23A1.75 1.75 0 017 5.25V3.75C7 2.784 7.784 2 8.75 2h.5c.966 0 1.75.784 1.75 1.75v1.5c0 .078-.005.155-.015.23A1.75 1.75 0 0112 5.25v-1.5C12 2.784 12.784 2 13.75 2h.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14h-.5A1.75 1.75 0 0112 12.25v-1.5c0-.078.005-.155.015-.23A1.75 1.75 0 0111 10.75v1.5c0 .966-.784 1.75-1.75 1.75h-.5A1.75 1.75 0 016 12.25v-1.5c0-.078.005-.155.015-.23A1.75 1.75 0 015 10.75v1.5c0 .966-.784 1.75-1.75 1.75h-.5A1.75 1.75 0 011 12.25v-8.5z"}],"maximize":[{"d":"M6.137 8.803a.749.749 0 1 1 1.06 1.06L3.811 13.25H6a.75.75 0 0 1 0 1.5H2a.75.75 0 0 1-.75-.75v-4a.75.75 0 0 1 1.5 0v2.19l3.387-3.387ZM14.75 6a.75.75 0 0 1-1.5 0V3.811L9.863 7.197a.749.749 0 1 1-1.06-1.06L12.19 2.75H10a.75.75 0 0 1 0-1.5h4a.75.75 0 0 1 .75.75v4Z"}],"mcp":[{"d":"M5.52 1.12a3.578 3.578 0 0 1 6.078 2.98 3.578 3.578 0 0 1 2.982 6.08l-3.292 3.293a.252.252 0 0 0 0 .354l.843.843a.749.749 0 1 1-1.06 1.06l-.844-.843a1.75 1.75 0 0 1 0-2.474L13.52 9.12a2.08 2.08 0 0 0 0-2.94 2.08 2.08 0 0 0-2.94 0L7.731 9.03A.75.75 0 0 1 6.67 7.97l2.85-2.85a2.08 2.08 0 0 0 0-2.94 2.08 2.08 0 0 0-2.94 0l-4.799 4.8A.75.75 0 0 1 .72 5.92Z"},{"d":"M7.52 3.12a.749.749 0 1 1 1.06 1.06L5.731 7.03A2.079 2.079 0 0 0 8.67 9.97l2.85-2.85a.749.749 0 1 1 1.06 1.06l-2.849 2.85A3.578 3.578 0 0 1 4.67 5.97Z"}],"megaphone":[{"d":"M3.25 9a.75.75 0 0 1 .75.75c0 2.142.456 3.828.733 4.653a.122.122 0 0 0 .05.064.212.212 0 0 0 .117.033h1.31c.085 0 .18-.042.258-.152a.45.45 0 0 0 .075-.366A16.743 16.743 0 0 1 6 9.75a.75.75 0 0 1 1.5 0c0 1.588.25 2.926.494 3.85.293 1.113-.504 2.4-1.783 2.4H4.9c-.686 0-1.35-.41-1.589-1.12A16.4 16.4 0 0 1 2.5 9.75.75.75 0 0 1 3.25 9Z"},{"d":"M0 6a4 4 0 0 1 4-4h2.75a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-.75.75H4a4 4 0 0 1-4-4Zm4-2.5a2.5 2.5 0 1 0 0 5h2v-5Z"},{"d":"M15.59.082A.75.75 0 0 1 16 .75v10.5a.75.75 0 0 1-1.189.608l-.002-.001h.001l-.014-.01a5.775 5.775 0 0 0-.422-.25 10.63 10.63 0 0 0-1.469-.64C11.576 10.484 9.536 10 6.75 10a.75.75 0 0 1 0-1.5c2.964 0 5.174.516 6.658 1.043.423.151.787.302 1.092.443V2.014c-.305.14-.669.292-1.092.443C11.924 2.984 9.713 3.5 6.75 3.5a.75.75 0 0 1 0-1.5c2.786 0 4.826-.484 6.155-.957.665-.236 1.154-.47 1.47-.64.144-.077.284-.161.421-.25l.014-.01a.75.75 0 0 1 .78-.061Z"}],"mention":[{"d":"M8 .5a7.499 7.499 0 0 1 7.499 7.462l.002.038v1.164a2.612 2.612 0 0 1-4.783 1.454A3.763 3.763 0 0 1 8 11.776 3.776 3.776 0 1 1 11.776 8v1.164a1.112 1.112 0 0 0 2.225 0L14 8a6 6 0 1 0-3.311 5.365.75.75 0 0 1 .673 1.341A7.5 7.5 0 1 1 8 .5Zm0 5.225a2.275 2.275 0 1 0 0 4.552 2.275 2.275 0 0 0 0-4.552Z"}],"meter":[{"d":"M8 1.5a6.5 6.5 0 1 0 6.016 4.035.75.75 0 0 1 1.388-.57 8 8 0 1 1-4.37-4.37.75.75 0 1 1-.569 1.389A6.473 6.473 0 0 0 8 1.5Zm6.28.22a.75.75 0 0 1 0 1.06l-4.063 4.064a2.5 2.5 0 1 1-1.06-1.06L13.22 1.72a.75.75 0 0 1 1.06 0ZM7 8a1 1 0 1 0 2 0 1 1 0 0 0-2 0Z"}],"milestone":[{"d":"M7.75 0a.75.75 0 0 1 .75.75V3h3.634c.414 0 .814.147 1.13.414l2.07 1.75a1.75 1.75 0 0 1 0 2.672l-2.07 1.75a1.75 1.75 0 0 1-1.13.414H8.5v5.25a.75.75 0 0 1-1.5 0V10H2.75A1.75 1.75 0 0 1 1 8.25v-3.5C1 3.784 1.784 3 2.75 3H7V.75A.75.75 0 0 1 7.75 0Zm4.384 8.5a.25.25 0 0 0 .161-.06l2.07-1.75a.248.248 0 0 0 0-.38l-2.07-1.75a.25.25 0 0 0-.161-.06H2.75a.25.25 0 0 0-.25.25v3.5c0 .138.112.25.25.25h9.384Z"}],"minimize":[{"d":"M6.668 8.583a.69.69 0 0 1 .13.012l.017.003c.01.002.02.007.031.01.036.009.073.017.108.032a.727.727 0 0 1 .243.163.711.711 0 0 1 .126.176.62.62 0 0 1 .036.067.822.822 0 0 1 .058.287l-.001 4a.75.75 0 0 1-1.5 0v-2.188L2.53 14.53a.749.749 0 1 1-1.06-1.06l3.387-3.387H2.666a.75.75 0 0 1 0-1.5h4.002ZM13.47 1.47a.749.749 0 1 1 1.06 1.06l-3.385 3.386h2.188a.75.75 0 0 1 0 1.5l-4 .001a.738.738 0 0 1-.231-.041c-.019-.006-.038-.01-.056-.017a.62.62 0 0 1-.067-.036.711.711 0 0 1-.384-.525.72.72 0 0 1-.012-.132v-4a.75.75 0 0 1 1.5 0v2.191L13.47 1.47Z"}],"mirror":[{"d":"M15.547 3.061A.75.75 0 0 1 16 3.75v8.5a.751.751 0 0 1-1.265.545l-4.5-4.25a.75.75 0 0 1 0-1.09l4.5-4.25a.75.75 0 0 1 .812-.144ZM0 12.25v-8.5a.751.751 0 0 1 1.265-.545l4.5 4.25a.75.75 0 0 1 0 1.09l-4.5 4.25A.75.75 0 0 1 0 12.25Zm1.5-6.76v5.02L4.158 8ZM11.842 8l2.658 2.51V5.49ZM8 4a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-1.5 0v-.5A.75.75 0 0 1 8 4Zm.75-2.25v.5a.75.75 0 0 1-1.5 0v-.5a.75.75 0 0 1 1.5 0Zm0 6v.5a.75.75 0 0 1-1.5 0v-.5a.75.75 0 0 1 1.5 0ZM8 10a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-1.5 0v-.5A.75.75 0 0 1 8 10Zm0 3a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-1.5 0v-.5A.75.75 0 0 1 8 13Z"}],"moon":[{"d":"M9.598 1.591a.749.749 0 0 1 .785-.175 7.001 7.001 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786Zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.499 5.499 0 1 0 7.678-7.678Z"}],"mortar-board":[{"d":"M7.693 1.066a.747.747 0 0 1 .614 0l7.25 3.25a.75.75 0 0 1 0 1.368L13 6.831v2.794c0 1.024-.81 1.749-1.66 2.173-.893.447-2.075.702-3.34.702-.278 0-.55-.012-.816-.036a.75.75 0 0 1 .133-1.494c.22.02.45.03.683.03 1.082 0 2.025-.221 2.67-.543.69-.345.83-.682.83-.832V7.503L8.307 8.934a.747.747 0 0 1-.614 0L4 7.28v1.663c.296.105.575.275.812.512.438.438.688 1.059.688 1.796v3a.75.75 0 0 1-.75.75h-3a.75.75 0 0 1-.75-.75v-3c0-.737.25-1.358.688-1.796.237-.237.516-.407.812-.512V6.606L.443 5.684a.75.75 0 0 1 0-1.368ZM2.583 5 8 7.428 13.416 5 8 2.572ZM2.5 11.25v2.25H4v-2.25c0-.388-.125-.611-.25-.735a.697.697 0 0 0-.5-.203.707.707 0 0 0-.5.203c-.125.124-.25.347-.25.735Z"}],"move-to-bottom":[{"d":"M7.47 10.78a.749.749 0 0 0 1.06 0l3.75-3.75a.749.749 0 1 0-1.06-1.06L8.75 8.439V1.75a.75.75 0 0 0-1.5 0v6.689L4.78 5.97a.749.749 0 1 0-1.06 1.06l3.75 3.75ZM3.75 13a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z"}],"move-to-end":[{"d":"m10.78 8.53-3.75 3.75a.749.749 0 1 1-1.06-1.06l2.469-2.47H1.75a.75.75 0 0 1 0-1.5h6.689L5.97 4.78a.749.749 0 1 1 1.06-1.06l3.75 3.75a.749.749 0 0 1 0 1.06ZM13 12.25v-8.5a.75.75 0 0 1 1.5 0v8.5a.75.75 0 0 1-1.5 0Z"}],"move-to-start":[{"d":"M5.22 7.47a.749.749 0 0 0 0 1.06l3.75 3.75a.749.749 0 1 0 1.06-1.06L7.561 8.75h6.689a.75.75 0 0 0 0-1.5H7.561l2.469-2.47a.749.749 0 1 0-1.06-1.06L5.22 7.47ZM3 3.75a.75.75 0 0 0-1.5 0v8.5a.75.75 0 0 0 1.5 0v-8.5Z"}],"move-to-top":[{"d":"M3 2.25a.75.75 0 0 1 .75-.75h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 3 2.25Zm5.53 2.97 3.75 3.75a.749.749 0 1 1-1.06 1.06L8.75 7.561v6.689a.75.75 0 0 1-1.5 0V7.561L4.78 10.03a.749.749 0 1 1-1.06-1.06l3.75-3.75a.749.749 0 0 1 1.06 0Z"}],"multi-select":[{"d":"M5.75 7.5h7.5a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1 0-1.5Zm0 5h7.5a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1 0-1.5Zm-4-10h6.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5ZM2 14a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-6a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm10.314-3.082L11.07 2.417A.25.25 0 0 1 11.256 2h4.488a.25.25 0 0 1 .186.417l-2.244 2.5a.25.25 0 0 1-.372 0Z"}],"mute":[{"d":"M8 2.75v10.5a.751.751 0 0 1-1.238.57L3.473 11H1.75A1.75 1.75 0 0 1 0 9.25v-2.5C0 5.784.784 5 1.75 5h1.722l3.29-2.82A.75.75 0 0 1 8 2.75Zm3.28 2.47L13 6.94l1.72-1.72a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L14.06 8l1.72 1.72a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L13 9.06l-1.72 1.72a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L11.94 8l-1.72-1.72a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215Zm-7.042 1.1a.752.752 0 0 1-.488.18h-2a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h2c.179 0 .352.064.488.18L6.5 11.62V4.38Z"}],"no-entry":[{"d":"M4.25 7.25a.75.75 0 0 0 0 1.5h7.5a.75.75 0 0 0 0-1.5h-7.5Z"},{"d":"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z"}],"node":[{"d":"M8 5a1.507 1.507 0 0 0 1.061-.439 1.507 1.507 0 0 0 0-2.122 1.507 1.507 0 0 0-2.122 0 1.503 1.503 0 0 0 0 2.122C7.221 4.842 7.602 5 8 5Zm0 9a1.507 1.507 0 0 0 1.061-.439 1.507 1.507 0 0 0 0-2.122 1.507 1.507 0 0 0-2.122 0 1.503 1.503 0 0 0 0 2.122c.282.281.663.439 1.061.439Zm-7-2.5v-7a1.75 1.75 0 0 1 1.75-1.75H4.5a.75.75 0 0 1 0 1.5H2.75a.25.25 0 0 0-.25.25v7l.005.049a.246.246 0 0 0 .245.201H4.5a.75.75 0 0 1 0 1.5H2.75A1.75 1.75 0 0 1 1 11.5Zm12.5 0v-7a.25.25 0 0 0-.201-.245l-.049-.005H11.5a.75.75 0 0 1 0-1.5h1.75A1.75 1.75 0 0 1 15 4.5v7c0 .464-.184.909-.513 1.237a1.746 1.746 0 0 1-1.237.513H11.5a.75.75 0 0 1 0-1.5h1.75a.25.25 0 0 0 .25-.25Z"}],"north-star":[{"d":"M8.5.75a.75.75 0 0 0-1.5 0v5.19L4.391 3.33a.75.75 0 1 0-1.06 1.061L5.939 7H.75a.75.75 0 0 0 0 1.5h5.19l-2.61 2.609a.75.75 0 1 0 1.061 1.06L7 9.561v5.189a.75.75 0 0 0 1.5 0V9.56l2.609 2.61a.75.75 0 1 0 1.06-1.061L9.561 8.5h5.189a.75.75 0 0 0 0-1.5H9.56l2.61-2.609a.75.75 0 0 0-1.061-1.06L8.5 5.939V.75Z"}],"note":[{"d":"M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25Zm1.75-.25a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25ZM3.5 6.25a.75.75 0 0 1 .75-.75h7a.75.75 0 0 1 0 1.5h-7a.75.75 0 0 1-.75-.75Zm.75 2.25h4a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1 0-1.5Z"}],"number":[{"d":"M9 4.75A.75.75 0 0 1 9.75 4h4a.75.75 0 0 1 .53 1.28l-1.89 1.892c.312.076.604.18.867.319.742.391 1.244 1.063 1.244 2.005 0 .653-.231 1.208-.629 1.627-.386.408-.894.653-1.408.777-1.01.243-2.225.063-3.124-.527a.751.751 0 0 1 .822-1.254c.534.35 1.32.474 1.951.322.306-.073.53-.201.67-.349.129-.136.218-.32.218-.596 0-.308-.123-.509-.444-.678-.373-.197-.98-.318-1.806-.318a.75.75 0 0 1-.53-1.28l1.72-1.72H9.75A.75.75 0 0 1 9 4.75Zm-3.587 5.763c-.35-.05-.77.113-.983.572a.75.75 0 1 1-1.36-.632c.508-1.094 1.589-1.565 2.558-1.425 1 .145 1.872.945 1.872 2.222 0 1.433-1.088 2.192-1.79 2.681-.308.216-.571.397-.772.573H7a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75c0-.69.3-1.211.67-1.61.348-.372.8-.676 1.15-.92.8-.56 1.18-.904 1.18-1.474 0-.473-.267-.69-.587-.737ZM5.604.089A.75.75 0 0 1 6 .75v4.77h.711a.75.75 0 0 1 0 1.5H3.759a.75.75 0 0 1 0-1.5H4.5V2.15l-.334.223a.75.75 0 0 1-.832-1.248l1.5-1a.75.75 0 0 1 .77-.037Z"}],"organization":[{"d":"M1.75 16A1.75 1.75 0 0 1 0 14.25V1.75C0 .784.784 0 1.75 0h8.5C11.216 0 12 .784 12 1.75v12.5c0 .085-.006.168-.018.25h2.268a.25.25 0 0 0 .25-.25V8.285a.25.25 0 0 0-.111-.208l-1.055-.703a.749.749 0 1 1 .832-1.248l1.055.703c.487.325.779.871.779 1.456v5.965A1.75 1.75 0 0 1 14.25 16h-3.5a.766.766 0 0 1-.197-.026c-.099.017-.2.026-.303.026h-3a.75.75 0 0 1-.75-.75V14h-1v1.25a.75.75 0 0 1-.75.75Zm-.25-1.75c0 .138.112.25.25.25H4v-1.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 .75.75v1.25h2.25a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25ZM3.75 6h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM3 3.75A.75.75 0 0 1 3.75 3h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 3 3.75Zm4 3A.75.75 0 0 1 7.75 6h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 7 6.75ZM7.75 3h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM3 9.75A.75.75 0 0 1 3.75 9h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 3 9.75ZM7.75 9h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5Z"}],"package":[{"d":"m8.878.392 5.25 3.045c.54.314.872.89.872 1.514v6.098a1.75 1.75 0 0 1-.872 1.514l-5.25 3.045a1.75 1.75 0 0 1-1.756 0l-5.25-3.045A1.75 1.75 0 0 1 1 11.049V4.951c0-.624.332-1.201.872-1.514L7.122.392a1.75 1.75 0 0 1 1.756 0ZM7.875 1.69l-4.63 2.685L8 7.133l4.755-2.758-4.63-2.685a.248.248 0 0 0-.25 0ZM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432Zm6.25 8.271 4.625-2.683a.25.25 0 0 0 .125-.216V5.677L8.75 8.432Z"}],"package-dependencies":[{"d":"M6.122.392a1.75 1.75 0 0 1 1.756 0l5.25 3.045c.54.313.872.89.872 1.514V7.25a.75.75 0 0 1-1.5 0V5.677L7.75 8.432v6.384a1 1 0 0 1-1.502.865L.872 12.563A1.75 1.75 0 0 1 0 11.049V4.951c0-.624.332-1.2.872-1.514ZM7.125 1.69a.248.248 0 0 0-.25 0l-4.63 2.685L7 7.133l4.755-2.758ZM1.5 11.049a.25.25 0 0 0 .125.216l4.625 2.683V8.432L1.5 5.677Zm11.672-.282L11.999 12h3.251a.75.75 0 0 1 0 1.5h-3.251l1.173 1.233a.75.75 0 1 1-1.087 1.034l-2.378-2.5a.75.75 0 0 1 0-1.034l2.378-2.5a.75.75 0 0 1 1.087 1.034Z"}],"package-dependents":[{"d":"M6.122.392a1.75 1.75 0 0 1 1.756 0l5.25 3.045c.54.313.872.89.872 1.514V7.25a.75.75 0 0 1-1.5 0V5.677L7.75 8.432v6.384a1 1 0 0 1-1.502.865L.872 12.563A1.75 1.75 0 0 1 0 11.049V4.951c0-.624.332-1.2.872-1.514ZM7.125 1.69a.248.248 0 0 0-.25 0l-4.63 2.685L7 7.133l4.755-2.758ZM1.5 11.049a.25.25 0 0 0 .125.216l4.625 2.683V8.432L1.5 5.677Zm10.828 3.684 1.173-1.233H10.25a.75.75 0 0 1 0-1.5h3.251l-1.173-1.233a.75.75 0 1 1 1.087-1.034l2.378 2.5a.75.75 0 0 1 0 1.034l-2.378 2.5a.75.75 0 0 1-1.087-1.034Z"}],"paintbrush":[{"d":"M11.134 1.535c.7-.509 1.416-.942 2.076-1.155.649-.21 1.463-.267 2.069.34.603.601.568 1.411.368 2.07-.202.668-.624 1.39-1.125 2.096-1.011 1.424-2.496 2.987-3.775 4.249-1.098 1.084-2.132 1.839-3.04 2.3a3.744 3.744 0 0 1-1.055 3.217c-.431.431-1.065.691-1.657.861-.614.177-1.294.287-1.914.357A21.151 21.151 0 0 1 .797 16H.743l.007-.75H.749L.742 16a.75.75 0 0 1-.743-.742l.743-.008-.742.007v-.054a21.25 21.25 0 0 1 .13-2.284c.067-.647.187-1.287.358-1.914.17-.591.43-1.226.86-1.657a3.746 3.746 0 0 1 3.227-1.054c.466-.893 1.225-1.907 2.314-2.982 1.271-1.255 2.833-2.75 4.245-3.777ZM1.62 13.089c-.051.464-.086.929-.104 1.395.466-.018.932-.053 1.396-.104a10.511 10.511 0 0 0 1.668-.309c.526-.151.856-.325 1.011-.48a2.25 2.25 0 1 0-3.182-3.182c-.155.155-.329.485-.48 1.01a10.515 10.515 0 0 0-.309 1.67Zm10.396-10.34c-1.224.89-2.605 2.189-3.822 3.384l1.718 1.718c1.21-1.205 2.51-2.597 3.387-3.833.47-.662.78-1.227.912-1.662.134-.444.032-.551.009-.575h-.001V1.78c-.014-.014-.113-.113-.548.027-.432.14-.995.462-1.655.942Zm-4.832 7.266-.001.001a9.859 9.859 0 0 0 1.63-1.142L7.155 7.216a9.7 9.7 0 0 0-1.161 1.607c.482.302.889.71 1.19 1.192Z"}],"paper-airplane":[{"d":"M.989 8 .064 2.68a1.342 1.342 0 0 1 1.85-1.462l13.402 5.744a1.13 1.13 0 0 1 0 2.076L1.913 14.782a1.343 1.343 0 0 1-1.85-1.463L.99 8Zm.603-5.288L2.38 7.25h4.87a.75.75 0 0 1 0 1.5H2.38l-.788 4.538L13.929 8Z"}],"paperclip":[{"d":"M12.212 3.02a1.753 1.753 0 0 0-2.478.003l-5.83 5.83a3.007 3.007 0 0 0-.88 2.127c0 .795.315 1.551.88 2.116.567.567 1.333.89 2.126.89.79 0 1.548-.321 2.116-.89l5.48-5.48a.75.75 0 0 1 1.061 1.06l-5.48 5.48a4.492 4.492 0 0 1-3.177 1.33c-1.2 0-2.345-.487-3.187-1.33a4.483 4.483 0 0 1-1.32-3.177c0-1.195.475-2.341 1.32-3.186l5.83-5.83a3.25 3.25 0 0 1 5.553 2.297c0 .863-.343 1.691-.953 2.301L7.439 12.39c-.375.377-.884.59-1.416.593a1.998 1.998 0 0 1-1.412-.593 1.992 1.992 0 0 1 0-2.828l5.48-5.48a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-5.48 5.48a.492.492 0 0 0 0 .707.499.499 0 0 0 .352.154.51.51 0 0 0 .356-.154l5.833-5.827a1.755 1.755 0 0 0 0-2.481Z"}],"passkey-fill":[{"d":"M2.743 4.757a3.757 3.757 0 1 1 5.851 3.119 5.991 5.991 0 0 1 2.15 1.383c.17.17.257.405.258.646.003.598.001 1.197 0 1.795L11 12.778v.721a.5.5 0 0 1-.5.5H1.221a.749.749 0 0 1-.714-.784 6.004 6.004 0 0 1 3.899-5.339 3.754 3.754 0 0 1-1.663-3.119Z"},{"d":"M15.75 6.875c0 .874-.448 1.643-1.127 2.09a.265.265 0 0 0-.123.22v.59c0 .067-.026.13-.073.177l-.356.356a.125.125 0 0 0 0 .177l.356.356c.047.047.073.11.073.176v.231c0 .067-.026.13-.073.177l-.356.356a.125.125 0 0 0 0 .177l.356.356c.047.047.073.11.073.177v.287a.247.247 0 0 1-.065.168l-.8.88a.52.52 0 0 1-.77 0l-.8-.88a.247.247 0 0 1-.065-.168V9.185a.264.264 0 0 0-.123-.22 2.5 2.5 0 1 1 3.873-2.09ZM14 6.5a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"}],"paste":[{"d":"M3.626 3.533a.249.249 0 0 0-.126.217v9.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-9.5a.249.249 0 0 0-.126-.217.75.75 0 0 1 .752-1.298c.541.313.874.89.874 1.515v9.5A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25v-9.5c0-.625.333-1.202.874-1.515a.75.75 0 0 1 .752 1.298ZM5.75 1h4.5a.75.75 0 0 1 .75.75v3a.75.75 0 0 1-.75.75h-4.5A.75.75 0 0 1 5 4.75v-3A.75.75 0 0 1 5.75 1Zm.75 3h3V2.5h-3Z"}],"pause":[{"d":"M5 2h1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1Zm5 0h1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1Z"}],"pencil":[{"d":"M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.253.253 0 0 0-.064.108l-.558 1.953 1.953-.558a.253.253 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z"}],"pencil-ai":[{"d":"M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.748 1.748 0 0 1 0 2.474l-8.609 8.61c-.21.21-.471.363-.757.445l-3.251.929a.748.748 0 0 1-.736-.191.748.748 0 0 1-.191-.736l.929-3.251a1.76 1.76 0 0 1 .445-.757Zm-7.549 9.67a.253.253 0 0 0-.064.108l-.558 1.953 1.953-.558a.253.253 0 0 0 .108-.064l6.286-6.286L9.75 4.811Zm8.963-8.61a.252.252 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.252.252 0 0 0 0-.354Zm-.158 6.676A.246.246 0 0 1 12.502 9a.246.246 0 0 1 .232.163l.238.648a3.724 3.724 0 0 0 2.219 2.219l.649.238a.248.248 0 0 1 .16.202v.063a.248.248 0 0 1-.16.202l-.649.238a3.721 3.721 0 0 0-2.219 2.218l-.238.649a.246.246 0 0 1-.193.16h-.079a.245.245 0 0 1-.193-.16l-.239-.649a3.737 3.737 0 0 0-2.218-2.218l-.649-.238a.248.248 0 0 1-.118-.376.254.254 0 0 1 .118-.091l.649-.238a3.724 3.724 0 0 0 2.218-2.219Z"}],"people":[{"d":"M2 5.5a3.5 3.5 0 1 1 5.898 2.549 5.508 5.508 0 0 1 3.034 4.084.75.75 0 1 1-1.482.235 4 4 0 0 0-7.9 0 .75.75 0 0 1-1.482-.236A5.507 5.507 0 0 1 3.102 8.05 3.493 3.493 0 0 1 2 5.5ZM11 4a3.001 3.001 0 0 1 2.22 5.018 5.01 5.01 0 0 1 2.56 3.012.749.749 0 0 1-.885.954.752.752 0 0 1-.549-.514 3.507 3.507 0 0 0-2.522-2.372.75.75 0 0 1-.574-.73v-.352a.75.75 0 0 1 .416-.672A1.5 1.5 0 0 0 11 5.5.75.75 0 0 1 11 4Zm-5.5-.5a2 2 0 1 0-.001 3.999A2 2 0 0 0 5.5 3.5Z"}],"person":[{"d":"M10.561 8.073a6.005 6.005 0 0 1 3.432 5.142.75.75 0 1 1-1.498.07 4.5 4.5 0 0 0-8.99 0 .75.75 0 0 1-1.498-.07 6.004 6.004 0 0 1 3.431-5.142 3.999 3.999 0 1 1 5.123 0ZM10.5 5a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"}],"person-add":[{"d":"M7.9 8.548h-.001a5.528 5.528 0 0 1 3.1 4.659.75.75 0 1 1-1.498.086A4.01 4.01 0 0 0 5.5 9.5a4.01 4.01 0 0 0-4.001 3.793.75.75 0 1 1-1.498-.085 5.527 5.527 0 0 1 3.1-4.66 3.5 3.5 0 1 1 4.799 0ZM13.25 0a.75.75 0 0 1 .75.75V2h1.25a.75.75 0 0 1 0 1.5H14v1.25a.75.75 0 0 1-1.5 0V3.5h-1.25a.75.75 0 0 1 0-1.5h1.25V.75a.75.75 0 0 1 .75-.75ZM5.5 4a2 2 0 1 0-.001 3.999A2 2 0 0 0 5.5 4Z"}],"person-fill":[{"d":"M4.243 4.757a3.757 3.757 0 1 1 5.851 3.119 6.006 6.006 0 0 1 3.9 5.339.75.75 0 0 1-.715.784H2.721a.75.75 0 0 1-.714-.784 6.006 6.006 0 0 1 3.9-5.34 3.753 3.753 0 0 1-1.664-3.118Z"}],"pin":[{"d":"m11.294.984 3.722 3.722a1.75 1.75 0 0 1-.504 2.826l-1.327.613a3.089 3.089 0 0 0-1.707 2.084l-.584 2.454c-.317 1.332-1.972 1.8-2.94.832L5.75 11.311 1.78 15.28a.749.749 0 1 1-1.06-1.06l3.969-3.97-2.204-2.204c-.968-.968-.5-2.623.832-2.94l2.454-.584a3.08 3.08 0 0 0 2.084-1.707l.613-1.327a1.75 1.75 0 0 1 2.826-.504ZM6.283 9.723l2.732 2.731a.25.25 0 0 0 .42-.119l.584-2.454a4.586 4.586 0 0 1 2.537-3.098l1.328-.613a.25.25 0 0 0 .072-.404l-3.722-3.722a.25.25 0 0 0-.404.072l-.613 1.328a4.584 4.584 0 0 1-3.098 2.537l-2.454.584a.25.25 0 0 0-.119.42l2.731 2.732Z"}],"pin-slash":[{"d":"m1.655.595 13.75 13.75q.22.219.22.53 0 .311-.22.53-.219.22-.53.22-.311 0-.53-.22L.595 1.655q-.22-.219-.22-.53 0-.311.22-.53.219-.22.53-.22.311 0 .53.22ZM.72 14.22l4.5-4.5q.219-.22.53-.22.311 0 .53.22.22.219.22.53 0 .311-.22.53l-4.5 4.5q-.219.22-.53.22-.311 0-.53-.22-.22-.219-.22-.53 0-.311.22-.53Z"},{"d":"m5.424 6.146-1.759.419q-.143.034-.183.175-.04.141.064.245l5.469 5.469q.104.104.245.064.141-.04.175-.183l.359-1.509q.072-.302.337-.465.264-.163.567-.091.302.072.465.337.162.264.09.567l-.359 1.509q-.238.999-1.226 1.278-.988.28-1.714-.446L2.485 8.046q-.726-.726-.446-1.714.279-.988 1.278-1.226l1.759-.419q.303-.072.567.091.265.163.337.465.072.302-.091.567-.163.264-.465.336ZM7.47 3.47q.155-.156.247-.355l.751-1.627Q8.851.659 9.75.498q.899-.16 1.544.486l3.722 3.722q.646.645.486 1.544-.161.899-.99 1.282l-1.627.751q-.199.092-.355.247-.219.22-.53.22-.311 0-.53-.22-.22-.219-.22-.53 0-.311.22-.53.344-.345.787-.549l1.627-.751q.118-.055.141-.183.023-.128-.069-.221l-3.722-3.722q-.092-.092-.221-.069-.128.023-.183.141l-.751 1.627q-.204.443-.549.787-.219.22-.53.22-.311 0-.53-.22-.22-.219-.22-.53 0-.311.22-.53Z"}],"pivot-column":[{"d":"M0 1.75C0 .784.784 0 1.75 0h12.5C15.217 0 16 .784 16 1.75v5.5a.75.75 0 0 1-1.5 0V6.5h-8v8h.75a.75.75 0 0 1 0 1.5h-5.5A1.75 1.75 0 0 1 0 14.25ZM1.5 6.5v7.75c0 .138.112.25.25.25H5v-8Zm5-1.5h8V1.75a.25.25 0 0 0-.25-.25H6.5ZM5 5V1.5H1.75a.25.25 0 0 0-.25.25V5Z"},{"d":"m11.017 9.89-2.882 2.677a.249.249 0 0 0 0 .366l2.882 2.677a.25.25 0 0 0 .421-.183V13.5H12.5A3.5 3.5 0 0 0 16 10a.75.75 0 0 0-1.5 0 2 2 0 0 1-2 2h-1.062v-1.927a.25.25 0 0 0-.421-.183Z"}],"play":[{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4.879-2.773 4.264 2.559a.25.25 0 0 1 0 .428l-4.264 2.559A.25.25 0 0 1 6 10.559V5.442a.25.25 0 0 1 .379-.215Z"}],"plug":[{"d":"M4 8H2.5a1 1 0 0 0-1 1v5.25a.75.75 0 0 1-1.5 0V9a2.5 2.5 0 0 1 2.5-2.5H4V5.133a1.75 1.75 0 0 1 1.533-1.737l2.831-.353.76-.913c.332-.4.825-.63 1.344-.63h.782c.966 0 1.75.784 1.75 1.75V4h2.25a.75.75 0 0 1 0 1.5H13v4h2.25a.75.75 0 0 1 0 1.5H13v.75a1.75 1.75 0 0 1-1.75 1.75h-.782c-.519 0-1.012-.23-1.344-.63l-.761-.912-2.83-.354A1.75 1.75 0 0 1 4 9.867Zm6.276-4.91-.95 1.14a.753.753 0 0 1-.483.265l-3.124.39a.25.25 0 0 0-.219.248v4.734c0 .126.094.233.219.249l3.124.39a.752.752 0 0 1 .483.264l.95 1.14a.25.25 0 0 0 .192.09h.782a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25h-.782a.25.25 0 0 0-.192.09Z"}],"plus":[{"d":"M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"}],"plus-circle":[{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7.25-3.25v2.5h2.5a.75.75 0 0 1 0 1.5h-2.5v2.5a.75.75 0 0 1-1.5 0v-2.5h-2.5a.75.75 0 0 1 0-1.5h2.5v-2.5a.75.75 0 0 1 1.5 0Z"}],"project":[{"d":"M1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25V1.75C0 .784.784 0 1.75 0ZM1.5 1.75v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25ZM11.75 3a.75.75 0 0 1 .75.75v7.5a.75.75 0 0 1-1.5 0v-7.5a.75.75 0 0 1 .75-.75Zm-8.25.75a.75.75 0 0 1 1.5 0v5.5a.75.75 0 0 1-1.5 0ZM8 3a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 3Z"}],"project-check":[{"d":"M14.25 0C15.216 0 16 .784 16 1.75v6.498a.75.75 0 1 1-1.5.002V6.5h-8v8h.75a.75.75 0 1 1-.001 1.5H1.75A1.75 1.75 0 0 1 0 14.25V1.75C0 .784.784 0 1.75 0ZM1.5 14.25c0 .138.112.25.25.25H5v-8H1.5ZM1.75 1.5a.25.25 0 0 0-.25.25V5H5V1.5Zm4.75 0V5h8V1.75a.25.25 0 0 0-.25-.25Z"},{"d":"M15.963 11.737a.75.75 0 0 1-.202.524l-3.5 3.5a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l1.47 1.47 2.97-2.97a.75.75 0 0 1 1.261.536"}],"project-roadmap":[{"d":"M4.75 7a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5h-4.5ZM5 4.75A.75.75 0 0 1 5.75 4h5.5a.75.75 0 0 1 0 1.5h-5.5A.75.75 0 0 1 5 4.75ZM6.75 10a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5h-4.5Z"},{"d":"M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Z"}],"project-symlink":[{"d":"M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16h-8.5a.75.75 0 0 1 0-1.5h8.5a.25.25 0 0 0 .25-.25V6.5h-13v1.75a.75.75 0 0 1-1.5 0ZM6.5 5h8V1.75a.25.25 0 0 0-.25-.25H6.5Zm-5 0H5V1.5H1.75a.25.25 0 0 0-.25.25Z"},{"d":"M1.5 13.737a2.25 2.25 0 0 1 2.262-2.25L4 11.49v1.938c0 .218.26.331.42.183l2.883-2.677a.25.25 0 0 0 0-.366L4.42 7.89a.25.25 0 0 0-.42.183V9.99l-.23-.001A3.75 3.75 0 0 0 0 13.738v1.012a.75.75 0 0 0 1.5 0v-1.013Z"}],"project-template":[{"d":"M12.688 0h1.562C15.217 0 16 .784 16 1.75v1.562c0 .199-.079.39-.22.53a.747.747 0 0 1-1.06 0 .75.75 0 0 1-.22-.53V1.75a.25.25 0 0 0-.25-.25h-1.562a.75.75 0 0 1-.53-.22.747.747 0 0 1 0-1.06.75.75 0 0 1 .53-.22M1.5 1.75v1.562c0 .199-.079.39-.22.53a.747.747 0 0 1-1.06 0 .75.75 0 0 1-.22-.53V1.75C0 .784.784 0 1.75 0h1.562c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.75.75 0 0 1-.53.22H1.75a.25.25 0 0 0-.25.25m0 10.938v1.562c0 .138.112.25.25.25h1.562c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.75.75 0 0 1-.53.22H1.75c-.464 0-.909-.184-1.237-.513A1.75 1.75 0 0 1 0 14.25v-1.562c0-.199.079-.39.22-.53a.747.747 0 0 1 1.06 0c.141.14.22.331.22.53m14.5 0v1.562a1.75 1.75 0 0 1-1.08 1.617c-.213.088-.44.133-.67.133h-1.562a.75.75 0 0 1-.53-.22.747.747 0 0 1 0-1.06.75.75 0 0 1 .53-.22h1.562a.25.25 0 0 0 .25-.25v-1.562c0-.199.079-.39.22-.53a.747.747 0 0 1 1.06 0c.141.14.22.331.22.53M6.438 0h3.124c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.75.75 0 0 1-.53.22H6.438a.75.75 0 0 1-.53-.22.747.747 0 0 1 0-1.06.75.75 0 0 1 .53-.22M1.5 6.438v3.124c0 .199-.079.39-.22.53a.747.747 0 0 1-1.06 0 .75.75 0 0 1-.22-.53V6.438c0-.199.079-.39.22-.53a.747.747 0 0 1 1.06 0c.141.14.22.331.22.53M6.438 14.5h3.124c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.75.75 0 0 1-.53.22H6.438a.75.75 0 0 1-.53-.22.747.747 0 0 1 0-1.06.75.75 0 0 1 .53-.22M16 6.438v3.124c0 .199-.079.39-.22.53a.747.747 0 0 1-1.06 0 .75.75 0 0 1-.22-.53V6.438c0-.199.079-.39.22-.53a.747.747 0 0 1 1.06 0c.141.14.22.331.22.53M6.5 10.75v2.5c0 .199-.079.39-.22.53a.747.747 0 0 1-1.06 0 .75.75 0 0 1-.22-.53v-2.5c0-.199.079-.39.22-.53a.747.747 0 0 1 1.06 0c.141.14.22.331.22.53m3.5-5c0-.199.079-.39.22-.53a.75.75 0 0 1 .53-.22h2.5c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.75.75 0 0 1-.53.22h-2.5a.75.75 0 0 1-.53-.22.75.75 0 0 1-.22-.53m-4.25 1c.199 0 .39.079.53.22.141.14.22.331.22.53v1c0 .199-.079.39-.22.53a.747.747 0 0 1-1.06 0A.75.75 0 0 1 5 8.5v-1c0-.199.079-.39.22-.53a.75.75 0 0 1 .53-.22m1-1c0-.199.079-.39.22-.53A.75.75 0 0 1 7.5 5h1c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.75.75 0 0 1-.53.22h-1a.75.75 0 0 1-.53-.22.75.75 0 0 1-.22-.53m-4.5 0c0-.199.079-.39.22-.53A.75.75 0 0 1 3 5h1c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.75.75 0 0 1-.53.22H3a.75.75 0 0 1-.53-.22.75.75 0 0 1-.22-.53M6.5 3v1c0 .199-.079.39-.22.53a.747.747 0 0 1-1.06 0A.75.75 0 0 1 5 4V3c0-.199.079-.39.22-.53a.747.747 0 0 1 1.06 0c.141.14.22.331.22.53"}],"pull-request":[{"d":"M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z"}],"pulse":[{"d":"M6 2c.306 0 .582.187.696.471L10 10.731l1.304-3.26A.751.751 0 0 1 12 7h3.25a.75.75 0 0 1 0 1.5h-2.742l-1.812 4.528a.751.751 0 0 1-1.392 0L6 4.77 4.696 8.03A.75.75 0 0 1 4 8.5H.75a.75.75 0 0 1 0-1.5h2.742l1.812-4.529A.751.751 0 0 1 6 2Z"}],"question":[{"d":"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"}],"quote":[{"d":"M1.75 2.5h10.5a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1 0-1.5Zm4 5h8.5a.75.75 0 0 1 0 1.5h-8.5a.75.75 0 0 1 0-1.5Zm0 5h8.5a.75.75 0 0 1 0 1.5h-8.5a.75.75 0 0 1 0-1.5ZM2.5 7.75v6a.75.75 0 0 1-1.5 0v-6a.75.75 0 0 1 1.5 0Z"}],"read":[{"d":"M7.115.65a1.752 1.752 0 0 1 1.77 0l6.25 3.663c.536.314.865.889.865 1.51v6.427A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25V5.823c0-.621.33-1.196.865-1.51Zm1.011 1.293a.252.252 0 0 0-.252 0l-5.72 3.353L6.468 7.76a2.748 2.748 0 0 1 3.066 0l4.312-2.464-5.719-3.353ZM13.15 12.5 8.772 9.06a1.25 1.25 0 0 0-1.544 0L2.85 12.5Zm1.35-5.85-3.687 2.106 3.687 2.897ZM5.187 8.756 1.5 6.65v5.003Z"}],"redo":[{"d":"M14.78 6.28a.749.749 0 0 0 0-1.06l-3.5-3.5a.749.749 0 1 0-1.06 1.06L12.439 5H5.251l-.001.007L5.251 5a.8.8 0 0 0-.171.019A4.501 4.501 0 0 0 5.5 14h1.704a.75.75 0 0 0 0-1.5H5.5a3 3 0 1 1 0-6h6.939L10.22 8.72a.749.749 0 1 0 1.06 1.06l3.5-3.5Z"}],"rel-file-path":[{"d":"M13.94 3.045a.75.75 0 0 0-1.38-.59l-4.5 10.5a.75.75 0 1 0 1.38.59l4.5-10.5ZM5 11.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z"}],"release":[{"d":"M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"}],"reply":[{"d":"M6.78 1.97a.75.75 0 0 1 0 1.06L3.81 6h6.44A4.75 4.75 0 0 1 15 10.75v2.5a.75.75 0 0 1-1.5 0v-2.5a3.25 3.25 0 0 0-3.25-3.25H3.81l2.97 2.97a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L1.47 7.28a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"}],"repo":[{"d":"M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z"}],"repo-clone":[{"d":"M3.5 0A2.5 2.5 0 0 0 1 2.5v9A2.5 2.5 0 0 0 3.5 14h2.75a.75.75 0 0 0 0-1.5H3.5a1 1 0 0 1 0-2h2.75a.75.75 0 0 0 0-1.5H3.5c-.356 0-.694.074-1 .208V2.5a1 1 0 0 1 1-1h8v2.75a.75.75 0 0 0 1.5 0V.75a.75.75 0 0 0-.75-.75H3.5Z"},{"d":"M8 8.058C8 7.023 8.75 6 9.887 6h5.363a.75.75 0 0 1 .75.75v8.5a.75.75 0 0 1-.75.75h-5A2.25 2.25 0 0 1 8 13.75Zm6.5 3.442v-4H9.887c-.07 0-.156.031-.238.125a.663.663 0 0 0-.149.433v3.57c.235-.083.487-.128.75-.128Zm-5 2.25c0 .414.336.75.75.75h4.25V13h-4.25a.75.75 0 0 0-.75.75Z"}],"repo-deleted":[{"d":"M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v7.5a.75.75 0 0 1-1.5 0V1.5h-8a1 1 0 0 0-1 1v6.708A2.492 2.492 0 0 1 4.5 9h3.75a.75.75 0 0 1 0 1.5H4.5a1 1 0 1 0 0 2h3.75a.75.75 0 0 1 0 1.5H4.5A2.5 2.5 0 0 1 2 11.5v-9Z"},{"d":"M11.28 10.22a.75.75 0 1 0-1.06 1.06L11.94 13l-1.72 1.72a.75.75 0 1 0 1.06 1.06L13 14.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L14.06 13l1.72-1.72a.75.75 0 1 0-1.06-1.06L13 11.94l-1.72-1.72Z"}],"repo-forked":[{"d":"M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"}],"repo-forked-locked":[{"d":"M9 10.167V9a3 3 0 1 1 6 0v1.168c.591.281 1 .884 1 1.582v2.5A1.75 1.75 0 0 1 14.25 16h-4.5A1.75 1.75 0 0 1 8 14.249v-2.5c0-.698.409-1.3 1-1.582M3.25 1A2.251 2.251 0 0 1 4 5.371v.878c0 .414.336.75.75.75h3.096c-.051.429.008.987.067 1.5H7.75v1.749a2.5 2.5 0 0 0-.5 1.501v.295a.75.75 0 1 0 0 1.409v.796q.002.376.107.72a2.25 2.25 0 0 1-1.106-4.343V8.5h-1.5a2.25 2.25 0 0 1-2.25-2.25v-.878A2.25 2.25 0 0 1 3.25 1m7.5 0a2.25 2.25 0 0 1 1.94 3.388c-.222.379-.55.68-.94.874h-.003a2.22 2.22 0 0 1-1.82.079l-.003-.001A2.248 2.248 0 0 1 10.75 1m2.75 9V9a1.5 1.5 0 0 0-3 0v1ZM3.25 2.499a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5m7.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5"},{"d":"M8.63 7.749a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0"}],"repo-locked":[{"d":"M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0V1.5h-8a1 1 0 0 0-1 1v6.708A2.492 2.492 0 0 1 4.5 9h1.75a.75.75 0 0 1 0 1.5H4.5a1 1 0 1 0 0 2h1.75a.75.75 0 0 1 0 1.5H4.5A2.5 2.5 0 0 1 2 11.5v-9Z"},{"d":"M9 10.168V9a3 3 0 1 1 6 0v1.168c.591.281 1 .884 1 1.582v2.5A1.75 1.75 0 0 1 14.25 16h-4.5A1.75 1.75 0 0 1 8 14.25v-2.5c0-.698.409-1.3 1-1.582ZM13.5 10V9a1.5 1.5 0 0 0-3 0v1Z"}],"repo-pull":[{"d":"M1 2.5A2.5 2.5 0 0 1 3.5 0h8.75a.75.75 0 0 1 .75.75V6a.75.75 0 0 1-1.5 0V1.5h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 3.5 9h3a.75.75 0 0 1 0 1.5h-3a1 1 0 0 0 0 2h3a.75.75 0 0 1 0 1.5h-3A2.5 2.5 0 0 1 1 11.5v-9Z"},{"d":"M12.21 13.479a.75.75 0 1 0 1.061 1.061l2.504-2.505a.75.75 0 0 0 0-1.061L13.271 8.47a.75.75 0 0 0-1.061 1.06l1.224 1.225H8.75a.75.75 0 1 0 0 1.5h4.685l-1.225 1.224Z"}],"repo-push":[{"d":"M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0V1.5h-8a1 1 0 0 0-1 1v6.708A2.493 2.493 0 0 1 4.5 9h2.25a.75.75 0 0 1 0 1.5H4.5a1 1 0 0 0 0 2h4.75a.75.75 0 0 1 0 1.5H4.5A2.5 2.5 0 0 1 2 11.5Zm12.23 7.79h-.001l-1.224-1.224v6.184a.75.75 0 0 1-1.5 0V9.066L10.28 10.29a.75.75 0 0 1-1.06-1.061l2.505-2.504a.75.75 0 0 1 1.06 0L15.29 9.23a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018Z"}],"repo-template":[{"d":"M13.25 8a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-.75a.75.75 0 0 1 0-1.5h.75v-.25a.75.75 0 0 1 .75-.75ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2ZM2.75 8a.75.75 0 0 1 .75.75v.268c.083-.012.166-.018.25-.018h.5a.75.75 0 0 1 0 1.5h-.5a.25.25 0 0 0-.25.25v.75c0 .28.114.532.3.714a.75.75 0 1 1-1.05 1.072A2.495 2.495 0 0 1 2 11.5V8.75A.75.75 0 0 1 2.75 8ZM11 .75a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V1.5h-.75A.75.75 0 0 1 11 .75Zm-5 0A.75.75 0 0 1 6.75 0h2.5a.75.75 0 0 1 0 1.5h-2.5A.75.75 0 0 1 6 .75Zm0 9A.75.75 0 0 1 6.75 9h2.5a.75.75 0 0 1 0 1.5h-2.5A.75.75 0 0 1 6 9.75ZM4.992.662a.75.75 0 0 1-.636.848c-.436.063-.783.41-.846.846a.751.751 0 0 1-1.485-.212A2.501 2.501 0 0 1 4.144.025a.75.75 0 0 1 .848.637ZM2.75 4a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 2.75 4Zm10.5 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5a.75.75 0 0 1 .75-.75Z"}],"report":[{"d":"M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"}],"rocket":[{"d":"M14.064 0h.186C15.216 0 16 .784 16 1.75v.186a8.752 8.752 0 0 1-2.564 6.186l-.458.459c-.314.314-.641.616-.979.904v3.207c0 .608-.315 1.172-.833 1.49l-2.774 1.707a.749.749 0 0 1-1.11-.418l-.954-3.102a1.214 1.214 0 0 1-.145-.125L3.754 9.816a1.218 1.218 0 0 1-.124-.145L.528 8.717a.749.749 0 0 1-.418-1.11l1.71-2.774A1.748 1.748 0 0 1 3.31 4h3.204c.288-.338.59-.665.904-.979l.459-.458A8.749 8.749 0 0 1 14.064 0ZM8.938 3.623h-.002l-.458.458c-.76.76-1.437 1.598-2.02 2.5l-1.5 2.317 2.143 2.143 2.317-1.5c.902-.583 1.74-1.26 2.499-2.02l.459-.458a7.25 7.25 0 0 0 2.123-5.127V1.75a.25.25 0 0 0-.25-.25h-.186a7.249 7.249 0 0 0-5.125 2.123ZM3.56 14.56c-.732.732-2.334 1.045-3.005 1.148a.234.234 0 0 1-.201-.064.234.234 0 0 1-.064-.201c.103-.671.416-2.273 1.15-3.003a1.502 1.502 0 1 1 2.12 2.12Zm6.94-3.935c-.088.06-.177.118-.266.175l-2.35 1.521.548 1.783 1.949-1.2a.25.25 0 0 0 .119-.213ZM3.678 8.116 5.2 5.766c.058-.09.117-.178.176-.266H3.309a.25.25 0 0 0-.213.119l-1.2 1.95ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"}],"rows":[{"d":"M16 10.75v2.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25v-2.5C0 9.784.784 9 1.75 9h12.5c.966 0 1.75.784 1.75 1.75Zm0-8v2.5A1.75 1.75 0 0 1 14.25 7H1.75A1.75 1.75 0 0 1 0 5.25v-2.5C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75Zm-1.75-.25H1.75a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-2.5a.25.25 0 0 0-.25-.25Zm0 8H1.75a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-2.5a.25.25 0 0 0-.25-.25Z"}],"rss":[{"d":"M2.002 2.725a.75.75 0 0 1 .797-.699C8.79 2.42 13.58 7.21 13.974 13.201a.75.75 0 0 1-1.497.098 10.502 10.502 0 0 0-9.776-9.776.747.747 0 0 1-.7-.798ZM2.84 7.05h-.002a7.002 7.002 0 0 1 6.113 6.111.75.75 0 0 1-1.49.178 5.503 5.503 0 0 0-4.8-4.8.75.75 0 0 1 .179-1.489ZM2 13a1 1 0 1 1 2 0 1 1 0 0 1-2 0Z"}],"ruby":[{"d":"M3.637 2.291A.748.748 0 0 1 4.23 2h7.54c.232 0 .451.107.593.291l3.48 4.5a.75.75 0 0 1-.072.999l-7.25 7a.75.75 0 0 1-1.042 0l-7.25-7a.75.75 0 0 1-.072-.999ZM4.598 3.5 1.754 7.177 8 13.207l6.246-6.03L11.402 3.5Z"}],"sandbox":[{"d":"M7.71973 8.96973C8.01262 8.67683 8.48738 8.67683 8.78027 8.96973C9.07317 9.26262 9.07317 9.73738 8.78027 10.0303L7.31055 11.5L8.78027 12.9697C9.07317 13.2626 9.07317 13.7374 8.78027 14.0303C8.48738 14.3232 8.01262 14.3232 7.71973 14.0303L5.71973 12.0303C5.42683 11.7374 5.42683 11.2626 5.71973 10.9697L7.71973 8.96973ZM11.2197 8.96973C11.5126 8.67683 11.9874 8.67683 12.2803 8.96973L14.2803 10.9697C14.5732 11.2626 14.5732 11.7374 14.2803 12.0303L12.2803 14.0303C11.9874 14.3232 11.5126 14.3232 11.2197 14.0303C10.9268 13.7374 10.9268 13.2626 11.2197 12.9697L12.6895 11.5L11.2197 10.0303C10.9268 9.73738 10.9268 9.26262 11.2197 8.96973ZM14.5 8V1.75C14.5 1.61193 14.3881 1.5 14.25 1.5H1.75C1.61193 1.5 1.5 1.61193 1.5 1.75V14.25C1.5 14.3881 1.61193 14.5 1.75 14.5H3.5C3.91421 14.5 4.25 14.8358 4.25 15.25C4.25 15.6642 3.91421 16 3.5 16H1.75C0.783502 16 0 15.2165 0 14.25V1.75C0 0.783501 0.783502 0 1.75 0H14.25C15.2165 0 16 0.783502 16 1.75V8C16 8.41421 15.6642 8.75 15.25 8.75C14.8358 8.75 14.5 8.41421 14.5 8Z"}],"screen-full":[{"d":"M1.75 10a.75.75 0 0 1 .75.75v2.5c0 .138.112.25.25.25h2.5a.75.75 0 0 1 0 1.5h-2.5A1.75 1.75 0 0 1 1 13.25v-2.5a.75.75 0 0 1 .75-.75Zm12.5 0a.75.75 0 0 1 .75.75v2.5A1.75 1.75 0 0 1 13.25 15h-2.5a.75.75 0 0 1 0-1.5h2.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 .75-.75ZM2.75 2.5a.25.25 0 0 0-.25.25v2.5a.75.75 0 0 1-1.5 0v-2.5C1 1.784 1.784 1 2.75 1h2.5a.75.75 0 0 1 0 1.5ZM10 1.75a.75.75 0 0 1 .75-.75h2.5c.966 0 1.75.784 1.75 1.75v2.5a.75.75 0 0 1-1.5 0v-2.5a.25.25 0 0 0-.25-.25h-2.5a.75.75 0 0 1-.75-.75Z"}],"screen-normal":[{"d":"M10.75 1a.75.75 0 0 1 .75.75v2.5c0 .138.112.25.25.25h2.5a.75.75 0 0 1 0 1.5h-2.5A1.75 1.75 0 0 1 10 4.25v-2.5a.75.75 0 0 1 .75-.75Zm-5.5 0a.75.75 0 0 1 .75.75v2.5A1.75 1.75 0 0 1 4.25 6h-2.5a.75.75 0 0 1 0-1.5h2.5a.25.25 0 0 0 .25-.25v-2.5A.75.75 0 0 1 5.25 1ZM1 10.75a.75.75 0 0 1 .75-.75h2.5c.966 0 1.75.784 1.75 1.75v2.5a.75.75 0 0 1-1.5 0v-2.5a.25.25 0 0 0-.25-.25h-2.5a.75.75 0 0 1-.75-.75Zm9 1c0-.966.784-1.75 1.75-1.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.25.25 0 0 0-.25.25v2.5a.75.75 0 0 1-1.5 0Z"}],"search":[{"d":"M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"}],"server":[{"d":"M1.75 1h12.5c.966 0 1.75.784 1.75 1.75v4c0 .372-.116.717-.314 1 .198.283.314.628.314 1v4a1.75 1.75 0 0 1-1.75 1.75H1.75A1.75 1.75 0 0 1 0 12.75v-4c0-.358.109-.707.314-1a1.739 1.739 0 0 1-.314-1v-4C0 1.784.784 1 1.75 1ZM1.5 2.75v4c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-4a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25Zm.25 5.75a.25.25 0 0 0-.25.25v4c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-4a.25.25 0 0 0-.25-.25ZM7 4.75A.75.75 0 0 1 7.75 4h4.5a.75.75 0 0 1 0 1.5h-4.5A.75.75 0 0 1 7 4.75ZM7.75 10h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM3 4.75A.75.75 0 0 1 3.75 4h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 3 4.75ZM3.75 10h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5Z"}],"share":[{"d":"M3.75 6.5a.25.25 0 0 0-.25.25v6.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-6.5a.25.25 0 0 0-.25-.25h-1a.75.75 0 0 1 0-1.5h1c.966 0 1.75.784 1.75 1.75v6.5A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25v-6.5C2 5.784 2.784 5 3.75 5h1a.75.75 0 0 1 0 1.5ZM7.823.177a.25.25 0 0 1 .354 0l2.896 2.896a.25.25 0 0 1-.177.427H8.75v5.75a.75.75 0 0 1-1.5 0V3.5H5.104a.25.25 0 0 1-.177-.427Z"}],"share-android":[{"d":"M15 3a3 3 0 0 1-5.175 2.066l-3.92 2.179a2.994 2.994 0 0 1 0 1.51l3.92 2.179a3 3 0 1 1-.73 1.31l-3.92-2.178a3 3 0 1 1 0-4.133l3.92-2.178A3 3 0 1 1 15 3Zm-1.5 10a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 13.5 13Zm-9-5a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 4.5 8Zm9-5a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 13.5 3Z"}],"shield":[{"d":"M7.467.133a1.748 1.748 0 0 1 1.066 0l5.25 1.68A1.75 1.75 0 0 1 15 3.48V7c0 1.566-.32 3.182-1.303 4.682-.983 1.498-2.585 2.813-5.032 3.855a1.697 1.697 0 0 1-1.33 0c-2.447-1.042-4.049-2.357-5.032-3.855C1.32 10.182 1 8.566 1 7V3.48a1.75 1.75 0 0 1 1.217-1.667Zm.61 1.429a.25.25 0 0 0-.153 0l-5.25 1.68a.25.25 0 0 0-.174.238V7c0 1.358.275 2.666 1.057 3.86.784 1.194 2.121 2.34 4.366 3.297a.196.196 0 0 0 .154 0c2.245-.956 3.582-2.104 4.366-3.298C13.225 9.666 13.5 8.36 13.5 7V3.48a.251.251 0 0 0-.174-.237l-5.25-1.68ZM8.75 4.75v3a.75.75 0 0 1-1.5 0v-3a.75.75 0 0 1 1.5 0ZM9 10.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"}],"shield-check":[{"d":"m8.533.133 5.25 1.68A1.75 1.75 0 0 1 15 3.48V7c0 1.566-.32 3.182-1.303 4.682-.983 1.498-2.585 2.813-5.032 3.855a1.697 1.697 0 0 1-1.33 0c-2.447-1.042-4.049-2.357-5.032-3.855C1.32 10.182 1 8.566 1 7V3.48a1.75 1.75 0 0 1 1.217-1.667l5.25-1.68a1.748 1.748 0 0 1 1.066 0Zm-.61 1.429.001.001-5.25 1.68a.251.251 0 0 0-.174.237V7c0 1.36.275 2.666 1.057 3.859.784 1.194 2.121 2.342 4.366 3.298a.196.196 0 0 0 .154 0c2.245-.957 3.582-2.103 4.366-3.297C13.225 9.666 13.5 8.358 13.5 7V3.48a.25.25 0 0 0-.174-.238l-5.25-1.68a.25.25 0 0 0-.153 0ZM11.28 6.28l-3.5 3.5a.75.75 0 0 1-1.06 0l-1.5-1.5a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l.97.97 2.97-2.97a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"}],"shield-lock":[{"d":"m8.533.133 5.25 1.68A1.75 1.75 0 0 1 15 3.48V7c0 1.566-.32 3.182-1.303 4.682-.983 1.498-2.585 2.813-5.032 3.855a1.697 1.697 0 0 1-1.33 0c-2.447-1.042-4.049-2.357-5.032-3.855C1.32 10.182 1 8.566 1 7V3.48a1.75 1.75 0 0 1 1.217-1.667l5.25-1.68a1.748 1.748 0 0 1 1.066 0Zm-.61 1.429.001.001-5.25 1.68a.251.251 0 0 0-.174.237V7c0 1.36.275 2.666 1.057 3.859.784 1.194 2.121 2.342 4.366 3.298a.196.196 0 0 0 .154 0c2.245-.957 3.582-2.103 4.366-3.297C13.225 9.666 13.5 8.358 13.5 7V3.48a.25.25 0 0 0-.174-.238l-5.25-1.68a.25.25 0 0 0-.153 0ZM9.5 6.5c0 .536-.286 1.032-.75 1.3v2.45a.75.75 0 0 1-1.5 0V7.8A1.5 1.5 0 1 1 9.5 6.5Z"}],"shield-slash":[{"d":"M8.533.133a1.75 1.75 0 0 0-1.066 0l-2.091.67a.75.75 0 0 0 .457 1.428l2.09-.67a.25.25 0 0 1 .153 0l5.25 1.68a.25.25 0 0 1 .174.239V7c0 .233-.008.464-.025.694a.75.75 0 1 0 1.495.112c.02-.27.03-.538.03-.806V3.48a1.75 1.75 0 0 0-1.217-1.667L8.533.133ZM1 2.857l-.69-.5a.75.75 0 1 1 .88-1.214l14.5 10.5a.75.75 0 1 1-.88 1.214l-1.282-.928c-.995 1.397-2.553 2.624-4.864 3.608-.425.181-.905.18-1.329 0-2.447-1.042-4.049-2.356-5.032-3.855C1.32 10.182 1 8.566 1 7Zm1.5 1.086V7c0 1.358.275 2.666 1.057 3.86.784 1.194 2.121 2.34 4.366 3.297.05.02.106.02.153 0 2.127-.905 3.439-1.982 4.237-3.108Z"}],"shield-x":[{"d":"m8.533.133 5.25 1.68A1.75 1.75 0 0 1 15 3.48V7c0 1.566-.32 3.182-1.303 4.682-.983 1.498-2.585 2.813-5.032 3.855a1.697 1.697 0 0 1-1.33 0c-2.447-1.042-4.049-2.357-5.032-3.855C1.32 10.182 1 8.566 1 7V3.48a1.75 1.75 0 0 1 1.217-1.667l5.25-1.68a1.748 1.748 0 0 1 1.066 0Zm-.61 1.429.001.001-5.25 1.68a.251.251 0 0 0-.174.237V7c0 1.36.275 2.666 1.057 3.859.784 1.194 2.121 2.342 4.366 3.298a.196.196 0 0 0 .154 0c2.245-.957 3.582-2.103 4.366-3.297C13.225 9.666 13.5 8.358 13.5 7V3.48a.25.25 0 0 0-.174-.238l-5.25-1.68a.25.25 0 0 0-.153 0ZM6.78 5.22 8 6.44l1.22-1.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 7.5l1.22 1.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 8.56 6.78 9.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 7.5 5.72 6.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215Z"}],"sidebar-collapse":[{"d":"M6.823 7.823a.25.25 0 0 1 0 .354l-2.396 2.396A.25.25 0 0 1 4 10.396V5.604a.25.25 0 0 1 .427-.177Z"},{"d":"M1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25V1.75C0 .784.784 0 1.75 0ZM1.5 1.75v12.5c0 .138.112.25.25.25H9.5v-13H1.75a.25.25 0 0 0-.25.25ZM11 14.5h3.25a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25H11Z"}],"sidebar-expand":[{"d":"m4.177 7.823 2.396-2.396A.25.25 0 0 1 7 5.604v4.792a.25.25 0 0 1-.427.177L4.177 8.177a.25.25 0 0 1 0-.354Z"},{"d":"M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25H9.5v-13Zm12.5 13a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25H11v13Z"}],"sign-in":[{"d":"M2 2.75C2 1.784 2.784 1 3.75 1h2.5a.75.75 0 0 1 0 1.5h-2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h2.5a.75.75 0 0 1 0 1.5h-2.5A1.75 1.75 0 0 1 2 13.25Zm6.56 4.5h5.69a.75.75 0 0 1 0 1.5H8.56l1.97 1.97a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L6.22 8.53a.75.75 0 0 1 0-1.06l3.25-3.25a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734Z"}],"sign-out":[{"d":"M2 2.75C2 1.784 2.784 1 3.75 1h2.5a.75.75 0 0 1 0 1.5h-2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h2.5a.75.75 0 0 1 0 1.5h-2.5A1.75 1.75 0 0 1 2 13.25Zm10.44 4.5-1.97-1.97a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l1.97-1.97H6.75a.75.75 0 0 1 0-1.5Z"}],"single-select":[{"d":"m5.06 7.356 2.795 2.833c.08.081.21.081.29 0l2.794-2.833c.13-.131.038-.356-.145-.356H5.206c-.183 0-.275.225-.145.356Z"},{"d":"M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25Z"}],"skip":[{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"}],"skip-fill":[{"d":"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm11.333-2.167a.825.825 0 0 0-1.166-1.166l-5.5 5.5a.825.825 0 0 0 1.166 1.166Z"}],"sliders":[{"d":"M15 2.75a.75.75 0 0 1-.75.75h-4a.75.75 0 0 1 0-1.5h4a.75.75 0 0 1 .75.75Zm-8.5.75v1.25a.75.75 0 0 0 1.5 0v-4a.75.75 0 0 0-1.5 0V2H1.75a.75.75 0 0 0 0 1.5H6.5Zm1.25 5.25a.75.75 0 0 0 0-1.5h-6a.75.75 0 0 0 0 1.5h6ZM15 8a.75.75 0 0 1-.75.75H11.5V10a.75.75 0 1 1-1.5 0V6a.75.75 0 0 1 1.5 0v1.25h2.75A.75.75 0 0 1 15 8Zm-9 5.25v-2a.75.75 0 0 0-1.5 0v1.25H1.75a.75.75 0 0 0 0 1.5H4.5v1.25a.75.75 0 0 0 1.5 0v-2Zm9 0a.75.75 0 0 1-.75.75h-6a.75.75 0 0 1 0-1.5h6a.75.75 0 0 1 .75.75Z"}],"smiley":[{"d":"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm3.82 1.636a.75.75 0 0 1 1.038.175l.007.009c.103.118.22.222.35.31.264.178.683.37 1.285.37.602 0 1.02-.192 1.285-.371.13-.088.247-.192.35-.31l.007-.008a.75.75 0 0 1 1.222.87l-.022-.015c.02.013.021.015.021.015v.001l-.001.002-.002.003-.005.007-.014.019a2.066 2.066 0 0 1-.184.213c-.16.166-.338.316-.53.445-.63.418-1.37.638-2.127.629-.946 0-1.652-.308-2.126-.63a3.331 3.331 0 0 1-.715-.657l-.014-.02-.005-.006-.002-.003v-.002h-.001l.613-.432-.614.43a.75.75 0 0 1 .183-1.044ZM12 7a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm5.25 2.25.592.416a97.71 97.71 0 0 0-.592-.416Z"}],"smiley-frown":[{"d":"M8 0a7.996 7.996 0 0 1 8 8 8 8 0 1 1-8-8Zm0 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Zm0 7.996a3.775 3.775 0 0 1 2.127.629c.192.13.37.279.53.445.066.067.128.138.184.213l.015.019.004.008.002.002.001.002v.001a.625.625 0 0 1-.07.05l.071-.05a.753.753 0 0 1-.175 1.046.749.749 0 0 1-1.047-.175l-.007-.009a1.843 1.843 0 0 0-.35-.31c-.265-.179-.683-.371-1.285-.371-.602 0-1.021.192-1.285.37a1.843 1.843 0 0 0-.35.31l-.007.01a.747.747 0 0 1-1.038.174h-.001a.749.749 0 0 1-.183-1.044l.614.43-.612-.432v-.002l.002-.002.005-.007.014-.02a3.31 3.31 0 0 1 .715-.657c.474-.322 1.18-.63 2.126-.63ZM5 6a1 1 0 1 1 0 1.998A1 1 0 0 1 5 6Zm6 0a1 1 0 1 1 0 1.998A1 1 0 0 1 11 6Z"}],"smiley-frustrated":[{"d":"M8 0a7.996 7.996 0 0 1 8 8 8 8 0 1 1-8-8Zm0 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Zm0 6.75c2.487 0 3.518 1.98 3.727 2.818A.751.751 0 0 1 11 12H5a.75.75 0 0 1-.728-.932C4.482 10.23 5.513 8.25 8 8.25Zm3.259-3.854a.651.651 0 0 1 .482 1.208L10.75 6l.991.396a.651.651 0 0 1-.482 1.208L8.99 6.696a.75.75 0 0 1 0-1.392Zm-7.363.363a.651.651 0 0 1 .845-.363l2.268.908a.75.75 0 0 1 0 1.392l-2.268.908a.651.651 0 0 1-.483-1.208L5.25 6l-.992-.396a.65.65 0 0 1-.362-.845ZM8 9.75c-.822 0-1.383.351-1.746.75h3.492c-.363-.399-.924-.75-1.746-.75Z"}],"smiley-grin":[{"d":"M8 0a7.996 7.996 0 0 1 8 8 8 8 0 1 1-8-8Zm0 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Zm3 7.75a.75.75 0 0 1 .727.932C11.518 11.02 10.487 13 8 13s-3.518-1.98-3.728-2.818A.75.75 0 0 1 5 9.25ZM8.329 6.164c.895-1.788 3.447-1.788 4.342 0a.75.75 0 0 1-1.342.671.927.927 0 0 0-1.658 0 .75.75 0 0 1-1.342-.671Zm-5 0c.895-1.788 3.447-1.788 4.342 0a.75.75 0 0 1-1.342.671.927.927 0 0 0-1.658 0 .75.75 0 0 1-1.342-.671Zm2.925 4.586c.363.399.924.75 1.746.75s1.383-.351 1.746-.75Z"}],"smiley-neutral":[{"d":"M8 0a7.996 7.996 0 0 1 8 8 8 8 0 1 1-8-8Zm0 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Zm2 8.75a.75.75 0 0 1 0 1.5H6a.75.75 0 0 1 0-1.5ZM5 6a1 1 0 1 1 0 1.998A1 1 0 0 1 5 6Zm6 0a1 1 0 1 1 0 1.998A1 1 0 0 1 11 6Z"}],"sort-asc":[{"d":"m12.927 2.573 3 3A.25.25 0 0 1 15.75 6H13.5v6.75a.75.75 0 0 1-1.5 0V6H9.75a.25.25 0 0 1-.177-.427l3-3a.25.25 0 0 1 .354 0ZM0 12.25a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1-.75-.75Zm0-4a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8.25Zm0-4a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 4.25Z"}],"sort-desc":[{"d":"M0 4.25a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 4.25Zm0 4a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8.25Zm0 4a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1-.75-.75ZM13.5 10h2.25a.25.25 0 0 1 .177.427l-3 3a.25.25 0 0 1-.354 0l-3-3A.25.25 0 0 1 9.75 10H12V3.75a.75.75 0 0 1 1.5 0V10Z"}],"space":[{"d":"M0 13.25V2.75C0 1.784.784 1 1.75 1H5c.551 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1h6.75c.966 0 1.75.784 1.75 1.75v3.638a.75.75 0 0 1-1.5 0V4.75a.25.25 0 0 0-.25-.25H7.5a1.75 1.75 0 0 1-1.4-.7l-.9-1.2a.25.25 0 0 0-.2-.1H1.75a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h5.663l.076.004a.75.75 0 0 1 0 1.492L7.413 15H1.75A1.75 1.75 0 0 1 0 13.25Z"},{"d":"M12.265 9.16a.248.248 0 0 1 .467 0l.237.649a3.726 3.726 0 0 0 2.219 2.218l.649.238a.249.249 0 0 1 0 .467l-.649.237a3.728 3.728 0 0 0-2.219 2.219l-.237.649a.249.249 0 0 1-.467 0l-.238-.649a3.726 3.726 0 0 0-2.218-2.219l-.649-.237a.248.248 0 0 1 0-.467l.649-.238a3.725 3.725 0 0 0 2.218-2.218l.238-.649Z"}],"spacing-large":[{"d":"M13.25 2H2.75a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5Zm-3 5h-4.5a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5Zm3 5H2.75a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5Z"}],"spacing-medium":[{"d":"M13.25 3H2.75a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5Zm-3 4h-4.5a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5Zm3 4H2.75a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5Z"}],"spacing-small":[{"d":"M13.25 4H2.75a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5Zm-3 3h-4.5a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5Zm3 3H2.75a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5Z"}],"sparkle":[{"d":"M7.198.57c.275-.752 1.34-.752 1.615 0l.849 2.317a5.819 5.819 0 0 0 3.462 3.463l2.317.848c.753.275.753 1.34 0 1.615l-2.317.849a5.815 5.815 0 0 0-3.462 3.462l-.849 2.317c-.275.753-1.34.753-1.615 0l-.848-2.317a5.819 5.819 0 0 0-3.463-3.462L.57 8.813c-.752-.275-.752-1.34 0-1.615l2.317-.848A5.823 5.823 0 0 0 6.35 2.887L7.198.57Zm.562 2.833A7.323 7.323 0 0 1 3.403 7.76l-.673.246.673.246a7.324 7.324 0 0 1 4.357 4.356l.246.673.246-.673a7.322 7.322 0 0 1 4.356-4.356l.673-.246-.673-.246a7.324 7.324 0 0 1-4.356-4.357l-.246-.673-.246.673Z"}],"sparkle-fill":[{"d":"M7.53 1.282a.5.5 0 0 1 .94 0l.478 1.306a7.492 7.492 0 0 0 4.464 4.464l1.305.478a.5.5 0 0 1 0 .94l-1.305.478a7.492 7.492 0 0 0-4.464 4.464l-.478 1.305a.5.5 0 0 1-.94 0l-.478-1.305a7.492 7.492 0 0 0-4.464-4.464L1.282 8.47a.5.5 0 0 1 0-.94l1.306-.478a7.492 7.492 0 0 0 4.464-4.464Z"}],"sparkles-fill":[{"d":"M9.6 2.279a.426.426 0 0 1 .8 0l.407 1.112a6.386 6.386 0 0 0 3.802 3.802l1.112.407a.426.426 0 0 1 0 .8l-1.112.407a6.386 6.386 0 0 0-3.802 3.802l-.407 1.112a.426.426 0 0 1-.8 0l-.407-1.112a6.386 6.386 0 0 0-3.802-3.802L4.279 8.4a.426.426 0 0 1 0-.8l1.112-.407a6.386 6.386 0 0 0 3.802-3.802L9.6 2.279Zm-4.267 8.837a.178.178 0 0 1 .334 0l.169.464a2.662 2.662 0 0 0 1.584 1.584l.464.169a.178.178 0 0 1 0 .334l-.464.169a2.662 2.662 0 0 0-1.584 1.584l-.169.464a.178.178 0 0 1-.334 0l-.169-.464a2.662 2.662 0 0 0-1.584-1.584l-.464-.169a.178.178 0 0 1 0-.334l.464-.169a2.662 2.662 0 0 0 1.584-1.584l.169-.464ZM2.8.14a.213.213 0 0 1 .4 0l.203.556a3.2 3.2 0 0 0 1.901 1.901l.556.203a.213.213 0 0 1 0 .4l-.556.203a3.2 3.2 0 0 0-1.901 1.901L3.2 5.86a.213.213 0 0 1-.4 0l-.203-.556A3.2 3.2 0 0 0 .696 3.403L.14 3.2a.213.213 0 0 1 0-.4l.556-.203A3.2 3.2 0 0 0 2.597.696L2.8.14Z"}],"split-view":[{"d":"M1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5c0 .464-.184.909-.513 1.237A1.746 1.746 0 0 1 14.25 16H1.75c-.464 0-.909-.184-1.237-.513A1.746 1.746 0 0 1 0 14.25V1.75C0 .784.784 0 1.75 0ZM1.5 1.75v12.5c0 .138.112.25.25.25H7.5v-13H1.75a.25.25 0 0 0-.25.25ZM9 14.5h5.25a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25H9Z"}],"sponsor-tiers":[{"d":"M10.586 1C12.268 1 13.5 2.37 13.5 4.25c0 1.745-.996 3.359-2.622 4.831-.166.15-.336.297-.509.438l1.116 5.584a.75.75 0 0 1-.991.852l-2.409-.876a.25.25 0 0 0-.17 0l-2.409.876a.75.75 0 0 1-.991-.852L5.63 9.519a13.78 13.78 0 0 1-.51-.438C3.497 7.609 2.5 5.995 2.5 4.25 2.5 2.37 3.732 1 5.414 1c.963 0 1.843.403 2.474 1.073L8 2.198l.112-.125a3.385 3.385 0 0 1 2.283-1.068L10.586 1Zm-3.621 9.495-.718 3.594 1.155-.42a1.75 1.75 0 0 1 1.028-.051l.168.051 1.154.42-.718-3.592c-.199.13-.37.235-.505.314l-.169.097a.75.75 0 0 1-.72 0 9.54 9.54 0 0 1-.515-.308l-.16-.105ZM10.586 2.5c-.863 0-1.611.58-1.866 1.459-.209.721-1.231.721-1.44 0C7.025 3.08 6.277 2.5 5.414 2.5 4.598 2.5 4 3.165 4 4.25c0 1.23.786 2.504 2.128 3.719.49.443 1.018.846 1.546 1.198l.325.21.076-.047.251-.163a13.341 13.341 0 0 0 1.546-1.198C11.214 6.754 12 5.479 12 4.25c0-1.085-.598-1.75-1.414-1.75Z"}],"square":[{"d":"M4 5.75C4 4.784 4.784 4 5.75 4h4.5c.966 0 1.75.784 1.75 1.75v4.5A1.75 1.75 0 0 1 10.25 12h-4.5A1.75 1.75 0 0 1 4 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v4.5c0 .138.112.25.25.25h4.5a.25.25 0 0 0 .25-.25v-4.5a.25.25 0 0 0-.25-.25Z"}],"square-circle":[{"d":"M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm0-1.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z"},{"d":"M5 5.75A.75.75 0 0 1 5.75 5h4.5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75Z"}],"square-fill":[{"d":"M5.75 4h4.5c.966 0 1.75.784 1.75 1.75v4.5A1.75 1.75 0 0 1 10.25 12h-4.5A1.75 1.75 0 0 1 4 10.25v-4.5C4 4.784 4.784 4 5.75 4Z"}],"squirrel":[{"d":"M3.499.75a.75.75 0 0 1 1.5 0v.996C5.9 2.903 6.793 3.65 7.662 4.376l.24.202c-.036-.694.055-1.422.426-2.163C9.1.873 10.794-.045 12.622.26 14.408.558 16 1.94 16 4.25c0 1.278-.954 2.575-2.44 2.734l.146.508.065.22c.203.701.412 1.455.476 2.226.142 1.707-.4 3.03-1.487 3.898C11.714 14.671 10.27 15 8.75 15h-6a.75.75 0 0 1 0-1.5h1.376a4.484 4.484 0 0 1-.563-1.191 3.835 3.835 0 0 1-.05-2.063 4.647 4.647 0 0 1-2.025-.293.75.75 0 0 1 .525-1.406c1.357.507 2.376-.006 2.698-.318l.009-.01a.747.747 0 0 1 1.06 0 .748.748 0 0 1-.012 1.074c-.912.92-.992 1.835-.768 2.586.221.74.745 1.337 1.196 1.621H8.75c1.343 0 2.398-.296 3.074-.836.635-.507 1.036-1.31.928-2.602-.05-.603-.216-1.224-.422-1.93l-.064-.221c-.12-.407-.246-.84-.353-1.29a2.425 2.425 0 0 1-.507-.441 3.075 3.075 0 0 1-.633-1.248.75.75 0 0 1 1.455-.364c.046.185.144.436.31.627.146.168.353.305.712.305.738 0 1.25-.615 1.25-1.25 0-1.47-.95-2.315-2.123-2.51-1.172-.196-2.227.387-2.706 1.345-.46.92-.27 1.774.019 3.062l.042.19a.884.884 0 0 1 .01.05c.348.443.666.949.94 1.553a.75.75 0 1 1-1.365.62c-.553-1.217-1.32-1.94-2.3-2.768L6.7 5.527c-.814-.68-1.75-1.462-2.692-2.619a3.737 3.737 0 0 0-1.023.88c-.406.495-.663 1.036-.722 1.508.116.122.306.21.591.239.388.038.797-.06 1.032-.19a.75.75 0 0 1 .728 1.31c-.515.287-1.23.439-1.906.373-.682-.067-1.473-.38-1.879-1.193L.75 5.677V5.5c0-.984.48-1.94 1.077-2.664.46-.559 1.05-1.055 1.673-1.353V.75Z"}],"stack":[{"d":"M7.122.392a1.75 1.75 0 0 1 1.756 0l5.003 2.902c.83.481.83 1.68 0 2.162L8.878 8.358a1.75 1.75 0 0 1-1.756 0L2.119 5.456a1.251 1.251 0 0 1 0-2.162ZM8.125 1.69a.248.248 0 0 0-.25 0l-4.63 2.685 4.63 2.685a.248.248 0 0 0 .25 0l4.63-2.685ZM1.601 7.789a.75.75 0 0 1 1.025-.273l5.249 3.044a.248.248 0 0 0 .25 0l5.249-3.044a.75.75 0 0 1 .752 1.298l-5.248 3.044a1.75 1.75 0 0 1-1.756 0L1.874 8.814A.75.75 0 0 1 1.6 7.789Zm0 3.5a.75.75 0 0 1 1.025-.273l5.249 3.044a.248.248 0 0 0 .25 0l5.249-3.044a.75.75 0 0 1 .752 1.298l-5.248 3.044a1.75 1.75 0 0 1-1.756 0l-5.248-3.044a.75.75 0 0 1-.273-1.025Z"}],"stack-add":[{"d":"M7.122.392a1.75 1.75 0 0 1 1.756 0l5.003 2.902c.83.481.83 1.68 0 2.162L8.878 8.358a1.75 1.75 0 0 1-1.756 0L2.119 5.456a1.25 1.25 0 0 1 0-2.162ZM8.125 1.69a.25.25 0 0 0-.25 0L3.244 4.375 7.875 7.06a.25.25 0 0 0 .25 0l4.63-2.685ZM1.602 7.789a.75.75 0 0 1 1.024-.272l5.249 3.044a.749.749 0 1 1-.753 1.296L1.874 8.813a.75.75 0 0 1-.272-1.024m0 3.5a.75.75 0 0 1 1.024-.272l5.249 3.044a.749.749 0 1 1-.753 1.296l-5.248-3.044a.75.75 0 0 1-.272-1.024M11.75 15.25v-2h-2a.75.75 0 0 1 0-1.5h2v-2a.75.75 0 0 1 1.5 0v2h2a.75.75 0 0 1 0 1.5h-2v2a.75.75 0 0 1-1.5 0"}],"stack-check":[{"d":"M7.12208 0.392445C7.66506 0.0775182 8.33496 0.0775182 8.87794 0.392445L13.8809 3.29381C14.711 3.77549 14.7108 4.97413 13.8809 5.45592L8.87794 8.35827C8.33508 8.67301 7.66494 8.67302 7.12208 8.35827L2.11915 5.45592C1.28917 4.97413 1.28904 3.77549 2.11915 3.29381L7.12208 0.392445ZM8.12501 1.6903C8.04751 1.64541 7.95251 1.64541 7.87501 1.6903L3.24415 4.37487L7.87501 7.06041C7.95239 7.10512 8.04762 7.10512 8.12501 7.06041L12.7549 4.37487L8.12501 1.6903Z"},{"d":"M1.60157 7.78917C1.80937 7.4309 2.26769 7.30897 2.62598 7.51671L7.87501 10.5607C8.23282 10.7685 8.35484 11.227 8.14747 11.5851C7.93965 11.9434 7.48039 12.0654 7.12208 11.8575L1.87403 8.81359C1.51575 8.60579 1.39382 8.14747 1.60157 7.78917Z"},{"d":"M1.60157 11.2892C1.80937 10.9309 2.26769 10.809 2.62598 11.0167L7.87501 14.0607C8.23282 14.2685 8.35484 14.727 8.14747 15.0851C7.93965 15.4434 7.48039 15.5654 7.12208 15.3575L1.87403 12.3136C1.51575 12.1058 1.39382 11.6475 1.60157 11.2892Z"},{"d":"M14.7012 10.4894C14.9832 10.1863 15.4575 10.1693 15.7607 10.4513C16.0639 10.7334 16.0808 11.2076 15.7988 11.5109L12.0801 15.5109C11.9422 15.6591 11.7503 15.7456 11.5479 15.7501C11.3453 15.7546 11.1493 15.6764 11.0049 15.5343L9.22364 13.7823C8.92841 13.4918 8.9253 13.016 9.21583 12.7208C9.5064 12.426 9.98128 12.4226 10.2764 12.713L11.5068 13.9239L14.7012 10.4894Z"}],"stack-remove":[{"d":"M14.7193 10.2198C15.0121 9.92692 15.4869 9.92701 15.7798 10.2198C16.0727 10.5127 16.0727 10.9874 15.7798 11.2803L14.0601 13L15.7798 14.7198C16.0727 15.0127 16.0727 15.4874 15.7798 15.7803C15.4869 16.0729 15.0121 16.0731 14.7193 15.7803L12.9996 14.0606L11.2798 15.7803C10.9869 16.0729 10.5121 16.0731 10.2193 15.7803C9.92663 15.4875 9.92676 15.0127 10.2193 14.7198L11.939 13L10.2193 11.2803C9.92663 10.9875 9.92676 10.5127 10.2193 10.2198C10.5121 9.92692 10.9869 9.92701 11.2798 10.2198L12.9996 11.9395L14.7193 10.2198Z"},{"d":"M1.60113 11.2891C1.80888 10.9309 2.26729 10.8091 2.62555 11.0167L7.87457 14.0606C8.23239 14.2684 8.3544 14.7269 8.14703 15.085C7.93913 15.4431 7.47985 15.5652 7.12164 15.3575L1.87359 12.3135C1.51562 12.1057 1.39367 11.6473 1.60113 11.2891Z"},{"d":"M1.60113 7.78911C1.80888 7.43092 2.26729 7.30912 2.62555 7.51665L7.87457 10.5606C8.23239 10.7684 8.3544 11.2269 8.14703 11.585C7.93913 11.9431 7.47985 12.0652 7.12164 11.8575L1.87359 8.81353C1.51562 8.60571 1.39367 8.14728 1.60113 7.78911Z"},{"d":"M7.12164 0.392627C7.66455 0.0777416 8.33456 0.0778247 8.8775 0.392627L13.8804 3.29399C14.7105 3.77567 14.7104 4.97432 13.8804 5.4561L8.8775 8.35845C8.33472 8.67294 7.66435 8.67311 7.12164 8.35845L2.11871 5.4561C1.28909 4.97432 1.28912 3.7758 2.11871 3.29399L7.12164 0.392627ZM8.12457 1.69048C8.04711 1.64572 7.952 1.64563 7.87457 1.69048L3.24371 4.37505L7.87457 7.0606C7.9518 7.10522 8.04727 7.10505 8.12457 7.0606L12.7545 4.37505L8.12457 1.69048Z","fill-rule":"evenodd"}],"star":[{"d":"M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"}],"star-fill":[{"d":"M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"}],"stop":[{"d":"M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"}],"stopwatch":[{"d":"M5.75.75A.75.75 0 0 1 6.5 0h3a.75.75 0 0 1 0 1.5h-.75v1l-.001.041a6.724 6.724 0 0 1 3.464 1.435l.007-.006.75-.75a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734l-.75.75-.006.007a6.75 6.75 0 1 1-10.548 0L2.72 5.03l-.75-.75a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018l.75.75.007.006A6.72 6.72 0 0 1 7.25 2.541V1.5H6.5a.75.75 0 0 1-.75-.75ZM8 14.5a5.25 5.25 0 1 0-.001-10.501A5.25 5.25 0 0 0 8 14.5Zm.389-6.7 1.33-1.33a.75.75 0 1 1 1.061 1.06L9.45 8.861A1.503 1.503 0 0 1 8 10.75a1.499 1.499 0 1 1 .389-2.95Z"}],"strikethrough":[{"d":"M11.055 8.5c.524.536.815 1.257.811 2.007a3.133 3.133 0 0 1-1.12 2.408C9.948 13.597 8.748 14 7.096 14c-1.706 0-3.104-.607-3.902-1.377a.751.751 0 0 1 1.042-1.079c.48.463 1.487.956 2.86.956 1.422 0 2.232-.346 2.676-.726.435-.372.594-.839.594-1.267 0-.472-.208-.857-.647-1.197-.448-.346-1.116-.623-1.951-.81H1.75a.75.75 0 0 1 0-1.5h12.5a.75.75 0 0 1 0 1.5ZM7.581 3.25c-2.036 0-2.778 1.082-2.778 1.786 0 .055.002.107.006.157a.75.75 0 0 1-1.496.114 3.506 3.506 0 0 1-.01-.271c0-1.832 1.75-3.286 4.278-3.286 1.418 0 2.721.58 3.514 1.093a.75.75 0 1 1-.814 1.26c-.64-.414-1.662-.853-2.7-.853Z"}],"sun":[{"d":"M8 12a4 4 0 1 1 0-8 4 4 0 0 1 0 8Zm0-1.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Zm5.657-8.157a.75.75 0 0 1 0 1.061l-1.061 1.06a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l1.06-1.06a.75.75 0 0 1 1.06 0Zm-9.193 9.193a.75.75 0 0 1 0 1.06l-1.06 1.061a.75.75 0 1 1-1.061-1.06l1.06-1.061a.75.75 0 0 1 1.061 0ZM8 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0ZM3 8a.75.75 0 0 1-.75.75H.75a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 3 8Zm13 0a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 16 8Zm-8 5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 8 13Zm3.536-1.464a.75.75 0 0 1 1.06 0l1.061 1.06a.75.75 0 0 1-1.06 1.061l-1.061-1.06a.75.75 0 0 1 0-1.061ZM2.343 2.343a.75.75 0 0 1 1.061 0l1.06 1.061a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-1.06-1.06a.75.75 0 0 1 0-1.06Z"}],"sync":[{"d":"M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z"}],"tab":[{"d":"m10.78 8.53-3.75 3.75a.749.749 0 1 1-1.06-1.06l2.469-2.47H1.75a.75.75 0 0 1 0-1.5h6.689L5.97 4.78a.749.749 0 1 1 1.06-1.06l3.75 3.75a.749.749 0 0 1 0 1.06ZM13 12.25v-8.5a.75.75 0 0 1 1.5 0v8.5a.75.75 0 0 1-1.5 0Z"}],"tab-external":[{"d":"M3.25 4a.25.25 0 0 0-.25.25v9a.75.75 0 0 1-.75.75H.75a.75.75 0 0 1 0-1.5h.75V4.25c0-.966.784-1.75 1.75-1.75h9.5c.966 0 1.75.784 1.75 1.75v8.25h.75a.75.75 0 0 1 0 1.5h-1.5a.75.75 0 0 1-.75-.75v-9a.25.25 0 0 0-.25-.25h-9.5Z"},{"d":"m7.97 7.97-2.75 2.75a.75.75 0 1 0 1.06 1.06l2.75-2.75 1.543 1.543a.25.25 0 0 0 .427-.177V6.25a.25.25 0 0 0-.25-.25H6.604a.25.25 0 0 0-.177.427L7.97 7.97Z"}],"table":[{"d":"M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25ZM6.5 6.5v8h7.75a.25.25 0 0 0 .25-.25V6.5Zm8-1.5V1.75a.25.25 0 0 0-.25-.25H6.5V5Zm-13 1.5v7.75c0 .138.112.25.25.25H5v-8ZM5 5V1.5H1.75a.25.25 0 0 0-.25.25V5Z"}],"tag":[{"d":"M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"}],"tasklist":[{"d":"M2 2h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1Zm4.655 8.595a.75.75 0 0 1 0 1.06L4.03 14.28a.75.75 0 0 1-1.06 0l-1.5-1.5a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l.97.97 2.095-2.095a.75.75 0 0 1 1.06 0ZM9.75 2.5h5.5a.75.75 0 0 1 0 1.5h-5.5a.75.75 0 0 1 0-1.5Zm0 5h5.5a.75.75 0 0 1 0 1.5h-5.5a.75.75 0 0 1 0-1.5Zm0 5h5.5a.75.75 0 0 1 0 1.5h-5.5a.75.75 0 0 1 0-1.5Zm-7.25-9v3h3v-3Z"}],"telescope":[{"d":"M14.184 1.143v-.001l1.422 2.464a1.75 1.75 0 0 1-.757 2.451L3.104 11.713a1.75 1.75 0 0 1-2.275-.702l-.447-.775a1.75 1.75 0 0 1 .53-2.32L11.682.573a1.748 1.748 0 0 1 2.502.57Zm-4.709 9.32h-.001l2.644 3.863a.75.75 0 1 1-1.238.848l-1.881-2.75v2.826a.75.75 0 0 1-1.5 0v-2.826l-1.881 2.75a.75.75 0 1 1-1.238-.848l2.049-2.992a.746.746 0 0 1 .293-.253l1.809-.87a.749.749 0 0 1 .944.252ZM9.436 3.92h-.001l-4.97 3.39.942 1.63 5.42-2.61Zm3.091-2.108h.001l-1.85 1.26 1.505 2.605 2.016-.97a.247.247 0 0 0 .13-.151.247.247 0 0 0-.022-.199l-1.422-2.464a.253.253 0 0 0-.161-.119.254.254 0 0 0-.197.038ZM1.756 9.157a.25.25 0 0 0-.075.33l.447.775a.25.25 0 0 0 .325.1l1.598-.769-.83-1.436-1.465 1Z"}],"telescope-fill":[{"d":"M11.905.42a1.5 1.5 0 0 1 2.144.49l1.692 2.93a1.5 1.5 0 0 1-.649 2.102L2.895 11.815a1.5 1.5 0 0 1-1.95-.602l-.68-1.176a1.5 1.5 0 0 1 .455-1.99L11.905.422Zm-3.374 9.79a.75.75 0 0 1 .944.253l2.644 3.864a.751.751 0 0 1-1.238.847L9 12.424v2.826a.75.75 0 0 1-1.5 0v-2.826l-1.881 2.75a.75.75 0 1 1-1.238-.848l2.048-2.992a.752.752 0 0 1 .293-.252l1.81-.871Zm2.476-3.965v-.001l1.356-.653-1.52-2.631-1.243.848ZM3.279 8.119l.835 1.445 1.355-.653-.947-1.64Z"}],"terminal":[{"d":"M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25ZM7.25 8a.749.749 0 0 1-.22.53l-2.25 2.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L5.44 8 3.72 6.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.25 2.25c.141.14.22.331.22.53Zm1.5 1.5h3a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5Z"}],"three-bars":[{"d":"M1 2.75A.75.75 0 0 1 1.75 2h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75Zm0 5A.75.75 0 0 1 1.75 7h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 7.75ZM1.75 12h12.5a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1 0-1.5Z"}],"thumbsdown":[{"d":"M7.653 15.369a.75.75 0 0 1-.776.371l-.238-.04a3.25 3.25 0 0 1-2.591-4.099L4.506 10h-.665A3.25 3.25 0 0 1 .723 5.833l1.135-3.859A2.75 2.75 0 0 1 4.482 0H9.43c.78.003 1.538.25 2.168.702A1.752 1.752 0 0 1 12.989 0h1.272A1.75 1.75 0 0 1 16 1.75v6.5A1.75 1.75 0 0 1 14.25 10h-3.417a.25.25 0 0 0-.217.127ZM11.25 2.351l-.396-.33a2.248 2.248 0 0 0-1.44-.521H4.496a1.25 1.25 0 0 0-1.199.897L2.162 6.256A1.75 1.75 0 0 0 3.841 8.5H5.5a.75.75 0 0 1 .721.956l-.731 2.558a1.75 1.75 0 0 0 1.127 2.14L9.31 9.389a1.75 1.75 0 0 1 1.523-.889h.417Zm1.5 6.149h1.5a.25.25 0 0 0 .25-.25v-6.5a.25.25 0 0 0-.25-.25H13a.25.25 0 0 0-.25.25Z"}],"thumbsup":[{"d":"M8.347.631A.75.75 0 0 1 9.123.26l.238.04a3.25 3.25 0 0 1 2.591 4.098L11.494 6h.665a3.25 3.25 0 0 1 3.118 4.167l-1.135 3.859A2.751 2.751 0 0 1 11.503 16H6.586a3.75 3.75 0 0 1-2.184-.702A1.75 1.75 0 0 1 3 16H1.75A1.75 1.75 0 0 1 0 14.25v-6.5C0 6.784.784 6 1.75 6h3.417a.25.25 0 0 0 .217-.127ZM4.75 13.649l.396.33c.404.337.914.521 1.44.521h4.917a1.25 1.25 0 0 0 1.2-.897l1.135-3.859A1.75 1.75 0 0 0 12.159 7.5H10.5a.75.75 0 0 1-.721-.956l.731-2.558a1.75 1.75 0 0 0-1.127-2.14L6.69 6.611a1.75 1.75 0 0 1-1.523.889H4.75ZM3.25 7.5h-1.5a.25.25 0 0 0-.25.25v6.5c0 .138.112.25.25.25H3a.25.25 0 0 0 .25-.25Z"}],"tools":[{"d":"M5.433 2.304A4.492 4.492 0 0 0 3.5 6c0 1.598.832 3.002 2.09 3.802.518.328.929.923.902 1.64v.008l-.164 3.337a.75.75 0 1 1-1.498-.073l.163-3.33c.002-.085-.05-.216-.207-.316A5.996 5.996 0 0 1 2 6a5.993 5.993 0 0 1 2.567-4.92 1.482 1.482 0 0 1 1.673-.04c.462.296.76.827.76 1.423v2.82c0 .082.041.16.11.206l.75.51a.25.25 0 0 0 .28 0l.75-.51A.249.249 0 0 0 9 5.282V2.463c0-.596.298-1.127.76-1.423a1.482 1.482 0 0 1 1.673.04A5.993 5.993 0 0 1 14 6a5.996 5.996 0 0 1-2.786 5.068c-.157.1-.209.23-.207.315l.163 3.33a.752.752 0 0 1-1.094.714.75.75 0 0 1-.404-.64l-.164-3.345c-.027-.717.384-1.312.902-1.64A4.495 4.495 0 0 0 12.5 6a4.492 4.492 0 0 0-1.933-3.696c-.024.017-.067.067-.067.16v2.818a1.75 1.75 0 0 1-.767 1.448l-.75.51a1.75 1.75 0 0 1-1.966 0l-.75-.51A1.75 1.75 0 0 1 5.5 5.282V2.463c0-.092-.043-.142-.067-.159Z"}],"tracked-by-closed-completed":[{"d":"M1.5 8a6.5 6.5 0 1 1 13 0A.75.75 0 0 0 16 8a8 8 0 1 0-8 8 .75.75 0 0 0 0-1.5A6.5 6.5 0 0 1 1.5 8Z"},{"d":"m8.677 12.427 2.896 2.896a.25.25 0 0 0 .427-.177V13h3.25a.75.75 0 0 0 0-1.5H12V9.354a.25.25 0 0 0-.427-.177l-2.896 2.896a.25.25 0 0 0 0 .354ZM11.28 6.78a.749.749 0 1 0-1.06-1.06L7.25 8.689 5.78 7.22a.749.749 0 1 0-1.06 1.06l2 2a.749.749 0 0 0 1.06 0l3.5-3.5Z"}],"tracked-by-closed-not-planned":[{"d":"M1.5 8a6.5 6.5 0 1 1 13 0A.75.75 0 0 0 16 8a8 8 0 1 0-8 8 .75.75 0 0 0 0-1.5A6.5 6.5 0 0 1 1.5 8Z"},{"d":"m8.677 12.427 2.896 2.896a.25.25 0 0 0 .427-.177V13h3.25a.75.75 0 0 0 0-1.5H12V9.354a.25.25 0 0 0-.427-.177l-2.896 2.896a.25.25 0 0 0 0 .354ZM11.28 5.78a.749.749 0 1 0-1.06-1.06l-5.5 5.5a.749.749 0 1 0 1.06 1.06l5.5-5.5Z"}],"trash":[{"d":"M11 1.75V3h2.25a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75ZM4.496 6.675l.66 6.6a.25.25 0 0 0 .249.225h5.19a.25.25 0 0 0 .249-.225l.66-6.6a.75.75 0 0 1 1.492.149l-.66 6.6A1.748 1.748 0 0 1 10.595 15h-5.19a1.75 1.75 0 0 1-1.741-1.575l-.66-6.6a.75.75 0 1 1 1.492-.15ZM6.5 1.75V3h3V1.75a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25Z"}],"triangle-down":[{"d":"m4.427 7.427 3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.396 7H4.604a.25.25 0 0 0-.177.427Z"}],"triangle-left":[{"d":"M9.573 4.427 6.177 7.823a.25.25 0 0 0 0 .354l3.396 3.396a.25.25 0 0 0 .427-.177V4.604a.25.25 0 0 0-.427-.177Z"}],"triangle-right":[{"d":"m6.427 4.427 3.396 3.396a.25.25 0 0 1 0 .354l-3.396 3.396A.25.25 0 0 1 6 11.396V4.604a.25.25 0 0 1 .427-.177Z"}],"triangle-up":[{"d":"m4.427 9.573 3.396-3.396a.25.25 0 0 1 .354 0l3.396 3.396a.25.25 0 0 1-.177.427H4.604a.25.25 0 0 1-.177-.427Z"}],"trophy":[{"d":"M3.217 6.962A3.75 3.75 0 0 1 0 3.25v-.5C0 1.784.784 1 1.75 1h1.356c.228-.585.796-1 1.462-1h6.864c.647 0 1.227.397 1.462 1h1.356c.966 0 1.75.784 1.75 1.75v.5a3.75 3.75 0 0 1-3.217 3.712 5.014 5.014 0 0 1-2.771 3.117l.144 1.446c.005.05.03.12.114.204.086.087.217.17.373.227.283.103.618.274.89.568.285.31.467.723.467 1.226v.75h1.25a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5H4v-.75c0-.503.182-.916.468-1.226.27-.294.606-.465.889-.568.139-.048.266-.126.373-.227.084-.085.109-.153.114-.204l.144-1.446a5.015 5.015 0 0 1-2.77-3.117ZM4.5 1.568V5.5a3.5 3.5 0 1 0 7 0V1.568a.068.068 0 0 0-.068-.068H4.568a.068.068 0 0 0-.068.068Zm2.957 8.902-.12 1.204c-.093.925-.858 1.47-1.467 1.691a.766.766 0 0 0-.3.176c-.037.04-.07.093-.07.21v.75h5v-.75c0-.117-.033-.17-.07-.21a.766.766 0 0 0-.3-.176c-.609-.221-1.374-.766-1.466-1.69l-.12-1.204a5.064 5.064 0 0 1-1.087 0ZM13 2.5v2.872a2.25 2.25 0 0 0 1.5-2.122v-.5a.25.25 0 0 0-.25-.25H13Zm-10 0H1.75a.25.25 0 0 0-.25.25v.5c0 .98.626 1.813 1.5 2.122Z"}],"typography":[{"d":"M6.71 10H2.332l-.874 2.498a.75.75 0 0 1-1.415-.496l3.39-9.688a1.217 1.217 0 0 1 2.302.018l3.227 9.681a.75.75 0 0 1-1.423.474Zm3.13-4.358C10.53 4.374 11.87 4 13 4c1.5 0 3 .939 3 2.601v5.649a.75.75 0 0 1-1.448.275C13.995 12.82 13.3 13 12.5 13c-.77 0-1.514-.231-2.078-.709-.577-.488-.922-1.199-.922-2.041 0-.694.265-1.411.887-1.944C11 7.78 11.88 7.5 13 7.5h1.5v-.899c0-.54-.5-1.101-1.5-1.101-.869 0-1.528.282-1.84.858a.75.75 0 1 1-1.32-.716ZM6.21 8.5 4.574 3.594 2.857 8.5Zm8.29.5H13c-.881 0-1.375.22-1.637.444-.253.217-.363.5-.363.806 0 .408.155.697.39.896.249.21.63.354 1.11.354.732 0 1.26-.209 1.588-.449.35-.257.412-.495.412-.551Z"}],"undo":[{"d":"M1.22 6.28a.749.749 0 0 1 0-1.06l3.5-3.5a.749.749 0 1 1 1.06 1.06L3.561 5h7.188l.001.007L10.749 5c.058 0 .116.007.171.019A4.501 4.501 0 0 1 10.5 14H8.796a.75.75 0 0 1 0-1.5H10.5a3 3 0 1 0 0-6H3.561L5.78 8.72a.749.749 0 1 1-1.06 1.06l-3.5-3.5Z"}],"unfold":[{"d":"m8.177.677 2.896 2.896a.25.25 0 0 1-.177.427H8.75v1.25a.75.75 0 0 1-1.5 0V4H5.104a.25.25 0 0 1-.177-.427L7.823.677a.25.25 0 0 1 .354 0ZM7.25 10.75a.75.75 0 0 1 1.5 0V12h2.146a.25.25 0 0 1 .177.427l-2.896 2.896a.25.25 0 0 1-.354 0l-2.896-2.896A.25.25 0 0 1 5.104 12H7.25v-1.25Zm-5-2a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM6 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 6 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM12 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 12 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5Z"}],"unlink":[{"d":"M12.914 5.914a2 2 0 0 0-2.828-2.828l-.837.837a.75.75 0 1 1-1.06-1.061l.836-.837a3.5 3.5 0 1 1 4.95 4.95l-.195.194a.75.75 0 0 1-1.06-1.06l.194-.195Zm-1.87 3.482a.759.759 0 0 1-.07.079c-.63.63-1.468 1.108-2.343 1.263-.89.159-1.86-.017-2.606-.763a.75.75 0 1 1 1.06-1.06c.329.327.767.438 1.284.347.493-.088 1.018-.36 1.445-.752l-1.247-.897a.709.709 0 0 1-.01-.008l-.295-.212c-.94-.597-1.984-.499-2.676.193l-2.5 2.5a2 2 0 1 0 2.828 2.828l.837-.836a.75.75 0 0 1 1.06 1.06l-.836.837a3.5 3.5 0 0 1-4.95-4.95l2.5-2.5a3.472 3.472 0 0 1 1.354-.848L2.312 3.109a.75.75 0 0 1 .876-1.218l5.93 4.27c.115.074.226.155.335.24l6.235 4.49a.75.75 0 0 1-.876 1.218l-3.768-2.713Z"}],"unlock":[{"d":"M5.5 4v2h7A1.5 1.5 0 0 1 14 7.5v6a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 13.5v-6A1.5 1.5 0 0 1 3.499 6H4V4a4 4 0 0 1 7.371-2.154.75.75 0 0 1-1.264.808A2.5 2.5 0 0 0 5.5 4Zm-2 3.5v6h9v-6h-9Z"}],"unmute":[{"d":"M7.563 2.069A.75.75 0 0 1 8 2.75v10.5a.751.751 0 0 1-1.238.57L3.472 11H1.75A1.75 1.75 0 0 1 0 9.25v-2.5C0 5.784.784 5 1.75 5h1.723l3.289-2.82a.75.75 0 0 1 .801-.111ZM6.5 4.38 4.238 6.319a.748.748 0 0 1-.488.181h-2a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h2c.179 0 .352.064.488.18L6.5 11.62Zm6.096-2.038a.75.75 0 0 1 1.06 0 8 8 0 0 1 0 11.314.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042 6.5 6.5 0 0 0 0-9.193.75.75 0 0 1 0-1.06Zm-1.06 2.121-.001.001a5 5 0 0 1 0 7.07.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734 3.5 3.5 0 0 0 0-4.95.75.75 0 1 1 1.061-1.061Z"}],"unread":[{"d":"M10.5 3.5H1.75a.25.25 0 0 0-.25.25v.32L8 7.88l3.02-1.77a.75.75 0 0 1 .758 1.295L8.379 9.397a.75.75 0 0 1-.758 0L1.5 5.809v6.441c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-4.5a.75.75 0 0 1 1.5 0v4.5A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25V4.513a.75.75 0 0 1 0-.027V3.75C0 2.784.784 2 1.75 2h8.75a.75.75 0 0 1 0 1.5Z"},{"d":"M14 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"}],"unverified":[{"d":"M6.415.52a2.677 2.677 0 0 1 3.17 0l.928.68c.153.113.33.186.518.215l1.138.175a2.678 2.678 0 0 1 2.241 2.24l.175 1.138c.029.187.102.365.215.518l.68.928a2.677 2.677 0 0 1 0 3.17l-.68.928a1.186 1.186 0 0 0-.215.518l-.175 1.138a2.678 2.678 0 0 1-2.241 2.241l-1.138.175a1.186 1.186 0 0 0-.518.215l-.928.68a2.677 2.677 0 0 1-3.17 0l-.928-.68a1.186 1.186 0 0 0-.518-.215L3.83 14.41a2.678 2.678 0 0 1-2.24-2.24l-.175-1.138a1.186 1.186 0 0 0-.215-.518l-.68-.928a2.677 2.677 0 0 1 0-3.17l.68-.928a1.17 1.17 0 0 0 .215-.518l.175-1.14a2.678 2.678 0 0 1 2.24-2.24l1.138-.175c.187-.029.365-.102.518-.215l.928-.68Zm2.282 1.209a1.18 1.18 0 0 0-1.394 0l-.928.68a2.67 2.67 0 0 1-1.18.489l-1.136.174a1.18 1.18 0 0 0-.987.987l-.174 1.137a2.67 2.67 0 0 1-.489 1.18l-.68.927c-.305.415-.305.98 0 1.394l.68.928c.256.348.423.752.489 1.18l.174 1.136c.078.51.478.909.987.987l1.137.174c.427.066.831.233 1.18.489l.927.68c.415.305.98.305 1.394 0l.928-.68a2.67 2.67 0 0 1 1.18-.489l1.136-.174c.51-.078.909-.478.987-.987l.174-1.137c.066-.427.233-.831.489-1.18l.68-.927c.305-.415.305-.98 0-1.394l-.68-.928a2.67 2.67 0 0 1-.489-1.18l-.174-1.136a1.18 1.18 0 0 0-.987-.987l-1.137-.174a2.67 2.67 0 0 1-1.18-.489ZM6.92 6.085h.001a.75.75 0 0 1-1.342-.67c.169-.339.436-.701.849-.977C6.846 4.16 7.369 4 8 4a2.76 2.76 0 0 1 1.638.525c.502.377.862.965.862 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.109-.073.22-.139.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"}],"unwrap":[{"d":"M1 2.75c0-.199.079-.39.22-.53A.747.747 0 0 1 1.75 2h12.5c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.747.747 0 0 1-.53.22H1.75a.747.747 0 0 1-.53-.22.747.747 0 0 1-.22-.53Zm0 5c0-.199.079-.39.22-.53A.747.747 0 0 1 1.75 7h12.5c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.747.747 0 0 1-.53.22H1.75a.747.747 0 0 1-.53-.22.747.747 0 0 1-.22-.53ZM1.75 12h3.5c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.747.747 0 0 1-.53.22h-3.5a.747.747 0 0 1-.53-.22.747.747 0 0 1 0-1.06.747.747 0 0 1 .53-.22Z"}],"upload":[{"d":"M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"},{"d":"M11.78 4.72a.749.749 0 1 1-1.06 1.06L8.75 3.811V9.5a.75.75 0 0 1-1.5 0V3.811L5.28 5.78a.749.749 0 1 1-1.06-1.06l3.25-3.25a.749.749 0 0 1 1.06 0l3.25 3.25Z"}],"verified":[{"d":"m9.585.52.929.68c.153.112.331.186.518.215l1.138.175a2.678 2.678 0 0 1 2.24 2.24l.174 1.139c.029.187.103.365.215.518l.68.928a2.677 2.677 0 0 1 0 3.17l-.68.928a1.174 1.174 0 0 0-.215.518l-.175 1.138a2.678 2.678 0 0 1-2.241 2.241l-1.138.175a1.17 1.17 0 0 0-.518.215l-.928.68a2.677 2.677 0 0 1-3.17 0l-.928-.68a1.174 1.174 0 0 0-.518-.215L3.83 14.41a2.678 2.678 0 0 1-2.24-2.24l-.175-1.138a1.17 1.17 0 0 0-.215-.518l-.68-.928a2.677 2.677 0 0 1 0-3.17l.68-.928c.112-.153.186-.331.215-.518l.175-1.14a2.678 2.678 0 0 1 2.24-2.24l1.139-.175c.187-.029.365-.103.518-.215l.928-.68a2.677 2.677 0 0 1 3.17 0ZM7.303 1.728l-.927.68a2.67 2.67 0 0 1-1.18.489l-1.137.174a1.179 1.179 0 0 0-.987.987l-.174 1.136a2.677 2.677 0 0 1-.489 1.18l-.68.928a1.18 1.18 0 0 0 0 1.394l.68.927c.256.348.424.753.489 1.18l.174 1.137c.078.509.478.909.987.987l1.136.174a2.67 2.67 0 0 1 1.18.489l.928.68c.414.305.979.305 1.394 0l.927-.68a2.67 2.67 0 0 1 1.18-.489l1.137-.174a1.18 1.18 0 0 0 .987-.987l.174-1.136a2.67 2.67 0 0 1 .489-1.18l.68-.928a1.176 1.176 0 0 0 0-1.394l-.68-.927a2.686 2.686 0 0 1-.489-1.18l-.174-1.137a1.179 1.179 0 0 0-.987-.987l-1.136-.174a2.677 2.677 0 0 1-1.18-.489l-.928-.68a1.176 1.176 0 0 0-1.394 0ZM11.28 6.78l-3.75 3.75a.75.75 0 0 1-1.06 0L4.72 8.78a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L7 8.94l3.22-3.22a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"}],"versions":[{"d":"M7.75 14A1.75 1.75 0 0 1 6 12.25v-8.5C6 2.784 6.784 2 7.75 2h6.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 14Zm-.25-1.75c0 .138.112.25.25.25h6.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25h-6.5a.25.25 0 0 0-.25.25ZM4.9 3.508a.75.75 0 0 1-.274 1.025.249.249 0 0 0-.126.217v6.5c0 .09.048.173.126.217a.75.75 0 0 1-.752 1.298A1.75 1.75 0 0 1 3 11.25v-6.5c0-.649.353-1.214.874-1.516a.75.75 0 0 1 1.025.274ZM1.625 5.533h.001a.249.249 0 0 0-.126.217v4.5c0 .09.048.173.126.217a.75.75 0 0 1-.752 1.298A1.748 1.748 0 0 1 0 10.25v-4.5a1.748 1.748 0 0 1 .873-1.516.75.75 0 1 1 .752 1.299Z"}],"video":[{"d":"M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25Zm1.75-.25a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25Z"},{"d":"M6 10.559V5.442a.25.25 0 0 1 .379-.215l4.264 2.559a.25.25 0 0 1 0 .428l-4.264 2.559A.25.25 0 0 1 6 10.559Z"}],"view-files":[{"d":"M1.75 10a.75.75 0 0 1 .75.75v2.5c0 .138.112.25.25.25h2.5a.75.75 0 1 1 0 1.5h-2.5A1.75 1.75 0 0 1 1 13.25v-2.5a.75.75 0 0 1 .75-.75m12.5 0a.75.75 0 0 1 .75.75v2.5A1.75 1.75 0 0 1 13.25 15h-2.5a.75.75 0 1 1 0-1.5h2.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 .75-.75m-6 0a.75.75 0 1 1 0 1.5h-3a.75.75 0 1 1 0-1.5Zm3-2.5a.75.75 0 1 1 0 1.5h-6a.75.75 0 0 1 0-1.5Zm-1-2.5a.75.75 0 1 1 0 1.5h-5a.75.75 0 0 1 0-1.5Zm-5-4a.75.75 0 0 1 0 1.5h-2.5a.25.25 0 0 0-.25.25v2.5a.75.75 0 0 1-1.5 0v-2.5C1 1.784 1.784 1 2.75 1Zm8 0c.966 0 1.75.784 1.75 1.75v2.5a.75.75 0 1 1-1.5 0v-2.5a.25.25 0 0 0-.25-.25h-2.5a.75.75 0 1 1 0-1.5Z"}],"vscode":[{"d":"M11.0977 1.01264c.1781-.030611.3616-.00542.5244.07325l2.8828 1.39453c.3025.14648.4951.45487.4951.79297v9.45311c-.0001.3379-.1928.6455-.4951.792l-2.8828 1.3936c-.1074.0525-.2253.0822-.3448.0869-.1192.0045-.2386-.0155-.3496-.0596-.1128-.0437-.2148-.1112-.2998-.1973L5.10938 9.68257 2.70605 11.5166c-.10824.0823-.24206.1241-.37793.1181-.13595-.0061-.26569-.0596-.36621-.1514l-.77148-.705c-.06008-.0551-.1078-.1227-.14063-.1973-.03273-.0745-.04979-.1549-.0498-.2363 0-.0815.01698-.1627.0498-.2373.03281-.0744.08069-.14135.14063-.19632l2.08398-1.91113-2.08398-1.91211c-.05994-.05502-.10786-.12182-.14063-.19629-.03276-.07447-.04974-.15497-.0498-.23633 0-.08152.01698-.16268.0498-.2373.03278-.07447.08069-.14128.14063-.19629l.77148-.70606c.10049-.09162.23036-.14527.36621-.15136.13586-.00602.26969.03583.37793.11816l2.40333 1.83399 5.51852-5.0586c.1271-.12866.2915-.21441.4698-.24512M7.31543 7.99995 11.502 11.1933v-.1934H11.5V5.99995h.002V4.80561z","fill-rule":"evenodd"}],"webhook":[{"d":"M5.5 4.25a2.25 2.25 0 0 1 4.5 0 .75.75 0 0 0 1.5 0 3.75 3.75 0 1 0-6.14 2.889l-2.272 4.258a.75.75 0 0 0 1.324.706L7 7.25a.75.75 0 0 0-.309-1.015A2.25 2.25 0 0 1 5.5 4.25Z"},{"d":"M7.364 3.607a.75.75 0 0 1 1.03.257l2.608 4.349a3.75 3.75 0 1 1-.628 6.785.75.75 0 0 1 .752-1.299 2.25 2.25 0 1 0-.033-3.88.75.75 0 0 1-1.03-.256L7.107 4.636a.75.75 0 0 1 .257-1.03Z"},{"d":"M2.9 8.776A.75.75 0 0 1 2.625 9.8 2.25 2.25 0 1 0 6 11.75a.75.75 0 0 1 .75-.751h5.5a.75.75 0 0 1 0 1.5H7.425a3.751 3.751 0 1 1-5.55-3.998.75.75 0 0 1 1.024.274Z"}],"workflow":[{"d":"M0 1.75C0 .784.784 0 1.75 0h3.5C6.216 0 7 .784 7 1.75v3.5A1.75 1.75 0 0 1 5.25 7H4v4a1 1 0 0 0 1 1h4v-1.25C9 9.784 9.784 9 10.75 9h3.5c.966 0 1.75.784 1.75 1.75v3.5A1.75 1.75 0 0 1 14.25 16h-3.5A1.75 1.75 0 0 1 9 14.25v-.75H5A2.5 2.5 0 0 1 2.5 11V7h-.75A1.75 1.75 0 0 1 0 5.25Zm1.75-.25a.25.25 0 0 0-.25.25v3.5c0 .138.112.25.25.25h3.5a.25.25 0 0 0 .25-.25v-3.5a.25.25 0 0 0-.25-.25Zm9 9a.25.25 0 0 0-.25.25v3.5c0 .138.112.25.25.25h3.5a.25.25 0 0 0 .25-.25v-3.5a.25.25 0 0 0-.25-.25Z"}],"wrap":[{"d":"M1.75 7a.747.747 0 0 0-.53.22.747.747 0 0 0 0 1.06c.14.141.331.22.53.22h10c.464 0 .909.184 1.237.513a1.746 1.746 0 0 1 0 2.474A1.746 1.746 0 0 1 11.75 12h-1.464v-.464a.675.675 0 0 0-.375-.607.687.687 0 0 0-.711.064l-1.619 1.214a.679.679 0 0 0 0 1.086L9.2 14.507a.683.683 0 0 0 .711.064.685.685 0 0 0 .375-.607V13.5h1.464A3.247 3.247 0 0 0 15 10.25 3.247 3.247 0 0 0 11.75 7h-10ZM1 2.75c0-.199.079-.39.22-.53A.747.747 0 0 1 1.75 2h12.5c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.747.747 0 0 1-.53.22H1.75a.747.747 0 0 1-.53-.22.747.747 0 0 1-.22-.53ZM1.75 12h3.5c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.747.747 0 0 1-.53.22h-3.5a.747.747 0 0 1-.53-.22.747.747 0 0 1 0-1.06.747.747 0 0 1 .53-.22Z"}],"x":[{"d":"M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"}],"x-circle":[{"d":"M2.344 2.343h-.001a8 8 0 0 1 11.314 11.314A8.002 8.002 0 0 1 .234 10.089a8 8 0 0 1 2.11-7.746Zm1.06 10.253a6.5 6.5 0 1 0 9.108-9.275 6.5 6.5 0 0 0-9.108 9.275ZM6.03 4.97 8 6.94l1.97-1.97a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l1.97 1.97a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-1.97 1.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L6.94 8 4.97 6.03a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018Z"}],"x-circle-fill":[{"d":"M2.343 13.657A8 8 0 1 1 13.658 2.343 8 8 0 0 1 2.343 13.657ZM6.03 4.97a.751.751 0 0 0-1.042.018.751.751 0 0 0-.018 1.042L6.94 8 4.97 9.97a.749.749 0 0 0 .326 1.275.749.749 0 0 0 .734-.215L8 9.06l1.97 1.97a.749.749 0 0 0 1.275-.326.749.749 0 0 0-.215-.734L9.06 8l1.97-1.97a.749.749 0 0 0-.326-1.275.749.749 0 0 0-.734.215L8 6.94Z"}],"zap":[{"d":"M9.504.43a1.516 1.516 0 0 1 2.437 1.713L10.415 5.5h2.123c1.57 0 2.346 1.909 1.22 3.004l-7.34 7.142a1.249 1.249 0 0 1-.871.354h-.302a1.25 1.25 0 0 1-1.157-1.723L5.633 10.5H3.462c-1.57 0-2.346-1.909-1.22-3.004L9.503.429Zm1.047 1.074L3.286 8.571A.25.25 0 0 0 3.462 9H6.75a.75.75 0 0 1 .694 1.034l-1.713 4.188 6.982-6.793A.25.25 0 0 0 12.538 7H9.25a.75.75 0 0 1-.683-1.06l2.008-4.418.003-.006a.036.036 0 0 0-.004-.009l-.006-.006-.008-.001c-.003 0-.006.002-.009.004Z"}],"zoom-in":[{"d":"M3.75 7.5a.75.75 0 0 1 .75-.75h2.25V4.5a.75.75 0 0 1 1.5 0v2.25h2.25a.75.75 0 0 1 0 1.5H8.25v2.25a.75.75 0 0 1-1.5 0V8.25H4.5a.75.75 0 0 1-.75-.75Z"},{"d":"M7.5 0a7.5 7.5 0 0 1 5.807 12.247l2.473 2.473a.749.749 0 1 1-1.06 1.06l-2.473-2.473A7.5 7.5 0 1 1 7.5 0Zm-6 7.5a6 6 0 1 0 12 0 6 6 0 0 0-12 0Z"}],"zoom-out":[{"d":"M4.5 6.75h6a.75.75 0 0 1 0 1.5h-6a.75.75 0 0 1 0-1.5Z"},{"d":"M0 7.5a7.5 7.5 0 1 1 13.307 4.747l2.473 2.473a.749.749 0 1 1-1.06 1.06l-2.473-2.473A7.5 7.5 0 0 1 0 7.5Zm7.5-6a6 6 0 1 0 0 12 6 6 0 0 0 0-12Z"}]} diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/platforms.yml b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/platforms.yml new file mode 100644 index 0000000..2ca9a54 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/platforms.yml @@ -0,0 +1,175 @@ +# Platform Configuration for Gitlink Extension +# This file defines the built-in platform configurations and serves as a template +# for custom platform definitions. + +platforms: + github: + display-name: GitHub + base-url: https://github.com + patterns: + issue: + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" + - "GH%-(%d+)" + merge-request: + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" + commit: + - "^(%x+)$" + - "([^/]+/[^/@]+)@(%x+)" + - "([^/@]+)@(%x+)" + user: "@(%w[%w%-]*)" + url-formats: + issue: "/{repo}/issues/{number}" + merge-request: "/{repo}/pull/{number}" + pull: "/{repo}/pull/{number}" + commit: "/{repo}/commit/{sha}" + user: "/{username}" + widget: + paths: + issues: "/{repo}/issues" + pull-requests: "/{repo}/pulls" + releases: "/{repo}/releases" + discussions: "/{repo}/discussions" + star: "/{repo}" + fork: "/{repo}/fork" + sponsor-url: "{base-url}/sponsors/{username}?o=esb" + api: + endpoint: "https://api.github.com/repos/{repo}" + stars-field: "stargazers_count" + forks-field: "forks_count" + # Headers are "Name: value" strings (not a map) so header names keep + # their exact casing through YAML key conversion. + headers: + - "Accept: application/vnd.github+json" + - "X-GitHub-Api-Version: 2022-11-28" + + gitlab: + display-name: GitLab + base-url: https://gitlab.com + patterns: + issue: + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" + merge-request: + - "!(%d+)" + - "([^/]+/[^/#]+)!(%d+)" + commit: + - "^(%x+)$" + - "([^/]+/[^/@]+)@(%x+)" + - "([^/@]+)@(%x+)" + user: "@(%w[%w%-%._]*)" + url-formats: + issue: "/{repo}/-/issues/{number}" + merge-request: "/{repo}/-/merge_requests/{number}" + pull: "/{repo}/-/merge_requests/{number}" + commit: "/{repo}/-/commit/{sha}" + user: "/{username}" + widget: + labels: + pull-requests: "Merge Requests" + paths: + issues: "/{repo}/-/issues" + pull-requests: "/{repo}/-/merge_requests" + releases: "/{repo}/-/releases" + star: "/{repo}" + fork: "/{repo}/-/forks/new" + api: + endpoint: "{base-url}/api/v4/projects/{repo-encoded}" + stars-field: "star_count" + forks-field: "forks_count" + + codeberg: + display-name: Codeberg + base-url: https://codeberg.org + patterns: + issue: + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" + merge-request: + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" + commit: + - "^(%x+)$" + - "([^/]+/[^/@]+)@(%x+)" + - "([^/@]+)@(%x+)" + user: "@(%w[%w%-%._]*)" + url-formats: + issue: "/{repo}/issues/{number}" + merge-request: "/{repo}/pulls/{number}" + pull: "/{repo}/pulls/{number}" + commit: "/{repo}/commit/{sha}" + user: "/{username}" + widget: + paths: + issues: "/{repo}/issues" + pull-requests: "/{repo}/pulls" + releases: "/{repo}/releases" + star: "/{repo}" + fork: "/{repo}/fork" + api: + endpoint: "{base-url}/api/v1/repos/{repo}" + stars-field: "stars_count" + forks-field: "forks_count" + + gitea: + display-name: Gitea + base-url: https://gitea.com + patterns: + issue: + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" + merge-request: + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" + commit: + - "^(%x+)$" + - "([^/]+/[^/@]+)@(%x+)" + - "([^/@]+)@(%x+)" + user: "@(%w[%w%-%._]*)" + url-formats: + issue: "/{repo}/issues/{number}" + merge-request: "/{repo}/pulls/{number}" + pull: "/{repo}/pulls/{number}" + commit: "/{repo}/commit/{sha}" + user: "/{username}" + widget: + paths: + issues: "/{repo}/issues" + pull-requests: "/{repo}/pulls" + releases: "/{repo}/releases" + star: "/{repo}" + fork: "/{repo}/fork" + api: + endpoint: "{base-url}/api/v1/repos/{repo}" + stars-field: "stars_count" + forks-field: "forks_count" + + bitbucket: + display-name: Bitbucket + base-url: https://bitbucket.org + patterns: + issue: + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" + merge-request: + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" + commit: + - "^(%x+)$" + - "([^/]+/[^/@]+)@(%x+)" + - "([^/@]+)@(%x+)" + user: "@([%l%d][%l%d%-_]*)" + url-formats: + issue: "/{repo}/issues/{number}" + merge-request: "/{repo}/pull-requests/{number}" + pull: "/{repo}/pull-requests/{number}" + commit: "/{repo}/commits/{sha}" + user: "/{username}" + # No `api` block: the Bitbucket 2.0 API exposes no star count, so the + # widget renders without the stars/forks counters on this platform. + widget: + paths: + issues: "/{repo}/issues" + pull-requests: "/{repo}/pull-requests" + star: "/{repo}" diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/widget-navbar-tools.css b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/widget-navbar-tools.css new file mode 100644 index 0000000..f3530a7 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/widget-navbar-tools.css @@ -0,0 +1,31 @@ +/* Optional companion to the gitlink widget (`widget.style-navbar-tools`). + Applies the widget's bordered pill style to Quarto's navbar search button + and colour-scheme toggle so the navbar-right controls read as one set. + Sizing and spacing are handled unconditionally by widget.css; this file + carries only the border/pill visuals. */ + +.navbar #quarto-search .aa-DetachedSearchButton, +.navbar .quarto-navbar-tools .quarto-color-scheme-toggle { + background: var(--gitlink-widget-pill-bg, transparent); + border: 1px solid var(--gitlink-widget-border, var(--bs-border-color-translucent, rgba(128, 128, 128, 0.3))); + border-radius: 0.375rem; + color: inherit; + cursor: pointer; + transition: background 150ms ease, border-color 150ms ease, color 150ms ease; +} + +.navbar #quarto-search .aa-DetachedSearchButton:hover, +.navbar #quarto-search .aa-DetachedSearchButton:focus-visible, +.navbar .quarto-navbar-tools .quarto-color-scheme-toggle:hover, +.navbar .quarto-navbar-tools .quarto-color-scheme-toggle:focus-visible { + background: var(--gitlink-widget-accent-soft, color-mix(in srgb, var(--gitlink-widget-accent, var(--bs-link-color, #0d6efd)) 15%, transparent)); + border-color: var(--gitlink-widget-accent, var(--bs-link-color, #0d6efd)); + color: var(--gitlink-widget-accent, var(--bs-link-color, #0d6efd)); +} + +@media (prefers-reduced-motion: reduce) { + .navbar #quarto-search .aa-DetachedSearchButton, + .navbar .quarto-navbar-tools .quarto-color-scheme-toggle { + transition: none; + } +} diff --git a/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/widget.css b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/widget.css new file mode 100644 index 0000000..38ac1be --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/_extensions/mcanouil/gitlink/widget.css @@ -0,0 +1,319 @@ +/* Gitlink repository navigation widget (navbar or sidebar). + Structural styling only; theming happens through the `--gitlink-widget-*` + custom properties, which fall back to Bootstrap tokens so unthemed Quarto + sites work out of the box. */ + +.gitlink-widget { + position: relative; + display: inline-flex; + align-items: center; +} + +.navbar .navbar-nav.ms-auto { + align-items: center; + gap: 0.4rem; +} + +/* Quarto's search button and colour-scheme toggle are sized to match the + widget trigger (2rem tall, glyph centred) with consistent spacing across + the navbar-right control group. Layout only; the bordered pill style is + opt-in via `widget.style-navbar-tools`. */ +.navbar #quarto-search { + display: inline-flex; + align-items: center; + margin-left: 0.4rem; +} + +.navbar .quarto-navbar-tools { + display: inline-flex; + align-items: center; + gap: 0.4rem; + margin-left: 0.4rem; +} + +.navbar #quarto-search .aa-DetachedSearchButton, +.navbar .quarto-navbar-tools .quarto-color-scheme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + padding: 0; + box-shadow: none; +} + +/* Collapse the autocomplete library's icon slot to a compact icon following + currentColor, and hide the placeholder and query slots so the detached + button stays an icon-only square. `#quarto-search` outranks `.aa-*`. */ +.navbar #quarto-search .aa-DetachedSearchButtonIcon { + width: auto; + margin: 0; + padding: 0; + color: currentColor; +} + +.navbar #quarto-search .aa-DetachedSearchButtonIcon svg { + width: 1.05rem; + height: 1.05rem; +} + +.navbar #quarto-search .aa-DetachedSearchButtonPlaceholder, +.navbar #quarto-search .aa-DetachedSearchButtonQuery { + display: none; +} + +/* The trigger is a native