From 3ed24decec860cfb5a61056b9c69be38e0b158ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Wed, 3 Dec 2025 21:55:42 +0100 Subject: [PATCH 01/15] feat: add support for custom Git hosting platforms Introduce a new configuration system allowing users to define custom Git hosting platforms via a YAML file. --- README.md | 52 ++++ _extensions/gitlink/_modules/platforms.lua | 262 +++++++++++++++++++ _extensions/gitlink/gitlink.lua | 278 ++++++++------------- _extensions/gitlink/platforms.yml | 105 ++++++++ example.qmd | 56 +++++ 5 files changed, 585 insertions(+), 168 deletions(-) create mode 100644 _extensions/gitlink/_modules/platforms.lua create mode 100644 _extensions/gitlink/platforms.yml diff --git a/README.md b/README.md index c3ef7b9..1862bee 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,58 @@ In non-HTML formats, platform names appear in parentheses after the link text, s - Workspace-based repository structure - Follows [official Bitbucket markup syntax](https://support.atlassian.com/bitbucket-cloud/docs/markup-comments/) +## Custom Platforms + +You can add support for additional Git hosting platforms by creating a custom YAML configuration file. + +### Creating a Custom Platform + +Create a YAML file (e.g., `my-platforms.yml`): + +```yaml +platforms: + gitplatform: + default_url: https://git.example.com + patterns: + issue: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + merge_request: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + commit: + - '^(%x+)$' + - '([^/]+/[^/@]+)@(%x+)' + - '(%w+)@(%x+)' + user: '@([%w%-%.]+)' + url_formats: + issue: '/{repo}/issues/{number}' + pull: '/{repo}/pulls/{number}' + commit: '/{repo}/commit/{sha}' + user: '/{username}' +``` + +Reference it in your document: + +```yaml +extensions: + gitlink: + platform: gitplatform + custom-platforms-file: my-platforms.yml + repository-name: owner/repo +``` + +### Contributing New Platforms + +To add a new platform to the built-in configuration: + +1. Fork the repository. +2. Edit [`_extensions/gitlink/platforms.yml`](_extensions/gitlink/platforms.yml). +3. Test your configuration using a custom platforms file first. +4. Submit a pull request. + +This approach makes it easy to add support for new platforms without modifying Lua code. + ## Example Document Here is the source code for a comprehensive example: [example.qmd](example.qmd). diff --git a/_extensions/gitlink/_modules/platforms.lua b/_extensions/gitlink/_modules/platforms.lua new file mode 100644 index 0000000..2cbf169 --- /dev/null +++ b/_extensions/gitlink/_modules/platforms.lua @@ -0,0 +1,262 @@ +--[[ +# MIT License +# +# Copyright (c) 2025 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. +]] + +--- Platform Configuration Module +--- Manages platform-specific configurations for Git hosting services +--- @module platforms +--- @author Mickaël Canouil +--- @version 1.0.0 + +local platforms_module = {} + +-- ============================================================================ +-- CONFIGURATION STORAGE +-- ============================================================================ + +--- @type table Platform configurations cache +local platform_configs = nil + +--- @type table Custom platform configurations +local custom_platforms = {} + +-- ============================================================================ +-- 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 + result[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') + + -- Check if file exists + local file = io.open(config_path, 'r') + if not file then + return nil + end + local content = file:read('*all') + file:close() + + -- Parse YAML using Pandoc + 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 + +--- Initialize platform configurations +--- @param yaml_path string|nil Optional path to custom YAML file +--- @return boolean True if initialization was successful, false otherwise +--- @usage platforms_module.initialize('custom-platforms.yml') +function platforms_module.initialize(yaml_path) + if platform_configs then + return true + end + + platform_configs = load_platforms(yaml_path) + + if not platform_configs then + -- Fallback to hardcoded configurations if YAML loading fails + platform_configs = {} + return false + end + + return true +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) + -- Initialize if not already done + if not platform_configs then + platforms_module.initialize() + end + + local name_lower = platform_name:lower() + + -- Check custom platforms first + if custom_platforms[name_lower] then + return custom_platforms[name_lower] + end + + -- Fall back to loaded platforms + if platform_configs and platform_configs[name_lower] then + return platform_configs[name_lower] + end + + return nil +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() + -- Initialize if not already done + if not platform_configs then + platforms_module.initialize() + end + + local names = {} + + -- Add loaded platform names + if platform_configs then + for name, _ in pairs(platform_configs) do + table.insert(names, name) + end + end + + -- Add custom platform names + 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 +--- @param platform_name string The platform name +--- @param config table The platform configuration +--- @return boolean True if registration was successful, false otherwise +--- @usage 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 + end + + -- Validate required fields + if not config.default_url then + return false + end + + if not config.patterns or not config.url_formats then + return false + end + + local name_lower = platform_name:lower() + custom_platforms[name_lower] = config + + return true +end + +--- Load custom platform from YAML string +--- @param yaml_string string The YAML string containing platform configuration +--- @param platform_name string The platform name to register +--- @return boolean True if registration was successful, false otherwise +--- @usage 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 + 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 success and result then + return platforms_module.register_custom_platform(platform_name, result) + end + + return false +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 + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return platforms_module diff --git a/_extensions/gitlink/gitlink.lua b/_extensions/gitlink/gitlink.lua index c69cf1d..fb70529 100644 --- a/_extensions/gitlink/gitlink.lua +++ b/_extensions/gitlink/gitlink.lua @@ -25,10 +25,11 @@ --- Extension name constant local EXTENSION_NAME = "gitlink" ---- Load utils, git, and bitbucket modules -local utils = require(quarto.utils.resolve_path("_modules/utils.lua"):gsub("%.lua$", "")) -local git = require(quarto.utils.resolve_path("_modules/git.lua"):gsub("%.lua$", "")) -local bitbucket = require(quarto.utils.resolve_path("_modules/bitbucket.lua"):gsub("%.lua$", "")) +--- Load utils, git, bitbucket, and platforms modules +local utils = require(quarto.utils.resolve_path('_modules/utils.lua'):gsub('%.lua$', '')) +local git = require(quarto.utils.resolve_path('_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$', '')) --- @type string The platform type (github, gitlab, codeberg, gitea, bitbucket) local platform = "github" @@ -57,92 +58,11 @@ local COMMIT_SHA_SHORT_LENGTH = 7 --- @type integer Minimum length for a valid git commit SHA local COMMIT_SHA_MIN_LENGTH = 7 ---- @type table Platform-specific configuration -local platform_configs = { - github = { - default_url = "https://github.com", - patterns = { - issue = { "#(%d+)", "([^/]+/[^/#]+)#(%d+)", "GH%-(%d+)" }, - merge_request = { "#(%d+)", "([^/]+/[^/#]+)#(%d+)" }, - commit = { "^(%x+)$", "([^/]+/[^/@]+)@(%x+)", "(%w+)@(%x+)" }, - user = "@([%w%-%.]+)" - }, - url_formats = { - issue = "/{repo}/issues/{number}", - pull = "/{repo}/pull/{number}", - commit = "/{repo}/commit/{sha}", - user = "/{username}" - } - }, - gitlab = { - default_url = "https://gitlab.com", - patterns = { - issue = { "#(%d+)", "([^/]+/[^/#]+)#(%d+)" }, - merge_request = { "!(%d+)", "([^/]+/[^/#]+)!(%d+)" }, - commit = { "^(%x+)$", "([^/]+/[^/@]+)@(%x+)", "(%w+)@(%x+)" }, - user = "@([%w%-%.]+)" - }, - url_formats = { - issue = "/{repo}/-/issues/{number}", - merge_request = "/{repo}/-/merge_requests/{number}", - commit = "/{repo}/-/commit/{sha}", - user = "/{username}" - } - }, - codeberg = { - default_url = "https://codeberg.org", - patterns = { - issue = { "#(%d+)", "([^/]+/[^/#]+)#(%d+)" }, - merge_request = { "#(%d+)", "([^/]+/[^/#]+)#(%d+)" }, - commit = { "^(%x+)$", "([^/]+/[^/@]+)@(%x+)", "(%w+)@(%x+)" }, - user = "@([%w%-%.]+)" - }, - url_formats = { - issue = "/{repo}/issues/{number}", - pull = "/{repo}/pulls/{number}", - commit = "/{repo}/commit/{sha}", - user = "/{username}" - } - }, - gitea = { - default_url = "https://gitea.com", - patterns = { - issue = { "#(%d+)", "([^/]+/[^/#]+)#(%d+)" }, - merge_request = { "#(%d+)", "([^/]+/[^/#]+)#(%d+)" }, - commit = { "^(%x+)$", "([^/]+/[^/@]+)@(%x+)", "(%w+)@(%x+)" }, - user = "@([%w%-%.]+)" - }, - url_formats = { - issue = "/{repo}/issues/{number}", - pull = "/{repo}/pulls/{number}", - commit = "/{repo}/commit/{sha}", - user = "/{username}" - } - }, - bitbucket = { - default_url = "https://bitbucket.org", - patterns = { - issue = { "#(%d+)", "([^/]+/[^/#]+)#(%d+)" }, - merge_request = { "#(%d+)", "([^/]+/[^/#]+)#(%d+)" }, - commit = { "^(%x+)$", "([^/]+/[^/@]+)@(%x+)", "(%w+)@(%x+)" }, - user = "@([%w%-%.]+)" - }, - url_formats = { - issue = "/{repo}/issues/{number}", - pull = "/{repo}/pull-requests/{number}", - commit = "/{repo}/commits/{sha}", - user = "/{username}" - } - } -} - - - --- Get platform configuration --- @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) - return platform_configs[platform_name:lower()] + return platforms.get_platform_config(platform_name:lower()) end --- Create a link with platform label @@ -214,18 +134,28 @@ local function get_repository(meta) local meta_platform = utils.get_metadata_value(meta, 'gitlink', 'platform') local meta_base_url = utils.get_metadata_value(meta, 'gitlink', 'base-url') local meta_repository = utils.get_metadata_value(meta, 'gitlink', 'repository-name') + local meta_custom_platforms = utils.get_metadata_value(meta, 'gitlink', 'custom-platforms-file') + + -- Load custom platforms file if specified + if not utils.is_empty(meta_custom_platforms) then + local custom_file_path = quarto.utils.resolve_path(meta_custom_platforms --[[@as string]]) + platforms.initialize(custom_file_path) + else + platforms.initialize() + end if not utils.is_empty(meta_platform) then platform = (meta_platform --[[@as string]]):lower() else - platform = "github" + platform = 'github' end local config = get_platform_config(platform) if not config then + local available_platforms = table.concat(platforms.get_all_platform_names(), ', ') utils.log_error( EXTENSION_NAME, "Unsupported platform: '" .. platform .. - "'. Supported platforms are: github, gitlab, codeberg, gitea, bitbucket." + "'. Supported platforms are: " .. available_platforms .. '.' ) return meta end @@ -356,62 +286,66 @@ local function process_issues_and_mrs(elem, current_platform, current_base_url) if not number then -- Try to match URLs from any supported platform - for platform_name, platform_config in pairs(platform_configs) do - local platform_base_url = platform_config.default_url - local escaped_platform_url = utils.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 + local all_platform_names = platforms.get_all_platform_names() + for _, platform_name in ipairs(all_platform_names) do + local platform_config = platforms.get_platform_config(platform_name) + if platform_config then + local platform_base_url = platform_config.default_url + local escaped_platform_url = utils.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 - matched_platform = platform_name - matched_base_url = platform_base_url - config = platform_config - break end end end @@ -451,17 +385,21 @@ local function process_users(elem, current_platform, current_base_url) local text = elem.text local username = nil - for platform_name, platform_config in pairs(platform_configs) do - local platform_base_url = platform_config.default_url - local escaped_platform_url = utils.escape_pattern(platform_base_url) - local url_pattern = "^" .. escaped_platform_url .. "/([%w%-%.]+)$" + local all_platform_names = platforms.get_all_platform_names() + for _, platform_name in ipairs(all_platform_names) do + local platform_config = platforms.get_platform_config(platform_name) + if platform_config then + local platform_base_url = platform_config.default_url + local escaped_platform_url = utils.escape_pattern(platform_base_url) + local url_pattern = '^' .. escaped_platform_url .. '/([%w%-%.]+)$' - if text:match(url_pattern) then - username = text:match(url_pattern) - 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 + if text:match(url_pattern) then + username = text:match(url_pattern) + 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 end @@ -514,22 +452,26 @@ local function process_commits(elem, current_platform, current_base_url) end if not commit_sha then - for platform_name, platform_config in pairs(platform_configs) do - local platform_base_url = platform_config.default_url - local escaped_platform_url = utils.escape_pattern(platform_base_url) - local url_pattern = "^" .. escaped_platform_url .. "/([^/]+/[^/]+)/%-?/?commits?/(%x+)" - if text:match(url_pattern) then - repo, commit_sha = text:match(url_pattern) - if commit_sha:len() >= COMMIT_SHA_MIN_LENGTH then - 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) + local all_platform_names = platforms.get_all_platform_names() + for _, platform_name in ipairs(all_platform_names) do + local platform_config = platforms.get_platform_config(platform_name) + if platform_config then + local platform_base_url = platform_config.default_url + local escaped_platform_url = utils.escape_pattern(platform_base_url) + local url_pattern = '^' .. escaped_platform_url .. '/([^/]+/[^/]+)/%-?/?commits?/(%x+)' + if text:match(url_pattern) then + repo, commit_sha = text:match(url_pattern) + if commit_sha:len() >= COMMIT_SHA_MIN_LENGTH then + 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 - matched_platform = platform_name - matched_base_url = platform_base_url - config = platform_config - break end end end diff --git a/_extensions/gitlink/platforms.yml b/_extensions/gitlink/platforms.yml new file mode 100644 index 0000000..9a36f04 --- /dev/null +++ b/_extensions/gitlink/platforms.yml @@ -0,0 +1,105 @@ +# Platform Configuration for Gitlink Extension +# This file defines the built-in platform configurations and serves as a template +# for custom platform definitions. + +platforms: + github: + default_url: https://github.com + patterns: + issue: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + - 'GH%-(%d+)' + merge_request: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + commit: + - '^(%x+)$' + - '([^/]+/[^/@]+)@(%x+)' + - '(%w+)@(%x+)' + user: '@([%w%-%.]+)' + url_formats: + issue: '/{repo}/issues/{number}' + pull: '/{repo}/pull/{number}' + commit: '/{repo}/commit/{sha}' + user: '/{username}' + + gitlab: + default_url: https://gitlab.com + patterns: + issue: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + merge_request: + - '!(%d+)' + - '([^/]+/[^/#]+)!(%d+)' + commit: + - '^(%x+)$' + - '([^/]+/[^/@]+)@(%x+)' + - '(%w+)@(%x+)' + user: '@([%w%-%.]+)' + url_formats: + issue: '/{repo}/-/issues/{number}' + merge_request: '/{repo}/-/merge_requests/{number}' + commit: '/{repo}/-/commit/{sha}' + user: '/{username}' + + codeberg: + default_url: https://codeberg.org + patterns: + issue: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + merge_request: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + commit: + - '^(%x+)$' + - '([^/]+/[^/@]+)@(%x+)' + - '(%w+)@(%x+)' + user: '@([%w%-%.]+)' + url_formats: + issue: '/{repo}/issues/{number}' + pull: '/{repo}/pulls/{number}' + commit: '/{repo}/commit/{sha}' + user: '/{username}' + + gitea: + default_url: https://gitea.com + patterns: + issue: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + merge_request: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + commit: + - '^(%x+)$' + - '([^/]+/[^/@]+)@(%x+)' + - '(%w+)@(%x+)' + user: '@([%w%-%.]+)' + url_formats: + issue: '/{repo}/issues/{number}' + pull: '/{repo}/pulls/{number}' + commit: '/{repo}/commit/{sha}' + user: '/{username}' + + bitbucket: + default_url: https://bitbucket.org + patterns: + issue: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + merge_request: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + commit: + - '^(%x+)$' + - '([^/]+/[^/@]+)@(%x+)' + - '(%w+)@(%x+)' + user: '@([%w%-%.]+)' + url_formats: + issue: '/{repo}/issues/{number}' + pull: '/{repo}/pull-requests/{number}' + commit: '/{repo}/commits/{sha}' + user: '/{username}' diff --git a/example.qmd b/example.qmd index 7a6244e..078bba7 100644 --- a/example.qmd +++ b/example.qmd @@ -308,6 +308,62 @@ If not provided, the extension will attempt to detect it from the Git remote: git remote get-url origin ``` +### Custom Platforms + +You can add support for additional platforms or customise existing ones by providing a custom platforms configuration file. + +**Creating a Custom Platforms File**: + +Create a YAML file (e.g., `custom-platforms.yml`) with your platform definitions: + +```yaml +platforms: + gitplatform: + default_url: https://git.example.com + patterns: + issue: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + merge_request: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + commit: + - '^(%x+)$' + - '([^/]+/[^/@]+)@(%x+)' + - '(%w+)@(%x+)' + user: '@([%w%-%.]+)' + url_formats: + issue: '/{repo}/issues/{number}' + pull: '/{repo}/pulls/{number}' + commit: '/{repo}/commit/{sha}' + user: '/{username}' +``` + +**Using Custom Platforms**: + +Reference your custom platforms file in the document metadata: + +```yaml +extensions: + gitlink: + platform: gitplatform + custom-platforms-file: custom-platforms.yml + repository-name: owner/repo +``` + +The extension will load both built-in and custom platform definitions, allowing you to use any platform you define. + +**Contributing New Platforms**: + +To contribute a new platform to the built-in configuration: + +1. Fork the repository. +2. Edit [`_extensions/gitlink/platforms.yml`](_extensions/gitlink/platforms.yml) to add your platform definition. +3. Test your configuration with a custom platforms file first. +4. Submit a pull request with your changes. + +This makes it easy for the community to add support for new Git hosting platforms without modifying the main Lua code. + ### Platform Badges In HTML output, Gitlink displays subtle platform badges next to links for improved accessibility and quick visual identification. From 2b149417cacde5eba77146d8170c1f40ef3d62d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Wed, 3 Dec 2025 21:59:50 +0100 Subject: [PATCH 02/15] fix: use british english --- _extensions/gitlink/_modules/platforms.lua | 35 ++++++++++++---------- _extensions/gitlink/gitlink.lua | 12 ++++---- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/_extensions/gitlink/_modules/platforms.lua b/_extensions/gitlink/_modules/platforms.lua index 2cbf169..2fcd2e0 100644 --- a/_extensions/gitlink/_modules/platforms.lua +++ b/_extensions/gitlink/_modules/platforms.lua @@ -111,23 +111,32 @@ local function load_platforms(yaml_path) return nil end ---- Initialize platform configurations +--- Initialise platform configurations --- @param yaml_path string|nil Optional path to custom YAML file ---- @return boolean True if initialization was successful, false otherwise ---- @usage platforms_module.initialize('custom-platforms.yml') -function platforms_module.initialize(yaml_path) - if platform_configs then +--- @return boolean True if initialisation was successful, false otherwise +--- @usage platforms_module.initialise('custom-platforms.yml') +function platforms_module.initialise(yaml_path) + if platform_configs and not yaml_path then return true end - platform_configs = load_platforms(yaml_path) + local loaded_configs = load_platforms(yaml_path) - if not platform_configs then - -- Fallback to hardcoded configurations if YAML loading fails - platform_configs = {} + if not loaded_configs then + if not platform_configs then + platform_configs = {} + end return false end + if platform_configs and yaml_path then + for name, config in pairs(loaded_configs) do + custom_platforms[name] = config + end + else + platform_configs = loaded_configs + end + return true end @@ -140,19 +149,16 @@ end --- @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) - -- Initialize if not already done if not platform_configs then - platforms_module.initialize() + platforms_module.initialise() end local name_lower = platform_name:lower() - -- Check custom platforms first if custom_platforms[name_lower] then return custom_platforms[name_lower] end - -- Fall back to loaded platforms if platform_configs and platform_configs[name_lower] then return platform_configs[name_lower] end @@ -164,9 +170,8 @@ end --- @return table List of available platform names --- @usage local platforms = platforms_module.get_all_platform_names() function platforms_module.get_all_platform_names() - -- Initialize if not already done if not platform_configs then - platforms_module.initialize() + platforms_module.initialise() end local names = {} diff --git a/_extensions/gitlink/gitlink.lua b/_extensions/gitlink/gitlink.lua index fb70529..b807d01 100644 --- a/_extensions/gitlink/gitlink.lua +++ b/_extensions/gitlink/gitlink.lua @@ -136,12 +136,11 @@ local function get_repository(meta) local meta_repository = utils.get_metadata_value(meta, 'gitlink', 'repository-name') local meta_custom_platforms = utils.get_metadata_value(meta, 'gitlink', 'custom-platforms-file') - -- Load custom platforms file if specified if not utils.is_empty(meta_custom_platforms) then local custom_file_path = quarto.utils.resolve_path(meta_custom_platforms --[[@as string]]) - platforms.initialize(custom_file_path) + platforms.initialise(custom_file_path) else - platforms.initialize() + platforms.initialise() end if not utils.is_empty(meta_platform) then @@ -371,12 +370,11 @@ end --- Process user/organisation references --- @param elem pandoc.Str The string element to process ---- @param current_platform string The current platform name (unused but kept for consistency) ---- @param current_base_url string The current base URL (unused but kept for consistency) +--- @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, current_base_url) +local function process_users(elem, current_platform) local config = get_platform_config(current_platform) if not config then return nil, nil, nil @@ -506,7 +504,7 @@ local function process_gitlink(elem) end if link == nil then - link = process_users(elem, platform, base_url) + link = process_users(elem, platform) end if link == nil then From 6d75255fe03d8b9967997338a0604143ba39fc14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:35:36 +0100 Subject: [PATCH 03/15] feat: add schema validation for custom platform configurations Implement schema validation for custom platforms to ensure proper configuration. This includes validation of required fields and patterns, enhancing error handling during platform loading. --- README.md | 258 ++++++++---- _extensions/gitlink/_modules/platforms.lua | 113 +++++- _extensions/gitlink/_modules/schema.lua | 437 +++++++++++++++++++++ _extensions/gitlink/gitlink.lua | 15 +- _extensions/gitlink/platforms.yml | 5 + example.qmd | 1 + 6 files changed, 731 insertions(+), 98 deletions(-) create mode 100644 _extensions/gitlink/_modules/schema.lua diff --git a/README.md b/README.md index 1862bee..7064101 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,11 @@ extensions: at: post-quarto ``` -### Supported Platforms and Reference Formats +## Supported Platforms -#### GitHub +Each platform has different reference formats. Choose your platform below: + +### GitHub Official documentation: [Autolinked references](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/autolinked-references-and-urls) @@ -74,11 +76,11 @@ extensions: **References:** -- Issues/PRs: `#123`, `owner/repo#123` +- Issues/PRs: `#123`, `owner/repo#123`, `GH-123` - Commits: `a5c3785`, `owner/repo@a5c3785` - Users: `@username` -#### GitLab +### GitLab Official documentation: [GitLab Flavored Markdown](https://docs.gitlab.com/ee/user/markdown.html#gitlab-specific-references) @@ -97,7 +99,7 @@ extensions: - Commits: `9ba12248`, `group/project@9ba12248` - Users: `@username` -#### Codeberg +### Codeberg Official documentation: [Codeberg Documentation](https://docs.codeberg.org/) (uses Forgejo) @@ -115,7 +117,7 @@ extensions: - Commits: `e59ff077`, `user/repo@e59ff077` - Users: `@username` -#### Gitea +### Gitea Official documentation: [Gitea Documentation](https://docs.gitea.com/usage/issues-prs/automatically-linked-references) @@ -133,7 +135,7 @@ extensions: - Commits: `e59ff077`, `user/repo@e59ff077` - Users: `@username` -#### Bitbucket +### Bitbucket Official documentation: [Bitbucket markup syntax](https://support.atlassian.com/bitbucket-cloud/docs/markup-comments/) @@ -157,44 +159,31 @@ Bitbucket requires keyword prefixes: > [!NOTE] > The `issue` and `pull request` keywords are required to distinguish reference types. +## Features and Configuration + ### URL Processing -The extension automatically processes full URLs and converts them to the appropriate short references: +The extension automatically processes full URLs and converts them to short references: **Input:** `https://github.com/owner/repo/issues/123` - -**Output:** `owner/repo#123` (or `#123` if it's the current repository) +**Output:** `owner/repo#123` (or `#123` if current repository) > [!TIP] -> For best results, wrap URLs in angle brackets (``) rather than using bare URLs. -> For example, use `` instead of `https://github.com/owner/repo/issues/123`. +> Wrap URLs in angle brackets (``) for best results instead of bare URLs. ### Repository Detection -If `repository-name` is not specified, the extension attempts to detect it from the Git remote: +If `repository-name` is not specified, the extension auto-detects from git remote: ```bash git remote get-url origin ``` -This works for most Git hosting platforms and extracts the `owner/repo` format from URLs like: - -- `https://github.com/owner/repo.git` -- `git@gitlab.com:group/project.git` -- `ssh://git@codeberg.org/user/repo.git` +Supports: `https://github.com/owner/repo.git`, `git@gitlab.com:group/project.git`, `ssh://git@codeberg.org/user/repo.git` ### Platform Badges -In HTML output, Gitlink adds subtle platform badges to links, making it easy to identify which platform a link references at a glance. - -Platform badges are: - -- Always visible (not just on hover). -- Accessible to screen readers with proper ARIA labels. -- Styled using Bootstrap for automatic theme compatibility. -- Accompanied by tooltips with full platform names. - -You can control badge appearance using metadata options: +In HTML output, Gitlink adds subtle platform badges to links. You can control them with: ```yaml extensions: @@ -203,53 +192,7 @@ extensions: badge-position: "after" # "after" or "before" link (default: "after") ``` -**Configuration Options**: - -- `show-platform-badge` (boolean): Toggle badge visibility, default `true`. -- `badge-position` (string): Badge placement relative to link, default `"after"`. - -In non-HTML formats, platform names appear in parentheses after the link text, such as `#123 (GitHub)`. - -### Platform-Specific Features - -#### GitHub Features - -- Supports `GH-123` format for issues -- Pull requests use same format as issues (`#123`) -- Automatic 7-character SHA shortening for commits -- Reference: [GitHub Autolinked references](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/autolinked-references-and-urls) - -#### GitLab Features - -- Merge requests use `!123` format (distinct from issues) -- Issues use `#123` format -- URLs include `/-/` in the path structure -- Full SHA support with automatic shortening -- Reference: [GitLab Flavored Markdown](https://docs.gitlab.com/ee/user/markdown.html#gitlab-specific-references) - -#### Codeberg Features (Forgejo) - -- Issues and pull requests both use `#123` format -- Follows Forgejo/Gitea conventions -- Automatic reference linking in comments -- Reference: [Codeberg Documentation](https://docs.codeberg.org/) - -#### Gitea Features - -- Issues use `#123` format -- Pull requests use `#123` format (same as issues) -- Supports both internal and external issue trackers -- Actionable references (closes, fixes, etc.) -- Reference: [Gitea Documentation](https://docs.gitea.com/usage/issues-prs/automatically-linked-references) - -#### Bitbucket Features - -- Issues require `issue #123` format (with "issue" keyword) -- Pull requests require `pull request #456` format (with "pull request" keyword) -- Cross-repository references: `issue workspace/repo#123` or `pull request workspace/repo#456` -- Pull request URLs use `/pull-requests/` path -- Workspace-based repository structure -- Follows [official Bitbucket markup syntax](https://support.atlassian.com/bitbucket-cloud/docs/markup-comments/) +Badges are always visible, accessible, styled with Bootstrap, and include tooltips. In non-HTML formats, platform names appear in parentheses (e.g., `#123 (GitHub)`). ## Custom Platforms @@ -277,6 +220,7 @@ platforms: user: '@([%w%-%.]+)' url_formats: issue: '/{repo}/issues/{number}' + merge_request: '/{repo}/pull/{number}' pull: '/{repo}/pulls/{number}' commit: '/{repo}/commit/{sha}' user: '/{username}' @@ -292,6 +236,170 @@ extensions: repository-name: owner/repo ``` +### Platform Configuration Schema Reference + +Every platform configuration must follow this schema for validation and proper functionality. + +#### Platform Configuration Structure + +```yaml +platforms: + platform_name: + default_url: string # Required: Base URL for the platform + patterns: + issue: [string, ...] # Required: Lua regex patterns for issues + merge_request: [string, ...] # Required: Lua regex patterns for merge requests/PRs + commit: [string, ...] # Required: Lua regex patterns for commits + user: string # Required: Lua regex pattern for user mentions + url_formats: + issue: string # Required: URL template for issues + pull: string # Required: URL template for pull requests + commit: string # Required: URL template for commits + user: string # Required: URL template for user profiles + merge_request: string # Required: URL template for merge requests +``` + +#### Field Descriptions + +**default_url** (required, string): + +- The base URL of the Git hosting platform. +- Must start with `http://` or `https://`. +- Example: `https://git.example.com` + +**patterns** (required, object): + +- Regular expressions for matching references. +- Uses Lua regex syntax. +- Must contain four pattern types. + +**patterns.issue** (required, array of strings): + +- Lua regex patterns for matching issue references. +- Should have 1-2 patterns (single issue, cross-repository issue). +- Example: `['#(%d+)', '([^/]+/[^/#]+)#(%d+)']` + +**patterns.merge_request** (required, array of strings): + +- Lua regex patterns for matching merge request/pull request references. +- Should have 1-2 patterns (similar to issue patterns). +- Example: `['!(%d+)', '([^/]+/[^/#]+)!(%d+)']` + +**patterns.commit** (required, array of strings): + +- Lua regex patterns for matching commit references. +- Should have 2-3 patterns (SHA, cross-repository, user@SHA). +- Example: `['^(%x+)$', '([^/]+/[^/@]+)@(%x+)', '(%w+)@(%x+)']` + +**patterns.user** (required, string): + +- Single Lua regex pattern for matching user mentions. +- Typically starts with `@`. +- Example: `'@([%w%-%.]+)'` + +**url_formats** (required, object): + +- URL templates for generating links. +- Must contain five format types. + +**url_formats.issue** (required, string): + +- Template for issue URLs. +- Placeholders: `{repo}` (repository), `{number}` (issue number). +- Example: `'/{repo}/issues/{number}'` + +**url_formats.pull** (required, string): + +- Template for pull request URLs. +- Placeholders: `{repo}`, `{number}`. +- Example: `'/{repo}/pull/{number}'` + +**url_formats.merge_request** (required, string): + +- Template for merge request URLs. +- Placeholders: `{repo}`, `{number}`. +- Example: `'/{repo}/-/merge_requests/{number}'` + +**url_formats.commit** (required, string): + +- Template for commit URLs. +- Placeholders: `{repo}`, `{sha}` (commit hash). +- Example: `'/{repo}/commit/{sha}'` + +**url_formats.user** (required, string): + +- Template for user profile URLs. +- Placeholder: `{username}`. +- Example: `'/{username}'` + +#### Lua Regex Pattern Guide + +Common patterns used in Gitlink configurations: + +| Pattern | Matches | Example | +| ---------------------- | ------------------------- | ----------------- | +| `#(%d+)` | Issue with number | `#123` | +| `!(%d+)` | Merge request with number | `!456` | +| `(%x+)` | Hexadecimal string (SHA) | `a5c3785d9` | +| `@([%w%-%.]+)` | User mention | `@username` | +| `([^/]+/[^/#]+)#(%d+)` | Cross-repo issue | `owner/repo#123` | +| `^(%x+)$` | Full commit SHA | `abc123def` | +| `(%w+)@(%x+)` | User with commit | `username@abc123` | + +#### Validation Rules + +Platform configurations are automatically validated for: + +1. **Required fields**: `default_url`, `patterns`, `url_formats` must all exist. +2. **Pattern syntax**: All regex patterns are checked for valid Lua regex syntax. +3. **URL format syntax**: URL templates must start with `/` and contain at least one placeholder. +4. **Field completeness**: All required pattern and format types must be defined. +5. **Type correctness**: Patterns must be arrays, URL formats must be strings. + +#### Validation Errors + +If your platform configuration is invalid, you will see detailed error messages such as: + +- `Missing required field: "patterns"` - The patterns object is missing. +- `Invalid Lua regex in issue[1]: ... bad escape ...` - Pattern has invalid regex syntax. +- `Missing required pattern type: "commit"` - A required pattern type is missing. +- `Missing required URL format: "pull"` - A required URL format is missing. +- `Invalid url_formats.issue: URL format must contain at least one placeholder` - Template missing placeholders. + +#### Example: Complete Gitea Platform + +```yaml +platforms: + gitea: + default_url: https://gitea.io + patterns: + issue: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + merge_request: + - '#(%d+)' + - '([^/]+/[^/#]+)#(%d+)' + commit: + - '^(%x+)$' + - '([^/]+/[^/@]+)@(%x+)' + - '(%w+)@(%x+)' + user: '@([%w%-%.]+)' + url_formats: + issue: '/{repo}/issues/{number}' + pull: '/{repo}/pulls/{number}' + merge_request: '/{repo}/pulls/{number}' + commit: '/{repo}/commit/{sha}' + user: '/{username}' +``` + +#### Testing Custom Platforms + +After creating a custom platform YAML file, you can validate it by: + +1. Using the Gitlink extension with `custom-platforms-file` option. +2. Checking the Quarto output for validation errors. +3. Creating a test document and running `quarto render`. + ### Contributing New Platforms To add a new platform to the built-in configuration: diff --git a/_extensions/gitlink/_modules/platforms.lua b/_extensions/gitlink/_modules/platforms.lua index 2fcd2e0..d9be9f5 100644 --- a/_extensions/gitlink/_modules/platforms.lua +++ b/_extensions/gitlink/_modules/platforms.lua @@ -30,6 +30,9 @@ local platforms_module = {} +-- Load schema validation module +local schema = require(quarto.utils.resolve_path('_modules/schema.lua'):gsub('%.lua$', '')) + -- ============================================================================ -- CONFIGURATION STORAGE -- ============================================================================ @@ -40,6 +43,9 @@ 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 -- ============================================================================ @@ -112,12 +118,13 @@ local function load_platforms(yaml_path) end --- Initialise platform configurations +--- Loads platforms from YAML and validates them --- @param yaml_path string|nil Optional path to custom YAML file ---- @return boolean True if initialisation was successful, false otherwise ---- @usage platforms_module.initialise('custom-platforms.yml') +--- @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 + return true, nil end local loaded_configs = load_platforms(yaml_path) @@ -126,18 +133,41 @@ function platforms_module.initialise(yaml_path) if not platform_configs then platform_configs = {} end - return false + local msg = yaml_path and ('Failed to load platforms from ' .. yaml_path) or 'Failed to load built-in platforms' + return false, msg + end + + -- Validate all loaded platforms + local validation_results_all = 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 + return true, nil end -- ============================================================================ @@ -195,38 +225,44 @@ function platforms_module.get_all_platform_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 True if registration was successful, false otherwise ---- @usage platforms_module.register_custom_platform('forgejo', {...}) +--- @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 + return false, 'Platform name and configuration are required' end - -- Validate required fields - if not config.default_url then - return false - end + -- Validate configuration against schema + local result = schema.validate_platform(platform_name, config) - if not config.patterns or not config.url_formats then - return false + 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 + 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 True if registration was successful, false otherwise ---- @usage platforms_module.load_custom_platform_from_yaml(yaml_str, 'forgejo') +--- @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 + return false, 'YAML string and platform name are required' end local success, result = pcall(function() @@ -237,11 +273,15 @@ function platforms_module.load_custom_platform_from_yaml(yaml_string, platform_n return nil end) - if success and result then - return platforms_module.register_custom_platform(platform_name, result) + if not success then + return false, 'Failed to parse YAML: ' .. tostring(result) end - return false + 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 @@ -260,6 +300,37 @@ function platforms_module.is_platform_available(platform_name) 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 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 schema.validate_all_platforms(platforms) +end + -- ============================================================================ -- MODULE EXPORT -- ============================================================================ diff --git a/_extensions/gitlink/_modules/schema.lua b/_extensions/gitlink/_modules/schema.lua new file mode 100644 index 0000000..34b029e --- /dev/null +++ b/_extensions/gitlink/_modules/schema.lua @@ -0,0 +1,437 @@ +--[[ +# MIT License +# +# Copyright (c) 2025 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. +]] + +--- Schema Validation Module +--- Provides comprehensive schema validation for platform configurations +--- @module schema +--- @author Mickaël Canouil +--- @version 1.0.0 + +local schema_module = {} + +-- ============================================================================ +-- 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 +-- ============================================================================ + +--- 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 .. ': [\'pattern1\', \'pattern2\']' + add_error(result, string.format('Missing required pattern type: "%s" (%s)', pattern_type, hint)) + return + end + + -- User patterns can be a single string + if pattern_type == 'user' and is_string(patterns) then + 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)) + 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, 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)) + 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, i, err)) + 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 + + -- Warn about unknown pattern types + 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)) + 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, 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, err)) + end + end + end + + -- Warn about unknown format types + 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)) + 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 = schema_module.validate_platform('github', config) +--- if not result.valid then +--- for _, err in ipairs(result.errors) do +--- print('ERROR: ' .. err) +--- end +--- end +function schema_module.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 + + -- Validate default_url + if not config.default_url then + add_error(result, 'Missing required field: "default_url" (e.g., https://github.com)') + else + local valid, err = is_valid_base_url(config.default_url) + if not valid then + add_error(result, string.format('Invalid default_url: %s (e.g., https://git.example.com)', err)) + end + end + + -- Validate patterns section + validate_patterns_section(config.patterns, result) + + -- Validate url_formats section + 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 = schema_module.validate_all_platforms(platforms_config) +function schema_module.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] = schema_module.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 = schema_module.format_result(result) +--- print(msg) +function schema_module.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 = schema_module.get_summary(result) +function schema_module.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 schema_module diff --git a/_extensions/gitlink/gitlink.lua b/_extensions/gitlink/gitlink.lua index b807d01..27fdf54 100644 --- a/_extensions/gitlink/gitlink.lua +++ b/_extensions/gitlink/gitlink.lua @@ -138,9 +138,20 @@ local function get_repository(meta) if not utils.is_empty(meta_custom_platforms) then local custom_file_path = quarto.utils.resolve_path(meta_custom_platforms --[[@as string]]) - platforms.initialise(custom_file_path) + local ok, err = platforms.initialise(custom_file_path) + if not ok then + utils.log_error( + EXTENSION_NAME, + "Failed to load custom platforms from '" .. meta_custom_platforms .. "':\n" .. (err or 'unknown error') + ) + return meta + end else - platforms.initialise() + local ok, err = platforms.initialise() + if not ok then + utils.log_error(EXTENSION_NAME, "Failed to load built-in platforms:\n" .. (err or 'unknown error')) + return meta + end end if not utils.is_empty(meta_platform) then diff --git a/_extensions/gitlink/platforms.yml b/_extensions/gitlink/platforms.yml index 9a36f04..3f54b19 100644 --- a/_extensions/gitlink/platforms.yml +++ b/_extensions/gitlink/platforms.yml @@ -20,6 +20,7 @@ platforms: user: '@([%w%-%.]+)' url_formats: issue: '/{repo}/issues/{number}' + merge_request: '/{repo}/pull/{number}' pull: '/{repo}/pull/{number}' commit: '/{repo}/commit/{sha}' user: '/{username}' @@ -41,6 +42,7 @@ platforms: url_formats: issue: '/{repo}/-/issues/{number}' merge_request: '/{repo}/-/merge_requests/{number}' + pull: '/{repo}/-/merge_requests/{number}' commit: '/{repo}/-/commit/{sha}' user: '/{username}' @@ -60,6 +62,7 @@ platforms: user: '@([%w%-%.]+)' url_formats: issue: '/{repo}/issues/{number}' + merge_request: '/{repo}/pulls/{number}' pull: '/{repo}/pulls/{number}' commit: '/{repo}/commit/{sha}' user: '/{username}' @@ -80,6 +83,7 @@ platforms: user: '@([%w%-%.]+)' url_formats: issue: '/{repo}/issues/{number}' + merge_request: '/{repo}/pulls/{number}' pull: '/{repo}/pulls/{number}' commit: '/{repo}/commit/{sha}' user: '/{username}' @@ -100,6 +104,7 @@ platforms: user: '@([%w%-%.]+)' url_formats: issue: '/{repo}/issues/{number}' + merge_request: '/{repo}/pull-requests/{number}' pull: '/{repo}/pull-requests/{number}' commit: '/{repo}/commits/{sha}' user: '/{username}' diff --git a/example.qmd b/example.qmd index 078bba7..82048c1 100644 --- a/example.qmd +++ b/example.qmd @@ -334,6 +334,7 @@ platforms: user: '@([%w%-%.]+)' url_formats: issue: '/{repo}/issues/{number}' + merge_request: '/{repo}/pull/{number}' pull: '/{repo}/pulls/{number}' commit: '/{repo}/commit/{sha}' user: '/{username}' From 1d814490239fecbbeac418a7e896241b81d7c8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:47:28 +0100 Subject: [PATCH 04/15] fix: path should not be resolved from extension root --- _extensions/gitlink/gitlink.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_extensions/gitlink/gitlink.lua b/_extensions/gitlink/gitlink.lua index 27fdf54..58732d5 100644 --- a/_extensions/gitlink/gitlink.lua +++ b/_extensions/gitlink/gitlink.lua @@ -137,7 +137,7 @@ local function get_repository(meta) local meta_custom_platforms = utils.get_metadata_value(meta, 'gitlink', 'custom-platforms-file') if not utils.is_empty(meta_custom_platforms) then - local custom_file_path = quarto.utils.resolve_path(meta_custom_platforms --[[@as string]]) + local custom_file_path = meta_custom_platforms --[[@as string]] local ok, err = platforms.initialise(custom_file_path) if not ok then utils.log_error( From 5afa5b37a92750bdd30b9badd7cc1d4ac3754d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:49:59 +0100 Subject: [PATCH 05/15] refactor: reorder sections --- example.qmd | 66 ++++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/example.qmd b/example.qmd index 82048c1..316707f 100644 --- a/example.qmd +++ b/example.qmd @@ -308,6 +308,39 @@ If not provided, the extension will attempt to detect it from the Git remote: git remote get-url origin ``` +### Platform Badges + +In HTML output, Gitlink displays subtle platform badges next to links for improved accessibility and quick visual identification. + +**Features**: + +- Always visible badges showing the full platform name (GitHub, GitLab, etc.). +- Tooltips on both links and badges. +- Accessible to screen readers with proper ARIA labels. +- Theme-aware styling using Bootstrap classes. + +**Configuration**: + +```yaml +extensions: + gitlink: + show-platform-badge: true # Show/hide badges (default: true) + badge-position: "after" # "after" or "before" link (default: "after") +``` + +**Options**: + +- `show-platform-badge` (boolean): Toggle badge visibility, default `true`. +- `badge-position` (string): Badge placement relative to link, default `"after"`. + +To disable badges and use only tooltips: + +```yaml +extensions: + gitlink: + show-platform-badge: false +``` + ### Custom Platforms You can add support for additional platforms or customise existing ones by providing a custom platforms configuration file. @@ -364,36 +397,3 @@ To contribute a new platform to the built-in configuration: 4. Submit a pull request with your changes. This makes it easy for the community to add support for new Git hosting platforms without modifying the main Lua code. - -### Platform Badges - -In HTML output, Gitlink displays subtle platform badges next to links for improved accessibility and quick visual identification. - -**Features**: - -- Always visible badges showing the full platform name (GitHub, GitLab, etc.). -- Tooltips on both links and badges. -- Accessible to screen readers with proper ARIA labels. -- Theme-aware styling using Bootstrap classes. - -**Configuration**: - -```yaml -extensions: - gitlink: - show-platform-badge: true # Show/hide badges (default: true) - badge-position: "after" # "after" or "before" link (default: "after") -``` - -**Options**: - -- `show-platform-badge` (boolean): Toggle badge visibility, default `true`. -- `badge-position` (string): Badge placement relative to link, default `"after"`. - -To disable badges and use only tooltips: - -```yaml -extensions: - gitlink: - show-platform-badge: false -``` From 8d0cf71353b2b5c4c6694c53d5da2655ef2b2ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:51:30 +0100 Subject: [PATCH 06/15] chore: remove redundant explanation for community contributions --- example.qmd | 2 -- 1 file changed, 2 deletions(-) diff --git a/example.qmd b/example.qmd index 316707f..71b0a38 100644 --- a/example.qmd +++ b/example.qmd @@ -395,5 +395,3 @@ To contribute a new platform to the built-in configuration: 2. Edit [`_extensions/gitlink/platforms.yml`](_extensions/gitlink/platforms.yml) to add your platform definition. 3. Test your configuration with a custom platforms file first. 4. Submit a pull request with your changes. - -This makes it easy for the community to add support for new Git hosting platforms without modifying the main Lua code. From da12f535e7cc4518f8165ab9748d1d42c4f4ff55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Wed, 3 Dec 2025 23:03:53 +0100 Subject: [PATCH 07/15] fix: lua warning about value that cannot be discarded --- _extensions/gitlink/_modules/schema.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_extensions/gitlink/_modules/schema.lua b/_extensions/gitlink/_modules/schema.lua index 34b029e..0145134 100644 --- a/_extensions/gitlink/_modules/schema.lua +++ b/_extensions/gitlink/_modules/schema.lua @@ -139,7 +139,7 @@ local function is_valid_lua_pattern(pattern) end local success, err = pcall(function() - string.find('test', pattern) + _ = string.find('test', pattern) end) if not success then From e2e9d9a320cd369e023606f940200f6eef8f7dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Wed, 3 Dec 2025 23:14:38 +0100 Subject: [PATCH 08/15] refactor: move hardcoded platform names to platforms module/config file --- _extensions/gitlink/_modules/platforms.lua | 20 +++++++--- _extensions/gitlink/_modules/schema.lua | 16 +++----- _extensions/gitlink/gitlink.lua | 16 +------- _extensions/gitlink/platforms.yml | 45 ++++++++++++---------- 4 files changed, 45 insertions(+), 52 deletions(-) diff --git a/_extensions/gitlink/_modules/platforms.lua b/_extensions/gitlink/_modules/platforms.lua index d9be9f5..357ccf8 100644 --- a/_extensions/gitlink/_modules/platforms.lua +++ b/_extensions/gitlink/_modules/platforms.lua @@ -93,7 +93,6 @@ end local function load_platforms(yaml_path) local config_path = yaml_path or quarto.utils.resolve_path('platforms.yml') - -- Check if file exists local file = io.open(config_path, 'r') if not file then return nil @@ -101,7 +100,6 @@ local function load_platforms(yaml_path) local content = file:read('*all') file:close() - -- Parse YAML using Pandoc local success, result = pcall(function() local meta = pandoc.read('---\n' .. content .. '\n---', 'markdown').meta if meta and meta.platforms then @@ -137,7 +135,6 @@ function platforms_module.initialise(yaml_path) return false, msg end - -- Validate all loaded platforms local validation_results_all = schema.validate_all_platforms(loaded_configs) local has_errors = false local error_messages = {} @@ -196,6 +193,20 @@ function platforms_module.get_platform_config(platform_name) 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() @@ -206,14 +217,12 @@ function platforms_module.get_all_platform_names() local names = {} - -- Add loaded platform names if platform_configs then for name, _ in pairs(platform_configs) do table.insert(names, name) end end - -- Add custom platform names for name, _ in pairs(custom_platforms) do if not platform_configs or not platform_configs[name] then table.insert(names, name) @@ -235,7 +244,6 @@ function platforms_module.register_custom_platform(platform_name, config) return false, 'Platform name and configuration are required' end - -- Validate configuration against schema local result = schema.validate_platform(platform_name, config) if not result.valid then diff --git a/_extensions/gitlink/_modules/schema.lua b/_extensions/gitlink/_modules/schema.lua index 0145134..a416bfe 100644 --- a/_extensions/gitlink/_modules/schema.lua +++ b/_extensions/gitlink/_modules/schema.lua @@ -194,12 +194,11 @@ end --- @return nil local function validate_patterns(patterns, pattern_type, result) if not patterns then - local hint = 'Expected: patterns:\n ' .. pattern_type .. ': [\'pattern1\', \'pattern2\']' - add_error(result, string.format('Missing required pattern type: "%s" (%s)', pattern_type, hint)) + 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 - -- User patterns can be a single string if pattern_type == 'user' and is_string(patterns) then local valid, err = is_valid_lua_pattern(patterns) if not valid then @@ -211,20 +210,20 @@ local function validate_patterns(patterns, pattern_type, result) 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, type(patterns)) + 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)) + 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, i, err)) + add_error(result, string.format('Invalid Lua regex in %s[%d]: %s', pattern_type:gsub('_', '-'), i, err)) end end end @@ -248,7 +247,6 @@ local function validate_patterns_section(patterns, result) validate_patterns(patterns[pattern_type], pattern_type, result) end - -- Warn about unknown pattern types for key, _ in pairs(patterns) do local found = false for _, pattern_type in ipairs(REQUIRED_PATTERN_TYPES) do @@ -295,7 +293,6 @@ local function validate_url_formats_section(url_formats, result) end end - -- Warn about unknown format types for key, _ in pairs(url_formats) do local found = false for _, format_type in ipairs(REQUIRED_URL_FORMAT_TYPES) do @@ -339,7 +336,6 @@ function schema_module.validate_platform(platform_name, config) return result end - -- Validate default_url if not config.default_url then add_error(result, 'Missing required field: "default_url" (e.g., https://github.com)') else @@ -349,10 +345,8 @@ function schema_module.validate_platform(platform_name, config) end end - -- Validate patterns section validate_patterns_section(config.patterns, result) - -- Validate url_formats section validate_url_formats_section(config.url_formats, result) return result diff --git a/_extensions/gitlink/gitlink.lua b/_extensions/gitlink/gitlink.lua index 58732d5..cd6002a 100644 --- a/_extensions/gitlink/gitlink.lua +++ b/_extensions/gitlink/gitlink.lua @@ -75,15 +75,7 @@ local function create_platform_link(text, uri, platform_name) return nil end - local platform_names = { - github = "GitHub", - gitlab = "GitLab", - codeberg = "Codeberg", - gitea = "Gitea", - bitbucket = "Bitbucket" - } - local platform_label = platform_names[(platform_name --[[@as string]]):lower()] or - (platform_name --[[@as string]]):sub(1, 1):upper() .. (platform_name --[[@as string]]):sub(2) + 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('', {}, {}) @@ -182,7 +174,6 @@ local function get_repository(meta) repository_name = meta_repository - -- Read badge configuration local show_badge_meta = utils.get_metadata_value(meta, 'gitlink', 'show-platform-badge') if show_badge_meta ~= nil then show_platform_badge = (show_badge_meta == "true" or show_badge_meta == true) @@ -295,7 +286,6 @@ local function process_issues_and_mrs(elem, current_platform, current_base_url) end if not number then - -- Try to match URLs from any supported platform local all_platform_names = platforms.get_all_platform_names() for _, platform_name in ipairs(all_platform_names) do local platform_config = platforms.get_platform_config(platform_name) @@ -539,17 +529,13 @@ end --- @param elem pandoc.Link The link element to process --- @return pandoc.Link The original or modified link local function process_link(elem) - -- Only process links where the text is the same as the URL (auto-generated links) local link_text = pandoc.utils.stringify(elem.content) local link_target = elem.target - -- If the link text equals the target URL, try to shorten it if link_text == link_target then - -- Create a temporary Str element to use existing processing logic local temp_str = pandoc.Str(link_text) local result = process_gitlink(temp_str) - -- If process_gitlink returned a link, use its content as the new link text if pandoc.utils.type(result) == "Link" then return result end diff --git a/_extensions/gitlink/platforms.yml b/_extensions/gitlink/platforms.yml index 3f54b19..819e47c 100644 --- a/_extensions/gitlink/platforms.yml +++ b/_extensions/gitlink/platforms.yml @@ -4,13 +4,14 @@ platforms: github: - default_url: https://github.com + display-name: GitHub + default-url: https://github.com patterns: issue: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' - 'GH%-(%d+)' - merge_request: + merge-request: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' commit: @@ -18,20 +19,21 @@ platforms: - '([^/]+/[^/@]+)@(%x+)' - '(%w+)@(%x+)' user: '@([%w%-%.]+)' - url_formats: + url-formats: issue: '/{repo}/issues/{number}' - merge_request: '/{repo}/pull/{number}' + merge-request: '/{repo}/pull/{number}' pull: '/{repo}/pull/{number}' commit: '/{repo}/commit/{sha}' user: '/{username}' gitlab: - default_url: https://gitlab.com + display-name: GitLab + default-url: https://gitlab.com patterns: issue: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' - merge_request: + merge-request: - '!(%d+)' - '([^/]+/[^/#]+)!(%d+)' commit: @@ -39,20 +41,21 @@ platforms: - '([^/]+/[^/@]+)@(%x+)' - '(%w+)@(%x+)' user: '@([%w%-%.]+)' - url_formats: + url-formats: issue: '/{repo}/-/issues/{number}' - merge_request: '/{repo}/-/merge_requests/{number}' + merge-request: '/{repo}/-/merge_requests/{number}' pull: '/{repo}/-/merge_requests/{number}' commit: '/{repo}/-/commit/{sha}' user: '/{username}' codeberg: - default_url: https://codeberg.org + display-name: Codeberg + default-url: https://codeberg.org patterns: issue: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' - merge_request: + merge-request: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' commit: @@ -60,20 +63,21 @@ platforms: - '([^/]+/[^/@]+)@(%x+)' - '(%w+)@(%x+)' user: '@([%w%-%.]+)' - url_formats: + url-formats: issue: '/{repo}/issues/{number}' - merge_request: '/{repo}/pulls/{number}' + merge-request: '/{repo}/pulls/{number}' pull: '/{repo}/pulls/{number}' commit: '/{repo}/commit/{sha}' user: '/{username}' gitea: - default_url: https://gitea.com + display-name: Gitea + default-url: https://gitea.com patterns: issue: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' - merge_request: + merge-request: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' commit: @@ -81,20 +85,21 @@ platforms: - '([^/]+/[^/@]+)@(%x+)' - '(%w+)@(%x+)' user: '@([%w%-%.]+)' - url_formats: + url-formats: issue: '/{repo}/issues/{number}' - merge_request: '/{repo}/pulls/{number}' + merge-request: '/{repo}/pulls/{number}' pull: '/{repo}/pulls/{number}' commit: '/{repo}/commit/{sha}' user: '/{username}' bitbucket: - default_url: https://bitbucket.org + display-name: Bitbucket + default-url: https://bitbucket.org patterns: issue: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' - merge_request: + merge-request: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' commit: @@ -102,9 +107,9 @@ platforms: - '([^/]+/[^/@]+)@(%x+)' - '(%w+)@(%x+)' user: '@([%w%-%.]+)' - url_formats: + url-formats: issue: '/{repo}/issues/{number}' - merge_request: '/{repo}/pull-requests/{number}' + merge-request: '/{repo}/pull-requests/{number}' pull: '/{repo}/pull-requests/{number}' commit: '/{repo}/commits/{sha}' user: '/{username}' From 201e75900dd570305cdb674186abcc0eaba73ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Wed, 3 Dec 2025 23:20:30 +0100 Subject: [PATCH 09/15] refactor: use "-" instead of "_" in platform config keys for consistency with Quarto --- README.md | 44 +++++++++++----------- _extensions/gitlink/_modules/platforms.lua | 3 +- _extensions/gitlink/_modules/schema.lua | 22 +++++------ _extensions/gitlink/gitlink.lua | 8 ++-- example.qmd | 8 ++-- 5 files changed, 43 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 7064101..d060dce 100644 --- a/README.md +++ b/README.md @@ -205,12 +205,12 @@ Create a YAML file (e.g., `my-platforms.yml`): ```yaml platforms: gitplatform: - default_url: https://git.example.com + default-url: https://git.example.com patterns: issue: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' - merge_request: + merge-request: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' commit: @@ -218,9 +218,9 @@ platforms: - '([^/]+/[^/@]+)@(%x+)' - '(%w+)@(%x+)' user: '@([%w%-%.]+)' - url_formats: + url-formats: issue: '/{repo}/issues/{number}' - merge_request: '/{repo}/pull/{number}' + merge-request: '/{repo}/pull/{number}' pull: '/{repo}/pulls/{number}' commit: '/{repo}/commit/{sha}' user: '/{username}' @@ -245,23 +245,23 @@ Every platform configuration must follow this schema for validation and proper f ```yaml platforms: platform_name: - default_url: string # Required: Base URL for the platform + default-url: string # Required: Base URL for the platform patterns: issue: [string, ...] # Required: Lua regex patterns for issues - merge_request: [string, ...] # Required: Lua regex patterns for merge requests/PRs + merge-request: [string, ...] # Required: Lua regex patterns for merge requests/PRs commit: [string, ...] # Required: Lua regex patterns for commits user: string # Required: Lua regex pattern for user mentions - url_formats: + url-formats: issue: string # Required: URL template for issues pull: string # Required: URL template for pull requests commit: string # Required: URL template for commits user: string # Required: URL template for user profiles - merge_request: string # Required: URL template for merge requests + merge-request: string # Required: URL template for merge requests ``` #### Field Descriptions -**default_url** (required, string): +**default-url** (required, string): - The base URL of the Git hosting platform. - Must start with `http://` or `https://`. @@ -279,7 +279,7 @@ platforms: - Should have 1-2 patterns (single issue, cross-repository issue). - Example: `['#(%d+)', '([^/]+/[^/#]+)#(%d+)']` -**patterns.merge_request** (required, array of strings): +**patterns.merge-request** (required, array of strings): - Lua regex patterns for matching merge request/pull request references. - Should have 1-2 patterns (similar to issue patterns). @@ -297,36 +297,36 @@ platforms: - Typically starts with `@`. - Example: `'@([%w%-%.]+)'` -**url_formats** (required, object): +**url-formats** (required, object): - URL templates for generating links. - Must contain five format types. -**url_formats.issue** (required, string): +**url-formats.issue** (required, string): - Template for issue URLs. - Placeholders: `{repo}` (repository), `{number}` (issue number). - Example: `'/{repo}/issues/{number}'` -**url_formats.pull** (required, string): +**url-formats.pull** (required, string): - Template for pull request URLs. - Placeholders: `{repo}`, `{number}`. - Example: `'/{repo}/pull/{number}'` -**url_formats.merge_request** (required, string): +**url-formats.merge-request** (required, string): - Template for merge request URLs. - Placeholders: `{repo}`, `{number}`. - Example: `'/{repo}/-/merge_requests/{number}'` -**url_formats.commit** (required, string): +**url-formats.commit** (required, string): - Template for commit URLs. - Placeholders: `{repo}`, `{sha}` (commit hash). - Example: `'/{repo}/commit/{sha}'` -**url_formats.user** (required, string): +**url-formats.user** (required, string): - Template for user profile URLs. - Placeholder: `{username}`. @@ -350,7 +350,7 @@ Common patterns used in Gitlink configurations: Platform configurations are automatically validated for: -1. **Required fields**: `default_url`, `patterns`, `url_formats` must all exist. +1. **Required fields**: `default-url`, `patterns`, `url-formats` must all exist. 2. **Pattern syntax**: All regex patterns are checked for valid Lua regex syntax. 3. **URL format syntax**: URL templates must start with `/` and contain at least one placeholder. 4. **Field completeness**: All required pattern and format types must be defined. @@ -364,19 +364,19 @@ If your platform configuration is invalid, you will see detailed error messages - `Invalid Lua regex in issue[1]: ... bad escape ...` - Pattern has invalid regex syntax. - `Missing required pattern type: "commit"` - A required pattern type is missing. - `Missing required URL format: "pull"` - A required URL format is missing. -- `Invalid url_formats.issue: URL format must contain at least one placeholder` - Template missing placeholders. +- `Invalid url-formats.issue: URL format must contain at least one placeholder` - Template missing placeholders. #### Example: Complete Gitea Platform ```yaml platforms: gitea: - default_url: https://gitea.io + default-url: https://gitea.io patterns: issue: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' - merge_request: + merge-request: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' commit: @@ -384,10 +384,10 @@ platforms: - '([^/]+/[^/@]+)@(%x+)' - '(%w+)@(%x+)' user: '@([%w%-%.]+)' - url_formats: + url-formats: issue: '/{repo}/issues/{number}' pull: '/{repo}/pulls/{number}' - merge_request: '/{repo}/pulls/{number}' + merge-request: '/{repo}/pulls/{number}' commit: '/{repo}/commit/{sha}' user: '/{username}' ``` diff --git a/_extensions/gitlink/_modules/platforms.lua b/_extensions/gitlink/_modules/platforms.lua index 357ccf8..8252acb 100644 --- a/_extensions/gitlink/_modules/platforms.lua +++ b/_extensions/gitlink/_modules/platforms.lua @@ -74,7 +74,8 @@ local function convert_yaml_value(yaml_value) elseif type(yaml_value) == 'table' then local result = {} for key, value in pairs(yaml_value) do - result[key] = convert_yaml_value(value) + local converted_key = key:gsub('-', '_') + result[converted_key] = convert_yaml_value(value) end return result else diff --git a/_extensions/gitlink/_modules/schema.lua b/_extensions/gitlink/_modules/schema.lua index a416bfe..a4d53c3 100644 --- a/_extensions/gitlink/_modules/schema.lua +++ b/_extensions/gitlink/_modules/schema.lua @@ -234,7 +234,7 @@ end --- @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)') + add_error(result, 'Missing required field: "patterns" (add patterns section with: issue, merge-request, commit, user)') return end @@ -256,23 +256,23 @@ local function validate_patterns_section(patterns, result) end end if not found then - add_warning(result, string.format('Unknown pattern type: "%s" (not recognised)', key)) + 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 +--- 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)') + 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))) + add_error(result, string.format('Field "url-formats" must be a table, got %s', type(url_formats))) return end @@ -284,11 +284,11 @@ local function validate_url_formats_section(url_formats, result) 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, hint)) + 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, err)) + add_error(result, string.format('Invalid url-formats.%s: %s', format_type:gsub('_', '-'), err)) end end end @@ -302,7 +302,7 @@ local function validate_url_formats_section(url_formats, result) end end if not found then - add_warning(result, string.format('Unknown URL format type: "%s" (not recognised)', key)) + add_warning(result, string.format('Unknown URL format type: "%s" (not recognised)', key:gsub('_', '-'))) end end end @@ -337,11 +337,11 @@ function schema_module.validate_platform(platform_name, config) end if not config.default_url then - add_error(result, 'Missing required field: "default_url" (e.g., https://github.com)') + add_error(result, 'Missing required field: "default-url" (e.g., https://github.com)') else local valid, err = is_valid_base_url(config.default_url) if not valid then - add_error(result, string.format('Invalid default_url: %s (e.g., https://git.example.com)', err)) + add_error(result, string.format('Invalid default-url: %s (e.g., https://git.example.com)', err)) end end diff --git a/_extensions/gitlink/gitlink.lua b/_extensions/gitlink/gitlink.lua index cd6002a..4607da8 100644 --- a/_extensions/gitlink/gitlink.lua +++ b/_extensions/gitlink/gitlink.lua @@ -117,11 +117,11 @@ local function create_platform_link(text, uri, platform_name) end end ---- Get repository name from metadata or git remote +--- 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) +--- 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) local meta_platform = utils.get_metadata_value(meta, 'gitlink', 'platform') local meta_base_url = utils.get_metadata_value(meta, 'gitlink', 'base-url') diff --git a/example.qmd b/example.qmd index 71b0a38..e7fa211 100644 --- a/example.qmd +++ b/example.qmd @@ -352,12 +352,12 @@ Create a YAML file (e.g., `custom-platforms.yml`) with your platform definitions ```yaml platforms: gitplatform: - default_url: https://git.example.com + default-url: https://git.example.com patterns: issue: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' - merge_request: + merge-request: - '#(%d+)' - '([^/]+/[^/#]+)#(%d+)' commit: @@ -365,9 +365,9 @@ platforms: - '([^/]+/[^/@]+)@(%x+)' - '(%w+)@(%x+)' user: '@([%w%-%.]+)' - url_formats: + url-formats: issue: '/{repo}/issues/{number}' - merge_request: '/{repo}/pull/{number}' + merge-request: '/{repo}/pull/{number}' pull: '/{repo}/pulls/{number}' commit: '/{repo}/commit/{sha}' user: '/{username}' From 83cdb8fa407ffd7ad27904374bf67c4c85b7c56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Wed, 3 Dec 2025 23:28:31 +0100 Subject: [PATCH 10/15] refactor: rename default to base for more clarity --- README.md | 12 +- _extensions/gitlink/_modules/bitbucket.lua | 32 ++--- _extensions/gitlink/_modules/schema.lua | 29 +++-- _extensions/gitlink/gitlink.lua | 8 +- _extensions/gitlink/platforms.yml | 142 ++++++++++----------- example.qmd | 2 +- 6 files changed, 115 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index d060dce..6551945 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ Create a YAML file (e.g., `my-platforms.yml`): ```yaml platforms: gitplatform: - default-url: https://git.example.com + base-url: https://git.example.com patterns: issue: - '#(%d+)' @@ -244,8 +244,8 @@ Every platform configuration must follow this schema for validation and proper f ```yaml platforms: - platform_name: - default-url: string # Required: Base URL for the platform + platform-name: + base-url: string # Required: Base URL for the platform patterns: issue: [string, ...] # Required: Lua regex patterns for issues merge-request: [string, ...] # Required: Lua regex patterns for merge requests/PRs @@ -261,7 +261,7 @@ platforms: #### Field Descriptions -**default-url** (required, string): +**base-url** (required, string): - The base URL of the Git hosting platform. - Must start with `http://` or `https://`. @@ -350,7 +350,7 @@ Common patterns used in Gitlink configurations: Platform configurations are automatically validated for: -1. **Required fields**: `default-url`, `patterns`, `url-formats` must all exist. +1. **Required fields**: `base-url`, `patterns`, `url-formats` must all exist. 2. **Pattern syntax**: All regex patterns are checked for valid Lua regex syntax. 3. **URL format syntax**: URL templates must start with `/` and contain at least one placeholder. 4. **Field completeness**: All required pattern and format types must be defined. @@ -371,7 +371,7 @@ If your platform configuration is invalid, you will see detailed error messages ```yaml platforms: gitea: - default-url: https://gitea.io + base-url: https://gitea.io patterns: issue: - '#(%d+)' diff --git a/_extensions/gitlink/_modules/bitbucket.lua b/_extensions/gitlink/_modules/bitbucket.lua index b4946b4..0e9d2ce 100644 --- a/_extensions/gitlink/_modules/bitbucket.lua +++ b/_extensions/gitlink/_modules/bitbucket.lua @@ -64,10 +64,10 @@ function bitbucket_module.process_inlines(inlines, base_url, repository_name, cr -- Try to match "issue #123" pattern if i + 2 <= #inlines then - local elem1, elem2, elem3 = inlines[i], inlines[i+1], inlines[i+2] + 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" and elem3.text:match("^#(%d+)$") then + elem2.t == "Space" and + elem3.t == "Str" and elem3.text:match("^#(%d+)$") then local number = elem3.text:match("^#(%d+)$") local uri if repository_name then @@ -86,10 +86,10 @@ function bitbucket_module.process_inlines(inlines, base_url, repository_name, cr -- 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] + 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" and elem3.text:match("^([^/]+/[^/#]+)#(%d+)$") then + elem2.t == "Space" and + elem3.t == "Str" and elem3.text:match("^([^/]+/[^/#]+)#(%d+)$") then local repo, number = elem3.text:match("^([^/]+/[^/#]+)#(%d+)$") local uri = base_url .. "/" .. repo .. "/issues/" .. number local link = create_bitbucket_link("issue " .. elem3.text, uri, create_link_fn) @@ -103,12 +103,12 @@ function bitbucket_module.process_inlines(inlines, base_url, repository_name, cr -- 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] + 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" and elem5.text:match("^#(%d+)$") then + elem2.t == "Space" and + elem3.t == "Str" and elem3.text == "request" and + elem4.t == "Space" and + elem5.t == "Str" and elem5.text:match("^#(%d+)$") then local number = elem5.text:match("^#(%d+)$") local uri if repository_name then @@ -127,12 +127,12 @@ function bitbucket_module.process_inlines(inlines, base_url, repository_name, cr -- 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] + 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" and elem5.text:match("^([^/]+/[^/#]+)#(%d+)$") then + elem2.t == "Space" and + elem3.t == "Str" and elem3.text == "request" and + elem4.t == "Space" and + elem5.t == "Str" and elem5.text:match("^([^/]+/[^/#]+)#(%d+)$") then local repo, number = elem5.text:match("^([^/]+/[^/#]+)#(%d+)$") local uri = base_url .. "/" .. repo .. "/pull-requests/" .. number local link = create_bitbucket_link("pull request " .. elem5.text, uri, create_link_fn) diff --git a/_extensions/gitlink/_modules/schema.lua b/_extensions/gitlink/_modules/schema.lua index a4d53c3..fa4d01d 100644 --- a/_extensions/gitlink/_modules/schema.lua +++ b/_extensions/gitlink/_modules/schema.lua @@ -210,13 +210,16 @@ local function validate_patterns(patterns, pattern_type, result) 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)) + 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('_', '-'))) + add_warning(result, + string.format('Pattern type "%s" is empty (add at least one pattern)', pattern_type:gsub('_', '-'))) return end @@ -234,7 +237,8 @@ end --- @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)') + add_error(result, + 'Missing required field: "patterns" (add patterns section with: issue, merge-request, commit, user)') return end @@ -267,7 +271,8 @@ end --- @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)') + add_error(result, + 'Missing required field: "url-formats" (add url-formats section with: issue, pull, merge-request, commit, user)') return end @@ -280,10 +285,10 @@ local function validate_url_formats_section(url_formats, result) 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}' + 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) @@ -336,12 +341,12 @@ function schema_module.validate_platform(platform_name, config) return result end - if not config.default_url then - add_error(result, 'Missing required field: "default-url" (e.g., https://github.com)') + 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.default_url) + local valid, err = is_valid_base_url(config.base_url) if not valid then - add_error(result, string.format('Invalid default-url: %s (e.g., https://git.example.com)', err)) + add_error(result, string.format('Invalid base-url: %s (e.g., https://git.example.com)', err)) end end diff --git a/_extensions/gitlink/gitlink.lua b/_extensions/gitlink/gitlink.lua index 4607da8..69f4598 100644 --- a/_extensions/gitlink/gitlink.lua +++ b/_extensions/gitlink/gitlink.lua @@ -165,7 +165,7 @@ local function get_repository(meta) if not utils.is_empty(meta_base_url) then base_url = meta_base_url --[[@as string]] else - base_url = config.default_url + base_url = config.base_url end if utils.is_empty(meta_repository) then @@ -290,7 +290,7 @@ local function process_issues_and_mrs(elem, current_platform, current_base_url) for _, platform_name in ipairs(all_platform_names) do local platform_config = platforms.get_platform_config(platform_name) if platform_config then - local platform_base_url = platform_config.default_url + local platform_base_url = platform_config.base_url local escaped_platform_url = utils.escape_pattern(platform_base_url) local url_pattern_issue = '^' .. escaped_platform_url .. '/([^/]+/[^/]+)/%-?/?issues?/(%d+)' local url_pattern_mr = '^' .. escaped_platform_url .. '/([^/]+/[^/]+)/%-?/?merge[_%-]requests/(%d+)' @@ -388,7 +388,7 @@ local function process_users(elem, current_platform) for _, platform_name in ipairs(all_platform_names) do local platform_config = platforms.get_platform_config(platform_name) if platform_config then - local platform_base_url = platform_config.default_url + local platform_base_url = platform_config.base_url local escaped_platform_url = utils.escape_pattern(platform_base_url) local url_pattern = '^' .. escaped_platform_url .. '/([%w%-%.]+)$' @@ -455,7 +455,7 @@ local function process_commits(elem, current_platform, current_base_url) for _, platform_name in ipairs(all_platform_names) do local platform_config = platforms.get_platform_config(platform_name) if platform_config then - local platform_base_url = platform_config.default_url + local platform_base_url = platform_config.base_url local escaped_platform_url = utils.escape_pattern(platform_base_url) local url_pattern = '^' .. escaped_platform_url .. '/([^/]+/[^/]+)/%-?/?commits?/(%x+)' if text:match(url_pattern) then diff --git a/_extensions/gitlink/platforms.yml b/_extensions/gitlink/platforms.yml index 819e47c..314336f 100644 --- a/_extensions/gitlink/platforms.yml +++ b/_extensions/gitlink/platforms.yml @@ -5,111 +5,111 @@ platforms: github: display-name: GitHub - default-url: https://github.com + base-url: https://github.com patterns: issue: - - '#(%d+)' - - '([^/]+/[^/#]+)#(%d+)' - - 'GH%-(%d+)' + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" + - "GH%-(%d+)" merge-request: - - '#(%d+)' - - '([^/]+/[^/#]+)#(%d+)' + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" commit: - - '^(%x+)$' - - '([^/]+/[^/@]+)@(%x+)' - - '(%w+)@(%x+)' - user: '@([%w%-%.]+)' + - "^(%x+)$" + - "([^/]+/[^/@]+)@(%x+)" + - "(%w+)@(%x+)" + user: "@([%w%-%.]+)" url-formats: - issue: '/{repo}/issues/{number}' - merge-request: '/{repo}/pull/{number}' - pull: '/{repo}/pull/{number}' - commit: '/{repo}/commit/{sha}' - user: '/{username}' + issue: "/{repo}/issues/{number}" + merge-request: "/{repo}/pull/{number}" + pull: "/{repo}/pull/{number}" + commit: "/{repo}/commit/{sha}" + user: "/{username}" gitlab: display-name: GitLab - default-url: https://gitlab.com + base-url: https://gitlab.com patterns: issue: - - '#(%d+)' - - '([^/]+/[^/#]+)#(%d+)' + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" merge-request: - - '!(%d+)' - - '([^/]+/[^/#]+)!(%d+)' + - "!(%d+)" + - "([^/]+/[^/#]+)!(%d+)" commit: - - '^(%x+)$' - - '([^/]+/[^/@]+)@(%x+)' - - '(%w+)@(%x+)' - user: '@([%w%-%.]+)' + - "^(%x+)$" + - "([^/]+/[^/@]+)@(%x+)" + - "(%w+)@(%x+)" + user: "@([%w%-%.]+)" url-formats: - issue: '/{repo}/-/issues/{number}' - merge-request: '/{repo}/-/merge_requests/{number}' - pull: '/{repo}/-/merge_requests/{number}' - commit: '/{repo}/-/commit/{sha}' - user: '/{username}' + issue: "/{repo}/-/issues/{number}" + merge-request: "/{repo}/-/merge_requests/{number}" + pull: "/{repo}/-/merge_requests/{number}" + commit: "/{repo}/-/commit/{sha}" + user: "/{username}" codeberg: display-name: Codeberg - default-url: https://codeberg.org + base-url: https://codeberg.org patterns: issue: - - '#(%d+)' - - '([^/]+/[^/#]+)#(%d+)' + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" merge-request: - - '#(%d+)' - - '([^/]+/[^/#]+)#(%d+)' + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" commit: - - '^(%x+)$' - - '([^/]+/[^/@]+)@(%x+)' - - '(%w+)@(%x+)' - user: '@([%w%-%.]+)' + - "^(%x+)$" + - "([^/]+/[^/@]+)@(%x+)" + - "(%w+)@(%x+)" + user: "@([%w%-%.]+)" url-formats: - issue: '/{repo}/issues/{number}' - merge-request: '/{repo}/pulls/{number}' - pull: '/{repo}/pulls/{number}' - commit: '/{repo}/commit/{sha}' - user: '/{username}' + issue: "/{repo}/issues/{number}" + merge-request: "/{repo}/pulls/{number}" + pull: "/{repo}/pulls/{number}" + commit: "/{repo}/commit/{sha}" + user: "/{username}" gitea: display-name: Gitea - default-url: https://gitea.com + base-url: https://gitea.com patterns: issue: - - '#(%d+)' - - '([^/]+/[^/#]+)#(%d+)' + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" merge-request: - - '#(%d+)' - - '([^/]+/[^/#]+)#(%d+)' + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" commit: - - '^(%x+)$' - - '([^/]+/[^/@]+)@(%x+)' - - '(%w+)@(%x+)' - user: '@([%w%-%.]+)' + - "^(%x+)$" + - "([^/]+/[^/@]+)@(%x+)" + - "(%w+)@(%x+)" + user: "@([%w%-%.]+)" url-formats: - issue: '/{repo}/issues/{number}' - merge-request: '/{repo}/pulls/{number}' - pull: '/{repo}/pulls/{number}' - commit: '/{repo}/commit/{sha}' - user: '/{username}' + issue: "/{repo}/issues/{number}" + merge-request: "/{repo}/pulls/{number}" + pull: "/{repo}/pulls/{number}" + commit: "/{repo}/commit/{sha}" + user: "/{username}" bitbucket: display-name: Bitbucket - default-url: https://bitbucket.org + base-url: https://bitbucket.org patterns: issue: - - '#(%d+)' - - '([^/]+/[^/#]+)#(%d+)' + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" merge-request: - - '#(%d+)' - - '([^/]+/[^/#]+)#(%d+)' + - "#(%d+)" + - "([^/]+/[^/#]+)#(%d+)" commit: - - '^(%x+)$' - - '([^/]+/[^/@]+)@(%x+)' - - '(%w+)@(%x+)' - user: '@([%w%-%.]+)' + - "^(%x+)$" + - "([^/]+/[^/@]+)@(%x+)" + - "(%w+)@(%x+)" + user: "@([%w%-%.]+)" url-formats: - issue: '/{repo}/issues/{number}' - merge-request: '/{repo}/pull-requests/{number}' - pull: '/{repo}/pull-requests/{number}' - commit: '/{repo}/commits/{sha}' - user: '/{username}' + issue: "/{repo}/issues/{number}" + merge-request: "/{repo}/pull-requests/{number}" + pull: "/{repo}/pull-requests/{number}" + commit: "/{repo}/commits/{sha}" + user: "/{username}" diff --git a/example.qmd b/example.qmd index e7fa211..ca47bf3 100644 --- a/example.qmd +++ b/example.qmd @@ -352,7 +352,7 @@ Create a YAML file (e.g., `custom-platforms.yml`) with your platform definitions ```yaml platforms: gitplatform: - default-url: https://git.example.com + base-url: https://git.example.com patterns: issue: - '#(%d+)' From 82eec74660eeebe5edcffeb04281abb613d82958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:12:38 +0100 Subject: [PATCH 11/15] feat: add customisation options for badge appearance --- README.md | 19 ++++++++--- _extensions/gitlink/gitlink.lua | 56 +++++++++++++++++++++++++++++++-- example.qmd | 31 ++++++++++++++++++ 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6551945..ac31417 100644 --- a/README.md +++ b/README.md @@ -183,16 +183,27 @@ Supports: `https://github.com/owner/repo.git`, `git@gitlab.com:group/project.git ### Platform Badges -In HTML output, Gitlink adds subtle platform badges to links. You can control them with: +Gitlink adds subtle platform badges to links in HTML and Typst output. You can control them with: ```yaml extensions: gitlink: - show-platform-badge: true # Show/hide badges (default: true) - badge-position: "after" # "after" or "before" link (default: "after") + show-platform-badge: true # Show/hide badges (default: true) + badge-position: "after" # "after" or "before" link (default: "after") + badge-background-colour: "#c3c3c3" # Badge background colour (default: "#c3c3c3") + badge-text-colour: "#000000" # Badge text colour (optional) ``` -Badges are always visible, accessible, styled with Bootstrap, and include tooltips. In non-HTML formats, platform names appear in parentheses (e.g., `#123 (GitHub)`). +**Features:** + +- **HTML output**: Badges are styled with Bootstrap classes and include tooltips. You can customise colours with hex codes or colour names. +- **Typst output**: Badges appear as styled boxes with configurable colours. +- **Other formats**: Platform names appear in parentheses (e.g., `#123 (GitHub)`). + +**Colour Customisation:** + +- `badge-background-colour`: Set the background colour (hex code or colour name). Defaults to `#c3c3c3` (grey). +- `badge-text-colour`: Set the text colour (hex code or colour name). If not specified, uses the default text colour. ## Custom Platforms diff --git a/_extensions/gitlink/gitlink.lua b/_extensions/gitlink/gitlink.lua index 69f4598..e16aa68 100644 --- a/_extensions/gitlink/gitlink.lua +++ b/_extensions/gitlink/gitlink.lua @@ -49,6 +49,12 @@ 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 Badge text colour (hex or colour name) +local badge_text_colour = nil + --- @type integer Full length of a git commit SHA local COMMIT_SHA_FULL_LENGTH = 40 @@ -92,10 +98,23 @@ local function create_platform_link(text, uri, platform_name) stylesheets = { css_path } }) + local badge_classes = { 'gitlink-badge', 'badge', 'text-bg-secondary' } + local badge_style = {} + if not utils.is_empty(badge_background_colour) then + table.insert(badge_style, 'background-color: ' .. badge_background_colour .. ';') + end + if not utils.is_empty(badge_text_colour) then + table.insert(badge_style, 'color: ' .. badge_text_colour .. ';') + end + local badge_attr = pandoc.Attr( '', - { 'gitlink-badge', 'badge', 'text-bg-secondary' }, - { title = platform_label, ['aria-label'] = platform_label .. ' platform' } + badge_classes, + { + title = platform_label, + ['aria-label'] = platform_label .. ' platform', + style = table.concat(badge_style, ' ') + } ) local badge = pandoc.Span({ pandoc.Str(platform_label) }, badge_attr) @@ -106,6 +125,29 @@ local function create_platform_link(text, uri, platform_name) 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 + local bg_colour = badge_background_colour + local text_colour_opt = '' + if not utils.is_empty(badge_text_colour) then + text_colour_opt = ', fill: rgb("' .. badge_text_colour .. '")' + end + local badge_raw = '#box(fill: rgb("' .. bg_colour .. '"), inset: 2pt, outset: 0pt, radius: 3pt, baseline: -0.3em, text(size: 0.45em' .. text_colour_opt .. ', [' .. 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 @@ -184,6 +226,16 @@ local function get_repository(meta) badge_position = badge_pos_meta --[[@as string]] end + local badge_bg_colour_meta = utils.get_metadata_value(meta, 'gitlink', 'badge-background-colour') + if not utils.is_empty(badge_bg_colour_meta) then + badge_background_colour = badge_bg_colour_meta --[[@as string]] + end + + local badge_text_colour_meta = utils.get_metadata_value(meta, 'gitlink', 'badge-text-colour') + if not utils.is_empty(badge_text_colour_meta) then + badge_text_colour = badge_text_colour_meta --[[@as string]] + end + return meta end diff --git a/example.qmd b/example.qmd index ca47bf3..934759a 100644 --- a/example.qmd +++ b/example.qmd @@ -387,6 +387,37 @@ extensions: The extension will load both built-in and custom platform definitions, allowing you to use any platform you define. +## Customising Badge Appearance + +You can customise the appearance of platform badges in HTML and Typst output: + +```yaml +extensions: + gitlink: + show-platform-badge: true # Show/hide badges (default: true) + badge-position: "after" # Position: "after" or "before" (default: "after") + badge-background-colour: "#e8f4f8" # Badge background colour (default: "#c3c3c3") + badge-text-colour: "#003366" # Badge text colour (optional) +``` + +**Colour Options:** + +- `badge-background-colour`: Set the background colour using hex codes (e.g. `#e8f4f8`) or colour names. +- `badge-text-colour`: Set the text colour (optional). If not specified, the default text colour is used. + +**Example with Custom Colours:** + +Here is an example with a light blue background and dark blue text: + +```yaml +extensions: + gitlink: + badge-background-colour: "#e8f4f8" + badge-text-colour: "#003366" +``` + +These colours will be applied to badges in both HTML and Typst output formats. + **Contributing New Platforms**: To contribute a new platform to the built-in configuration: From dc74290c8b2ea7b6659648f061530f5ad11f2858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:41:11 +0100 Subject: [PATCH 12/15] refactor: use module and style --- _extensions/gitlink/gitlink.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/_extensions/gitlink/gitlink.lua b/_extensions/gitlink/gitlink.lua index e16aa68..d9f3791 100644 --- a/_extensions/gitlink/gitlink.lua +++ b/_extensions/gitlink/gitlink.lua @@ -138,7 +138,10 @@ local function create_platform_link(text, uri, platform_name) if not utils.is_empty(badge_text_colour) then text_colour_opt = ', fill: rgb("' .. badge_text_colour .. '")' end - local badge_raw = '#box(fill: rgb("' .. bg_colour .. '"), inset: 2pt, outset: 0pt, radius: 3pt, baseline: -0.3em, text(size: 0.45em' .. text_colour_opt .. ', [' .. platform_label .. ']))' + local badge_raw = '#box(fill: rgb("' .. + bg_colour .. + '"), inset: 2pt, outset: 0pt, radius: 3pt, baseline: -0.3em, text(size: 0.45em' .. + text_colour_opt .. ', [' .. platform_label .. ']))' local badge = pandoc.RawInline('typst', ' ' .. badge_raw) local inlines = {} @@ -262,7 +265,7 @@ local function process_mentions(cite) if references_ids_set[cite.citations[1].id] then return cite else - local mention_text = pandoc.utils.stringify(cite.content) + local mention_text = utils.stringify(cite.content) local config = get_platform_config(platform) if config and config.patterns.user then local username = mention_text:match(config.patterns.user) @@ -581,7 +584,7 @@ end --- @param elem pandoc.Link The link element to process --- @return pandoc.Link The original or modified link local function process_link(elem) - local link_text = pandoc.utils.stringify(elem.content) + local link_text = utils.stringify(elem.content) local link_target = elem.target if link_text == link_target then From b0cd797f6abbfcd6839d98ba1cdf22151bd4f0a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:07:39 +0100 Subject: [PATCH 13/15] chore: clean project config from versioned files --- _quarto.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 _quarto.yml diff --git a/_quarto.yml b/_quarto.yml deleted file mode 100644 index e69de29..0000000 From 21e430205d4c4e23452cd62fc9a25ac2a24f92e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:10:30 +0100 Subject: [PATCH 14/15] feat: add path resolution utility for custom platforms Implement a new utility function to resolve paths relative to the project directory. --- _extensions/gitlink/_modules/utils.lua | 30 ++++++++++++++++++++++++++ _extensions/gitlink/gitlink.lua | 5 +++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/_extensions/gitlink/_modules/utils.lua b/_extensions/gitlink/_modules/utils.lua index e71ff8b..676461c 100644 --- a/_extensions/gitlink/_modules/utils.lua +++ b/_extensions/gitlink/_modules/utils.lua @@ -506,6 +506,36 @@ function utils_module.log_output(extension_name, message) quarto.log.output("[" .. extension_name .. "] " .. message) end +-- ============================================================================ +-- 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 = utils_module.resolve_project_path("/config.yml") +--- @usage local resolved = utils_module.resolve_project_path("config.yml") +function utils_module.resolve_project_path(path) + if utils_module.is_empty(path) then + return path + end + + if path:sub(1, 1) == "/" then + if quarto.project.directory then + -- Prepend project directory to absolute path + return quarto.project.directory .. path + else + -- Remove leading `/` if no project directory + return path:sub(2) + end + else + return path + end +end + -- ============================================================================ -- MODULE EXPORT -- ============================================================================ diff --git a/_extensions/gitlink/gitlink.lua b/_extensions/gitlink/gitlink.lua index d9f3791..c2bc01b 100644 --- a/_extensions/gitlink/gitlink.lua +++ b/_extensions/gitlink/gitlink.lua @@ -174,12 +174,13 @@ local function get_repository(meta) local meta_custom_platforms = utils.get_metadata_value(meta, 'gitlink', 'custom-platforms-file') if not utils.is_empty(meta_custom_platforms) then - local custom_file_path = meta_custom_platforms --[[@as string]] + local original_path = meta_custom_platforms --[[@as string]] + local custom_file_path = utils.resolve_project_path(original_path) local ok, err = platforms.initialise(custom_file_path) if not ok then utils.log_error( EXTENSION_NAME, - "Failed to load custom platforms from '" .. meta_custom_platforms .. "':\n" .. (err or 'unknown error') + "Failed to load custom platforms from '" .. original_path .. "':\n" .. (err or 'unknown error') ) return meta end From 7dd197dbf37e9f9457db9d3e716023e4889bf91b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Thu, 4 Dec 2025 19:56:27 +0100 Subject: [PATCH 15/15] refactor: use single quotes --- _extensions/gitlink/gitlink.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/_extensions/gitlink/gitlink.lua b/_extensions/gitlink/gitlink.lua index c2bc01b..f0e0b91 100644 --- a/_extensions/gitlink/gitlink.lua +++ b/_extensions/gitlink/gitlink.lua @@ -23,7 +23,7 @@ ]] --- Extension name constant -local EXTENSION_NAME = "gitlink" +local EXTENSION_NAME = 'gitlink' --- Load utils, git, bitbucket, and platforms modules local utils = require(quarto.utils.resolve_path('_modules/utils.lua'):gsub('%.lua$', '')) @@ -32,13 +32,13 @@ local bitbucket = require(quarto.utils.resolve_path('_modules/bitbucket.lua'):gs local platforms = require(quarto.utils.resolve_path('_modules/platforms.lua'):gsub('%.lua$', '')) --- @type string The platform type (github, gitlab, codeberg, gitea, bitbucket) -local platform = "github" +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" +local base_url = 'https://github.com' --- @type table Set of reference IDs from the document local references_ids_set = {} @@ -47,10 +47,10 @@ local references_ids_set = {} local show_platform_badge = true --- @type string Badge position: "after" or "before" -local badge_position = "after" +local badge_position = 'after' --- @type string Badge background colour (hex or colour name) -local badge_background_colour = "#c3c3c3" +local badge_background_colour = '#c3c3c3' --- @type string Badge text colour (hex or colour name) local badge_text_colour = nil