├── .gitignore ├── LICENSE ├── README.md ├── run_tools.py ├── test.py ├── test1-highlightened-bear.docx ├── test1-highlightened-tobear.docx ├── test1-highlightened.docx └── test1.docx /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | venv 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2020 Alex Potapenko, https://github.com/alllexx88 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-docx-split-run 2 | 3 | Some `python-docx` run manipulation, namely: 4 | * Insert a run into paragraph at given position number 5 | * Insert a run into paragraph before/after an existing run 6 | * Copy run formatting 7 | * Split existing run by indexes, while retaining formatting 8 | 9 | Inspired by [#519](https://github.com/python-openxml/python-docx/issues/519) `python-docx` issue discussion. 10 | 11 | Project files: 12 | * `run_tools.py`: the actual run manipulation functions, including descriptions 13 | * `test.py`: a simple usage example 14 | * `test1.docx`: used by `test.py` 15 | * `test1-highlightened.docx`, `test1-highlightened-bear.docx`, `test1-highlightened-tobear.docx`: `test.py` created output 16 | 17 | -------------------------------------------------------------------------------- /run_tools.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | 4 | def insert_run_at_position(par, pos, txt=''): 5 | """Insert a new run with text {txt} into paragraph {par} 6 | at given position {pos}. 7 | 8 | Returns the newly created run. 9 | """ 10 | p = par._p 11 | new_run = par.add_run(txt) 12 | p.insert(pos + 1, new_run._r) 13 | 14 | return new_run 15 | 16 | 17 | def insert_run_before(par, run, txt=''): 18 | """Insert a new run with text {txt} into paragraph before given {run}. 19 | 20 | Returns the newly created run. 21 | """ 22 | run_2 = par.add_run(txt) 23 | run._r.addprevious(run_2._r) 24 | 25 | return run_2 26 | 27 | 28 | def insert_run_after(par, run, txt=''): 29 | """Insert a new run with text {txt} into paragraph after given {run}. 30 | 31 | Returns the newly created run. 32 | """ 33 | run_2 = par.add_run(txt) 34 | run._r.addnext(run_2._r) 35 | 36 | return run_2 37 | 38 | 39 | def copy_run_format(run_src, run_dst): 40 | """Copy formatting from {run_src} to {run_dst}. 41 | """ 42 | rPr_target = run_dst._r.get_or_add_rPr() 43 | rPr_target.addnext(deepcopy(run_src._r.get_or_add_rPr())) 44 | run_dst._r.remove(rPr_target) 45 | 46 | 47 | def split_run_by(par, run, split_by): 48 | """Split text in {run} from paragraph {par} by positions 49 | provided by {split_by}, while retaining original {run} 50 | formatting. 51 | 52 | Returns list of split runs starting with original {run}. 53 | """ 54 | txt = run.text 55 | txt_len = len(txt) 56 | if not all(isinstance(i, int) for i in split_by): 57 | raise ValueError("Split positions must be integer numbers") 58 | split_list = [i if i >= 0 else txt_len + i for i in split_by] 59 | if not all(split_list[j] <= split_list[j + 1] 60 | for j in range(len(split_list) - 1)): 61 | raise ValueError("Split positions must be sorted to make sense") 62 | if split_list[0] < 0: 63 | raise ValueError("A split position cannot be less than -") 64 | split_list.insert(0, 0) 65 | split_list.append(None) 66 | split_txts = [txt[split_list[i]:split_list[i + 1]] 67 | for i in range(len(split_list) - 1)] 68 | run.text = split_txts[0] 69 | split_txts.pop(0) 70 | new_runs = [run] 71 | for next_txt in split_txts: 72 | new_runs.append(insert_run_after(par, new_runs[-1], next_txt)) 73 | copy_run_format(run, new_runs[-1]) 74 | 75 | return new_runs 76 | 77 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from docx import Document 2 | from run_tools import * 3 | from docx.enum.text import WD_COLOR_INDEX 4 | from docx.shared import RGBColor 5 | 6 | 7 | d = Document('test1.docx') 8 | 9 | par = d.paragraphs[0] 10 | run = par.runs[1] 11 | 12 | new_runs = split_run_by(par, run, (1, 2)) 13 | new_runs[0].font.highlight_color = WD_COLOR_INDEX.RED 14 | new_runs[1].font.highlight_color = WD_COLOR_INDEX.YELLOW 15 | new_runs[2].font.highlight_color = WD_COLOR_INDEX.GREEN 16 | 17 | d.save('test1-highlightened.docx') 18 | 19 | another_run = insert_run_at_position(par, 2, 'e') 20 | copy_run_format(run, another_run) 21 | another_run.font.highlight_color = WD_COLOR_INDEX.PINK 22 | 23 | d.save('test1-highlightened-bear.docx') 24 | 25 | to_run = insert_run_before(par, run, 'to') 26 | copy_run_format(run, to_run) 27 | to_run.font.highlight_color = WD_COLOR_INDEX.BLACK 28 | to_run.font.color.rgb = RGBColor(0xff, 0xff, 0xff) 29 | 30 | d.save('test1-highlightened-tobear.docx') 31 | -------------------------------------------------------------------------------- /test1-highlightened-bear.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alllexx88/python-docx-split-run/0eadf165d8ce55f9281de6e02260128fbe1da31c/test1-highlightened-bear.docx -------------------------------------------------------------------------------- /test1-highlightened-tobear.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alllexx88/python-docx-split-run/0eadf165d8ce55f9281de6e02260128fbe1da31c/test1-highlightened-tobear.docx -------------------------------------------------------------------------------- /test1-highlightened.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alllexx88/python-docx-split-run/0eadf165d8ce55f9281de6e02260128fbe1da31c/test1-highlightened.docx -------------------------------------------------------------------------------- /test1.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alllexx88/python-docx-split-run/0eadf165d8ce55f9281de6e02260128fbe1da31c/test1.docx --------------------------------------------------------------------------------