Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ concurrency:
cancel-in-progress: false

env:
CURRENT_MD_UTILS_JSONSCHEMA_VERSION: 0.2.0
CURRENT_MD_UTILS_JSONSCHEMA_VERSION: 0.2.1

jobs:
deploy:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/schema-publication.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Validate schema publication files
run: python3 scripts/validate-schema-publication.py

- name: Guard immutable published schema versions
if: github.event_name == 'pull_request'
run: |
git fetch --no-tags --depth=1 origin "${{ github.base_ref }}"
changed_files="$(git diff --name-only "origin/${{ github.base_ref }}"...HEAD -- 'site/schemas/*/*.json')"
if [ -z "$changed_files" ]; then
exit 0
Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ let package = Package(
.process("Resources/SKILL.md"),
.process("Resources/0.1.0_md-utils.schema.json"),
.process("Resources/0.2.0_md-utils.schema.json"),
.process("Resources/0.2.1_md-utils.schema.json"),
.process("Resources/1_md-utils-type.schema.json"),
.process("Resources/OKF-concept.schema.json"),
]
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,9 @@ Compatibility:

| CLI version | Supported config schema versions | Default generated config schema version |
| --- | --- | --- |
| 0.1.0-alpha | 0.1.0, 0.2.0 | 0.2.0 |
| 0.1.0-alpha | 0.1.0, 0.2.0, 0.2.1 | 0.2.1 |

The config schema is published at `https://dandylyons.github.io/md-utils/schemas/0.2.0/md-utils.schema.json` for editor integration and IDE IntelliSense. The moving `https://dandylyons.github.io/md-utils/schemas/latest/md-utils.schema.json` alias points to the latest published schema, and `https://dandylyons.github.io/md-utils/md-utils.schema.json` is maintained as a root compatibility alias. Runtime config validation does not fetch these URLs; the CLI validates configs against bundled schemas selected by `configVersion`.
The config schema is published at `https://dandylyons.github.io/md-utils/schemas/0.2.1/md-utils.schema.json` for editor integration and IDE IntelliSense. The moving `https://dandylyons.github.io/md-utils/schemas/latest/md-utils.schema.json` alias points to the latest published schema, and `https://dandylyons.github.io/md-utils/md-utils.schema.json` is maintained as a root compatibility alias. Runtime config validation does not fetch these URLs; the CLI validates configs against bundled schemas selected by `configVersion`.

Treat the directory containing `.md-utils/` as the md-utils project root. Commands that use project configuration read `.md-utils/md-utils.json` relative to the current working directory; md-utils does not search parent directories for project configuration. Run rules/config commands from the directory that contains `.md-utils/`:

Expand All @@ -281,8 +281,8 @@ Example config:

```json
{
"$schema": "https://dandylyons.github.io/md-utils/schemas/0.2.0/md-utils.schema.json",
"configVersion": "0.2.0",
"$schema": "https://dandylyons.github.io/md-utils/schemas/0.2.1/md-utils.schema.json",
"configVersion": "0.2.1",
"schemaDirectory": ".md-utils/schemas/",
"rules": [
{
Expand Down Expand Up @@ -334,6 +334,7 @@ Config fields:
- `$schema`: Optional editor hint for autocomplete, validation, and IDE IntelliSense. Runtime behavior is not driven by this URL.
- `configVersion`: md-utils config schema version. This is independent from the md-utils CLI version.
- `schemaDirectory`: Directory for JSON Schema files. Defaults to `.md-utils/schemas/`.
- `frontmatter`: Optional wrapped-frontmatter presets, named syntaxes, and extension mappings for explicitly opted-in non-Markdown scans.
- `rules`: Rules that map Markdown files to one or more checks.
- `rules[].name`: Unique rule name for `md-utils rules validate <rule-name>`.
- `rules[].match.paths`: Glob patterns matched against project-relative Markdown paths.
Expand Down Expand Up @@ -392,9 +393,12 @@ site/
0.2.0/
md-utils.schema.json
md-utils-0.2.0.schema.json
0.2.1/
md-utils.schema.json
md-utils-0.2.1.schema.json
```

The bundled CLI schema in `Sources/md-utils/Resources/0.2.0_md-utils.schema.json` remains canonical for CLI behavior for config schema `0.2.0`. Public schema copies must match it exactly, so run `python3 scripts/validate-schema-publication.py` before publishing schema changes.
The bundled CLI schema in `Sources/md-utils/Resources/0.2.1_md-utils.schema.json` is canonical for CLI behavior for config schema `0.2.1`. Public schema copies must match it exactly, so run `python3 scripts/validate-schema-publication.py` before publishing schema changes.

When the Pages workflow prepares its artifact, it copies `site/schemas/$CURRENT_MD_UTILS_JSONSCHEMA_VERSION/md-utils.schema.json` to both `md-utils.schema.json` at the site root and `schemas/latest/md-utils.schema.json`. Versioned schema URLs are immutable after release. For future schema releases, add a new versioned folder under `site/schemas/`, update `CURRENT_MD_UTILS_JSONSCHEMA_VERSION` in `.github/workflows/pages.yml`, and keep the canonical bundled schema synchronized with the new published copy. Do not edit already-published versioned schema files; publish a new version instead.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Normalize supported serialized configurations into one executable model.

## Supported Versions

``MarkdownRuleConfigurationDecoder`` accepts configuration versions `0.1.0` and `0.2.0`. Legacy `schemaRules`, `schema`, and `frontmatterRequired` fields normalize into the same ``MarkdownRuleDefinition`` and ``MarkdownRuleCheck`` values as version `0.2.0` `rules` and `checks`.
``MarkdownRuleConfigurationDecoder`` accepts configuration versions `0.1.0`, `0.2.0`, and `0.2.1`. Legacy `schemaRules`, `schema`, and `frontmatterRequired` fields normalize into the same ``MarkdownRuleDefinition`` and ``MarkdownRuleCheck`` values as the `rules` and `checks` used by versions `0.2.0` and `0.2.1`. Version `0.2.1` additionally carries optional wrapped-frontmatter syntax and extension configuration.

Unknown versions, unknown keys, invalid operands, and fields that cannot be represented without changing behavior fail with ``MarkdownRuleConfigurationError``. The decoder never drops an unsupported field. Grouping syntax and a `0.3.0` format are not part of this model.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import Foundation

/// Project-level named syntaxes and extension mappings for wrapped frontmatter.
public struct WrappedFrontMatterProjectConfiguration: Equatable, Sendable {
public var useBuiltInPresets: Bool
public var syntaxes: [String: WrappedFrontMatterSyntax]
public var extensionMappings: [String: String]

public init(
useBuiltInPresets: Bool = true,
syntaxes: [String: WrappedFrontMatterSyntax] = [:],
extensionMappings: [String: String] = [:]
) throws {
let builtInNames = Set(WrappedFrontMatterPreset.allCases.map(\.rawValue))
for name in syntaxes.keys {
guard name.isEmpty == false else {
throw WrappedFrontMatterConfigurationError.emptySyntaxName
}
guard builtInNames.contains(name) == false else {
throw WrappedFrontMatterConfigurationError.reservedSyntaxName(name)
}
}

var normalizedMappings: [String: String] = [:]
for (pathExtension, syntaxName) in extensionMappings {
let normalizedExtension = Self.normalize(pathExtension)
guard normalizedExtension.isEmpty == false else {
throw WrappedFrontMatterConfigurationError.emptyExtension
}
guard syntaxName.isEmpty == false else {
throw WrappedFrontMatterConfigurationError.emptyMappedSyntax(pathExtension)
}
guard syntaxes[syntaxName] != nil || builtInNames.contains(syntaxName) else {
throw WrappedFrontMatterConfigurationError.unknownSyntax(syntaxName)
}
guard normalizedMappings[normalizedExtension] == nil else {
throw WrappedFrontMatterConfigurationError.duplicateExtension(normalizedExtension)
}
normalizedMappings[normalizedExtension] = syntaxName
}

self.useBuiltInPresets = useBuiltInPresets
self.syntaxes = syntaxes
self.extensionMappings = normalizedMappings
}

/// Resolves a built-in or project-defined syntax by name.
public func syntax(named name: String) -> WrappedFrontMatterSyntax? {
if let custom = syntaxes[name] {
return custom
}
return WrappedFrontMatterPreset(rawValue: name)?.syntax
}

/// Resolves the project mapping first, then a built-in extension preset when enabled.
public func inferredSyntax(forExtension pathExtension: String) -> WrappedFrontMatterSyntax? {
let normalizedExtension = Self.normalize(pathExtension)
if let name = extensionMappings[normalizedExtension] {
return syntax(named: name)
}
guard useBuiltInPresets,
let preset = WrappedFrontMatterPreset.inferred(forExtension: normalizedExtension)
else {
return nil
}
return preset.syntax
}

private static func normalize(_ pathExtension: String) -> String {
pathExtension
.lowercased()
.trimmingCharacters(in: CharacterSet(charactersIn: "."))
}
}

/// Invalid project-level wrapped-frontmatter configuration.
public enum WrappedFrontMatterConfigurationError: Error, Equatable, LocalizedError {
case emptySyntaxName
case reservedSyntaxName(String)
case emptyExtension
case emptyMappedSyntax(String)
case unknownSyntax(String)
case duplicateExtension(String)

public var errorDescription: String? {
switch self {
case .emptySyntaxName:
return "A wrapped frontmatter syntax name cannot be empty"
case .reservedSyntaxName(let name):
return "A project syntax cannot replace the built-in syntax \"\(name)\""
case .emptyExtension:
return "A wrapped frontmatter extension mapping cannot use an empty extension"
case .emptyMappedSyntax(let pathExtension):
return "The wrapped frontmatter mapping for \"\(pathExtension)\" requires a syntax name"
case .unknownSyntax(let name):
return "Unknown wrapped frontmatter syntax \"\(name)\""
case .duplicateExtension(let pathExtension):
return "Duplicate wrapped frontmatter extension mapping for \"\(pathExtension)\""
}
}
}
Loading