Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .github/workflows/templates/tinytex-comment-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Update: {{DATE}}

New pattern changes detected.

<details>
<summary>Click to expand diff</summary>

```diff
{{DIFF}}
```

</details>
32 changes: 32 additions & 0 deletions .github/workflows/templates/tinytex-issue-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## TinyTeX Pattern Update: {{DATE}}

The daily TinyTeX regex patterns have changed and need review.

### Pattern Diff

<details>
<summary>Click to expand diff</summary>

```diff
{{DIFF}}
```

</details>

### Next Steps

See [dev-docs/tinytex-pattern-maintenance.md](./dev-docs/tinytex-pattern-maintenance.md) for detailed instructions.

**Review checklist:**

- [ ] Review diff for significant changes
- [ ] Determine if patterns need adaptation
- [ ] Update `parse-error.ts` if needed
- [ ] Add/update filter functions
- [ ] Run tests: `unit\latexmk\parse-error.test.ts`
- [ ] Add test cases for new patterns if needed
- [ ] Close this issue when complete

---

_Generated by [verify-tinytex-patterns.yml](./.github/workflows/verify-tinytex-patterns.yml)_
177 changes: 177 additions & 0 deletions .github/workflows/verify-tinytex-patterns.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
name: Verify TinyTeX Pattern Coverage

on:
schedule:
- cron: '0 2 * * *' # Daily 2am UTC (matches TinyTeX daily release)
workflow_dispatch: # Manual trigger for testing

permissions:
contents: read
issues: write
actions: write

jobs:
verify:
name: Check TinyTeX Pattern Updates
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Download and extract regex.json
env:
GH_TOKEN: ${{ github.token }}
run: |
if ! gh release download daily --repo rstudio/tinytex-releases --pattern "regex.tar.gz"; then
echo "::warning::Failed to download TinyTeX daily release - may not be published yet"
exit 0
fi
tar -xzf regex.tar.gz
echo "✓ Downloaded and extracted regex.json"

- name: Restore cached regex.json
id: cache-restore
uses: actions/cache/restore@v4
with:
path: .cache/regex.json
key: tinytex-regex-latest

- name: Compare versions
id: compare
run: |
if [ -f .cache/regex.json ]; then
if git diff --no-index --quiet .cache/regex.json regex.json; then
echo "changed=false" >> $GITHUB_OUTPUT
echo "first_run=false" >> $GITHUB_OUTPUT
echo "✓ No changes detected"
else
echo "changed=true" >> $GITHUB_OUTPUT
echo "first_run=false" >> $GITHUB_OUTPUT
echo "✗ Changes detected"
git diff --no-index .cache/regex.json regex.json > pattern-diff.txt || true
fi
else
echo "changed=false" >> $GITHUB_OUTPUT
echo "first_run=true" >> $GITHUB_OUTPUT
echo "⚠ No cached version (first run)"
fi

- name: Handle first run
if: steps.compare.outputs.first_run == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
# Get tinytex commit SHA
TINYTEX_COMMIT=$(gh api repos/rstudio/tinytex/commits/main --jq '.sha')
TINYTEX_SHORT=$(echo $TINYTEX_COMMIT | cut -c1-7)

# Count patterns and categories
PATTERN_COUNT=$(jq '[.[] | length] | add' regex.json)
CATEGORY_COUNT=$(jq 'keys | length' regex.json)

# Write GitHub Actions summary
echo "## TinyTeX Pattern Baseline Established" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- **Date:** $(date +%Y-%m-%d)" >> "$GITHUB_STEP_SUMMARY"
echo "- **TinyTeX commit:** [\`$TINYTEX_SHORT\`](https://github.com/rstudio/tinytex/commit/$TINYTEX_COMMIT)" >> "$GITHUB_STEP_SUMMARY"
echo "- **Pattern source:** [R/latex.R](https://github.com/rstudio/tinytex/blob/$TINYTEX_COMMIT/R/latex.R)" >> "$GITHUB_STEP_SUMMARY"
echo "- **Baseline:** $PATTERN_COUNT patterns across $CATEGORY_COUNT categories" >> "$GITHUB_STEP_SUMMARY"
echo "- **Cache key:** tinytex-regex-latest" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No issue created (first run - baseline established)." >> "$GITHUB_STEP_SUMMARY"

# Prepare cache directory
mkdir -p .cache
cp regex.json .cache/regex.json

echo "✓ Baseline established - cache will be saved"

- name: Exit if unchanged
if: steps.compare.outputs.changed == 'false' && steps.compare.outputs.first_run == 'false'
run: |
echo "No pattern changes detected. Cache hit - exiting."
exit 0

- name: Prepare readable diff
if: steps.compare.outputs.changed == 'true'
run: |
# Pretty-print both JSON files for readable diff
if [ -f .cache/regex.json ]; then
jq --sort-keys . .cache/regex.json > old-formatted.json
jq --sort-keys . regex.json > new-formatted.json
git diff --no-index old-formatted.json new-formatted.json > readable-diff.txt || true
else
jq --sort-keys . regex.json > new-formatted.json
echo "First run - no previous version to compare" > readable-diff.txt
fi

- name: Create or update issue
if: steps.compare.outputs.changed == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
ISSUE_TITLE="TinyTeX patterns require review"
CURRENT_DATE=$(date +%Y-%m-%d)

# Search for existing open issue
ISSUE_NUM=$(gh issue list \
--label "tinytex-patterns" \
--state open \
--json number,title \
--jq ".[] | select(.title == \"$ISSUE_TITLE\") | .number")

if [ -z "$ISSUE_NUM" ]; then
echo "No matching issue found, creating new one..."

# Use template and replace placeholders
sed "s|{{DATE}}|$CURRENT_DATE|g" .github/workflows/templates/tinytex-issue-body.md | \
sed -e "/{{DIFF}}/r readable-diff.txt" -e "/{{DIFF}}/d" > issue-body.md

gh issue create \
--title "$ISSUE_TITLE" \
--assignee cderv \
--label "tinytex-patterns" \
--body-file issue-body.md
else
echo "Found existing issue #$ISSUE_NUM, adding comment..."

# Use template and replace placeholders
sed "s|{{DATE}}|$CURRENT_DATE|g" .github/workflows/templates/tinytex-comment-body.md | \
sed -e "/{{DIFF}}/r readable-diff.txt" -e "/{{DIFF}}/d" > comment-body.md

gh issue comment "$ISSUE_NUM" --body-file comment-body.md
fi

- name: Update cache with new patterns
if: steps.compare.outputs.changed == 'true'
run: |
mkdir -p .cache
cp regex.json .cache/regex.json
echo "✓ Cache updated with new patterns"

- name: Delete old cache
if: steps.compare.outputs.changed == 'true'
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
gh cache delete tinytex-regex-latest || echo "No existing cache to delete"

- name: Save new cache
if: steps.compare.outputs.changed == 'true' || steps.compare.outputs.first_run == 'true'
uses: actions/cache/save@v4
with:
path: .cache/regex.json
key: tinytex-regex-latest

- name: Summary
if: always()
run: |
if [ "${{ steps.compare.outputs.first_run }}" == "true" ]; then
echo "✓ Baseline established - cache created"
elif [ "${{ steps.compare.outputs.changed }}" == "true" ]; then
echo "✗ Pattern changes detected - issue created/updated"
else
echo "✓ No pattern changes - cache hit"
fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package/dist/**
/tests/test-out.json
*~
.env
.private-journal/
# deno_std library
src/resources/deno_std/cache
src/vendor-*
Expand Down
118 changes: 118 additions & 0 deletions dev-docs/tinytex-pattern-maintenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Quarto LaTeX engine Pattern Maintenance

Quarto tracks **tinytex** R package's LaTeX error detection patterns to provide helpful diagnostics when LaTeX compilation fails. This document describes the automated verification process and manual adaptation workflow.

## Overview

The R package **tinytex** maintains a comprehensive database of LaTeX error patterns in its parsing error logic, and export this in `regex.json` in its daily release. It can detect missing packages and fonts. We track these patterns because:

- TinyTeX is the distribution maintain by Posit team actively maintains patterns based on user reports (@yihui and @cderv)
- It is used by Quarto (`quarto install tinytex`)
- Every problem will be fixed in the R package first
- Low update frequency (~4 changes/year) makes manual adaptation practical

**Our process:**

- Daily automated check detects when TinyTeX patterns change
- GitHub issue created/updated when changes detected
- Manual review and adaptation for Quarto's usage

## Pattern Differences

tinytex R package and Quarto LaTeX engine use patterns differently:

- R package: Matches patterns line-by-line against log array
- Quarto: Matches patterns against entire log file as string

### Common Adaptations

1. **Direct copy** (most common):

```typescript
// TinyTeX: ".*! LaTeX Error: File [`']([^']+)' not found.*"
// Quarto:
/.*! LaTeX Error: File [`']([^']+)' not found.*/g;
```

2. **Anchored patterns** need multiline flag or anchor removal:

```typescript
// TinyTeX: "^No file ([^`'. ]+[.]fd)[.].*"
// Quarto options:
/^No file ([^`'. ]+[.]fd)[.].*/gm // multiline flag
/.*No file ([^`'. ]+[.]fd)[.].*/g // remove anchor
```

3. **Filter functions** for post-processing:

```typescript
{
regex: /.*! Font [^=]+=([^ ]+).+ not loadable.*/g,
filter: formatFontFilter, // Cleans font names
}
```

## Manual Adaptation Process

When the automated workflow detects TinyTeX pattern changes, it creates/updates a GitHub issue with:

- Date of detection
- Category-by-category count changes
- Full diff of `regex.json` changes

### Adaptation Steps

1. Review the diff:

- Identify added, modified, or removed patterns

2. Update [parse-error.ts](../src/command/render/latexmk/parse-error.ts):

- Add new patterns to `packageMatchers` array
- Convert TinyTeX string patterns to TypeScript regex with `/g` flag
- Add multiline flag `/gm` if pattern uses `^` or `$` anchors
- Add filter function if pattern needs post-processing

3. Test changes

```bash
cd tests
# Windows
pwsh -Command '$env:QUARTO_TESTS_NO_CONFIG="true"; .\run-tests.ps1 unit\latexmk\parse-error.test.ts'
# Linux/macOS
QUARTO_TESTS_NO_CONFIG=true ./run-tests.sh unit/latexmk/parse-error.test.ts
```

4. Commit and close issue

## Verification Workflow

The automated workflow runs daily:

1. Downloads `regex.tar.gz` from [TinyTeX releases](https://github.com/rstudio/tinytex-releases)
2. Extracts and compares `regex.json` with cached version
3. If changed: generates diff and creates/updates issue
4. If unchanged: exits early (no notification)

**Workflow location**: [.github/workflows/verify-tinytex-patterns.yml](../.github/workflows/verify-tinytex-patterns.yml)

**Manual trigger**: Run workflow from GitHub Actions tab when testing or after TinyTeX release announcement

## Current Coverage

**Pattern implementation:** 22 of 23 patterns from TinyTeX (96%)

**Not implemented:**
- `l3backend` pattern for LaTeX3 version mismatch detection
- Reason: Complex context-aware logic required, rare error case

**Test coverage:** All documented TinyTeX error examples are tested

**Important:** Patterns should support both backtick (`` ` ``) and single quote (`'`) for LaTeX error messages

## Resources

- [parse-error.ts](../src/command/render/latexmk/parse-error.ts) - Pattern implementation
- [parse-error.test.ts](../tests/unit/latexmk/parse-error.test.ts) - Unit tests
- [TinyTeX R source](https://github.com/rstudio/tinytex/blob/main/R/latex.R) - How patterns are used in R
- [TinyTeX releases](https://github.com/rstudio/tinytex-releases) - Source of regex.json
22 changes: 17 additions & 5 deletions src/command/render/latexmk/parse-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ const packageMatchers = [
return `${match}.sty`;
},
},
{ regex: /.* File `(.+eps-converted-to.pdf)'.*/g, filter: estoPdfFilter },
{ regex: /.* File [`'](.+eps-converted-to.pdf)'.*/g, filter: estoPdfFilter },
{ regex: /.*xdvipdfmx:fatal: pdf_ref_obj.*/g, filter: estoPdfFilter },

{
Expand All @@ -267,15 +267,27 @@ const packageMatchers = [
return "lua-uni-algos.lua";
},
},
{
regex: /.* Package pdfx Error: No color profile ([^\s]*).*/g,
filter: (_match: string, _text: string) => {
return "colorprofiles.sty";
},
},
{
regex: /.*No file ([^`'. ]+[.]fd)[.].*/g,
filter: (match: string, _text: string) => {
return match.toLowerCase();
},
},
{ regex: /.* Loading '([^']+)' aborted!.*/g },
{ regex: /.*! LaTeX Error: File `([^']+)' not found.*/g },
{ regex: /.*! LaTeX Error: File [`']([^']+)' not found.*/g },
{ regex: /.* [fF]ile ['`]?([^' ]+)'? not found.*/g },
{ regex: /.*the language definition file ([^\s]*).*/g },
{ regex: /.* \\(file ([^)]+)\\): cannot open .*/g },
{ regex: /.*file `([^']+)' .*is missing.*/g },
{ regex: /.*! CTeX fontset `([^']+)' is unavailable.*/g },
{ regex: /.*file [`']([^']+)' .*is missing.*/g },
{ regex: /.*! CTeX fontset [`']([^']+)' is unavailable.*/g },
{ regex: /.*: ([^:]+): command not found.*/g },
{ regex: /.*! I can't find file `([^']+)'.*/g },
{ regex: /.*! I can't find file [`']([^']+)'.*/g },
];

function fontSearchTerm(font: string): string {
Expand Down
Loading
Loading