Fix issue with global justify, and add indentation to DocX

This commit is contained in:
Veronica Berglyd Olsen
2024-10-19 20:14:01 +02:00
parent a0ab507ed1
commit de6c3e9b45
6 changed files with 112 additions and 38 deletions
+63 -15
View File
@@ -110,7 +110,6 @@ S_HEAD2 = "Heading2"
S_HEAD3 = "Heading3" S_HEAD3 = "Heading3"
S_HEAD4 = "Heading4" S_HEAD4 = "Heading4"
S_SEP = "Separator" S_SEP = "Separator"
S_FIND = "FirstLineIndent"
S_META = "TextMeta" S_META = "TextMeta"
# Colours # Colours
@@ -132,10 +131,12 @@ class DocXParStyle(NamedTuple):
before: float | None = None before: float | None = None
after: float | None = None after: float | None = None
line: float | None = None line: float | None = None
indentFirst: float | None = None
align: str | None = None align: str | None = None
default: bool = False default: bool = False
level: int | None = None level: int | None = None
color: str | None = None color: str | None = None
bold: bool = False
class ToDocX(Tokenizer): class ToDocX(Tokenizer):
@@ -217,7 +218,8 @@ class ToDocX(Tokenizer):
"""Convert the list of text tokens into XML elements.""" """Convert the list of text tokens into XML elements."""
self._result = "" # Not used, but cleared just in case self._result = "" # Not used, but cleared just in case
# xText = self._xText bIndent = self._fontSize * self._blockIndent
for tType, _, tText, tFormat, tStyle in self._tokens: for tType, _, tText, tFormat, tStyle in self._tokens:
# Create Paragraph # Create Paragraph
@@ -232,8 +234,8 @@ class ToDocX(Tokenizer):
par.setAlignment("right") par.setAlignment("right")
elif tStyle & self.A_CENTRE: elif tStyle & self.A_CENTRE:
par.setAlignment("center") par.setAlignment("center")
# elif tStyle & self.A_JUSTIFY: elif tStyle & self.A_JUSTIFY:
# oStyle.setTextAlign("justify") par.setAlignment("both")
if tStyle & self.A_PBB: if tStyle & self.A_PBB:
par.setPageBreakBefore(True) par.setPageBreakBefore(True)
@@ -245,17 +247,17 @@ class ToDocX(Tokenizer):
if tStyle & self.A_Z_TOPMRG: if tStyle & self.A_Z_TOPMRG:
par.setMarginTop(0.0) par.setMarginTop(0.0)
# if tStyle & self.A_IND_L: if tStyle & self.A_IND_T:
# oStyle.setMarginLeft(self._fBlockIndent) par.setIndentFirst(True)
# if tStyle & self.A_IND_R: if tStyle & self.A_IND_L:
# oStyle.setMarginRight(self._fBlockIndent) par.setLeftMargin(bIndent)
if tStyle & self.A_IND_R:
par.setRightMargin(bIndent)
# Process Text Types # Process Text Types
if tType == self.T_TEXT: if tType == self.T_TEXT:
# Text indentation is processed here because there is a if self._doJustify and "\n" in tText:
# dedicated pre-defined style for it par.overrideJustify(self._defaultAlign)
# if tStyle & self.A_IND_T:
# else:
self._processFragments(par, S_NORM, tText, tFormat) self._processFragments(par, S_NORM, tText, tFormat)
elif tType == self.T_TITLE: elif tType == self.T_TITLE:
@@ -595,6 +597,7 @@ class ToDocX(Tokenizer):
fSz2 = (nwStyles.H_SIZES[2] * fSz) if hScale else fSz fSz2 = (nwStyles.H_SIZES[2] * fSz) if hScale else fSz
fSz3 = (nwStyles.H_SIZES[3] * fSz) if hScale else fSz fSz3 = (nwStyles.H_SIZES[3] * fSz) if hScale else fSz
fSz4 = (nwStyles.H_SIZES[4] * fSz) if hScale else fSz fSz4 = (nwStyles.H_SIZES[4] * fSz) if hScale else fSz
align = "both" if self._doJustify else "left"
# Add Normal Style # Add Normal Style
self._addParStyle(DocXParStyle( self._addParStyle(DocXParStyle(
@@ -605,6 +608,8 @@ class ToDocX(Tokenizer):
before=fSz * self._marginText[0], before=fSz * self._marginText[0],
after=fSz * self._marginText[1], after=fSz * self._marginText[1],
line=fSz * self._lineHeight, line=fSz * self._lineHeight,
indentFirst=fSz * self._firstWidth,
align=align,
)) ))
# Add Title # Add Title
@@ -618,6 +623,7 @@ class ToDocX(Tokenizer):
after=fSz * self._marginTitle[1], after=fSz * self._marginTitle[1],
line=fSz0 * self._lineHeight, line=fSz0 * self._lineHeight,
level=0, level=0,
bold=self._boldHeads,
)) ))
# Add Heading 1 # Add Heading 1
@@ -632,6 +638,7 @@ class ToDocX(Tokenizer):
line=fSz1 * self._lineHeight, line=fSz1 * self._lineHeight,
level=0, level=0,
color=COL_HEAD_L12 if hColor else None, color=COL_HEAD_L12 if hColor else None,
bold=self._boldHeads,
)) ))
# Add Heading 2 # Add Heading 2
@@ -646,6 +653,7 @@ class ToDocX(Tokenizer):
line=fSz2 * self._lineHeight, line=fSz2 * self._lineHeight,
level=1, level=1,
color=COL_HEAD_L12 if hColor else None, color=COL_HEAD_L12 if hColor else None,
bold=self._boldHeads,
)) ))
# Add Heading 3 # Add Heading 3
@@ -660,6 +668,7 @@ class ToDocX(Tokenizer):
line=fSz3 * self._lineHeight, line=fSz3 * self._lineHeight,
level=1, level=1,
color=COL_HEAD_L34 if hColor else None, color=COL_HEAD_L34 if hColor else None,
bold=self._boldHeads,
)) ))
# Add Heading 4 # Add Heading 4
@@ -674,6 +683,7 @@ class ToDocX(Tokenizer):
line=fSz4 * self._lineHeight, line=fSz4 * self._lineHeight,
level=1, level=1,
color=COL_HEAD_L34 if hColor else None, color=COL_HEAD_L34 if hColor else None,
bold=self._boldHeads,
)) ))
# Add Separator # Add Separator
@@ -737,6 +747,8 @@ class ToDocX(Tokenizer):
xmlSubElem(rPr, _wTag("szCs"), attrib={_wTag("val"): str(int(2.0 * size))}) xmlSubElem(rPr, _wTag("szCs"), attrib={_wTag("val"): str(int(2.0 * size))})
if style.color: if style.color:
xmlSubElem(rPr, _wTag("color"), attrib={_wTag("val"): style.color}) xmlSubElem(rPr, _wTag("color"), attrib={_wTag("val"): style.color})
if style.bold:
xmlSubElem(rPr, _wTag("b"))
self._styles[style.styleId] = style self._styles[style.styleId] = style
@@ -746,8 +758,9 @@ class ToDocX(Tokenizer):
class DocXParagraph: class DocXParagraph:
__slots__ = ( __slots__ = (
"_content", "_style", "_textAlign", "_topMargin", "_bottomMargin", "_content", "_style", "_textAlign",
"_breakBefore", "_breakAfter", "_topMargin", "_bottomMargin", "_leftMargin", "_rightMargin",
"_indentFirst", "_breakBefore", "_breakAfter",
) )
def __init__(self) -> None: def __init__(self) -> None:
@@ -756,6 +769,9 @@ class DocXParagraph:
self._textAlign: str | None = None self._textAlign: str | None = None
self._topMargin: float | None = None self._topMargin: float | None = None
self._bottomMargin: float | None = None self._bottomMargin: float | None = None
self._leftMargin: float | None = None
self._rightMargin: float | None = None
self._indentFirst = False
self._breakBefore = False self._breakBefore = False
self._breakAfter = False self._breakAfter = False
return return
@@ -785,7 +801,7 @@ class DocXParagraph:
def setAlignment(self, value: str) -> None: def setAlignment(self, value: str) -> None:
"""Set paragraph alignment.""" """Set paragraph alignment."""
if value in ("left", "center", "right"): if value in ("left", "center", "right", "both"):
self._textAlign = value self._textAlign = value
return return
@@ -799,6 +815,21 @@ class DocXParagraph:
self._bottomMargin = value self._bottomMargin = value
return return
def setLeftMargin(self, value: float) -> None:
"""Set left indent."""
self._leftMargin = value
return
def setRightMargin(self, value: float) -> None:
"""Set right line indent."""
self._rightMargin = value
return
def setIndentFirst(self, state: bool) -> None:
"""Set first line indent."""
self._indentFirst = state
return
def setPageBreakBefore(self, state: bool) -> None: def setPageBreakBefore(self, state: bool) -> None:
"""Set page break before flag.""" """Set page break before flag."""
self._breakBefore = state self._breakBefore = state
@@ -813,6 +844,12 @@ class DocXParagraph:
# Methods # Methods
## ##
def overrideJustify(self, default: str) -> None:
"""Override inherited justify setting if None is set."""
if self._textAlign is None:
self.setAlignment(default)
return
def addContent(self, run: ET.Element) -> None: def addContent(self, run: ET.Element) -> None:
"""Add a run segment to the paragraph.""" """Add a run segment to the paragraph."""
self._content.append(run) self._content.append(run)
@@ -823,6 +860,15 @@ class DocXParagraph:
if style := self._style: if style := self._style:
par = xmlSubElem(body, _wTag("p")) par = xmlSubElem(body, _wTag("p"))
# Values
indent = {}
if self._indentFirst and style.indentFirst is not None:
indent[_wTag("firstLine")] = str(int(20.0 * style.indentFirst))
if self._leftMargin is not None:
indent[_wTag("left")] = str(int(20.0 * self._leftMargin))
if self._rightMargin is not None:
indent[_wTag("right")] = str(int(20.0 * self._rightMargin))
# Paragraph # Paragraph
pPr = xmlSubElem(par, _wTag("pPr")) pPr = xmlSubElem(par, _wTag("pPr"))
xmlSubElem(pPr, _wTag("pStyle"), attrib={_wTag("val"): style.styleId}) xmlSubElem(pPr, _wTag("pStyle"), attrib={_wTag("val"): style.styleId})
@@ -834,6 +880,8 @@ class DocXParagraph:
}) })
if self._textAlign: if self._textAlign:
xmlSubElem(pPr, _wTag("jc"), attrib={_wTag("val"): self._textAlign}) xmlSubElem(pPr, _wTag("jc"), attrib={_wTag("val"): self._textAlign})
if indent:
xmlSubElem(pPr, _wTag("ind"), attrib=indent)
# Text # Text
if self._breakBefore: if self._breakBefore:
+1 -1
View File
@@ -372,7 +372,7 @@ class ToHtml(Tokenizer):
"margin-top: {2:.2f}em; margin-bottom: {3:.2f}em;" "margin-top: {2:.2f}em; margin-bottom: {3:.2f}em;"
"}}" "}}"
).format( ).format(
"justify" if self._doJustify else "left", "justify" if self._doJustify else self._defaultAlign,
round(100 * self._lineHeight), round(100 * self._lineHeight),
mScale * self._marginText[0], mScale * self._marginText[0],
mScale * self._marginText[1], mScale * self._marginText[1],
+16 -15
View File
@@ -152,21 +152,22 @@ class Tokenizer(ABC):
# User Settings # User Settings
self._textFont = QFont("Serif", 11) # Output text font self._textFont = QFont("Serif", 11) # Output text font
self._lineHeight = 1.15 # Line height in units of em self._lineHeight = 1.15 # Line height in units of em
self._colorHeads = True # Colourise headings self._colorHeads = True # Colourise headings
self._scaleHeads = True # Scale headings to larger font size self._scaleHeads = True # Scale headings to larger font size
self._boldHeads = True # Bold headings self._boldHeads = True # Bold headings
self._blockIndent = 4.00 # Block indent in units of em self._blockIndent = 4.00 # Block indent in units of em
self._firstIndent = False # Enable first line indent self._firstIndent = False # Enable first line indent
self._firstWidth = 1.40 # First line indent in units of em self._firstWidth = 1.40 # First line indent in units of em
self._indentFirst = False # Indent first paragraph self._indentFirst = False # Indent first paragraph
self._doJustify = False # Justify text self._doJustify = False # Justify text
self._doBodyText = True # Include body text self._doBodyText = True # Include body text
self._doSynopsis = False # Also process synopsis comments self._doSynopsis = False # Also process synopsis comments
self._doComments = False # Also process comments self._doComments = False # Also process comments
self._doKeywords = False # Also process keywords like tags and references self._doKeywords = False # Also process keywords like tags and references
self._skipKeywords = set() # Keywords to ignore self._skipKeywords = set() # Keywords to ignore
self._keepBreaks = True # Keep line breaks in paragraphs self._keepBreaks = True # Keep line breaks in paragraphs
self._defaultAlign = "left" # The default text alignment
# Margins # Margins
self._marginTitle = nwStyles.T_MARGIN["H0"] self._marginTitle = nwStyles.T_MARGIN["H0"]
+10 -1
View File
@@ -327,7 +327,7 @@ class ToOdt(Tokenizer):
self._fLineHeight = f"{round(100 * self._lineHeight):d}%" self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
self._fBlockIndent = self._emToCm(self._blockIndent) self._fBlockIndent = self._emToCm(self._blockIndent)
self._fTextIndent = self._emToCm(self._firstWidth) self._fTextIndent = self._emToCm(self._firstWidth)
self._textAlign = "justify" if self._doJustify else "left" self._textAlign = "justify" if self._doJustify else self._defaultAlign
# Clear Errors # Clear Errors
self._errData = [] self._errData = []
@@ -457,6 +457,9 @@ class ToOdt(Tokenizer):
# Process Text Types # Process Text Types
if tType == self.T_TEXT: if tType == self.T_TEXT:
if self._doJustify and "\n" in tText:
oStyle.overrideJustify(self._defaultAlign)
# Text indentation is processed here because there is a # Text indentation is processed here because there is a
# dedicated pre-defined style for it # dedicated pre-defined style for it
if tStyle & self.A_IND_T: if tStyle & self.A_IND_T:
@@ -1313,6 +1316,12 @@ class ODTParagraphStyle:
# Methods # Methods
## ##
def overrideJustify(self, default: str) -> None:
"""Override inherited justify setting if None is set."""
if self._pAttr["text-align"][1] is None:
self.setTextAlign(default)
return
def checkNew(self, style: ODTParagraphStyle) -> bool: def checkNew(self, style: ODTParagraphStyle) -> bool:
"""Check if there are new settings in style that differ from """Check if there are new settings in style that differ from
those in this object. Unset styles are ignored as they can be those in this object. Unset styles are ignored as they can be
+5 -5
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.5.2" hexVersion="0x020502f0" fileVersion="1.5" fileRevision="4" timeStamp="2024-09-17 15:33:20"> <novelWriterXML appVersion="2.6a1" hexVersion="0x020600a1" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-19 20:03:34">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1956" autoCount="277" editTime="91001"> <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2073" autoCount="277" editTime="93036">
<name>Sample Project</name> <name>Sample Project</name>
<author>Jane Smith</author> <author>Jane Smith</author>
</project> </project>
@@ -46,7 +46,7 @@
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name> <name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
</item> </item>
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H0" charCount="233" wordCount="47" paraCount="2" cursorPos="275" /> <meta expanded="no" heading="H0" charCount="233" wordCount="47" paraCount="2" cursorPos="30" />
<name status="sf12341" import="ia857f0" active="yes">Page</name> <name status="sf12341" import="ia857f0" active="yes">Page</name>
</item> </item>
<item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -58,7 +58,7 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name> <name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item> </item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="2953" wordCount="520" paraCount="15" cursorPos="0" /> <meta expanded="no" heading="H3" charCount="2953" wordCount="520" paraCount="15" cursorPos="66" />
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name> <name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item> </item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -66,7 +66,7 @@
<name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name> <name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name>
</item> </item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="0" /> <meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="357" />
<name status="s78ea90" import="ia857f0" active="yes">Interlude</name> <name status="s78ea90" import="ia857f0" active="yes">Interlude</name>
</item> </item>
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE"> <item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
+17 -1
View File
@@ -623,7 +623,7 @@ def testFmtToOdt_ConvertParagraphs(mockGUI):
'<office:text>' '<office:text>'
'<text:h text:style-name="Heading_20_3" text:outline-level="3">Scene</text:h>' '<text:h text:style-name="Heading_20_3" text:outline-level="3">Scene</text:h>'
'<text:p text:style-name="Text_20_body">Regular paragraph</text:p>' '<text:p text:style-name="Text_20_body">Regular paragraph</text:p>'
'<text:p text:style-name="Text_20_body">with<text:line-break />break</text:p>' '<text:p text:style-name="P7">with<text:line-break />break</text:p>'
'<text:p text:style-name="P7">Left Align</text:p>' '<text:p text:style-name="P7">Left Align</text:p>'
'</office:text>' '</office:text>'
) )
@@ -1127,6 +1127,22 @@ def testFmtToOdt_ODTParagraphStyle():
'</test>' '</test>'
) )
# Override Justify
# ================
aStyle = ODTParagraphStyle("test")
# When not set, override is possible
assert aStyle._pAttr["text-align"][1] is None
aStyle.overrideJustify("left")
assert aStyle._pAttr["text-align"][1] == "left"
# When explicitly set, not override
aStyle.setTextAlign("right")
assert aStyle._pAttr["text-align"][1] == "right"
aStyle.overrideJustify("left")
assert aStyle._pAttr["text-align"][1] == "right"
# Changes # Changes
# ======= # =======