diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index fac3f471..e8502a25 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -110,7 +110,6 @@ S_HEAD2 = "Heading2" S_HEAD3 = "Heading3" S_HEAD4 = "Heading4" S_SEP = "Separator" -S_FIND = "FirstLineIndent" S_META = "TextMeta" # Colours @@ -132,10 +131,12 @@ class DocXParStyle(NamedTuple): before: float | None = None after: float | None = None line: float | None = None + indentFirst: float | None = None align: str | None = None default: bool = False level: int | None = None color: str | None = None + bold: bool = False class ToDocX(Tokenizer): @@ -217,7 +218,8 @@ class ToDocX(Tokenizer): """Convert the list of text tokens into XML elements.""" 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: # Create Paragraph @@ -232,8 +234,8 @@ class ToDocX(Tokenizer): par.setAlignment("right") elif tStyle & self.A_CENTRE: par.setAlignment("center") - # elif tStyle & self.A_JUSTIFY: - # oStyle.setTextAlign("justify") + elif tStyle & self.A_JUSTIFY: + par.setAlignment("both") if tStyle & self.A_PBB: par.setPageBreakBefore(True) @@ -245,17 +247,17 @@ class ToDocX(Tokenizer): if tStyle & self.A_Z_TOPMRG: par.setMarginTop(0.0) - # if tStyle & self.A_IND_L: - # oStyle.setMarginLeft(self._fBlockIndent) - # if tStyle & self.A_IND_R: - # oStyle.setMarginRight(self._fBlockIndent) + if tStyle & self.A_IND_T: + par.setIndentFirst(True) + if tStyle & self.A_IND_L: + par.setLeftMargin(bIndent) + if tStyle & self.A_IND_R: + par.setRightMargin(bIndent) # Process Text Types if tType == self.T_TEXT: - # Text indentation is processed here because there is a - # dedicated pre-defined style for it - # if tStyle & self.A_IND_T: - # else: + if self._doJustify and "\n" in tText: + par.overrideJustify(self._defaultAlign) self._processFragments(par, S_NORM, tText, tFormat) elif tType == self.T_TITLE: @@ -595,6 +597,7 @@ class ToDocX(Tokenizer): fSz2 = (nwStyles.H_SIZES[2] * 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 + align = "both" if self._doJustify else "left" # Add Normal Style self._addParStyle(DocXParStyle( @@ -605,6 +608,8 @@ class ToDocX(Tokenizer): before=fSz * self._marginText[0], after=fSz * self._marginText[1], line=fSz * self._lineHeight, + indentFirst=fSz * self._firstWidth, + align=align, )) # Add Title @@ -618,6 +623,7 @@ class ToDocX(Tokenizer): after=fSz * self._marginTitle[1], line=fSz0 * self._lineHeight, level=0, + bold=self._boldHeads, )) # Add Heading 1 @@ -632,6 +638,7 @@ class ToDocX(Tokenizer): line=fSz1 * self._lineHeight, level=0, color=COL_HEAD_L12 if hColor else None, + bold=self._boldHeads, )) # Add Heading 2 @@ -646,6 +653,7 @@ class ToDocX(Tokenizer): line=fSz2 * self._lineHeight, level=1, color=COL_HEAD_L12 if hColor else None, + bold=self._boldHeads, )) # Add Heading 3 @@ -660,6 +668,7 @@ class ToDocX(Tokenizer): line=fSz3 * self._lineHeight, level=1, color=COL_HEAD_L34 if hColor else None, + bold=self._boldHeads, )) # Add Heading 4 @@ -674,6 +683,7 @@ class ToDocX(Tokenizer): line=fSz4 * self._lineHeight, level=1, color=COL_HEAD_L34 if hColor else None, + bold=self._boldHeads, )) # Add Separator @@ -737,6 +747,8 @@ class ToDocX(Tokenizer): xmlSubElem(rPr, _wTag("szCs"), attrib={_wTag("val"): str(int(2.0 * size))}) if style.color: xmlSubElem(rPr, _wTag("color"), attrib={_wTag("val"): style.color}) + if style.bold: + xmlSubElem(rPr, _wTag("b")) self._styles[style.styleId] = style @@ -746,8 +758,9 @@ class ToDocX(Tokenizer): class DocXParagraph: __slots__ = ( - "_content", "_style", "_textAlign", "_topMargin", "_bottomMargin", - "_breakBefore", "_breakAfter", + "_content", "_style", "_textAlign", + "_topMargin", "_bottomMargin", "_leftMargin", "_rightMargin", + "_indentFirst", "_breakBefore", "_breakAfter", ) def __init__(self) -> None: @@ -756,6 +769,9 @@ class DocXParagraph: self._textAlign: str | None = None self._topMargin: 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._breakAfter = False return @@ -785,7 +801,7 @@ class DocXParagraph: def setAlignment(self, value: str) -> None: """Set paragraph alignment.""" - if value in ("left", "center", "right"): + if value in ("left", "center", "right", "both"): self._textAlign = value return @@ -799,6 +815,21 @@ class DocXParagraph: self._bottomMargin = value 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: """Set page break before flag.""" self._breakBefore = state @@ -813,6 +844,12 @@ class DocXParagraph: # 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: """Add a run segment to the paragraph.""" self._content.append(run) @@ -823,6 +860,15 @@ class DocXParagraph: if style := self._style: 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 pPr = xmlSubElem(par, _wTag("pPr")) xmlSubElem(pPr, _wTag("pStyle"), attrib={_wTag("val"): style.styleId}) @@ -834,6 +880,8 @@ class DocXParagraph: }) if self._textAlign: xmlSubElem(pPr, _wTag("jc"), attrib={_wTag("val"): self._textAlign}) + if indent: + xmlSubElem(pPr, _wTag("ind"), attrib=indent) # Text if self._breakBefore: diff --git a/novelwriter/formats/tohtml.py b/novelwriter/formats/tohtml.py index 7eb4b33d..54827a2a 100644 --- a/novelwriter/formats/tohtml.py +++ b/novelwriter/formats/tohtml.py @@ -372,7 +372,7 @@ class ToHtml(Tokenizer): "margin-top: {2:.2f}em; margin-bottom: {3:.2f}em;" "}}" ).format( - "justify" if self._doJustify else "left", + "justify" if self._doJustify else self._defaultAlign, round(100 * self._lineHeight), mScale * self._marginText[0], mScale * self._marginText[1], diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index 916afab2..04ce876a 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -152,21 +152,22 @@ class Tokenizer(ABC): # User Settings self._textFont = QFont("Serif", 11) # Output text font - self._lineHeight = 1.15 # Line height in units of em - self._colorHeads = True # Colourise headings - self._scaleHeads = True # Scale headings to larger font size - self._boldHeads = True # Bold headings - self._blockIndent = 4.00 # Block indent in units of em - self._firstIndent = False # Enable first line indent - self._firstWidth = 1.40 # First line indent in units of em - self._indentFirst = False # Indent first paragraph - self._doJustify = False # Justify text - self._doBodyText = True # Include body text - self._doSynopsis = False # Also process synopsis comments - self._doComments = False # Also process comments - self._doKeywords = False # Also process keywords like tags and references - self._skipKeywords = set() # Keywords to ignore - self._keepBreaks = True # Keep line breaks in paragraphs + self._lineHeight = 1.15 # Line height in units of em + self._colorHeads = True # Colourise headings + self._scaleHeads = True # Scale headings to larger font size + self._boldHeads = True # Bold headings + self._blockIndent = 4.00 # Block indent in units of em + self._firstIndent = False # Enable first line indent + self._firstWidth = 1.40 # First line indent in units of em + self._indentFirst = False # Indent first paragraph + self._doJustify = False # Justify text + self._doBodyText = True # Include body text + self._doSynopsis = False # Also process synopsis comments + self._doComments = False # Also process comments + self._doKeywords = False # Also process keywords like tags and references + self._skipKeywords = set() # Keywords to ignore + self._keepBreaks = True # Keep line breaks in paragraphs + self._defaultAlign = "left" # The default text alignment # Margins self._marginTitle = nwStyles.T_MARGIN["H0"] diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py index 3fb77100..ad02d02c 100644 --- a/novelwriter/formats/toodt.py +++ b/novelwriter/formats/toodt.py @@ -327,7 +327,7 @@ class ToOdt(Tokenizer): self._fLineHeight = f"{round(100 * self._lineHeight):d}%" self._fBlockIndent = self._emToCm(self._blockIndent) 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 self._errData = [] @@ -457,6 +457,9 @@ class ToOdt(Tokenizer): # Process Text Types 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 # dedicated pre-defined style for it if tStyle & self.A_IND_T: @@ -1313,6 +1316,12 @@ class ODTParagraphStyle: # 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: """Check if there are new settings in style that differ from those in this object. Unset styles are ignored as they can be diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index b9e8108c..8ea8f9aa 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith @@ -46,7 +46,7 @@ Title Page - + Page @@ -58,7 +58,7 @@ Chapter One - + Making a Scene @@ -66,7 +66,7 @@ Another Scene - + Interlude diff --git a/tests/test_formats/test_fmt_toodt.py b/tests/test_formats/test_fmt_toodt.py index 3206b221..9305eada 100644 --- a/tests/test_formats/test_fmt_toodt.py +++ b/tests/test_formats/test_fmt_toodt.py @@ -623,7 +623,7 @@ def testFmtToOdt_ConvertParagraphs(mockGUI): '' 'Scene' 'Regular paragraph' - 'withbreak' + 'withbreak' 'Left Align' '' ) @@ -1127,6 +1127,22 @@ def testFmtToOdt_ODTParagraphStyle(): '' ) + # 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 # =======