Skip to content

Preserve leading whitespace in wrapped text; expand tabs to spaces#40

Merged
billdenney merged 4 commits into
mainfrom
claude/confident-meninsky-bb063d
Jun 19, 2026
Merged

Preserve leading whitespace in wrapped text; expand tabs to spaces#40
billdenney merged 4 commits into
mainfrom
claude/confident-meninsky-bb063d

Conversation

@billdenney

Copy link
Copy Markdown
Member

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 drop characters 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 0x09 and draws nothing). Tabs are now expanded before wrapping/measuring:

  • leading tab → tab_indent_spaces spaces (default 2)
  • in-line tab → tab_infix_spaces spaces (default 1)

Both counts are advanced knobs surfaced only via ... on export_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 through wrap_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

  • Vignette v03-tfl_table_styling.Rmd: new "Leading spaces" and "Tabs" subsections.
  • design/DECISIONS.md (D-49), ARCHITECTURE.md, TESTING.md updated; roxygen man pages regenerated.

Notes

  • tab_infix_spaces > 1 isn't visible through wrapping because the greedy packer collapses internal whitespace runs (pre-existing behavior); documented as such.
  • The tab knobs are not wired into tfl_table() (table cells use the defaults) to keep its signature lean.

🤖 Generated with Claude Code

billdenney and others added 4 commits June 19, 2026 05:46
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>
@billdenney billdenney merged commit 0a9899e into main Jun 19, 2026
9 checks passed
@billdenney billdenney deleted the claude/confident-meninsky-bb063d branch June 19, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant