diff --git a/CHANGELOG.md b/CHANGELOG.md index 455d9449..b7c7a039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # novelWriter Changelog +## Version 2.7.2 [2025-06-24] + +### Release Notes + +This is a patch release that fixes several issues related to DocX and PDF manuscript documents, +improves how line breaks, alignment and indentation is processed, and fixes some issues with +displaying tags for novel documents on the user interface. + +### Detailed Changelog + +**Bugfixes** + +* Fixed an issue where escaped markup characters were not replaced properly in DocX and PDF + documents, and in previews. Issue #2410. PR #2411. +* Fixed an issue where titles with line breaks in them would have page breaks applied to both lines + for preview and PDF documents. Issue #2415. PR #2416. +* Fixed an issue where the justified text setting would not be properly applied to a paragraph in a + manuscript document if there was a line break in the paragraph, but single line breaks were set + to be ignored. Issue #2426. PR #2427. +* Fixed an issue where the "Tag" field of the Outline View details panel remained blank even if a + tag was set for the novel document. Issue #2428. PR #2429. + +**Improvements** + +* When comments are enabled in the viewer, story comments should also be visible. A separate filter + button will be added for this in 2.8. PR #2420. +* When a paragraph has line breaks in it, the alignment tag will now override first line + indentation even if the alignment tag is not on the first line. This is more consistent with the + alignment behaviour for multi-line paragraphs in general. Issue #2425. PR #2427. +* Tags will now be shown in the Novel View tooltip pop-out under the triangle button, together with + all the other meta data collected about a document or heading. PR #2429. + +**Documentation** + +* The documentation on how alignment and first line indentation works in conjunction with + in-paragraph line breaks, the setting to keep or ignore such line breaks, and the forced line + break shortcode, has been improved. Issue #2425. PR #2427. + +---- + ## Version 2.7.1 [2025-06-09] ### Release Notes diff --git a/CREDITS.md b/CREDITS.md index b3c527a5..4c08b186 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -55,19 +55,33 @@ The following libraries are dependencies of novelWriter: * **Enchant** by Dom Lachowicz * **PyEnchant** by Dimitri Merejkowsky -## Assets +## Icon Themes -Some of the assets bundled with novelWriter were adapted from the following sources: +The following icon themes are bundles with novelWriter: * **Material Symbols** icons by Google Inc (Apache 2.0) * **Remix** icons by RemixIcon (Apache 2.0) * **Font Awesome** icons by Fonticons Inc (CC BY 4.0) -* **Tomorrow** themes by Chris Kempson (MIT License) -* **Owl** themes by Sarah Drasner (MIT License) -* **Solarized** themes by Ethan Schoonover (MIT License) -* **Cyberpunk Night** theme by Anders Lemvigh (CC BY-SA 4.0) -* **Dracula** theme by Zeno Rocha (MIT License) -* **Snazzy Light** theme by Florian Reuschel (MIT License) + +## Colour Themes + +Some colour themes included with novelWriter were adapted from the following sources: + +* **Dracula** theme by Zeno Rocha +* **Espresso** theme by Sainnhe Park and Antoine Cotten +* **Noctis** themes by Liviu Schera, and contributors +* **Owl** themes by Sarah Drasner +* **Primer** themes by GitHub Inc and Myian +* **Snazzy Light** theme by Florian Reuschel +* **Solarized** themes by Ethan Schoonover +* **Tango** themes by the Tango Desktop Project +* **Tomorrow** themes by Chris Kempson + +Other themes were created for novelWriter: + +* **Blue Streak** themes by Veronica Berglyd Olsen +* **Cyberpunk Night** theme by Anders Lemvigh +* **LCARS** theme by Veronica Berglyd Olsen ## Fonts diff --git a/docs/source/usage/advanced_formatting.rst b/docs/source/usage/advanced_formatting.rst index 557db1a7..556d6f1f 100644 --- a/docs/source/usage/advanced_formatting.rst +++ b/docs/source/usage/advanced_formatting.rst @@ -56,6 +56,20 @@ activated by clicking the left-most icon button in the editor header. .. versionadded:: 2.2 +.. _docs_usage_formatting_shortcodes_break: + +Forced Line Break +----------------- + +Inserting ``[br]`` in the text will ensure a line break is always inserted in that place, even if +you turn off **Preserve Hard Line Breaks** in your manuscript build settings. + +You can add a manual line break after it too, for a better visual representation in the editor, but +keep in mind that this line break is removed before the text is processed, so the text on either +side of the ``[br]`` shortcode will be considered as belonging to the same line. This can affect +how alignment is treated. See :ref:`docs_usage_align_indent_forced` for more details. + + .. _docs_usage_formatting_breaks: Vertical Space and Page Breaks diff --git a/docs/source/usage/alignment_and_indent.rst b/docs/source/usage/alignment_and_indent.rst index 2494ea32..60949b9a 100644 --- a/docs/source/usage/alignment_and_indent.rst +++ b/docs/source/usage/alignment_and_indent.rst @@ -53,18 +53,33 @@ the entire paragraph. For the following text, all lines will be centred: .. code-block:: md - >> I am the very model of a modern Major-General + >> I am the very model of a modern Major-General << I've information vegetable, animal, and mineral I know the kings of England, and I quote the fights historical - From Marathon to Waterloo, in order categorical << + From Marathon to Waterloo, in order categorical + +If you have multiple conflicting alignments on a paragraph, only one is applied. The order of +precedence is: + +#. Left alignment +#. Right alignment +#. Centred text +#. Justified text + +.. note:: + + It is strongly recommended that you keep the **Preserve Hard Line Breaks** setting enabled in + your manuscript build settings. This setting assumes all single line breaks in your text are + intended. Turning this off makes adding line breaks more complicated, but it is still possible. + See :ref:`docs_usage_align_indent_forced`. Alignment with First Line Indent ================================ If you have first line indent enabled in your manuscript build settings, you probably want to -disable it for text in verses. Adding any alignment tags will cause the first line indent to be -switched off for that paragraph. +disable it for text in verses. Adding any alignment tags on a paragraph will cause the first +line indent to be switched off for that paragraph. :bdg-info:`Example` @@ -76,3 +91,35 @@ The following text will always be aligned against the left margin: I've information vegetable, animal, and mineral I know the kings of England, and I quote the fights historical From Marathon to Waterloo, in order categorical + + +.. _docs_usage_align_indent_forced: + +Alignment with Forced Line Breaks +================================= + +If you turn off **Preserve Hard Line Breaks** in your manuscript build settings, you can still +force line breaks in paragraphs using the ``[br]`` shortcode. For clarity in the text, you can add +a line break after it as well. It doesn't result in two line breaks. + +Keep in mind that when the text is processed, the lines on either side of a ``[br]`` shortcode are +combined, and any trailing hard line break is *ignored*. This means that when such a paragraph is +processed, these line breaks count as the same line. This affects how alignment tags are handled. +For instance, this text becomes centred instead of left aligned. + +.. code-block:: md + + >> I am the very model of a modern Major-General[br] + I've information vegetable, animal, and mineral[br] + I know the kings of England, and I quote the fights historical[br] + From Marathon to Waterloo, in order categorical << + +Since this is understood as one line, this is the only way you can actually centre this paragraph. + +.. caution:: + + Due to this difference in how text with ``[br]`` tags are processed, it is generally better to + stick with the **Preserve Hard Line Breaks** setting enabled. It ensures a better correspondence + between what you see in the editor and what output you get. + +See also :ref:`docs_usage_formatting_shortcodes_break`. diff --git a/novelwriter/assets/text/credits_en.htm b/novelwriter/assets/text/credits_en.htm index 33903616..c8d868a9 100644 --- a/novelwriter/assets/text/credits_en.htm +++ b/novelwriter/assets/text/credits_en.htm @@ -67,20 +67,38 @@ more contributions are listed on the project's Members page.

  • PyEnchant by Dimitri Merejkowsky
  • -

    Assets

    +

    Icon Themes

    -

    Some of the assets bundled with novelWriter were adapted from the following sources:

    +

    The following icon themes are bundles with novelWriter:

    + +

    Colour Themes

    + +

    Some colour themes included with novelWriter were adapted from the following sources:

    + + + +

    Other themes were created for novelWriter:

    + +

    Fonts

    diff --git a/novelwriter/assets/themes/espresso.conf b/novelwriter/assets/themes/espresso.conf new file mode 100644 index 00000000..74ff4b33 --- /dev/null +++ b/novelwriter/assets/themes/espresso.conf @@ -0,0 +1,76 @@ +[Main] +name = Espresso +mode = dark +description = High contrast & vivid color scheme based on Monokai Pro +author = Veronica Berglyd Olsen +credit = Sainnhe Park and Antoine Cotten +url = https://github.com/sainnhe/sonokai + +[Base] +base = #2f2726 +default = #e5e4e2 +faded = #96817b +red = #f86882 +orange = #f08d71 +yellow = #f0c66f +green = #a6cd77 +cyan = #81d0c9 +blue = #89a8e0 +purple = #9fa0e1 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = base:L125 +windowtext = default +base = base +alternatebase = #51403c +text = default +tooltipbase = yellow:L125 +tooltiptext = base +button = base +buttontext = default +brighttext = base +highlight = #4e5f80 +highlightedtext = default +link = blue +linkvisited = blue +accent = blue + +[GUI] +helptext = blue +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #392e2c +link = blue +headertext = yellow:L125 +headertag = faded +emphasis = orange +dialog = yellow +altdialog = orange +note = yellow:L125 +hidden = faded +shortcode = green +keyword = red +tag = green +value = orange +optional = yellow +spellcheckline = red +errorline = green +replacetag = cyan +modifier = blue +texthighlight = yellow:108 diff --git a/novelwriter/assets/themes/noctis.conf b/novelwriter/assets/themes/noctis.conf new file mode 100644 index 00000000..35820118 --- /dev/null +++ b/novelwriter/assets/themes/noctis.conf @@ -0,0 +1,108 @@ +[Main] +name = Noctis +mode = dark +description = A dark theme with a well balanced blend of warm and cold medium contrast colors +author = Veronica Berglyd Olsen +credit = Liviu Schera, and contributors +url = https://github.com/liviuschera/noctis + +## +# Noctis Lux : themes/noctis.json +## +# colors +# selection.background : #107684 -> Palette.highlight +# sideBar.background : #041d20 -> Palette.window +# editor.background : #052529 -> Base.base +# editor.foreground : #b2cacd -> Base.default +# editor.selectionBackground : #169fb144 -> Syntax.line, Syntax.texthighlight +# textLink.foreground : #40d4e7 -> Palette.link, Syntax.link +# input.background : #052529 -> Palette.button +# input.foreground : #cdd3de -> Palette.buttontext +# notifications.foreground : #cdd3de -> Palette.tooltiptext +# notifications.background : #073940 -> Palette.tooltipbase +## +# tokenColors +# COMMENT : #5b858b -> Base.faded +# TEXT : #b2cacd -> Syntax.text, Syntax.optional +# KEYWORD : #df769b -> Base.red +# VARIABLE : #e4b781 -> Palette.accent +# ANNOTATION : #d67e5c -> Syntax.note, Syntax.emphasis +# CONSTANT : #d5971a -> Base.yellow +# TAG : #e66533 -> Base.orange +# STRING : #49e9a6 -> Syntax.headertext, Palette.accent +# STRINGINTERPOLATED : #16b673 -> Base.green +# NUMBER : #7060eb -> Base.purple +# FUNCTION : #16a3b6 -> Base.cyan +# SUPPORT : #49d6e9 -> Syntax.replacetag +# MISC : #49ace9 -> Base.blue +# INVALID : #e3541c -> GUI.errortext +## + +[Base] +base = #052529 +default = #b2cacd +faded = #5b858b +red = #df769b +orange = #e66533 +yellow = #d5971a +green = #16b673 +cyan = #16a3b6 +blue = #49ace9 +purple = #7060eb + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #041d20 +windowtext = default +base = base +alternatebase = base:L175 +text = default +tooltipbase = #073940 +tooltiptext = #cdd3de +button = base +buttontext = #cdd3de +brighttext = base +highlight = #107684 +highlightedtext = default +link = #40d4e7 +linkvisited = blue +accent = #49e9a6 + +[GUI] +helptext = #e4b781 +fadedtext = faded +errortext = #e3541c + +[Syntax] +background = base +text = #b2cacd +line = #169fb132 +link = #40d4e7 +headertext = #49e9a6 +headertag = green:D125 +emphasis = #d67e5c +dialog = green +altdialog = blue +note = #d67e5c +hidden = faded +shortcode = red +keyword = yellow +tag = blue +value = red:D125 +optional = #b2cacd +spellcheckline = red +errorline = green +replacetag = #49d6e9 +modifier = red +texthighlight = #169fb144 diff --git a/novelwriter/assets/themes/noctis_lux.conf b/novelwriter/assets/themes/noctis_lux.conf new file mode 100644 index 00000000..ddf34244 --- /dev/null +++ b/novelwriter/assets/themes/noctis_lux.conf @@ -0,0 +1,108 @@ +[Main] +name = Noctis Lux +mode = light +description = A light theme with a well balanced blend of warm and cold medium contrast colors +author = Veronica Berglyd Olsen +credit = Liviu Schera, and contributors +url = https://github.com/liviuschera/noctis + +## +# Noctis Lux : themes/lux.json +## +# colors +# selection.background : #169fb155 -> Palette.highlight +# sideBar.background : #f9f1e1 -> Palette.window +# editor.background : #fef8ec -> Base.base +# editor.foreground : #005661 -> Base.default +# editor.selectionBackground : #ade2eb77 -> Syntax.line, Syntax.texthighlight +# textLink.foreground : #00c6e0 -> Palette.link, Syntax.link +# input.background : #fef8ec -> Palette.button +# input.foreground : #6a7a7c -> Palette.buttontext +# notifications.foreground : #005661 -> Palette.tooltiptext +# notifications.background : #f2edde -> Palette.tooltipbase +## +# tokenColors +# COMMENT : #8ca6a6 -> Base.faded +# TEXT : #004d57 -> Syntax.text, Syntax.optional +# KEYWORD : #ff5792 -> Base.red +# VARIABLE : #fa8900 -> Palette.accent +# ANNOTATION : #b3694d -> Syntax.note, Syntax.emphasis +# CONSTANT : #a88c00 -> Base.yellow +# TAG : #e64100 -> Base.orange +# STRING : #00b368 -> Syntax.headertext, Palette.accent +# STRINGINTERPOLATED : #009456 -> Base.green +# NUMBER : #5842ff -> Base.purple +# FUNCTION : #0095a8 -> Base.cyan +# SUPPORT : #00bdd6 -> Syntax.replacetag +# MISC : #0094f0 -> Base.blue +# INVALID : #ff530f -> GUI.errortext +## + +[Base] +base = #fef8ec +default = #005661 +faded = #8ca6a6 +red = #ff5792 +orange = #e64100 +yellow = #a88c00 +green = #009456 +cyan = #0095a8 +blue = #0094f0 +purple = #5842ff + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #f9f1e1 +windowtext = default +base = base +alternatebase = base:D115 +text = default +tooltipbase = #f2edde +tooltiptext = #005661 +button = base +buttontext = #6a7a7c +brighttext = base +highlight = #169fb155 +highlightedtext = default +link = #00c6e0 +linkvisited = blue +accent = #00b368 + +[GUI] +helptext = #fa8900 +fadedtext = faded +errortext = #ff530f + +[Syntax] +background = base +text = #004d57 +line = #ade2eb56 +link = #00c6e0 +headertext = #00b368 +headertag = green:D125 +emphasis = #b3694d +dialog = green +altdialog = blue +note = #b3694d +hidden = faded +shortcode = red +keyword = yellow +tag = blue +value = red:D125 +optional = #004d57 +spellcheckline = red +errorline = green +replacetag = #00bdd6 +modifier = red +texthighlight = #ade2eb77 diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index d8e9c8e4..07933690 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -708,17 +708,19 @@ class Index: return 0, 0, 0 def getReferences(self, tHandle: str, sTitle: str | None = None) -> dict[str, list[str]]: - """Extract all references made in a file, and optionally title - section. + """Extract all tags and references made in a file, and + optionally title section. """ - tRefs = {x: [] for x in nwKeyWords.VALID_KEYS} + refs = {x: [] for x in nwKeyWords.VALID_KEYS} for rTitle, hItem in self._itemIndex.iterItemHeaders(tHandle): if sTitle is None or sTitle == rTitle: for aTag, refTypes in hItem.references.items(): for refType in refTypes: - if refType in tRefs: - tRefs[refType].append(self._tagsIndex.tagName(aTag)) - return tRefs + if refType in refs: + refs[refType].append(self._tagsIndex.tagName(aTag)) + if tag := hItem.tag: + refs[nwKeyWords.TAG_KEY] = [self._tagsIndex.tagName(tag)] + return refs def getReferenceForHeader(self, tHandle: str, nHead: int, keyClass: str) -> list[str]: """Get the display names for a tags class for insertion into a diff --git a/novelwriter/core/indexdata.py b/novelwriter/core/indexdata.py index 070558ce..b74ce4c4 100644 --- a/novelwriter/core/indexdata.py +++ b/novelwriter/core/indexdata.py @@ -342,20 +342,26 @@ class IndexHeading: ## def getReferences(self) -> dict[str, list[str]]: - """Extract all references for this heading.""" + """Extract all tags and references for this heading.""" refs = {x: [] for x in nwKeyWords.VALID_KEYS} for tag, types in self._refs.items(): for keyword in types: if keyword in refs and (name := self._cache.tags.tagName(tag)): refs[keyword].append(name) + if name := self._cache.tags.tagName(self._tag): + refs[nwKeyWords.TAG_KEY] = [name] return refs def getReferencesByKeyword(self, keyword: str) -> list[str]: """Extract all references for this heading.""" refs = [] - for tag, types in self._refs.items(): - if keyword in types and (name := self._cache.tags.tagName(tag)): + if keyword == nwKeyWords.TAG_KEY: + if name := self._cache.tags.tagName(self._tag): refs.append(name) + else: + for tag, types in self._refs.items(): + if keyword in types and (name := self._cache.tags.tagName(tag)): + refs.append(name) return refs ## diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index 176c4873..b4cce890 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -893,31 +893,21 @@ class Tokenizer(ABC): if nBlock[0] != BlockTyp.TEXT: # Next block is not text, so we add the buffer to blocks nLines = len(pLines) - cStyle = pLines[0][4] - if firstIndent and not (self._noIndent or cStyle & BlockFmt.ALIGNED): - # If paragraph indentation is enabled, not temporarily - # turned off, and the block is not aligned, we add the - # text indentation flag - cStyle |= BlockFmt.IND_T + tFmt: T_Formats = [] + pTxt = "" + cStyle = BlockFmt.NONE if nLines == 1: - # The paragraph contains a single line, so we just save - # that directly to the blocks list. If justify is - # enabled, and there is no alignment, we apply it. - if doJustify and not cStyle & BlockFmt.ALIGNED: - cStyle |= BlockFmt.JUSTIFY - + # The paragraph contains a single line + tFmt = pLines[0][3] pTxt = pLines[0][2].translate(transMapB) - sBlocks.append(( - BlockTyp.TEXT, pLines[0][1], pTxt, pLines[0][3], cStyle - )) + cStyle = pLines[0][4] elif nLines > 1: # The paragraph contains multiple lines, so we need to # join them according to the line break policy, and # recompute all the formatting markers tTxt = "" - tFmt: T_Formats = [] for aBlock in pLines: tLen = len(tTxt) tTxt += f"{aBlock[2]}{lineSep}" @@ -925,6 +915,18 @@ class Tokenizer(ABC): cStyle |= aBlock[4] pTxt = tTxt[:-1].translate(transMapB) + + if nLines: + isAligned = cStyle & BlockFmt.ALIGNED + if firstIndent and not (self._noIndent or isAligned): + # If paragraph indentation is enabled, not temporarily + # turned off, and the block is not aligned, we add the + # text indentation flag + cStyle |= BlockFmt.IND_T + + if doJustify and not isAligned: + cStyle |= BlockFmt.JUSTIFY + sBlocks.append(( BlockTyp.TEXT, pLines[0][1], pTxt, tFmt, cStyle )) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 752c6d39..86913b61 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -593,6 +593,7 @@ class GuiNovelTree(NTreeView): lines = [] if head := SHARED.project.index.getItemHeading(tHandle, sTitle): tags = head.getReferences() + appendTags(tags, nwKeyWords.TAG_KEY, lines) appendTags(tags, nwKeyWords.POV_KEY, lines) appendTags(tags, nwKeyWords.FOCUS_KEY, lines) appendTags(tags, nwKeyWords.CHAR_KEY, lines) diff --git a/pyproject.toml b/pyproject.toml index 9263eb81..3610e012 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,11 @@ authors = [ ] description = "A plain text editor for planning and writing novels" readme = {file = "setup/description_pypi.md", content-type = "text/markdown"} -license = {text = "GPL-3.0-or-later"} +license = "GPL-3.0-or-later AND Apache-2.0 AND CC-BY-4.0" +license-files = [ + "LICENSE.md", + "setup/LICENSE-Apache-2.0.txt", +] classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 12f0d4e0..11e59489 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -1,10 +1,11 @@ %%~name: Making a Scene %%~path: 6a2d6d5f4f401/636b6aa9b697b %%~kind: NOVEL/DOCUMENT -%%~hash: b85f815702763f58926edd72a2eeed1df5e1cbc9 +%%~hash: c1a75b18145e49e5e71b5284f66a261207da37d0 %%~date: Unknown/2025-06-16 12:49:10 ### Making a Scene +@tag: Scene @pov: Jane @char: John, Jane @location: Earth diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 587802ef..812795c4 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith @@ -58,7 +58,7 @@ Chapter One - + Making a Scene diff --git a/setup/LICENSE-Apache-2.0.txt b/setup/LICENSE-Apache-2.0.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/setup/LICENSE-Apache-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index dca4575a..e86b8dc0 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -695,6 +695,7 @@ def testCoreIndex_ExtractData(nwGUI, fncPath, mockRnd): )) assert index.scanText(nHandle, ( "# Hello World!\n" + "@tag: Scene\n" "@pov: Jane\n" "@char: Jane, John\n\n" "% this is a comment\n\n" @@ -749,11 +750,13 @@ def testCoreIndex_ExtractData(nwGUI, fncPath, mockRnd): # Look up an invalid handle refs = index.getReferences("Not a handle") + assert refs["@tag"] == [] assert refs["@pov"] == [] assert refs["@char"] == [] # The novel file should now refer to Jane as @pov and @char refs = index.getReferences(nHandle) + assert refs["@tag"] == ["Scene"] assert refs["@pov"] == ["Jane"] assert refs["@char"] == ["Jane", "John"] @@ -791,7 +794,7 @@ def testCoreIndex_ExtractData(nwGUI, fncPath, mockRnd): # getKeyWordTags # ============== - assert index.getKeyWordTags("@mention") == ["Jane", "John"] + assert index.getKeyWordTags("@mention") == ["Jane", "John", "Scene"] assert index.getKeyWordTags("@char") == ["Jane", "John"] assert index.getKeyWordTags("@plot") == [] assert index.getKeyWordTags("@tag") == [] diff --git a/tests/test_core/test_core_indexdata.py b/tests/test_core/test_core_indexdata.py index 5bf45c8a..d4797aee 100644 --- a/tests/test_core/test_core_indexdata.py +++ b/tests/test_core/test_core_indexdata.py @@ -267,6 +267,7 @@ def testCoreIndexData_IndexHeadingReferences(): head = IndexHeading(cache, "T0001") # Add some references + head.setTag("Scene") head.addReference("Jane", "@pov") head.addReference("Jane", "@char") head.addReference("John", "@char") @@ -290,6 +291,7 @@ def testCoreIndexData_IndexHeadingReferences(): } # Set names + cache.tags.add("Scene", "Scene", "0000000000000", "T00001", "NOVEL") cache.tags.add("Jane", "Jane", "0000000000000", "T00001", "CHARACTER") cache.tags.add("John", "John", "0000000000000", "T00001", "CHARACTER") cache.tags.add("Main", "Main", "0000000000000", "T00001", "PLOT") @@ -301,7 +303,7 @@ def testCoreIndexData_IndexHeadingReferences(): "@plot": ["Main"], "@object": ["Gun"], "@story": [], - "@tag": [], + "@tag": ["Scene"], "@focus": [], "@custom": [], "@time": [], @@ -316,7 +318,7 @@ def testCoreIndexData_IndexHeadingReferences(): assert head.getReferencesByKeyword("@plot") == ["Main"] assert head.getReferencesByKeyword("@object") == ["Gun"] assert head.getReferencesByKeyword("@story") == [] - assert head.getReferencesByKeyword("@tag") == [] + assert head.getReferencesByKeyword("@tag") == ["Scene"] assert head.getReferencesByKeyword("@focus") == [] assert head.getReferencesByKeyword("@custom") == [] assert head.getReferencesByKeyword("@time") == [] diff --git a/tests/test_formats/test_fmt_tohtml.py b/tests/test_formats/test_fmt_tohtml.py index 02a31451..c3bd64a7 100644 --- a/tests/test_formats/test_fmt_tohtml.py +++ b/tests/test_formats/test_fmt_tohtml.py @@ -325,6 +325,54 @@ def testFmtToHtml_ConvertMeta(mockGUI): ) +@pytest.mark.core +def testFmtToHtml_Alignment(mockGUI): + """Test paragraph alignment in the ToHtml class.""" + project = NWProject() + html = ToHtml(project) + html.initDocument() + + # Left + html._text = "This is text <<\nspanning multiple\nlines" + html.tokenizeText() + html.doConvert() + assert html._pages[-1] == ( + "

    This is text
    spanning multiple
    lines

    \n" + ) + + # Right + html._text = ">> This is text\nspanning multiple\nlines" + html.tokenizeText() + html.doConvert() + assert html._pages[-1] == ( + "

    This is text
    spanning multiple
    lines

    \n" + ) + + # Centre + html._text = ">> This is text <<\nspanning multiple\nlines" + html.tokenizeText() + html.doConvert() + assert html._pages[-1] == ( + "

    This is text
    spanning multiple
    lines

    \n" + ) + + # Left before Right + html._text = ">> This is text\nspanning multiple <<\nlines" + html.tokenizeText() + html.doConvert() + assert html._pages[-1] == ( + "

    This is text
    spanning multiple
    lines

    \n" + ) + + # Right before Centre + html._text = ">> This is text <<\n>> spanning multiple\nlines" + html.tokenizeText() + html.doConvert() + assert html._pages[-1] == ( + "

    This is text
    spanning multiple
    lines

    \n" + ) + + @pytest.mark.core def testFmtToHtml_Dialog(mockGUI): """Test paragraph formats in the ToHtml class.""" diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index 4f65e44c..63c9cab0 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -1118,6 +1118,80 @@ def testFmtToken_Paragraphs(mockGUI): ] +@pytest.mark.core +def testFmtToken_BreakAlignIndent(mockGUI): + """Test the splitting of paragraphs with alignment.""" + project = NWProject() + tokens = BareTokenizer(project) + tokens._handle = TMH + + for text in [ + "This is text <<\nspanning multiple\nlines", + "This is text\nspanning multiple <<\nlines", + "This is text\nspanning multiple\nlines <<", + ]: + # Preserve Breaks + tokens.setKeepLineBreaks(True) + tokens._text = text + tokens.tokenizeText() + assert tokens._blocks == [ + (BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.LEFT), + ] + + # Don't Preserve Breaks + tokens.setKeepLineBreaks(False) + tokens._text = text + tokens.tokenizeText() + assert tokens._blocks == [ + (BlockTyp.TEXT, "", "This is text spanning multiple lines", [], BlockFmt.LEFT), + ] + + # With Justify + # This should disable justify + tokens.setKeepLineBreaks(True) + tokens.setJustify(True) + tokens._text = text + tokens.tokenizeText() + assert tokens._blocks == [ + (BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.LEFT), + ] + + # With Indent + # This should disable indent + tokens.setKeepLineBreaks(True) + tokens.setFirstLineIndent(True, 1.0, False) + tokens._text = text + tokens.tokenizeText() + assert tokens._blocks == [ + (BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.LEFT), + ] + + +@pytest.mark.core +def testFmtToken_BreakJustify(mockGUI): + """Test the of processing of justify with breaks.""" + project = NWProject() + tokens = BareTokenizer(project) + tokens._handle = TMH + tokens.setJustify(True) + + # Applied to all lines when breaks are preserved + tokens._text = "This is text\nspanning multiple\nlines" + tokens.setKeepLineBreaks(True) + tokens.tokenizeText() + assert tokens._blocks == [ + (BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.JUSTIFY), + ] + + # Turning off breaks should make no difference (see issue #2426) + tokens._text = "This is text\nspanning multiple\nlines" + tokens.setKeepLineBreaks(False) + tokens.tokenizeText() + assert tokens._blocks == [ + (BlockTyp.TEXT, "", "This is text spanning multiple lines", [], BlockFmt.JUSTIFY), + ] + + @pytest.mark.core def testFmtToken_TextFormat(mockGUI): """Test the tokenization of text formats in the Tokenizer class.""" diff --git a/tests/test_formats/test_fmt_toodt.py b/tests/test_formats/test_fmt_toodt.py index 70b72874..eded1eeb 100644 --- a/tests/test_formats/test_fmt_toodt.py +++ b/tests/test_formats/test_fmt_toodt.py @@ -733,7 +733,7 @@ def testFmtToOdt_ConvertParagraphs(mockGUI): '' 'Scene' 'Regular paragraph' - 'withbreak' + 'withbreak' 'Left Align' '' )