Preserve leading whitespace in wrapped text; expand tabs to spaces#40
Merged
Conversation
The word-wrap module dropped a line's leading whitespace: the tokenizer treats a run of `drop` characters as a between-token separator and `.wrap_paragraph()` discarded the first token's separator, so an indented string like " Indented label" rendered without its indent. grid's own textGrob() renders leading spaces (a 3-space prefix measures ~0.139"), so the loss was purely in the wrap module. Leading whitespace is now preserved as a hanging indent: the prefix is re-attached to every wrapped line and its width is charged against the line so wrapping accounts for the indent. The per-column floor (.column_min_token_width_in) folds in the indent so narrowing cannot clip indented text. Tabs cannot be rendered by the PDF device (it warns "font width unknown for character 0x09"). They are now expanded to spaces before wrapping: a leading tab -> tab_indent_spaces (default 2), an in-line tab -> tab_infix_spaces (default 1). These two counts are advanced knobs surfaced only via `...` on export_tfl()/export_tfl_page() (forwarded to character content, captions and footnotes); table cells and headers use the defaults. See design/DECISIONS.md D-49. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes the R CMD check "Rd cross-references" WARNING that failed CI: the roxygen links [.convert_tabs()] and [.wrap_text()] pointed to functions with no Rd page. The tab-expansion counts (tab_indent_spaces, tab_infix_spaces) are now defined with their defaults in exactly one place -- .convert_tabs() -- which gains a roxygen block (so it has an Rd) and a `...` that absorbs unrelated pass-through args (e.g. overlap_warn_mm). Every function above it -- .wrap_paragraph(), .wrap_string(), .wrap_text(), wrap_normalized_text(), draw_content(), export_tfl_page() -- now forwards `...` instead of carrying its own copies of the args and defaults. export_tfl_page() no longer reads or validates the counts; they flow straight through to .convert_tabs(). Its own overlap_warn_mm dot-arg moved to @details so @inheritDotParams can own the `...` item. Documentation is shared via @inheritDotParams .convert_tabs tab_indent_spaces tab_infix_spaces, removing the duplicated descriptions on .wrap_string(), wrap_normalized_text(), draw_content(), and export_tfl_page(). R CMD check: 0 errors, 0 warnings. Full test suite passing (1158). See design/DECISIONS.md D-49. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ry .convert_tabs call - export_tfl_page: document overlap_warn_mm via a second `@inheritDotParams check_overlap overlap_warn_mm` (roxygen merges multiple into the one `...` item) instead of a hand-written @details paragraph. Removes the @details and the code comment about tab args flowing through, so no `...` arg is described by hand. - .convert_tabs: trim the roxygen prose that explained the `...` flow-through; keep only the @param descriptions. - .column_min_token_width_in: take `...` and forward it to .convert_tabs(p, ...) so EVERY .convert_tabs() call site receives the tab knobs (this floor-measurement call previously used defaults only, which could disagree with the drawn text under a non-default tab width). Add a test that forwards tab_indent_spaces and fails without the forwarding. R CMD check: 0 errors, 0 warnings. Full suite passing (1159). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Config/testthat/parallel: true (edition 3 was already set) with Config/testthat/start-first listing the slowest connector/rendering files (tfl_table, gt, integration, table1, flextable, rtables) so they launch on the first wave of workers. ~35-40% faster on 8 cores (~184s -> ~115s); all 22 files run, 608 blocks, 0 fail/skip/warn; R CMD check --as-cran clean. Parallel runs each file in its own worker subprocess, and a worker runs several files in sequence, so a test must not depend on ambient global state left by another file. Fix the ".measure_text_dims_in fails fast without an active device" guard test, which relied on the null-device state (dev.cur() == 1L): sequentially that held, but under parallel a device left open by an earlier file in the same worker was still current, so the guard did not fire. The test now closes any open device first to establish its own precondition. See design/DECISIONS.md D-50 and design/TESTING.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related fixes to the word-wrap module (
R/wrap.R), reported against "prefixed spaces on text in a box":Leading whitespace is preserved (as a hanging indent)
The tokenizer treats a run of
dropcharacters as a between-token separator, and.wrap_paragraph()discarded the first token's separator — so an indented label like" Indented label"rendered with its indent stripped. (grid::textGrob()itself renders leading spaces — a 3-space prefix measures ~0.139" — so the loss was purely in the wrap module.)Now the leading whitespace run is re-attached to every wrapped line of the paragraph, and its width is charged against the line so wrapping accounts for the indent. The per-column floor (
.column_min_token_width_in) folds in the indent so a narrowed column can't clip indented text.Tabs are expanded to spaces
The PDF device can't render the tab glyph (it warns
font width unknown for character 0x09and draws nothing). Tabs are now expanded before wrapping/measuring:tab_indent_spacesspaces (default 2)tab_infix_spacesspaces (default 1)Both counts are advanced knobs surfaced only via
...onexport_tfl()/export_tfl_page()(forwarded to character content, captions, and footnotes) — not added to the main signatures, documented only under@param .... Table cells and headers use the defaults.Tests
test-wrap.R:.leading_drop_run(),.convert_tabs(), exact leading-space count preservation, hanging indent across wraps, whitespace-only, per-paragraph indentation, tab expansion + custom counts.test-normalize.R: tab knobs forwarded throughwrap_normalized_text().test-export_tfl_page.R: tabbed character content renders without the device warning;...knobs accepted and validated.Full suite: 1159 passing, 0 failures, 0 warnings.
Docs
v03-tfl_table_styling.Rmd: new "Leading spaces" and "Tabs" subsections.design/DECISIONS.md(D-49),ARCHITECTURE.md,TESTING.mdupdated; roxygen man pages regenerated.Notes
tab_infix_spaces > 1isn't visible through wrapping because the greedy packer collapses internal whitespace runs (pre-existing behavior); documented as such.tfl_table()(table cells use the defaults) to keep its signature lean.🤖 Generated with Claude Code