Skip to content

TextRun#text= does nothing on a run with more than one w:t #197

Closed

Description

@satoryu

TextRun#text= only writes when the run has exactly one w:t, or none (it creates one in that case). If the run has two or more, it silently does nothing:

# lib/docx/containers/text_run.rb
def text=(content)
  if @text_nodes.size == 1
    @text_nodes.first.content = content
  elsif @text_nodes.empty?
    ...
  end            # 2+ text nodes: no-op
  reset_text
end

Such runs are valid and common:

  • LibreOffice and macOS textutil keep one run with w:t, w:br/w:tab, w:t.
  • A hyperlink with several runs is wrapped as a single TextRun (w:t|w:r/w:t).

Repro

w = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
node = Nokogiri::XML(%(<w:r xmlns:w="#{w}"><w:t>a</w:t><w:br/><w:t>b</w:t></w:r>)).root
run = Docx::Elements::Containers::TextRun.new(node)
run.text = 'new'
run.text # => "ab" (expected "new")

This also affects Paragraph#text= (single-run paragraphs) and Bookmark#insert_text_before/after. It was the main cause of #187. That issue is being fixed separately by making Paragraph#substitute work on w:t nodes, so it no longer depends on text=.

Proposed behavior: write the whole content into the first w:t and empty the others. A plain string can't say where the breaks and tabs inside the run should go, so they end up after the text. Document that, and point users to Paragraph#substitute when they need breaks to stay in place.

Activity

  1. satoryu commented on Oct 17, 2026

    @satoryu
    MemberAuthor

    Plan update: I'm going with a slightly different fix from the one proposed above.

    TextRun#text= will replace the run's text from its first w:t through its last one. The new text goes into the first w:t. The other w:t, and any w:br / w:tab / w:cr between them, are removed. Breaks outside that range stay, for example a page break (<w:br w:type="page"/>) at the end of the run, as does everything else (w:rPr, drawings, fields).

    I chose this over "fill the first w:t and leave the breaks after the text" because #text never includes breaks or tabs. A plain string can't say where they belong, so keeping them would only move them to the end of the line. With this approach the run reads and renders as exactly what was assigned. It also matches Paragraph#text=, which already replaces all runs when a paragraph has more than one. Runs with a single w:t behave as before.

    Bookmark#insert_text_before/after currently use run.text = .... With the change above, that would delete the adjacent run's line breaks just to add a few characters. So they will write into the w:t right next to the bookmark instead: the end of the preceding run, or the start of the following one. That leaves the rest of that run as it is. Today these calls do nothing on such runs, so this fixes them too.

    For edits that need line breaks to stay in place, Paragraph#substitute (#199) is the right tool.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions