Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package rumdl for openSUSE:Factory checked in at 2026-03-27 16:50:54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rumdl (Old) and /work/SRC/openSUSE:Factory/.rumdl.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rumdl" Fri Mar 27 16:50:54 2026 rev:51 rq:1343115 version:0.1.61 Changes: -------- --- /work/SRC/openSUSE:Factory/rumdl/rumdl.changes 2026-03-27 06:49:52.758890684 +0100 +++ /work/SRC/openSUSE:Factory/.rumdl.new.8177/rumdl.changes 2026-03-27 16:53:38.817845346 +0100 @@ -1,0 +2,13 @@ +Fri Mar 27 06:00:36 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.1.61: + * Added + - MD018: Add tags config option to recognize #word patterns as + tags independently of flavor (#544) + * Fixed + - MD042: Treat bare # URL as empty link (#546) + - MD013: Ignore punctuation inside inline code for sentence + splitting (#545) + - MD063: Skip invalid headings like Obsidian-style tags (#544) + +------------------------------------------------------------------- Old: ---- rumdl-0.1.60.obscpio New: ---- rumdl-0.1.61.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rumdl.spec ++++++ --- /var/tmp/diff_new_pack.7dGC65/_old 2026-03-27 16:53:39.913891228 +0100 +++ /var/tmp/diff_new_pack.7dGC65/_new 2026-03-27 16:53:39.913891228 +0100 @@ -17,7 +17,7 @@ Name: rumdl -Version: 0.1.60 +Version: 0.1.61 Release: 0 Summary: Markdown Linter written in Rust License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.7dGC65/_old 2026-03-27 16:53:39.957893070 +0100 +++ /var/tmp/diff_new_pack.7dGC65/_new 2026-03-27 16:53:39.965893405 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/rvben/rumdl.git</param> <param name="scm">git</param> <param name="submodules">enable</param> - <param name="revision">v0.1.60</param> + <param name="revision">v0.1.61</param> <param name="match-tag">v*.*.*</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.7dGC65/_old 2026-03-27 16:53:40.001894912 +0100 +++ /var/tmp/diff_new_pack.7dGC65/_new 2026-03-27 16:53:40.021895749 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/rvben/rumdl.git</param> - <param name="changesrevision">9ab9e256cf266908555448f396c725d50c103919</param></service></servicedata> + <param name="changesrevision">9ce5d163deb00a519597e730f3981c33219752d1</param></service></servicedata> (No newline at EOF) ++++++ rumdl-0.1.60.obscpio -> rumdl-0.1.61.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/.github/workflows/ci.yml new/rumdl-0.1.61/.github/workflows/ci.yml --- old/rumdl-0.1.60/.github/workflows/ci.yml 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/.github/workflows/ci.yml 2026-03-26 23:27:54.000000000 +0100 @@ -71,32 +71,10 @@ - name: Run tests run: make test-ci - check-links: - name: Check Links - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: jdx/mise-action@v4 - with: - experimental: true - install: false - - - name: Install mise tools - run: | - for attempt in 1 2 3; do - mise install cargo:lychee --yes && break - echo "Attempt $attempt failed, retrying in 10s..." - sleep 10 - done - - - name: Check links - run: make check-links - all-checks-passed: name: All checks passed runs-on: ubuntu-latest - needs: [lint, test, check-links] + needs: [lint, test] if: always() steps: - name: Verify all checks passed diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/.github/workflows/release.yml new/rumdl-0.1.61/.github/workflows/release.yml --- old/rumdl-0.1.60/.github/workflows/release.yml 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/.github/workflows/release.yml 2026-03-26 23:27:54.000000000 +0100 @@ -786,32 +786,6 @@ echo "- Archives:" find artifacts/release-* \( -name "*.tar.gz*" -o -name "*.zip*" \) -type f 2>/dev/null | sort - - name: Update major version tag - if: ${{ inputs.dry_run != true }} - run: | - # Extract major version (e.g., v0 from v0.0.191) - TAG_NAME=${GITHUB_REF#refs/tags/} - MAJOR_VERSION=${TAG_NAME%%.*} - - echo "Updating $MAJOR_VERSION tag to point to $TAG_NAME" - - # Configure git for tagging - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - # Force-update the major version tag - git tag -fa "$MAJOR_VERSION" -m "Update $MAJOR_VERSION tag to $TAG_NAME" - git push origin "$MAJOR_VERSION" --force - - echo "Users can now use: rvben/rumdl@$MAJOR_VERSION" - - - name: Test major version tag update (dry run) - if: ${{ inputs.dry_run == true }} - run: | - TAG_NAME=${GITHUB_REF#refs/tags/} - MAJOR_VERSION=${TAG_NAME%%.*} - echo "DRY RUN: Would update $MAJOR_VERSION tag to point to $TAG_NAME" - - name: Notify rumdl-pre-commit if: ${{ steps.publish-pypi.outcome == 'success' && inputs.dry_run != true }} continue-on-error: true @@ -868,6 +842,39 @@ https://api.github.com/repos/rvben/homebrew-rumdl/dispatches \ -d "{\"event_type\": \"rumdl_release\", \"client_payload\": {\"version\": \"$VERSION\"}}" + - name: Update major version tag + if: ${{ inputs.dry_run != true }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Extract major version (e.g., v0 from v0.1.60) + TAG_NAME=${GITHUB_REF#refs/tags/} + MAJOR_VERSION=${TAG_NAME%%.*} + COMMIT_SHA=${GITHUB_SHA} + + echo "Updating $MAJOR_VERSION tag to point to $TAG_NAME ($COMMIT_SHA)" + + # Use the GitHub API to update the tag ref, avoiding the workflows + # permission issue that blocks git push when the repo contains + # workflow files. + if gh api "repos/${{ github.repository }}/git/refs/tags/$MAJOR_VERSION" \ + --method PATCH --field sha="$COMMIT_SHA" --field force=true; then + echo "Updated existing $MAJOR_VERSION tag" + else + gh api "repos/${{ github.repository }}/git/refs" \ + --method POST --field ref="refs/tags/$MAJOR_VERSION" --field sha="$COMMIT_SHA" + echo "Created $MAJOR_VERSION tag" + fi + + echo "Users can now use: rvben/rumdl@$MAJOR_VERSION" + + - name: Test major version tag update (dry run) + if: ${{ inputs.dry_run == true }} + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + MAJOR_VERSION=${TAG_NAME%%.*} + echo "DRY RUN: Would update $MAJOR_VERSION tag to point to $TAG_NAME" + - name: Dry Run Summary if: ${{ inputs.dry_run == true }} run: | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/.pre-commit-config.yaml new/rumdl-0.1.61/.pre-commit-config.yaml --- old/rumdl-0.1.60/.pre-commit-config.yaml 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/.pre-commit-config.yaml 2026-03-26 23:27:54.000000000 +0100 @@ -87,6 +87,17 @@ pass_filenames: true require_serial: true + # Link checking + - repo: local + hooks: + - id: check-links + name: check links + entry: make check-links + language: system + files: \.md$ + pass_filenames: false + stages: [pre-push] + # Pre-push hooks for comprehensive validation - repo: local hooks: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/CHANGELOG.md new/rumdl-0.1.61/CHANGELOG.md --- old/rumdl-0.1.60/CHANGELOG.md 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/CHANGELOG.md 2026-03-26 23:27:54.000000000 +0100 @@ -7,7 +7,17 @@ ## [Unreleased] -## [0.1.60] - 2026-03-25 +## [0.1.61] - 2026-03-26 + +### Added + +- **MD018**: Add `tags` config option to recognize `#word` patterns as tags independently of flavor ([#544](https://github.com/rvben/rumdl/issues/544)) + +### Fixed + +- **MD042**: Treat bare `#` URL as empty link ([#546](https://github.com/rvben/rumdl/issues/546)) +- **MD013**: Ignore punctuation inside inline code for sentence splitting ([#545](https://github.com/rvben/rumdl/issues/545)) +- **MD063**: Skip invalid headings like Obsidian-style tags ([#544](https://github.com/rvben/rumdl/issues/544)) ## [0.1.60] - 2026-03-25 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/Cargo.lock new/rumdl-0.1.61/Cargo.lock --- old/rumdl-0.1.60/Cargo.lock 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/Cargo.lock 2026-03-26 23:27:54.000000000 +0100 @@ -2269,7 +2269,7 @@ [[package]] name = "rumdl" -version = "0.1.60" +version = "0.1.61" dependencies = [ "anyhow", "assert_cmd", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/Cargo.toml new/rumdl-0.1.61/Cargo.toml --- old/rumdl-0.1.60/Cargo.toml 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/Cargo.toml 2026-03-26 23:27:54.000000000 +0100 @@ -1,6 +1,6 @@ [package] name = "rumdl" -version = "0.1.60" +version = "0.1.61" edition = "2024" rust-version = "1.94.0" description = "A fast Markdown linter written in Rust (Ru(st) MarkDown Linter)" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/Makefile new/rumdl-0.1.61/Makefile --- old/rumdl-0.1.60/Makefile 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/Makefile 2026-03-26 23:27:54.000000000 +0100 @@ -248,6 +248,18 @@ @echo "" @echo "Configuration: cliff.toml" +_version-bump: + @$(MAKE) update-cargo-version VERSION_NO_V=$(VERSION_NO_V) + @$(MAKE) update-github-action-version VERSION_NO_V=$(VERSION_NO_V) + @$(MAKE) update-all-docs-version NEW_TAG=$(NEW_TAG) + @$(MAKE) update-changelog NEW_TAG=$(NEW_TAG) VERSION_NO_V=$(VERSION_NO_V) CURRENT=$(CURRENT) + @scripts/update-npm-versions.sh + @sed -i.bak -E 's/mise use rumdl@[0-9.]+/mise use rumdl@$(VERSION_NO_V)/g' README.md && rm -f README.md.bak + @git add Cargo.toml Cargo.lock README.md docs/global-settings.md CHANGELOG.md scripts/rumdl-action.sh npm/ + @git commit -m "chore(release): prepare $(NEW_TAG)" + @git tag -a $(NEW_TAG) -m "$(NEW_TAG)" + @echo "Version $(NEW_TAG) created and committed. Run 'git push origin main $(NEW_TAG)' to trigger release workflow." + version-major: @echo "Creating new major version tag..." $(eval CURRENT := $(shell git describe --tags --abbrev=0 2>/dev/null || echo v0.0.0)) @@ -256,14 +268,7 @@ $(eval NEW_TAG := v$(NEW_MAJOR).0.0) $(eval VERSION_NO_V := $(NEW_MAJOR).0.0) @echo "Current: $(CURRENT) -> New: $(NEW_TAG)" - @$(MAKE) update-cargo-version VERSION_NO_V=$(VERSION_NO_V) - @$(MAKE) update-github-action-version VERSION_NO_V=$(VERSION_NO_V) - @$(MAKE) update-all-docs-version NEW_TAG=$(NEW_TAG) - @$(MAKE) update-changelog NEW_TAG=$(NEW_TAG) VERSION_NO_V=$(VERSION_NO_V) CURRENT=$(CURRENT) - @git add Cargo.toml Cargo.lock README.md docs/global-settings.md CHANGELOG.md scripts/rumdl-action.sh - @git commit -m "Bump version to $(NEW_TAG)" - @git tag -a $(NEW_TAG) -m "Release $(NEW_TAG)" - @echo "Version $(NEW_TAG) created and committed. Run 'git push && git push origin $(NEW_TAG)' to trigger release workflow." + @$(MAKE) _version-bump NEW_TAG=$(NEW_TAG) VERSION_NO_V=$(VERSION_NO_V) CURRENT=$(CURRENT) version-minor: @echo "Creating new minor version tag..." @@ -274,14 +279,7 @@ $(eval NEW_TAG := v$(MAJOR).$(NEW_MINOR).0) $(eval VERSION_NO_V := $(MAJOR).$(NEW_MINOR).0) @echo "Current: $(CURRENT) -> New: $(NEW_TAG)" - @$(MAKE) update-cargo-version VERSION_NO_V=$(VERSION_NO_V) - @$(MAKE) update-github-action-version VERSION_NO_V=$(VERSION_NO_V) - @$(MAKE) update-all-docs-version NEW_TAG=$(NEW_TAG) - @$(MAKE) update-changelog NEW_TAG=$(NEW_TAG) VERSION_NO_V=$(VERSION_NO_V) CURRENT=$(CURRENT) - @git add Cargo.toml Cargo.lock README.md docs/global-settings.md CHANGELOG.md scripts/rumdl-action.sh - @git commit -m "Bump version to $(NEW_TAG)" - @git tag -a $(NEW_TAG) -m "Release $(NEW_TAG)" - @echo "Version $(NEW_TAG) created and committed. Run 'git push && git push origin $(NEW_TAG)' to trigger release workflow." + @$(MAKE) _version-bump NEW_TAG=$(NEW_TAG) VERSION_NO_V=$(VERSION_NO_V) CURRENT=$(CURRENT) version-patch: @echo "Creating new patch version tag..." @@ -293,14 +291,7 @@ $(eval NEW_TAG := v$(MAJOR).$(MINOR).$(NEW_PATCH)) $(eval VERSION_NO_V := $(MAJOR).$(MINOR).$(NEW_PATCH)) @echo "Current: $(CURRENT) -> New: $(NEW_TAG)" - @$(MAKE) update-cargo-version VERSION_NO_V=$(VERSION_NO_V) - @$(MAKE) update-github-action-version VERSION_NO_V=$(VERSION_NO_V) - @$(MAKE) update-all-docs-version NEW_TAG=$(NEW_TAG) - @$(MAKE) update-changelog NEW_TAG=$(NEW_TAG) VERSION_NO_V=$(VERSION_NO_V) CURRENT=$(CURRENT) - @git add Cargo.toml Cargo.lock README.md docs/global-settings.md CHANGELOG.md scripts/rumdl-action.sh - @git commit -m "Bump version to $(NEW_TAG)" - @git tag -a $(NEW_TAG) -m "Release $(NEW_TAG)" - @echo "Version $(NEW_TAG) created and committed. Run 'git push && git push origin $(NEW_TAG)' to trigger release workflow." + @$(MAKE) _version-bump NEW_TAG=$(NEW_TAG) VERSION_NO_V=$(VERSION_NO_V) CURRENT=$(CURRENT) # Target to push the new tag and changes automatically version-push: @@ -441,9 +432,11 @@ @echo "Generating benchmark chart..." @uv run --with matplotlib python3 scripts/generate_benchmark_chart.py +LYCHEE := $(shell command -v lychee 2>/dev/null || echo "mise exec -- lychee") + check-links: @echo "Checking links in markdown files..." - mise exec -- lychee --no-progress --config .lychee.toml --remap 'https://rumdl.dev/([^/]+)/? file://$(CURDIR)/docs/$$1.md' 'README.md' 'docs/**/*.md' + $(LYCHEE) --no-progress --config .lychee.toml --remap 'https://rumdl.dev/([^/]+)/? file://$(CURDIR)/docs/$$1.md' 'README.md' 'docs/**/*.md' # Documentation validation test-doc-completeness: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/README.md new/rumdl-0.1.61/README.md --- old/rumdl-0.1.60/README.md 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/README.md 2026-03-26 23:27:54.000000000 +0100 @@ -196,7 +196,7 @@ mise install rumdl # Use a specific version for the project -mise use [email protected] +mise use [email protected] ``` ### Using Nix (macOS/Linux) @@ -346,7 +346,7 @@ ```yaml repos: - repo: https://github.com/rvben/rumdl-pre-commit - rev: v0.1.60 + rev: v0.1.61 hooks: - id: rumdl # Lint only (fails on issues) - id: rumdl-fmt # Auto-format and fail if issues remain @@ -368,7 +368,7 @@ ```yaml repos: - repo: https://github.com/rvben/rumdl-pre-commit - rev: v0.1.60 + rev: v0.1.61 hooks: - id: rumdl args: [--no-exclude] # Disable all exclude patterns diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/docs/_redirects new/rumdl-0.1.61/docs/_redirects --- old/rumdl-0.1.60/docs/_redirects 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/docs/_redirects 2026-03-26 23:27:54.000000000 +0100 @@ -1,2 +1,3 @@ /RULES /rules 301 /RULES/ /rules/ 301 +/RULES/* /rules/:splat 301 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/docs/md018.md new/rumdl-0.1.61/docs/md018.md --- old/rumdl-0.1.60/docs/md018.md 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/docs/md018.md 2026-03-26 23:27:54.000000000 +0100 @@ -64,6 +64,7 @@ | Option | Type | Default | Description | |--------|------|---------|-------------| | `magiclink` | boolean | `false` | Enable MagicLink support for issue/PR references | +| `tags` | boolean | `null` | Recognize `#word` as tags instead of malformed headings. Defaults to `true` for Obsidian flavor, `false` otherwise | ### MagicLink support @@ -102,9 +103,18 @@ - YAML frontmatter comments - Indented patterns (not at column 1) -## Obsidian flavor: Tag syntax support +## Tag syntax support -When using `flavor = "obsidian"`, this rule automatically skips Obsidian tag syntax at the start of a line. In Obsidian, `#tagname` is used for organizing notes with tags, not as headings. +When `tags = true`, this rule skips `#word` patterns that look like tags (e.g., `#todo`, `#project/active`) instead of treating them as malformed headings. + +Tags are recognized when they start with `#` followed by a non-digit, non-space character. Multi-hash patterns like `##tag` are always treated as malformed headings, and `#123` (starting with a digit) is not a valid tag. + +When `tags` is not explicitly set, it defaults to `true` for Obsidian flavor and `false` otherwise. This means Obsidian users get tag support automatically, while users of other flavors can opt in: + +```toml +[MD018] +tags = true +``` ### Example @@ -122,21 +132,12 @@ <!-- rumdl-enable MD018 MD022 MD025 --> -With `flavor = "obsidian"`: +With `tags = true`: -- `#todo` and `#project/active` are **not flagged** (Obsidian tags) +- `#todo` and `#project/active` are **not flagged** (recognized as tags) - `##Introduction` is **flagged** (multi-hash, clearly a malformed heading) - `#123` is **flagged** (tags cannot start with digits) -### Configuration - -```toml -[global] -flavor = "obsidian" -``` - -This allows Obsidian users to use tag syntax without triggering false positives from MD018. - ## Automatic fixes This rule automatically adds a space after the # symbols to properly format the heading. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/npm/cli-darwin-arm64/package.json new/rumdl-0.1.61/npm/cli-darwin-arm64/package.json --- old/rumdl-0.1.60/npm/cli-darwin-arm64/package.json 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/npm/cli-darwin-arm64/package.json 2026-03-26 23:27:54.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-darwin-arm64", - "version": "0.1.60", + "version": "0.1.61", "description": "rumdl binary for macOS ARM64 (Apple Silicon)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/npm/cli-darwin-x64/package.json new/rumdl-0.1.61/npm/cli-darwin-x64/package.json --- old/rumdl-0.1.60/npm/cli-darwin-x64/package.json 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/npm/cli-darwin-x64/package.json 2026-03-26 23:27:54.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-darwin-x64", - "version": "0.1.60", + "version": "0.1.61", "description": "rumdl binary for macOS x64 (Intel)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/npm/cli-linux-arm64/package.json new/rumdl-0.1.61/npm/cli-linux-arm64/package.json --- old/rumdl-0.1.60/npm/cli-linux-arm64/package.json 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/npm/cli-linux-arm64/package.json 2026-03-26 23:27:54.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-arm64", - "version": "0.1.60", + "version": "0.1.61", "description": "rumdl binary for Linux ARM64 (glibc)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/npm/cli-linux-arm64-musl/package.json new/rumdl-0.1.61/npm/cli-linux-arm64-musl/package.json --- old/rumdl-0.1.60/npm/cli-linux-arm64-musl/package.json 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/npm/cli-linux-arm64-musl/package.json 2026-03-26 23:27:54.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-arm64-musl", - "version": "0.1.60", + "version": "0.1.61", "description": "rumdl binary for Linux ARM64 (musl/Alpine)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/npm/cli-linux-x64/package.json new/rumdl-0.1.61/npm/cli-linux-x64/package.json --- old/rumdl-0.1.60/npm/cli-linux-x64/package.json 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/npm/cli-linux-x64/package.json 2026-03-26 23:27:54.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-x64", - "version": "0.1.60", + "version": "0.1.61", "description": "rumdl binary for Linux x64 (glibc)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/npm/cli-linux-x64-musl/package.json new/rumdl-0.1.61/npm/cli-linux-x64-musl/package.json --- old/rumdl-0.1.60/npm/cli-linux-x64-musl/package.json 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/npm/cli-linux-x64-musl/package.json 2026-03-26 23:27:54.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-x64-musl", - "version": "0.1.60", + "version": "0.1.61", "description": "rumdl binary for Linux x64 (musl/Alpine)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/npm/cli-win32-x64/package.json new/rumdl-0.1.61/npm/cli-win32-x64/package.json --- old/rumdl-0.1.60/npm/cli-win32-x64/package.json 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/npm/cli-win32-x64/package.json 2026-03-26 23:27:54.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-win32-x64", - "version": "0.1.60", + "version": "0.1.61", "description": "rumdl binary for Windows x64", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/npm/rumdl/package.json new/rumdl-0.1.61/npm/rumdl/package.json --- old/rumdl-0.1.60/npm/rumdl/package.json 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/npm/rumdl/package.json 2026-03-26 23:27:54.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "rumdl", - "version": "0.1.60", + "version": "0.1.61", "description": "A fast Markdown linter written in Rust", "license": "MIT", "repository": { @@ -33,12 +33,12 @@ "node": ">=18.0.0" }, "optionalDependencies": { - "@rumdl/cli-darwin-x64": "0.1.60", - "@rumdl/cli-darwin-arm64": "0.1.60", - "@rumdl/cli-linux-x64": "0.1.60", - "@rumdl/cli-linux-arm64": "0.1.60", - "@rumdl/cli-linux-x64-musl": "0.1.60", - "@rumdl/cli-linux-arm64-musl": "0.1.60", - "@rumdl/cli-win32-x64": "0.1.60" + "@rumdl/cli-darwin-x64": "0.1.61", + "@rumdl/cli-darwin-arm64": "0.1.61", + "@rumdl/cli-linux-x64": "0.1.61", + "@rumdl/cli-linux-arm64": "0.1.61", + "@rumdl/cli-linux-x64-musl": "0.1.61", + "@rumdl/cli-linux-arm64-musl": "0.1.61", + "@rumdl/cli-win32-x64": "0.1.61" } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/src/rules/md018_no_missing_space_atx/md018_config.rs new/rumdl-0.1.61/src/rules/md018_no_missing_space_atx/md018_config.rs --- old/rumdl-0.1.60/src/rules/md018_no_missing_space_atx/md018_config.rs 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/src/rules/md018_no_missing_space_atx/md018_config.rs 2026-03-26 23:27:54.000000000 +0100 @@ -11,6 +11,22 @@ /// Default: false (all patterns are flagged) #[serde(default)] pub magiclink: bool, + + /// Recognize `#word` patterns as tags instead of malformed headings. + /// When true, single-hash patterns like `#tag`, `#project/active` are + /// skipped. When null/unset, defaults to true for Obsidian flavor + /// and false otherwise. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tags: Option<bool>, +} + +impl MD018Config { + /// Whether tags mode is enabled, considering the flavor default. + /// Explicit config takes precedence; otherwise Obsidian flavor enables tags. + pub fn tags_enabled(&self, flavor: crate::config::MarkdownFlavor) -> bool { + self.tags + .unwrap_or(matches!(flavor, crate::config::MarkdownFlavor::Obsidian)) + } } impl RuleConfig for MD018Config { @@ -50,6 +66,28 @@ let toml_str = ""; let config: MD018Config = toml::from_str(toml_str).unwrap(); assert!(!config.magiclink); + assert!(config.tags.is_none()); + } + + #[test] + fn test_tags_enabled() { + let config: MD018Config = toml::from_str("tags = true").unwrap(); + assert_eq!(config.tags, Some(true)); + assert!(config.tags_enabled(crate::config::MarkdownFlavor::Standard)); + } + + #[test] + fn test_tags_disabled() { + let config: MD018Config = toml::from_str("tags = false").unwrap(); + assert_eq!(config.tags, Some(false)); + assert!(!config.tags_enabled(crate::config::MarkdownFlavor::Obsidian)); + } + + #[test] + fn test_tags_default_follows_flavor() { + let config = MD018Config::default(); + assert!(!config.tags_enabled(crate::config::MarkdownFlavor::Standard)); + assert!(config.tags_enabled(crate::config::MarkdownFlavor::Obsidian)); } #[test] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/src/rules/md018_no_missing_space_atx.rs new/rumdl-0.1.61/src/rules/md018_no_missing_space_atx.rs --- old/rumdl-0.1.60/src/rules/md018_no_missing_space_atx.rs 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/src/rules/md018_no_missing_space_atx.rs 2026-03-26 23:27:54.000000000 +0100 @@ -19,11 +19,11 @@ // whitespace, or punctuation (not alphanumeric continuation) const MAGICLINK_REF_PATTERN_STR: &str = r"^#\d+(?:\s|[^a-zA-Z0-9]|$)"; -// Obsidian tag pattern: #tagname, #project/active, #my-tag_2023, etc. -// Obsidian tags start with # followed by a non-digit, non-space character, +// Tag pattern: #tagname, #project/active, #my-tag_2023, etc. +// Tags start with # followed by a non-digit, non-space character, // then any combination of word characters, hyphens, underscores, and slashes. // Tags cannot start with a number. -const OBSIDIAN_TAG_PATTERN_STR: &str = r"^#[^\d\s#][^\s#]*(?:\s|$)"; +const TAG_PATTERN_STR: &str = r"^#[^\d\s#][^\s#]*(?:\s|$)"; #[derive(Clone)] pub struct MD018NoMissingSpaceAtx { @@ -53,10 +53,14 @@ get_cached_regex(MAGICLINK_REF_PATTERN_STR).is_ok_and(|re| re.is_match(line.trim_start())) } - /// Check if a line is an Obsidian tag (e.g., #tagname, #project/active) - /// Used by Obsidian flavor to skip tag syntax - fn is_obsidian_tag(line: &str) -> bool { - get_cached_regex(OBSIDIAN_TAG_PATTERN_STR).is_ok_and(|re| re.is_match(line.trim_start())) + /// Check if a line is a tag (e.g., #tagname, #project/active) + fn is_tag(line: &str) -> bool { + get_cached_regex(TAG_PATTERN_STR).is_ok_and(|re| re.is_match(line.trim_start())) + } + + /// Whether tag patterns should be recognized for the given flavor + fn tags_enabled(&self, flavor: MarkdownFlavor) -> bool { + self.config.tags_enabled(flavor) } /// Check if an ATX heading line is missing space after the marker @@ -133,9 +137,9 @@ return None; } - // Obsidian flavor: skip tag syntax (#tagname, #project/active, etc.) - // Obsidian tags only use single # - if flavor == MarkdownFlavor::Obsidian && hash_count == 1 && Self::is_obsidian_tag(line) { + // Tags mode: skip tag syntax (#tagname, #project/active, etc.) + // Tags only use single # + if self.tags_enabled(flavor) && hash_count == 1 && Self::is_tag(line) { return None; } @@ -225,8 +229,8 @@ continue; } - // Obsidian flavor: skip tag syntax (#tagname, #project/active, etc.) - if ctx.flavor == MarkdownFlavor::Obsidian && heading.level == 1 && Self::is_obsidian_tag(line) { + // Tags mode: skip tag syntax (#tagname, #project/active, etc.) + if self.tags_enabled(ctx.flavor) && heading.level == 1 && Self::is_tag(line) { continue; } @@ -332,17 +336,11 @@ // MagicLink config: skip MagicLink-style issue/PR refs (#123, #10, etc.) let is_magiclink = self.config.magiclink && heading.level == 1 && Self::is_magiclink_ref(line); - // Obsidian flavor: skip tag syntax (#tagname, #project/active, etc.) - let is_obsidian_tag = - ctx.flavor == MarkdownFlavor::Obsidian && heading.level == 1 && Self::is_obsidian_tag(line); + // Tags mode: skip tag syntax (#tagname, #project/active, etc.) + let is_tag = self.tags_enabled(ctx.flavor) && heading.level == 1 && Self::is_tag(line); // Only attempt fix if not a special pattern - if !is_emoji - && !is_unicode - && !is_magiclink - && !is_obsidian_tag - && trimmed.len() > heading.marker.len() - { + if !is_emoji && !is_unicode && !is_magiclink && !is_tag && trimmed.len() > heading.marker.len() { let after_marker = &trimmed[heading.marker.len()..]; if !after_marker.is_empty() && !after_marker.starts_with(' ') && !after_marker.starts_with('\t') { @@ -1008,7 +1006,10 @@ #[test] fn test_mkdocs_magiclink_skips_numeric_refs() { // With magiclink config enabled, should skip MagicLink-style issue/PR refs (#123, #10, etc.) - let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { magiclink: true }); + let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { + magiclink: true, + ..Default::default() + }); // These numeric patterns should be SKIPPED with magiclink enabled assert!( @@ -1033,7 +1034,10 @@ #[test] fn test_mkdocs_magiclink_still_flags_non_numeric() { // With magiclink config enabled, should still flag non-numeric patterns - let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { magiclink: true }); + let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { + magiclink: true, + ..Default::default() + }); // Non-numeric patterns should still be flagged even with magiclink enabled assert!( @@ -1056,7 +1060,10 @@ #[test] fn test_mkdocs_magiclink_only_single_hash() { // MagicLink only uses single #, so ##10 should still be flagged - let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { magiclink: true }); + let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { + magiclink: true, + ..Default::default() + }); assert!( rule.check_atx_heading_line("##10", MarkdownFlavor::Standard).is_some(), @@ -1087,7 +1094,10 @@ #[test] fn test_mkdocs_magiclink_full_check() { // Integration test: verify magiclink config skips MagicLink refs through full check() flow - let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { magiclink: true }); + let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { + magiclink: true, + ..Default::default() + }); let content = r#"# PRs that are helpful for context @@ -1120,7 +1130,10 @@ #[test] fn test_mkdocs_magiclink_fix_exact_output() { // Verify fix() produces exact expected output with magiclink config - let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { magiclink: true }); + let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { + magiclink: true, + ..Default::default() + }); let content = "#10 discusses the issue.\n\n#Summary"; let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); @@ -1137,7 +1150,10 @@ #[test] fn test_mkdocs_magiclink_edge_cases() { // Test various edge cases for MagicLink pattern matching - let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { magiclink: true }); + let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { + magiclink: true, + ..Default::default() + }); // These should all be SKIPPED with magiclink config (valid MagicLink refs) // Note: #1 alone is skipped due to content length < 2, not MagicLink @@ -1185,7 +1201,10 @@ fn test_mkdocs_magiclink_hyphenated_continuation() { // Hyphenated patterns like #10-related should still be flagged // because they're likely malformed headings, not MagicLink refs - let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { magiclink: true }); + let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { + magiclink: true, + ..Default::default() + }); // Hyphen is not alphanumeric, so #10- would match as MagicLink // But #10-related has alphanumeric after the hyphen @@ -1200,7 +1219,10 @@ #[test] fn test_mkdocs_magiclink_standalone_number() { // #10 alone on a line (common in changelogs) - let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { magiclink: true }); + let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { + magiclink: true, + ..Default::default() + }); let content = "See issue:\n\n#10\n\nFor details."; let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); @@ -1246,7 +1268,10 @@ let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); // With magiclink: preserves #10, fixes #Summary - let rule_magiclink = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { magiclink: true }); + let rule_magiclink = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { + magiclink: true, + ..Default::default() + }); let fixed_magiclink = rule_magiclink.fix(&ctx).unwrap(); assert_eq!(fixed_magiclink, "#10 is an issue\n# Summary"); @@ -1256,6 +1281,78 @@ assert_eq!(fixed_default, "# 10 is an issue\n# Summary"); } + // ==================== Tags config tests ==================== + + #[test] + fn test_tags_config_standard_flavor() { + // tags = true with standard flavor should skip tag patterns + let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { + magiclink: false, + tags: Some(true), + }); + + let content = "#tag\n\n#project/active\n\n##Introduction"; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + + let flagged_lines: Vec<usize> = result.iter().map(|w| w.line).collect(); + assert!(!flagged_lines.contains(&1), "#tag should be skipped with tags = true"); + assert!( + !flagged_lines.contains(&3), + "#project/active should be skipped with tags = true" + ); + assert!(flagged_lines.contains(&5), "##Introduction should still be flagged"); + } + + #[test] + fn test_tags_config_fix_standard_flavor() { + let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { + magiclink: false, + tags: Some(true), + }); + + let content = "#tag\n\n##Introduction"; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let fixed = rule.fix(&ctx).unwrap(); + assert_eq!(fixed, "#tag\n\n## Introduction"); + } + + #[test] + fn test_tags_config_disabled_obsidian_flavor() { + // tags = false with Obsidian flavor should flag tag patterns + let rule = MD018NoMissingSpaceAtx::from_config_struct(MD018Config { + magiclink: false, + tags: Some(false), + }); + + let content = "#tag\n\n#project/active"; + let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None); + let result = rule.check(&ctx).unwrap(); + + assert_eq!( + result.len(), + 2, + "tags = false should flag tag patterns even in Obsidian" + ); + } + + #[test] + fn test_tags_config_default_follows_flavor() { + // Unset tags should default based on flavor + let rule = MD018NoMissingSpaceAtx::new(); // tags: None + + // Standard: should flag + let content = "#tag"; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!(!result.is_empty(), "Default standard should flag #tag"); + + // Obsidian: should skip + let ctx = LintContext::new(content, MarkdownFlavor::Obsidian, None); + let result = rule.check(&ctx).unwrap(); + assert!(result.is_empty(), "Default Obsidian should skip #tag"); + } + // ==================== Obsidian flavor tests ==================== #[test] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/src/rules/md042_no_empty_links.rs new/rumdl-0.1.61/src/rules/md042_no_empty_links.rs --- old/rumdl-0.1.60/src/rules/md042_no_empty_links.rs 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/src/rules/md042_no_empty_links.rs 2026-03-26 23:27:54.000000000 +0100 @@ -261,7 +261,8 @@ // Check for empty destination (URL) only // MD042 is about links that "do not lead anywhere" - focusing on empty destinations // Empty text with valid URL is NOT flagged (that's an accessibility concern, not "empty link") - if effective_url.trim().is_empty() { + let trimmed_url = effective_url.trim(); + if trimmed_url.is_empty() || trimmed_url == "#" { // In MkDocs mode, check if this is an attribute anchor: []() followed by { #anchor } if mkdocs_mode && link.text.trim().is_empty() @@ -655,6 +656,68 @@ } #[test] + fn test_bare_hash_treated_as_empty_url() { + let rule = MD042NoEmptyLinks::new(); + + // [](#) - bare fragment marker with no name is an empty/meaningless URL + let ctx = LintContext::new("# Title\n\n[](#)\n", crate::config::MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert_eq!( + result.len(), + 1, + "[](#) should be flagged as empty link. Got: {result:?}" + ); + assert!(result[0].message.contains("[](#)")); + + // [text](#) - text with bare # URL + let ctx = LintContext::new("# Title\n\n[text](#)\n", crate::config::MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert_eq!( + result.len(), + 1, + "[text](#) should be flagged as empty link. Got: {result:?}" + ); + assert!(result[0].message.contains("[text](#)")); + + // [text]( # ) - bare # with surrounding whitespace + let ctx = LintContext::new( + "# Title\n\n[text]( # )\n", + crate::config::MarkdownFlavor::Standard, + None, + ); + let result = rule.check(&ctx).unwrap(); + assert_eq!( + result.len(), + 1, + "[text]( # ) should be flagged as empty link. Got: {result:?}" + ); + + // [text](#foo) - actual fragment should NOT be flagged + let ctx = LintContext::new( + "# Title\n\n[text](#foo)\n", + crate::config::MarkdownFlavor::Standard, + None, + ); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "[text](#foo) has a real fragment, should NOT be flagged. Got: {result:?}" + ); + + // [](#section) - empty text but valid fragment URL should NOT be flagged + let ctx = LintContext::new( + "# Title\n\n[](#section)\n", + crate::config::MarkdownFlavor::Standard, + None, + ); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "[](#section) has a real URL, should NOT be flagged. Got: {result:?}" + ); + } + + #[test] fn test_reference_link_with_undefined_reference() { // Undefined references are handled by MD052, not MD042 // MD042 should NOT flag [text][undefined] - it's not an "empty link" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/src/rules/md063_heading_capitalization/mod.rs new/rumdl-0.1.61/src/rules/md063_heading_capitalization/mod.rs --- old/rumdl-0.1.60/src/rules/md063_heading_capitalization/mod.rs 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/src/rules/md063_heading_capitalization/mod.rs 2026-03-26 23:27:54.000000000 +0100 @@ -939,6 +939,11 @@ continue; } + // Skip invalid headings (e.g., `#tag` which lacks required space after #) + if !heading.is_valid { + continue; + } + // Apply capitalization and compare let original_text = &heading.raw_text; let fixed_text = self.apply_capitalization(original_text); @@ -1001,6 +1006,11 @@ continue; } + // Skip invalid headings (e.g., `#tag` which lacks required space after #) + if !heading.is_valid { + continue; + } + let original_text = &heading.raw_text; let fixed_text = self.apply_capitalization(original_text); @@ -1253,6 +1263,46 @@ // Acronym preservation tests #[test] + fn test_skip_obsidian_tags_not_headings() { + let rule = create_rule(); + + // #tag (no space after #) is an Obsidian tag, not a heading + let content = "# H1\n\n#tag\n"; + let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty() || result.iter().all(|w| w.line != 3), + "Obsidian tag #tag should not be treated as a heading: {result:?}" + ); + } + + #[test] + fn test_skip_invalid_atx_headings_no_space() { + let rule = create_rule(); + + // #NoSpace is not a valid ATX heading (requires space after #) + let content = "#notaheading\n"; + let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "Invalid ATX heading without space should not be flagged: {result:?}" + ); + } + + #[test] + fn test_fix_skips_obsidian_tags() { + let rule = create_rule(); + + let content = "# hello world\n\n#tag\n"; + let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None); + let fixed = rule.fix(&ctx).unwrap(); + // Should fix the real heading but leave the tag alone + assert!(fixed.contains("#tag"), "Fix should not modify Obsidian tag #tag"); + assert!(fixed.contains("# Hello World"), "Fix should still fix real headings"); + } + + #[test] fn test_preserve_all_caps_acronyms() { let rule = create_rule(); let ctx = |c| LintContext::new(c, crate::config::MarkdownFlavor::Standard, None); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/src/utils/text_reflow.rs new/rumdl-0.1.61/src/utils/text_reflow.rs --- old/rumdl-0.1.60/src/utils/text_reflow.rs 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/src/utils/text_reflow.rs 2026-03-26 23:27:54.000000000 +0100 @@ -92,6 +92,67 @@ } } +/// Build a boolean mask indicating which character positions are inside inline code spans. +/// Handles single, double, and triple backtick delimiters. +fn compute_inline_code_mask(text: &str) -> Vec<bool> { + let chars: Vec<char> = text.chars().collect(); + let len = chars.len(); + let mut mask = vec![false; len]; + let mut i = 0; + + while i < len { + if chars[i] == '`' { + // Count opening backticks + let open_start = i; + let mut backtick_count = 0; + while i < len && chars[i] == '`' { + backtick_count += 1; + i += 1; + } + + // Find matching closing backticks (same count) + let mut found_close = false; + let content_start = i; + while i < len { + if chars[i] == '`' { + let close_start = i; + let mut close_count = 0; + while i < len && chars[i] == '`' { + close_count += 1; + i += 1; + } + if close_count == backtick_count { + // Mark the content between the delimiters (not the backticks themselves) + for item in mask.iter_mut().take(close_start).skip(content_start) { + *item = true; + } + // Also mark the opening and closing backticks + for item in mask.iter_mut().take(content_start).skip(open_start) { + *item = true; + } + for item in mask.iter_mut().take(i).skip(close_start) { + *item = true; + } + found_close = true; + break; + } + } else { + i += 1; + } + } + + if !found_close { + // No matching close — backticks are literal, not code span + i = open_start + backtick_count; + } + } else { + i += 1; + } + } + + mask +} + /// Detect if a character position is a sentence boundary /// Based on the approach from github.com/JoshuaKGoldberg/sentences-per-line /// Supports both ASCII punctuation (. ! ?) and CJK punctuation (。 ! ?) @@ -275,6 +336,9 @@ abbreviations: &HashSet<String>, require_sentence_capital: bool, ) -> Vec<String> { + // Pre-compute which character positions are inside inline code spans + let in_code = compute_inline_code_mask(text); + let mut sentences = Vec::new(); let mut current_sentence = String::new(); let mut chars = text.chars().peekable(); @@ -283,7 +347,7 @@ while let Some(c) = chars.next() { current_sentence.push(c); - if is_sentence_boundary(text, pos, abbreviations, require_sentence_capital) { + if !in_code[pos] && is_sentence_boundary(text, pos, abbreviations, require_sentence_capital) { // Consume any trailing emphasis/strikethrough markers and quotes (they belong to the current sentence) while let Some(&next) = chars.peek() { if next == '*' || next == '_' || next == '~' || is_closing_quote(next) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/tests/cli/cli_flavor_test.rs new/rumdl-0.1.61/tests/cli/cli_flavor_test.rs --- old/rumdl-0.1.60/tests/cli/cli_flavor_test.rs 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/tests/cli/cli_flavor_test.rs 2026-03-26 23:27:54.000000000 +0100 @@ -428,6 +428,118 @@ ); } +/// End-to-end test: MD018 tags config enables tag recognition without Obsidian flavor +#[test] +fn test_md018_tags_config_standard_flavor() { + let temp_dir = tempdir().unwrap(); + + // Create config with tags enabled (no Obsidian flavor) + let config_content = r#" +[MD018] +tags = true +"#; + fs::write(temp_dir.path().join(".rumdl.toml"), config_content).unwrap(); + + // Create markdown with tag patterns and malformed headings + let md_content = r#"# Real Heading + +#todo this is a tag + +#project/active nested tag + +##Introduction + +#123 +"#; + fs::write(temp_dir.path().join("test.md"), md_content).unwrap(); + + // Run with tags config - should skip tags, flag ##Introduction and #123 + let (success, stdout, _stderr) = run_rumdl(temp_dir.path(), &["check", "test.md"]); + assert!(!success, "Should find issues (##Introduction, #123)"); + + let md018_count = stdout.matches("MD018").count(); + assert_eq!( + md018_count, 2, + "With tags=true, should flag exactly 2 MD018 issues (##Introduction, #123). Found {md018_count}. stdout: {stdout}" + ); + + // Tags should NOT be flagged + assert!( + !stdout.contains("test.md:3:"), + "#todo (line 3) should NOT be flagged with tags=true. stdout: {stdout}" + ); + assert!( + !stdout.contains("test.md:5:"), + "#project/active (line 5) should NOT be flagged with tags=true. stdout: {stdout}" + ); +} + +/// End-to-end test: MD018 tags=false overrides Obsidian flavor default +#[test] +fn test_md018_tags_config_override_obsidian() { + let temp_dir = tempdir().unwrap(); + + // Create config with Obsidian flavor but tags explicitly disabled + let config_content = r#" +[global] +flavor = "obsidian" + +[MD018] +tags = false +"#; + fs::write(temp_dir.path().join(".rumdl.toml"), config_content).unwrap(); + + let md_content = r#"# Real Heading + +#todo + +#project/active +"#; + fs::write(temp_dir.path().join("test.md"), md_content).unwrap(); + + // With tags=false, should flag tag patterns even in Obsidian flavor + let (success, stdout, _stderr) = run_rumdl(temp_dir.path(), &["check", "test.md"]); + assert!(!success, "Should find issues with tags=false"); + + let md018_count = stdout.matches("MD018").count(); + assert_eq!( + md018_count, 2, + "With tags=false in Obsidian flavor, should flag tag patterns. Found {md018_count}. stdout: {stdout}" + ); +} + +/// End-to-end test: MD018 tags config fix preserves tags +#[test] +fn test_md018_tags_config_fix_preserves_tags() { + let temp_dir = tempdir().unwrap(); + + let config_content = r#" +[MD018] +tags = true +"#; + fs::write(temp_dir.path().join(".rumdl.toml"), config_content).unwrap(); + + let md_content = "#todo\n\n#Summary\n"; + let md_path = temp_dir.path().join("test.md"); + fs::write(&md_path, md_content).unwrap(); + + let (success, _stdout, stderr) = run_rumdl(temp_dir.path(), &["check", "--fix", "test.md"]); + assert!(success, "Fix command should succeed. stderr: {stderr}"); + + let fixed_content = fs::read_to_string(&md_path).expect("Should read fixed file"); + + // Both #todo and #Summary match the tag pattern (# + non-digit non-space), + // so neither should be modified + assert!( + fixed_content.contains("#todo"), + "#todo should be preserved with tags=true. Fixed content: {fixed_content}" + ); + assert!( + fixed_content.contains("#Summary"), + "#Summary should be preserved with tags=true (matches tag pattern). Fixed content: {fixed_content}" + ); +} + /// Regression test: Fix coordination must respect per-file-flavor configuration. /// /// Bug: FixCoordinator used config.markdown_flavor() (global) instead of diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.60/tests/formats/sentence_per_line_test.rs new/rumdl-0.1.61/tests/formats/sentence_per_line_test.rs --- old/rumdl-0.1.60/tests/formats/sentence_per_line_test.rs 2026-03-25 11:45:42.000000000 +0100 +++ new/rumdl-0.1.61/tests/formats/sentence_per_line_test.rs 2026-03-26 23:27:54.000000000 +0100 @@ -47,6 +47,51 @@ } #[test] +fn test_inline_code_punctuation_not_sentence_boundary() { + let rule = create_sentence_per_line_rule(); + + // Punctuation inside inline code should not be treated as a sentence boundary + let content = "Rust macros look like `foo! {}` with the exclamation mark.\n"; + let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + + assert!( + result.is_empty(), + "Punctuation inside inline code should not split sentences: {result:?}" + ); +} + +#[test] +fn test_inline_code_with_period_not_sentence_boundary() { + let rule = create_sentence_per_line_rule(); + + // Period inside inline code should not be treated as a sentence boundary + let content = "Use `file.txt` as the input file for testing.\n"; + let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + + assert!( + result.is_empty(), + "Period inside inline code should not split sentences: {result:?}" + ); +} + +#[test] +fn test_inline_code_with_question_mark_not_sentence_boundary() { + let rule = create_sentence_per_line_rule(); + + // Question mark inside inline code should not be treated as a sentence boundary + let content = "The regex `is_valid?` matches optional characters.\n"; + let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + + assert!( + result.is_empty(), + "Question mark inside inline code should not split sentences: {result:?}" + ); +} + +#[test] fn test_abbreviations_not_split() { let rule = create_sentence_per_line_rule(); let content = "Mr. Smith met Dr. Jones at 3.14 PM."; ++++++ rumdl.obsinfo ++++++ --- /var/tmp/diff_new_pack.7dGC65/_old 2026-03-27 16:53:40.957934933 +0100 +++ /var/tmp/diff_new_pack.7dGC65/_new 2026-03-27 16:53:40.965935268 +0100 @@ -1,5 +1,5 @@ name: rumdl -version: 0.1.60 -mtime: 1774435542 -commit: 9ab9e256cf266908555448f396c725d50c103919 +version: 0.1.61 +mtime: 1774564074 +commit: 9ce5d163deb00a519597e730f3981c33219752d1 ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/rumdl/vendor.tar.zst /work/SRC/openSUSE:Factory/.rumdl.new.8177/vendor.tar.zst differ: char 7, line 1
