Skip to content

Splitting a run #519

Open
Open

Description

@aekoch

I am trying to apply formatting to a document that already exists. I am trying to highlight text within the document, and I would like to preserve formatting around the changes I make.

For example, highlight yellow all occurrences of "test" in the paragraph "This is a test. This is only a test"
In this example the paragraph is in one run to start, but will be in four runs after highlighting ["This is a ", 'test", ". This is only a ", "test"].

It is not immediately obvious how to accomplish this goal. There are no utility functions that I can find for copying the formatting of a run, or for splitting a run into multiple runs, or for adding a run to an arbitrary location in the paragraph. I am considering either constructing a whole new document paragraph by paragraph (grossly inefficient) or manually implementing run.split(start, end) and paragraph.addRunAt(n) functions.

Is there a better way that I'm just missing? If I make the functions I described would they be useful to add to the package?

Activity

  1. scanny commented on Jul 16, 2018

    @scanny
    Contributor

    I think your approach is about right. I think the right general architecture is to create functions or possibly wrapper objects that compose a proxy object (like Paragraph or Run) or the lxml element objects themselves. This way your work can be used by others without them having to use a fork or you having to get a commit (which is harder than most folks are willing to undertake).

    I think I would work at the paragraph level and consider using character position (within the paragraph) as the key parameter(s). So you could get a sequence of runs between character positions 15 and 42, say, and then apply something to all those. The method that gave those to you would split runs if necessary to start and 15 and end at 42.

    I think the formatting copy you could do with a deepcopy on the rPr element, possibly excepting certain attributes like revision-id or whatever might be sensible to skip.

    I think it's an interesting problem, pretty abstract, and an opportunity to use some interesting data structures. I'd love to see what you come up with and happy to consider any questions you come up with as you go.

  2. aekoch commented on Jul 30, 2018

    @aekoch
    Author

    Here's my solution: https://github.com/aekoch/docx_formatting_aekoch.git
    I haven't tested all the edge cases, but as you can see from the result.docx document it correctly applies formatting to all of the occurances of "test" in the original document.

    I am having trouble copying the format though. The main problem is that both the font property of the run and the rPr property of the element throw an Attribute Error when I try to set them. I manually implemented a deep copy of fonts. That's fine for my purposes, but I don't like that there is no general solution. Any thoughts on where I could actually set the rPr property for a run? I think the ability to copy the formatting of one run to another would be useful enough to add to the package, maybe to the Font class.

    The other problem I had was that I needed to access and modify the _p attribute of the paragraph object to get the runs to actually change order. When I changed the order of the runs in the paragraph.runs object it didn't actually affect the order of the runs in the saved document. Messing with the private variables probably isn't a good long term solution, but it was the only way I found to affect the order of the runs.

  3. michaelu123 commented on Nov 26, 2018

    @michaelu123

    It seems to me that the central part of aekochs code is the line
    paragraph._p[index_in_paragraph+1:index_in_paragraph+1] = [new_run.element]
    I tried something similar in my code:
    para._p.insert(runX, new_run._r)
    Our goal is to move a run that was appended to the end of the paragraph.runs list to a different place in the list. In my case I was astonished that this did not work. I splitted the third run of six runs, runX was 3, but the new_run ended up in the second position of the list, irrespective of the value of runX!? PyCharm does not let me debug into the insert call, and after hours of trial and error I came up with this solution:

    def split_run(para, runs, run, x):
        runX = runs.index(run) + 1
        t1 = run.text[0:x]
        t2 = run.text[x:]
        run.text = t1
        new_run = add_run_copy(para, run, text=t2)
        # the insert does not work as expected, the new_run is always inserted into the same place,
        # irrespective of runX
        #    para._p.insert(runX, new_run._r)
        # therefore, we append all runs behind runX AFTER the newly appended run
        # i.e. we copy a b t1 c d t2 to a b t1 t2 c d, by appending c, d
        # this is all trial and error, and completely obscure...
        while runX < len(runs):
            para._p.append(runs[runX]._r)
            runX += 1
        print("splitRes:", " ".join(["<" + run.text + ">" for run in para.runs]))
    
    

    Btw, add_run_copy is mainly a call of paragraph.add_run, then I try to copy as many properties from the run to the copy as possible:

    def add_run_copy(paragraph, run, text=None):
        r = paragraph.add_run(text=run.text if text is None else text, style=run.style)
        r.bold = run.bold
        r.italic = run.italic
        r.underline = run.underline
        r.font.all_caps = run.font.all_caps
        r.font.bold = run.font.bold
        r.font.color.rgb = run.font.color.rgb
        r.font.color.theme_color = run.font.color.theme_color
        #r.font.color.type = run.font.color.type
        r.font.complex_script = run.font.complex_script
        r.font.cs_bold = run.font.cs_bold
        r.font.cs_italic = run.font.cs_italic
        r.font.double_strike = run.font.double_strike
        r.font.emboss = run.font.emboss
        r.font.hidden = run.font.hidden
        r.font.highlight_color = run.font.highlight_color
        r.font.imprint = run.font.imprint
        r.font.italic = run.font.italic
        r.font.math = run.font.math
        r.font.name = run.font.name
        r.font.no_proof = run.font.no_proof
        r.font.outline = run.font.outline
        r.font.rtl = run.font.rtl
        r.font.shadow = run.font.shadow
        r.font.size = run.font.size
        r.font.small_caps = run.font.small_caps
        r.font.snap_to_grid = run.font.snap_to_grid
        r.font.spec_vanish = run.font.spec_vanish
        r.font.strike = run.font.strike
        r.font.subscript = run.font.subscript
        r.font.superscript = run.font.superscript
        r.font.underline = run.font.underline
        r.font.web_hidden = run.font.web_hidden
        return r
    
    

    But can someone (scanny?) perhaps comment on this code, and why the insert did not work?

  4. scanny commented on Nov 26, 2018

    @scanny
    Contributor

    Moving a run element (w:r) (or any element) must take place at the lxml level using lxml calls. No assignment at the proxy-object level (i.e. Run object) will get that done:

    >>> paragraph = document.add_paragraph()
    >>> run = paragraph.add_run('foo')
    >>> paragraph_2 = document.add_paragraph()
    >>> run_2 = paragraph_2.add_run('bar')
    >>> len(paragraph.runs), len(paragraph_2.runs)
    (1, 1)
    >>> paragraph.text, paragraph_2.text
    ('foo', 'bar')
    >>> run._r.addnext(run_2._r)
    >>> len(paragraph.runs), len(paragraph_2.runs)
    (2, 0)
    >>> paragraph.text, paragraph_2.text
    ('foobar', '')

    The basic idea is that when you assign/attach an lxml element to another parent or sibling, that element is moved, not copied.

    The various lxml methods you need are documented here:

    https://lxml.de/api/lxml.etree._Element-class.html

  5. scanny commented on Nov 26, 2018

    @scanny
    Contributor

    Regarding the "cloning" of run formatting, you might try something like this:

    >>> from copy import deepcopy
    >>> run_source, run_target = # ---however you get references to these---
    >>> rPr_source = run_source._r.get_or_add_rPr()
    >>> rPr_clone = deepcopy(rPr_source)
    >>> run_target._r.rPr = rPr_clone

    I'm not sure about the assignment at the end; we might need to do something slightly fancier. Let me know if that doesn't work. It might need something like:

    >>> rPr_target = run_target._r.get_or_add_rPr()
    >>> rPr_target.addnext(rPr_clone)
    >>> run_target._r.remove(rPr_target)
  6. pxxgogo commented on Sep 8, 2019

    @pxxgogo

    @scanny Hi, I just tried your format cloning method mentioned above. I found that I couldn't set run_target._r. which caused AttributeError: "can't set attribute". Do you have any other methods to realize cloning run formats beside @michaelu123's method?

  7. alllexx88 commented on Dec 3, 2020

    @alllexx88

    Hi, I created a small project based on @scanny suggestions. It provides functions to:

    • Insert a run into paragraph after an existing run
    • Copy run formatting
    • Split existing run by indexes, while retaining formatting

    Here it is: https://github.com/alllexx88/python-docx-split-run

  8. BartoszSiwinski commented on Jun 5, 2021

    @BartoszSiwinski

    Hey, funny thing, but this issue is actually part of the challenge project from "Impractical Python Projects" by Lee Vaughn, chapter 6. I comment here in hope that it may be resolved in the future and could be associated with this problem.

  9. scanny commented on May 15, 2026

    @scanny
    Contributor

    This StackOverflow (SO) answer has code for an isolate_run() function that helps with this sort of thing.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions