Skip to content

Commit a5e40c3

Browse files
authored
Merge pull request #13619 from quarto-dev/tinytex/check-pattern-daily
2 parents 038fec8 + d6a0895 commit a5e40c3

File tree

8 files changed

+387
-6
lines changed

8 files changed

+387
-6
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## Update: {{DATE}}
2+
3+
New pattern changes detected.
4+
5+
<details>
6+
<summary>Click to expand diff</summary>
7+
8+
```diff
9+
{{DIFF}}
10+
```
11+
12+
</details>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## TinyTeX Pattern Update: {{DATE}}
2+
3+
The daily TinyTeX regex patterns have changed and need review.
4+
5+
### Pattern Diff
6+
7+
<details>
8+
<summary>Click to expand diff</summary>
9+
10+
```diff
11+
{{DIFF}}
12+
```
13+
14+
</details>
15+
16+
### Next Steps
17+
18+
See [dev-docs/tinytex-pattern-maintenance.md](./dev-docs/tinytex-pattern-maintenance.md) for detailed instructions.
19+
20+
**Review checklist:**
21+
22+
- [ ] Review diff for significant changes
23+
- [ ] Determine if patterns need adaptation
24+
- [ ] Update `parse-error.ts` if needed
25+
- [ ] Add/update filter functions
26+
- [ ] Run tests: `unit\latexmk\parse-error.test.ts`
27+
- [ ] Add test cases for new patterns if needed
28+
- [ ] Close this issue when complete
29+
30+
---
31+
32+
_Generated by [verify-tinytex-patterns.yml](./.github/workflows/verify-tinytex-patterns.yml)_
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
name: Verify TinyTeX Pattern Coverage
2+
3+
on:
4+
schedule:
5+
- cron: '0 2 * * *' # Daily 2am UTC (matches TinyTeX daily release)
6+
workflow_dispatch: # Manual trigger for testing
7+
8+
permissions:
9+
contents: read
10+
issues: write
11+
actions: write
12+
13+
jobs:
14+
verify:
15+
name: Check TinyTeX Pattern Updates
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
21+
22+
- name: Download and extract regex.json
23+
env:
24+
GH_TOKEN: ${{ github.token }}
25+
run: |
26+
if ! gh release download daily --repo rstudio/tinytex-releases --pattern "regex.tar.gz"; then
27+
echo "::warning::Failed to download TinyTeX daily release - may not be published yet"
28+
exit 0
29+
fi
30+
tar -xzf regex.tar.gz
31+
echo "✓ Downloaded and extracted regex.json"
32+
33+
- name: Restore cached regex.json
34+
id: cache-restore
35+
uses: actions/cache/restore@v4
36+
with:
37+
path: .cache/regex.json
38+
key: tinytex-regex-latest
39+
40+
- name: Compare versions
41+
id: compare
42+
run: |
43+
if [ -f .cache/regex.json ]; then
44+
if git diff --no-index --quiet .cache/regex.json regex.json; then
45+
echo "changed=false" >> $GITHUB_OUTPUT
46+
echo "first_run=false" >> $GITHUB_OUTPUT
47+
echo "✓ No changes detected"
48+
else
49+
echo "changed=true" >> $GITHUB_OUTPUT
50+
echo "first_run=false" >> $GITHUB_OUTPUT
51+
echo "✗ Changes detected"
52+
git diff --no-index .cache/regex.json regex.json > pattern-diff.txt || true
53+
fi
54+
else
55+
echo "changed=false" >> $GITHUB_OUTPUT
56+
echo "first_run=true" >> $GITHUB_OUTPUT
57+
echo "⚠ No cached version (first run)"
58+
fi
59+
60+
- name: Handle first run
61+
if: steps.compare.outputs.first_run == 'true'
62+
env:
63+
GH_TOKEN: ${{ github.token }}
64+
run: |
65+
# Get tinytex commit SHA
66+
TINYTEX_COMMIT=$(gh api repos/rstudio/tinytex/commits/main --jq '.sha')
67+
TINYTEX_SHORT=$(echo $TINYTEX_COMMIT | cut -c1-7)
68+
69+
# Count patterns and categories
70+
PATTERN_COUNT=$(jq '[.[] | length] | add' regex.json)
71+
CATEGORY_COUNT=$(jq 'keys | length' regex.json)
72+
73+
# Write GitHub Actions summary
74+
echo "## TinyTeX Pattern Baseline Established" >> "$GITHUB_STEP_SUMMARY"
75+
echo "" >> "$GITHUB_STEP_SUMMARY"
76+
echo "- **Date:** $(date +%Y-%m-%d)" >> "$GITHUB_STEP_SUMMARY"
77+
echo "- **TinyTeX commit:** [\`$TINYTEX_SHORT\`](https://github.com/rstudio/tinytex/commit/$TINYTEX_COMMIT)" >> "$GITHUB_STEP_SUMMARY"
78+
echo "- **Pattern source:** [R/latex.R](https://github.com/rstudio/tinytex/blob/$TINYTEX_COMMIT/R/latex.R)" >> "$GITHUB_STEP_SUMMARY"
79+
echo "- **Baseline:** $PATTERN_COUNT patterns across $CATEGORY_COUNT categories" >> "$GITHUB_STEP_SUMMARY"
80+
echo "- **Cache key:** tinytex-regex-latest" >> "$GITHUB_STEP_SUMMARY"
81+
echo "" >> "$GITHUB_STEP_SUMMARY"
82+
echo "No issue created (first run - baseline established)." >> "$GITHUB_STEP_SUMMARY"
83+
84+
# Prepare cache directory
85+
mkdir -p .cache
86+
cp regex.json .cache/regex.json
87+
88+
echo "✓ Baseline established - cache will be saved"
89+
90+
- name: Exit if unchanged
91+
if: steps.compare.outputs.changed == 'false' && steps.compare.outputs.first_run == 'false'
92+
run: |
93+
echo "No pattern changes detected. Cache hit - exiting."
94+
exit 0
95+
96+
- name: Prepare readable diff
97+
if: steps.compare.outputs.changed == 'true'
98+
run: |
99+
# Pretty-print both JSON files for readable diff
100+
if [ -f .cache/regex.json ]; then
101+
jq --sort-keys . .cache/regex.json > old-formatted.json
102+
jq --sort-keys . regex.json > new-formatted.json
103+
git diff --no-index old-formatted.json new-formatted.json > readable-diff.txt || true
104+
else
105+
jq --sort-keys . regex.json > new-formatted.json
106+
echo "First run - no previous version to compare" > readable-diff.txt
107+
fi
108+
109+
- name: Create or update issue
110+
if: steps.compare.outputs.changed == 'true'
111+
env:
112+
GH_TOKEN: ${{ github.token }}
113+
run: |
114+
ISSUE_TITLE="TinyTeX patterns require review"
115+
CURRENT_DATE=$(date +%Y-%m-%d)
116+
117+
# Search for existing open issue
118+
ISSUE_NUM=$(gh issue list \
119+
--label "tinytex-patterns" \
120+
--state open \
121+
--json number,title \
122+
--jq ".[] | select(.title == \"$ISSUE_TITLE\") | .number")
123+
124+
if [ -z "$ISSUE_NUM" ]; then
125+
echo "No matching issue found, creating new one..."
126+
127+
# Use template and replace placeholders
128+
sed "s|{{DATE}}|$CURRENT_DATE|g" .github/workflows/templates/tinytex-issue-body.md | \
129+
sed -e "/{{DIFF}}/r readable-diff.txt" -e "/{{DIFF}}/d" > issue-body.md
130+
131+
gh issue create \
132+
--title "$ISSUE_TITLE" \
133+
--assignee cderv \
134+
--label "tinytex-patterns" \
135+
--body-file issue-body.md
136+
else
137+
echo "Found existing issue #$ISSUE_NUM, adding comment..."
138+
139+
# Use template and replace placeholders
140+
sed "s|{{DATE}}|$CURRENT_DATE|g" .github/workflows/templates/tinytex-comment-body.md | \
141+
sed -e "/{{DIFF}}/r readable-diff.txt" -e "/{{DIFF}}/d" > comment-body.md
142+
143+
gh issue comment "$ISSUE_NUM" --body-file comment-body.md
144+
fi
145+
146+
- name: Update cache with new patterns
147+
if: steps.compare.outputs.changed == 'true'
148+
run: |
149+
mkdir -p .cache
150+
cp regex.json .cache/regex.json
151+
echo "✓ Cache updated with new patterns"
152+
153+
- name: Delete old cache
154+
if: steps.compare.outputs.changed == 'true'
155+
continue-on-error: true
156+
env:
157+
GH_TOKEN: ${{ github.token }}
158+
run: |
159+
gh cache delete tinytex-regex-latest || echo "No existing cache to delete"
160+
161+
- name: Save new cache
162+
if: steps.compare.outputs.changed == 'true' || steps.compare.outputs.first_run == 'true'
163+
uses: actions/cache/save@v4
164+
with:
165+
path: .cache/regex.json
166+
key: tinytex-regex-latest
167+
168+
- name: Summary
169+
if: always()
170+
run: |
171+
if [ "${{ steps.compare.outputs.first_run }}" == "true" ]; then
172+
echo "✓ Baseline established - cache created"
173+
elif [ "${{ steps.compare.outputs.changed }}" == "true" ]; then
174+
echo "✗ Pattern changes detected - issue created/updated"
175+
else
176+
echo "✓ No pattern changes - cache hit"
177+
fi

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ package/dist/**
1313
/tests/test-out.json
1414
*~
1515
.env
16+
.private-journal/
1617
# deno_std library
1718
src/resources/deno_std/cache
1819
src/vendor-*
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Quarto LaTeX engine Pattern Maintenance
2+
3+
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.
4+
5+
## Overview
6+
7+
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:
8+
9+
- TinyTeX is the distribution maintain by Posit team actively maintains patterns based on user reports (@yihui and @cderv)
10+
- It is used by Quarto (`quarto install tinytex`)
11+
- Every problem will be fixed in the R package first
12+
- Low update frequency (~4 changes/year) makes manual adaptation practical
13+
14+
**Our process:**
15+
16+
- Daily automated check detects when TinyTeX patterns change
17+
- GitHub issue created/updated when changes detected
18+
- Manual review and adaptation for Quarto's usage
19+
20+
## Pattern Differences
21+
22+
tinytex R package and Quarto LaTeX engine use patterns differently:
23+
24+
- R package: Matches patterns line-by-line against log array
25+
- Quarto: Matches patterns against entire log file as string
26+
27+
### Common Adaptations
28+
29+
1. **Direct copy** (most common):
30+
31+
```typescript
32+
// TinyTeX: ".*! LaTeX Error: File [`']([^']+)' not found.*"
33+
// Quarto:
34+
/.*! LaTeX Error: File [`']([^']+)' not found.*/g;
35+
```
36+
37+
2. **Anchored patterns** need multiline flag or anchor removal:
38+
39+
```typescript
40+
// TinyTeX: "^No file ([^`'. ]+[.]fd)[.].*"
41+
// Quarto options:
42+
/^No file ([^`'. ]+[.]fd)[.].*/gm // multiline flag
43+
/.*No file ([^`'. ]+[.]fd)[.].*/g // remove anchor
44+
```
45+
46+
3. **Filter functions** for post-processing:
47+
48+
```typescript
49+
{
50+
regex: /.*! Font [^=]+=([^ ]+).+ not loadable.*/g,
51+
filter: formatFontFilter, // Cleans font names
52+
}
53+
```
54+
55+
## Manual Adaptation Process
56+
57+
When the automated workflow detects TinyTeX pattern changes, it creates/updates a GitHub issue with:
58+
59+
- Date of detection
60+
- Category-by-category count changes
61+
- Full diff of `regex.json` changes
62+
63+
### Adaptation Steps
64+
65+
1. Review the diff:
66+
67+
- Identify added, modified, or removed patterns
68+
69+
2. Update [parse-error.ts](../src/command/render/latexmk/parse-error.ts):
70+
71+
- Add new patterns to `packageMatchers` array
72+
- Convert TinyTeX string patterns to TypeScript regex with `/g` flag
73+
- Add multiline flag `/gm` if pattern uses `^` or `$` anchors
74+
- Add filter function if pattern needs post-processing
75+
76+
3. Test changes
77+
78+
```bash
79+
cd tests
80+
# Windows
81+
pwsh -Command '$env:QUARTO_TESTS_NO_CONFIG="true"; .\run-tests.ps1 unit\latexmk\parse-error.test.ts'
82+
# Linux/macOS
83+
QUARTO_TESTS_NO_CONFIG=true ./run-tests.sh unit/latexmk/parse-error.test.ts
84+
```
85+
86+
4. Commit and close issue
87+
88+
## Verification Workflow
89+
90+
The automated workflow runs daily:
91+
92+
1. Downloads `regex.tar.gz` from [TinyTeX releases](https://github.com/rstudio/tinytex-releases)
93+
2. Extracts and compares `regex.json` with cached version
94+
3. If changed: generates diff and creates/updates issue
95+
4. If unchanged: exits early (no notification)
96+
97+
**Workflow location**: [.github/workflows/verify-tinytex-patterns.yml](../.github/workflows/verify-tinytex-patterns.yml)
98+
99+
**Manual trigger**: Run workflow from GitHub Actions tab when testing or after TinyTeX release announcement
100+
101+
## Current Coverage
102+
103+
**Pattern implementation:** 22 of 23 patterns from TinyTeX (96%)
104+
105+
**Not implemented:**
106+
- `l3backend` pattern for LaTeX3 version mismatch detection
107+
- Reason: Complex context-aware logic required, rare error case
108+
109+
**Test coverage:** All documented TinyTeX error examples are tested
110+
111+
**Important:** Patterns should support both backtick (`` ` ``) and single quote (`'`) for LaTeX error messages
112+
113+
## Resources
114+
115+
- [parse-error.ts](../src/command/render/latexmk/parse-error.ts) - Pattern implementation
116+
- [parse-error.test.ts](../tests/unit/latexmk/parse-error.test.ts) - Unit tests
117+
- [TinyTeX R source](https://github.com/rstudio/tinytex/blob/main/R/latex.R) - How patterns are used in R
118+
- [TinyTeX releases](https://github.com/rstudio/tinytex-releases) - Source of regex.json

src/command/render/latexmk/parse-error.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ const packageMatchers = [
248248
return `${match}.sty`;
249249
},
250250
},
251-
{ regex: /.* File `(.+eps-converted-to.pdf)'.*/g, filter: estoPdfFilter },
251+
{ regex: /.* File [`'](.+eps-converted-to.pdf)'.*/g, filter: estoPdfFilter },
252252
{ regex: /.*xdvipdfmx:fatal: pdf_ref_obj.*/g, filter: estoPdfFilter },
253253

254254
{
@@ -267,15 +267,27 @@ const packageMatchers = [
267267
return "lua-uni-algos.lua";
268268
},
269269
},
270+
{
271+
regex: /.* Package pdfx Error: No color profile ([^\s]*).*/g,
272+
filter: (_match: string, _text: string) => {
273+
return "colorprofiles.sty";
274+
},
275+
},
276+
{
277+
regex: /.*No file ([^`'. ]+[.]fd)[.].*/g,
278+
filter: (match: string, _text: string) => {
279+
return match.toLowerCase();
280+
},
281+
},
270282
{ regex: /.* Loading '([^']+)' aborted!.*/g },
271-
{ regex: /.*! LaTeX Error: File `([^']+)' not found.*/g },
283+
{ regex: /.*! LaTeX Error: File [`']([^']+)' not found.*/g },
272284
{ regex: /.* [fF]ile ['`]?([^' ]+)'? not found.*/g },
273285
{ regex: /.*the language definition file ([^\s]*).*/g },
274286
{ regex: /.* \\(file ([^)]+)\\): cannot open .*/g },
275-
{ regex: /.*file `([^']+)' .*is missing.*/g },
276-
{ regex: /.*! CTeX fontset `([^']+)' is unavailable.*/g },
287+
{ regex: /.*file [`']([^']+)' .*is missing.*/g },
288+
{ regex: /.*! CTeX fontset [`']([^']+)' is unavailable.*/g },
277289
{ regex: /.*: ([^:]+): command not found.*/g },
278-
{ regex: /.*! I can't find file `([^']+)'.*/g },
290+
{ regex: /.*! I can't find file [`']([^']+)'.*/g },
279291
];
280292

281293
function fontSearchTerm(font: string): string {

0 commit comments

Comments
 (0)