diff --git a/CHANGELOG.md b/CHANGELOG.md index 555f131..2af7e3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,8 @@ All notable changes to this project are documented in this file. The format is b ### Fixed +- A change containing something LaTeX cannot carry there now stops with a message naming the construct and the way around it, instead of a cryptic error from TeX or xcolor that never mentioned this package. This covers `\verb`, verbatim-like environments, an alignment tab that would make a change span table cells, and a footnote inside a change. Citations, indexing, cross-references, and changes within a single table cell were checked and work as before. - Change commands survive the contexts a document actually moves them through. A change with an `id` inside a heading, a caption, or anywhere else that gets read a second time (a table of contents, a running header) no longer fails compilation with a duplicate-ID error, no longer records the change once per echo in the change report, and no longer leaves struck-through markup in a heading's PDF bookmark; hyperref's bookmark now shows the same resolved text a table of contents does. A margin or todo comment inside a float, a footnote, or a minipage no longer aborts the document with "Float(s) lost"; it falls back to an inline comment with a one-time warning, since `\marginpar` cannot be placed there. Report entries for a change written in math mode, or containing any token that only works in the context it came from, are shown as literal source text instead of being retypeset outside that context. - -### Fixed - - The documentation site stops widening past its design width. The sidebar, the content, the table of contents and the header bar now share one pair of outer edges on a large monitor instead of leaving an empty gutter on the right. - The theme and language pickers show one caret arrow rather than two. diff --git a/README.md b/README.md index fc655ac..4682ee2 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,17 @@ Bug reports and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md ## Limitations -- Complex display math, floats, headings, verbatim content, and some commands should be changed at a larger text boundary or reviewed with `latexdiff`. -- The merge CLI intentionally skips comments and common verbatim-like environments. Custom verbatim environments require manual review. +Four constructs cannot appear inside a change. Each stops compilation with a message naming the construct and the way around it, rather than a cryptic error from TeX or another package. + +| Inside a change | Instead | +|---|---| +| `\verb` | Move the verbatim text outside the change, or mark the whole paragraph. | +| `verbatim`, `lstlisting` and similar environments | Same: mark a boundary that contains the environment rather than one inside it. | +| An alignment tab `&` | A change cannot span table cells. Mark each cell separately. | +| `\footnote` | Put the footnote immediately after the change rather than inside it. | + +The first three fail the same way inside any LaTeX command that reads its argument, `\textbf` included, so they are properties of LaTeX rather than of this package. Citations, `\index`, `\ref`, and changes within a single table cell all work. + +The merge CLI intentionally skips comments and common verbatim-like environments. Custom verbatim environments require manual review. Maintained by Phuc Nguyen. Released under LPPL 1.3c or later. See `LICENSE`. diff --git a/scripts/test.sh b/scripts/test.sh index 0bb2e19..c0353a5 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -189,19 +189,47 @@ case_localization() { assert_contains "$TASK_TMP_DIR/localization.txt" "DANH SACH" } -case_error_fixtures() { +# A fixture that must fail, and must fail for the stated reason. Asserting +# only that compilation stops is not enough: every one of these constructs +# already stopped compilation before the package diagnosed them, with an +# error naming TeX or xcolor internals. The message assertion is what +# distinguishes a diagnosed context from a cryptic crash. +assert_fixture_fails() { + local name="$1" + local expected="$2" if TEXINPUTS="$PROJECT_ROOT:" pdflatex -halt-on-error -interaction=nonstopmode \ - -output-directory="$TASK_TMP_DIR" "$PROJECT_ROOT/tests/duplicate-id.tex" >/dev/null 2>&1; then - printf 'Expected duplicate change ID compilation to fail\n' >&2 + -output-directory="$TASK_TMP_DIR" "$PROJECT_ROOT/tests/$name.tex" >/dev/null 2>&1; then + printf 'Expected %s to fail compilation\n' "$name" >&2 exit 1 fi - if TEXINPUTS="$PROJECT_ROOT:" pdflatex -halt-on-error -interaction=nonstopmode \ - -output-directory="$TASK_TMP_DIR" "$PROJECT_ROOT/tests/undefined-author.tex" >/dev/null 2>&1; then - printf 'Expected undefined author compilation to fail\n' >&2 + # TeX wraps the log at 79 columns, so a message long enough to be useful is + # split across lines and a line-based grep misses it. Join the log first. + if ! tr -d '\n' < "$TASK_TMP_DIR/$name.log" | grep -Fq "$expected"; then + printf 'Expected %s in %s\n' "$expected" "$TASK_TMP_DIR/$name.log" >&2 exit 1 fi } +case_error_fixtures() { + assert_fixture_fails duplicate-id "Duplicate change ID" + assert_fixture_fails undefined-author "Undefined author" + + # Contexts a change cannot survive. Each was measured to abort with an + # error naming TeX or xcolor internals and never texchanges, leaving the + # user no clue which construct offended. + assert_fixture_fails verb-in-change "cannot be used inside a change" + assert_fixture_fails verbatim-in-change "The verbatim environment cannot be used inside a change" + assert_fixture_fails alignment-in-change "An alignment tab (&) cannot be used inside a change" + assert_fixture_fails footnote-in-change "A footnote cannot be used inside a change" + + # The old cryptic errors must not be what stops these documents any more. + local fixture + for fixture in verb-in-change verbatim-in-change alignment-in-change footnote-in-change; do + assert_not_contains "$TASK_TMP_DIR/$fixture.log" "XC@col@rlet" + assert_not_contains "$TASK_TMP_DIR/$fixture.log" "Not allowed in LR mode" + done +} + case_engines() { local engine for engine in xelatex lualatex; do diff --git a/tests/alignment-in-change.tex b/tests/alignment-in-change.tex new file mode 100644 index 0000000..ee1005a --- /dev/null +++ b/tests/alignment-in-change.tex @@ -0,0 +1,7 @@ +\documentclass{article} +\usepackage{texchanges} +\begin{document} +\begin{tabular}{ll} +\txadd{a & b} \\ +\end{tabular} +\end{document} diff --git a/tests/footnote-in-change.tex b/tests/footnote-in-change.tex new file mode 100644 index 0000000..87f7137 --- /dev/null +++ b/tests/footnote-in-change.tex @@ -0,0 +1,5 @@ +\documentclass{article} +\usepackage{texchanges} +\begin{document} +Text \txadd{added\footnote{note}} here. +\end{document} diff --git a/tests/verb-in-change.tex b/tests/verb-in-change.tex new file mode 100644 index 0000000..66172a6 --- /dev/null +++ b/tests/verb-in-change.tex @@ -0,0 +1,5 @@ +\documentclass{article} +\usepackage{texchanges} +\begin{document} +Code \txadd{\verb|x=1|} inline. +\end{document} diff --git a/tests/verbatim-in-change.tex b/tests/verbatim-in-change.tex new file mode 100644 index 0000000..210827c --- /dev/null +++ b/tests/verbatim-in-change.tex @@ -0,0 +1,7 @@ +\documentclass{article} +\usepackage{texchanges} +\begin{document} +\txadd{\begin{verbatim} +block +\end{verbatim}} +\end{document} diff --git a/texchanges.sty b/texchanges.sty index 63f9729..c10c89a 100644 --- a/texchanges.sty +++ b/texchanges.sty @@ -435,6 +435,76 @@ } } } +% -------------------------------------------------------------------------- +% Diagnostics for contexts a change cannot survive +% +% Each construct below aborts compilation with an error naming TeX or xcolor +% internals rather than this package, leaving no clue which construct +% offended. Every one was measured, and the message quoted in each comment is +% what a user saw before these guards existed. +% +% Three of them fail the same way inside plain \textbf, so they are LaTeX +% limits this package inherits rather than defects of its own; the guards +% exist to name them, not to make them work. The footnote case is different +% and is explained at its own guard. +% -------------------------------------------------------------------------- +\cs_new_protected:Npn \tx_error_context:nn #1#2 + {\PackageError{texchanges}{#1~cannot~be~used~inside~a~change}{#2}} + +% \verb reached this far only because the argument was already tokenized when +% xparse read it, so its catcode trickery cannot work. LaTeX's own guard does +% not fire here, and the failure surfaced as "Argument of \XC@col@rlet has an +% extra }". Redefined inside the change's group, so it is restored on exit. +\cs_new_protected:Npn \tx_guard_verbatim: + { + \cs_set_protected:Npn \verb + {\tx_error_context:nn {\string\verb}{Move~the~verbatim~text~outside~the~change,~or~mark~the~change~at~a~larger~boundary~such~as~the~whole~paragraph.}} + \clist_map_inline:nn {verbatim,verbatim*,Verbatim,lstlisting,minted} + { + \cs_if_exist:cT {##1} + {\cs_set_protected:cpn {##1} + {\tx_error_context:nn {The~##1~environment}{Move~the~environment~outside~the~change,~or~mark~the~change~at~a~larger~boundary~such~as~the~whole~paragraph.}}} + } + } + +% An alignment tab inside a change means the change spans table cells, which +% cannot work: the argument is one balanced group and & only has meaning to +% the alignment that encloses it. Failed as "Missing } inserted". Detected by +% catcode rather than by character, so an escaped \& in ordinary prose, which +% is catcode 12, does not trip it. +\bool_new:N \l_tx_found_align_bool +\cs_new_protected:Npn \tx_check_alignment:n #1 + { + \tl_analysis_map_inline:nn {#1} + {\int_compare:nNnT {"##3} = {4} {\bool_set_true:N \l_tx_found_align_bool}} + } + +% A footnote inside a change breaks the change report, not the markup. The +% report line is written with \addtocontents, and a footnote in written text +% derails the write: reproduced with no part of this package involved, as +% \addtocontents{x}{\protect\cmd{text\footnote{n}}}, which fails with the +% same error. Writing the text unexpanded does not help. The construct is +% also ambiguous by nature, since a report entry has no sensible footnote of +% its own, so this is a diagnosis rather than a defect to fix. Placing the +% footnote immediately after the change was verified to compile. +\cs_new_protected:Npn \tx_check_footnote:n #1 + { + \tl_if_in:nnT {#1} {\footnote} + {\tx_error_context:nn {A~footnote}{Put~the~\string\footnote~immediately~after~the~change~instead~of~inside~it.}} + } + +\cs_new_protected:Npn \tx_check_context:nn #1#2 + { + \bool_set_false:N \l_tx_found_align_bool + \tx_check_alignment:n {#1} + \tx_check_alignment:n {#2} + \bool_if:NT \l_tx_found_align_bool + {\tx_error_context:nn {An~alignment~tab~(&)}{A~change~cannot~span~table~cells.~Mark~each~cell~separately.}} + \tx_check_footnote:n {#1} + \tx_check_footnote:n {#2} + \tx_guard_verbatim: + } + \cs_new_protected:Npn \tx_change_begin:nn #1#2 { \tx_change_parse:n {#2} @@ -483,10 +553,11 @@ } } \cs_new_protected:Npn \tx_change:nnnn #1#2#3#4 - {\group_begin:\tx_change_begin:nn{#1}{#2}\tx_record:nn{#1}{#4}\tx_render:nn{#3}{#4}\group_end:} + {\group_begin:\tx_check_context:nn{#3}{#4}\tx_change_begin:nn{#1}{#2}\tx_record:nn{#1}{#4}\tx_render:nn{#3}{#4}\group_end:} \cs_new_protected:Npn \tx_compat_change:nnnn #1#2#3#4 { \group_begin: + \tx_check_context:nn{#3}{#4} \tx_change_reset: \keys_set:nn {tx/compat-change}{#2} \tx_change_validate: diff --git a/website/src/content/docs/roadmap.md b/website/src/content/docs/roadmap.md index 631c413..f75cad7 100644 --- a/website/src/content/docs/roadmap.md +++ b/website/src/content/docs/roadmap.md @@ -23,9 +23,9 @@ This is a living checklist. Features will be grouped into future minor releases - [ ] GitHub review annotations and CI summaries - [ ] Robust and accessible authoring - - [ ] Safe markup in headings, captions, footnotes, floats, and math + - [x] Safe markup in headings, captions, footnotes, floats, and math + - [x] Clear diagnostics for the contexts a change cannot survive: verbatim, alignment tabs, and footnotes. Citations, indexing, and cross-references were measured to work already and needed no change. - [ ] Accessible visual presets and tagged-PDF support where available - - [ ] Clear diagnostics for unsupported citation and verbatim contexts - [ ] Editor and optional browser tooling - [ ] Standard-library language server and thin VS Code client