pyDiffTools 0.1.35__tar.gz → 0.1.37__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pydifftools-0.1.35/pyDiffTools.egg-info → pydifftools-0.1.37}/PKG-INFO +10 -1
- {pydifftools-0.1.35 → pydifftools-0.1.37}/README.rst +9 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37/pyDiffTools.egg-info}/PKG-INFO +10 -1
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pyDiffTools.egg-info/SOURCES.txt +2 -0
- pydifftools-0.1.37/pydifftools/comment_migration.py +397 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/comment_tags.lua +70 -6
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/comment_tags_margin.lua +66 -6
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/comment_tags_no_comments.lua +22 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/comments.css +10 -5
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/continuous.py +185 -94
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/wrap_sentences.py +225 -3
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pyproject.toml +1 -1
- pydifftools-0.1.37/tests/test_comment_migration.py +206 -0
- pydifftools-0.1.37/tests/test_wrap_check.py +408 -0
- pydifftools-0.1.35/tests/test_wrap_check.py +0 -141
- {pydifftools-0.1.35 → pydifftools-0.1.37}/LICENSE.md +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/MANIFEST.in +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/bibliography.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/browser_lifecycle.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/citation_dialog.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/flowchart/comparison.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/flowchart/graph.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/flowchart/watch_graph.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/flowchart/wgrph_view.js +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/git_gd_history.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/pydifftools/zotero.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/setup.cfg +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_bibliography.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_check_single_use.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_continuous_shutdown.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_forward_search.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_git_gd_history.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_wrapping.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.37}/tests/test_zotero.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyDiffTools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.37
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -109,6 +109,15 @@ included are (listed in order of fun/utility):
|
|
|
109
109
|
Python file operations and Qt dialogs. Its automated runtime checks have
|
|
110
110
|
been performed on Linux, not on Windows.
|
|
111
111
|
|
|
112
|
+
Comment bubbles use author tags such as ``<JFcom>…</JFcom>`` and
|
|
113
|
+
``<JFcom-left>…</JFcom-left>``. Block comments can use Markdown Div classes
|
|
114
|
+
such as ``::: {.JFcom-left}``. Add a quoted six-digit hex color to the YAML
|
|
115
|
+
header (for example, ``JFcolor: '#5aa0ff'``) to enable that author's tags.
|
|
116
|
+
Before the first build, ``cpb`` offers to migrate legacy ``<comment>`` tags
|
|
117
|
+
and ``.comment-left/right`` blocks, and resolves author tags without a
|
|
118
|
+
matching color. New authors choose a hue in a Qt picker; ``cpb`` adds their
|
|
119
|
+
``XXcolor`` field to the header.
|
|
120
|
+
|
|
112
121
|
Declare a single local ``.bib`` file with ``bibliography: references.bib``
|
|
113
122
|
in the Markdown YAML header; relative paths are resolved beside the
|
|
114
123
|
Markdown file. Without that declaration, ``cpb`` uses a single adjacent
|
|
@@ -54,6 +54,15 @@ included are (listed in order of fun/utility):
|
|
|
54
54
|
Python file operations and Qt dialogs. Its automated runtime checks have
|
|
55
55
|
been performed on Linux, not on Windows.
|
|
56
56
|
|
|
57
|
+
Comment bubbles use author tags such as ``<JFcom>…</JFcom>`` and
|
|
58
|
+
``<JFcom-left>…</JFcom-left>``. Block comments can use Markdown Div classes
|
|
59
|
+
such as ``::: {.JFcom-left}``. Add a quoted six-digit hex color to the YAML
|
|
60
|
+
header (for example, ``JFcolor: '#5aa0ff'``) to enable that author's tags.
|
|
61
|
+
Before the first build, ``cpb`` offers to migrate legacy ``<comment>`` tags
|
|
62
|
+
and ``.comment-left/right`` blocks, and resolves author tags without a
|
|
63
|
+
matching color. New authors choose a hue in a Qt picker; ``cpb`` adds their
|
|
64
|
+
``XXcolor`` field to the header.
|
|
65
|
+
|
|
57
66
|
Declare a single local ``.bib`` file with ``bibliography: references.bib``
|
|
58
67
|
in the Markdown YAML header; relative paths are resolved beside the
|
|
59
68
|
Markdown file. Without that declaration, ``cpb`` uses a single adjacent
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyDiffTools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.37
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -109,6 +109,15 @@ included are (listed in order of fun/utility):
|
|
|
109
109
|
Python file operations and Qt dialogs. Its automated runtime checks have
|
|
110
110
|
been performed on Linux, not on Windows.
|
|
111
111
|
|
|
112
|
+
Comment bubbles use author tags such as ``<JFcom>…</JFcom>`` and
|
|
113
|
+
``<JFcom-left>…</JFcom-left>``. Block comments can use Markdown Div classes
|
|
114
|
+
such as ``::: {.JFcom-left}``. Add a quoted six-digit hex color to the YAML
|
|
115
|
+
header (for example, ``JFcolor: '#5aa0ff'``) to enable that author's tags.
|
|
116
|
+
Before the first build, ``cpb`` offers to migrate legacy ``<comment>`` tags
|
|
117
|
+
and ``.comment-left/right`` blocks, and resolves author tags without a
|
|
118
|
+
matching color. New authors choose a hue in a Qt picker; ``cpb`` adds their
|
|
119
|
+
``XXcolor`` field to the header.
|
|
120
|
+
|
|
112
121
|
Declare a single local ``.bib`` file with ``bibliography: references.bib``
|
|
113
122
|
in the Markdown YAML header; relative paths are resolved beside the
|
|
114
123
|
Markdown file. Without that declaration, ``cpb`` uses a single adjacent
|
|
@@ -52,6 +52,7 @@ pydifftools/citation_dialog.py
|
|
|
52
52
|
pydifftools/command_line.py
|
|
53
53
|
pydifftools/command_registry.py
|
|
54
54
|
pydifftools/comment_functions.py
|
|
55
|
+
pydifftools/comment_migration.py
|
|
55
56
|
pydifftools/comment_tags.lua
|
|
56
57
|
pydifftools/comment_tags_margin.lua
|
|
57
58
|
pydifftools/comment_tags_no_comments.lua
|
|
@@ -97,6 +98,7 @@ tests/test_bibliography.py
|
|
|
97
98
|
tests/test_browser_lifecycle.py
|
|
98
99
|
tests/test_check_single_use.py
|
|
99
100
|
tests/test_command_line_help.py
|
|
101
|
+
tests/test_comment_migration.py
|
|
100
102
|
tests/test_continuous_shutdown.py
|
|
101
103
|
tests/test_forward_search.py
|
|
102
104
|
tests/test_git_gd_history.py
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Pre-build migration of legacy and unregistered comment authors."""
|
|
2
|
+
|
|
3
|
+
import colorsys
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import textwrap
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
_FRONT_MATTER_RE = re.compile(
|
|
15
|
+
r"\A(\ufeff?---[ \t]*\r?\n)(.*?)(\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$))",
|
|
16
|
+
re.DOTALL,
|
|
17
|
+
)
|
|
18
|
+
_LEGACY_TAG_RE = re.compile(
|
|
19
|
+
r"(?P<closing></?)comment(?P<side>-(?:left|right))?>", re.I
|
|
20
|
+
)
|
|
21
|
+
_LEGACY_CLASS_RE = re.compile(r"(?<=\.)comment-(left|right)(?=[\s}])")
|
|
22
|
+
_AUTHOR_TAG_RE = re.compile(
|
|
23
|
+
r"</?([A-Za-z]{2})com(?:-(?:left|right))?>", re.I
|
|
24
|
+
)
|
|
25
|
+
_AUTHOR_CLASS_RE = re.compile(
|
|
26
|
+
r"\.([A-Za-z]{2})com(?:-(?:left|right))?(?=[\s}])", re.I
|
|
27
|
+
)
|
|
28
|
+
_HEX_RE = re.compile(r"^#[0-9a-fA-F]{6}$")
|
|
29
|
+
_SEED_COLOR = "#5aa0ff"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _front_matter(text):
|
|
33
|
+
match = _FRONT_MATTER_RE.match(text)
|
|
34
|
+
if not match:
|
|
35
|
+
if re.match(r"\A\ufeff?---[ \t]*(?:\r?\n|$)", text):
|
|
36
|
+
raise ValueError(
|
|
37
|
+
"YAML front matter starts with '---' but has no closing "
|
|
38
|
+
"delimiter"
|
|
39
|
+
)
|
|
40
|
+
return None, {}, {}
|
|
41
|
+
try:
|
|
42
|
+
metadata = yaml.safe_load(match.group(2)) or {}
|
|
43
|
+
except yaml.YAMLError as exc:
|
|
44
|
+
raise ValueError(f"invalid YAML front matter: {exc}") from exc
|
|
45
|
+
if not isinstance(metadata, dict):
|
|
46
|
+
raise ValueError("YAML front matter must contain a mapping")
|
|
47
|
+
colors = {}
|
|
48
|
+
for key, value in metadata.items():
|
|
49
|
+
initials = re.fullmatch(r"([A-Z]{2})color", str(key))
|
|
50
|
+
if not initials:
|
|
51
|
+
continue
|
|
52
|
+
if not isinstance(value, str) or not _HEX_RE.fullmatch(value):
|
|
53
|
+
raise ValueError(f"{key} must be a six-digit hex color")
|
|
54
|
+
colors[initials.group(1)] = value.lower()
|
|
55
|
+
return match, metadata, colors
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _most_distant_hue(colors):
|
|
59
|
+
"""Return the midpoint of the largest gap between existing hues."""
|
|
60
|
+
def hue(color):
|
|
61
|
+
red, green, blue = (
|
|
62
|
+
int(color[index:index + 2], 16) / 255 for index in (1, 3, 5)
|
|
63
|
+
)
|
|
64
|
+
return colorsys.rgb_to_hsv(red, green, blue)[0] * 360
|
|
65
|
+
|
|
66
|
+
if not colors:
|
|
67
|
+
return hue(_SEED_COLOR)
|
|
68
|
+
hues = sorted({hue(color) % 360 for color in colors})
|
|
69
|
+
gaps = [
|
|
70
|
+
(
|
|
71
|
+
(hues[index + 1] if index + 1 < len(hues) else hues[0] + 360)
|
|
72
|
+
- hue,
|
|
73
|
+
hue,
|
|
74
|
+
)
|
|
75
|
+
for index, hue in enumerate(hues)
|
|
76
|
+
]
|
|
77
|
+
gap_size, start = max(gaps, key=lambda gap: (gap[0], -gap[1]))
|
|
78
|
+
return (start + gap_size / 2) % 360
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _migration_dialog(text, colors, legacy):
|
|
82
|
+
"""Ask Qt for all migrations and return decisions without editing files."""
|
|
83
|
+
script = r"""
|
|
84
|
+
import colorsys
|
|
85
|
+
import json
|
|
86
|
+
import re
|
|
87
|
+
import sys
|
|
88
|
+
from PySide6.QtCore import Qt, Signal
|
|
89
|
+
from PySide6.QtGui import (
|
|
90
|
+
QColor, QIcon, QLinearGradient, QPainter, QPen, QPixmap,
|
|
91
|
+
)
|
|
92
|
+
from PySide6.QtWidgets import (
|
|
93
|
+
QApplication, QComboBox, QDialog, QDialogButtonBox, QLabel,
|
|
94
|
+
QLineEdit, QMessageBox, QVBoxLayout, QWidget,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
payload = json.loads(sys.argv[1])
|
|
98
|
+
colors = payload["colors"]
|
|
99
|
+
app = QApplication(sys.argv[:1])
|
|
100
|
+
result = {"accepted": True, "replacements": {}, "colors": {}}
|
|
101
|
+
|
|
102
|
+
def color_hue(color):
|
|
103
|
+
rgb = [int(color[i:i+2], 16) / 255 for i in (1, 3, 5)]
|
|
104
|
+
return colorsys.rgb_to_hsv(*rgb)[0] * 360
|
|
105
|
+
|
|
106
|
+
def most_distant_hue(used_colors):
|
|
107
|
+
if not used_colors:
|
|
108
|
+
return color_hue("#5aa0ff")
|
|
109
|
+
hues = sorted(set(color_hue(color) % 360 for color in used_colors))
|
|
110
|
+
gaps = [
|
|
111
|
+
((hues[i + 1] if i + 1 < len(hues) else hues[0] + 360) - hue, hue)
|
|
112
|
+
for i, hue in enumerate(hues)
|
|
113
|
+
]
|
|
114
|
+
gap, start = max(gaps, key=lambda item: (item[0], -item[1]))
|
|
115
|
+
return (start + gap / 2) % 360
|
|
116
|
+
|
|
117
|
+
class HueBar(QWidget):
|
|
118
|
+
hueChanged = Signal(int)
|
|
119
|
+
def __init__(self, hue):
|
|
120
|
+
super().__init__()
|
|
121
|
+
self.hue = int(round(hue)) % 360
|
|
122
|
+
self.setMinimumWidth(300)
|
|
123
|
+
self.setMinimumHeight(34)
|
|
124
|
+
def paintEvent(self, event):
|
|
125
|
+
painter = QPainter(self)
|
|
126
|
+
gradient = QLinearGradient(0, 0, self.width(), 0)
|
|
127
|
+
for hue in range(0, 361, 30):
|
|
128
|
+
gradient.setColorAt(hue / 360, QColor.fromHsv(hue % 360, 165, 255))
|
|
129
|
+
painter.fillRect(0, 7, self.width(), 20, gradient)
|
|
130
|
+
x = round(self.hue / 359 * (self.width() - 1))
|
|
131
|
+
painter.setPen(QPen(Qt.GlobalColor.black, 2))
|
|
132
|
+
painter.drawLine(x, 2, x, 31)
|
|
133
|
+
def mousePressEvent(self, event):
|
|
134
|
+
self.set_hue(event.position().x() / max(1, self.width() - 1) * 359)
|
|
135
|
+
def mouseMoveEvent(self, event):
|
|
136
|
+
if event.buttons() & Qt.MouseButton.LeftButton:
|
|
137
|
+
self.set_hue(event.position().x() / max(1, self.width() - 1) * 359)
|
|
138
|
+
def set_hue(self, hue):
|
|
139
|
+
self.hue = max(0, min(359, int(round(hue))))
|
|
140
|
+
self.update()
|
|
141
|
+
self.hueChanged.emit(self.hue)
|
|
142
|
+
|
|
143
|
+
def ask_hue(initial):
|
|
144
|
+
dlg = QDialog()
|
|
145
|
+
dlg.setWindowTitle("Create comment user")
|
|
146
|
+
layout = QVBoxLayout(dlg)
|
|
147
|
+
layout.addWidget(QLabel("Choose a hue for this comment user."))
|
|
148
|
+
bar = HueBar(initial)
|
|
149
|
+
layout.addWidget(bar)
|
|
150
|
+
preview = QLabel()
|
|
151
|
+
preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
152
|
+
preview.setMinimumHeight(48)
|
|
153
|
+
layout.addWidget(preview)
|
|
154
|
+
def update_preview():
|
|
155
|
+
# Match the saturation and opacity of the existing hard-coded blue.
|
|
156
|
+
color = QColor.fromHsv(bar.hue, 165, 255, 255).name()
|
|
157
|
+
preview.setText(color)
|
|
158
|
+
preview.setStyleSheet("background-color: " + color)
|
|
159
|
+
bar.update()
|
|
160
|
+
bar.hueChanged.connect(update_preview)
|
|
161
|
+
update_preview()
|
|
162
|
+
buttons = QDialogButtonBox(
|
|
163
|
+
QDialogButtonBox.StandardButton.Ok
|
|
164
|
+
| QDialogButtonBox.StandardButton.Cancel
|
|
165
|
+
)
|
|
166
|
+
buttons.accepted.connect(dlg.accept)
|
|
167
|
+
buttons.rejected.connect(dlg.reject)
|
|
168
|
+
layout.addWidget(buttons)
|
|
169
|
+
if dlg.exec() != QDialog.DialogCode.Accepted:
|
|
170
|
+
return None
|
|
171
|
+
return QColor.fromHsv(bar.hue, 165, 255, 255).name()
|
|
172
|
+
|
|
173
|
+
def ask_legacy_initials():
|
|
174
|
+
while True:
|
|
175
|
+
dlg = QDialog()
|
|
176
|
+
dlg.setWindowTitle("Migrate legacy comments")
|
|
177
|
+
layout = QVBoxLayout(dlg)
|
|
178
|
+
layout.addWidget(QLabel(
|
|
179
|
+
"Legacy comment tags and blocks were found. Enter the two "
|
|
180
|
+
"initials to use for all of them."
|
|
181
|
+
))
|
|
182
|
+
field = QLineEdit()
|
|
183
|
+
field.setMaxLength(2)
|
|
184
|
+
field.setPlaceholderText("JF")
|
|
185
|
+
layout.addWidget(field)
|
|
186
|
+
buttons = QDialogButtonBox(
|
|
187
|
+
QDialogButtonBox.StandardButton.Ok
|
|
188
|
+
| QDialogButtonBox.StandardButton.Cancel
|
|
189
|
+
)
|
|
190
|
+
buttons.accepted.connect(dlg.accept)
|
|
191
|
+
buttons.rejected.connect(dlg.reject)
|
|
192
|
+
layout.addWidget(buttons)
|
|
193
|
+
if dlg.exec() != QDialog.DialogCode.Accepted:
|
|
194
|
+
return None
|
|
195
|
+
initials = field.text().upper()
|
|
196
|
+
if re.fullmatch(r"[A-Z]{2}", initials):
|
|
197
|
+
return initials
|
|
198
|
+
QMessageBox.warning(None, "Invalid initials", "Enter two letters.")
|
|
199
|
+
|
|
200
|
+
legacy = payload["legacy"]
|
|
201
|
+
if legacy:
|
|
202
|
+
initials = ask_legacy_initials()
|
|
203
|
+
legacy_occurrences = len(re.findall(
|
|
204
|
+
r"</?comment(?:-(?:left|right))?>", payload["text"], re.I
|
|
205
|
+
)) + len(re.findall(
|
|
206
|
+
r"\.comment-(?:left|right)(?=[\s}])", payload["text"], re.I
|
|
207
|
+
))
|
|
208
|
+
if initials is None:
|
|
209
|
+
result["accepted"] = False
|
|
210
|
+
elif not QMessageBox.question(
|
|
211
|
+
None, "Replace legacy comments",
|
|
212
|
+
"Replace " + str(legacy_occurrences)
|
|
213
|
+
+ " legacy tag or block-class occurrences with "
|
|
214
|
+
+ initials + "com markup?",
|
|
215
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
216
|
+
QMessageBox.StandardButton.No,
|
|
217
|
+
) == QMessageBox.StandardButton.Yes:
|
|
218
|
+
result["accepted"] = False
|
|
219
|
+
else:
|
|
220
|
+
result["replacements"]["legacy"] = initials
|
|
221
|
+
|
|
222
|
+
if result["accepted"]:
|
|
223
|
+
authors = sorted(set(
|
|
224
|
+
initials.upper() for initials in re.findall(
|
|
225
|
+
r"</?([A-Za-z]{2})com(?:-(?:left|right))?>", payload["text"], re.I
|
|
226
|
+
)
|
|
227
|
+
) | set(
|
|
228
|
+
initials.upper() for initials in re.findall(
|
|
229
|
+
r"\.([A-Za-z]{2})com(?:-(?:left|right))?(?=[\s}])",
|
|
230
|
+
payload["text"], re.I
|
|
231
|
+
)
|
|
232
|
+
))
|
|
233
|
+
if result["replacements"].get("legacy"):
|
|
234
|
+
authors.append(result["replacements"]["legacy"])
|
|
235
|
+
authors = sorted(set(authors))
|
|
236
|
+
for author in authors:
|
|
237
|
+
if author in colors:
|
|
238
|
+
continue
|
|
239
|
+
box = QMessageBox()
|
|
240
|
+
box.setWindowTitle("Unknown comment author")
|
|
241
|
+
box.setText(
|
|
242
|
+
author + "com tags or block classes have no matching "
|
|
243
|
+
+ author + "color field."
|
|
244
|
+
)
|
|
245
|
+
selector = QComboBox()
|
|
246
|
+
for existing in sorted(colors):
|
|
247
|
+
selector.addItem(existing + " " + colors[existing], existing)
|
|
248
|
+
swatch = QPixmap(14, 14)
|
|
249
|
+
swatch.fill(QColor(colors[existing]))
|
|
250
|
+
selector.setItemIcon(selector.count() - 1, QIcon(swatch))
|
|
251
|
+
create = "Create " + author + " as a new comment user"
|
|
252
|
+
selector.addItem(create, "__create__")
|
|
253
|
+
selector.setCurrentIndex(selector.count() - 1)
|
|
254
|
+
box.layout().addWidget(selector, 1, 0, 1, box.layout().columnCount())
|
|
255
|
+
box.setStandardButtons(
|
|
256
|
+
QMessageBox.StandardButton.Cancel
|
|
257
|
+
| QMessageBox.StandardButton.Ok
|
|
258
|
+
)
|
|
259
|
+
box.setDefaultButton(QMessageBox.StandardButton.Ok)
|
|
260
|
+
if box.exec() != QMessageBox.StandardButton.Ok:
|
|
261
|
+
result["accepted"] = False
|
|
262
|
+
break
|
|
263
|
+
target = selector.currentData()
|
|
264
|
+
if target == "__create__":
|
|
265
|
+
seed = colors.get(author)
|
|
266
|
+
if seed is None:
|
|
267
|
+
used = [*colors.values(), *result["colors"].values()]
|
|
268
|
+
seed_hue = most_distant_hue(used)
|
|
269
|
+
seed = QColor.fromHsv(round(seed_hue), 165, 255).name()
|
|
270
|
+
color = ask_hue(QColor(seed).hue())
|
|
271
|
+
if color is None:
|
|
272
|
+
result["accepted"] = False
|
|
273
|
+
break
|
|
274
|
+
result["colors"][author] = color
|
|
275
|
+
colors[author] = color
|
|
276
|
+
elif target:
|
|
277
|
+
result["replacements"][author] = target
|
|
278
|
+
|
|
279
|
+
print(json.dumps(result))
|
|
280
|
+
"""
|
|
281
|
+
completed = subprocess.run(
|
|
282
|
+
[sys.executable, "-c", textwrap.dedent(script),
|
|
283
|
+
json.dumps({"text": text, "colors": colors, "legacy": legacy})],
|
|
284
|
+
capture_output=True, text=True,
|
|
285
|
+
)
|
|
286
|
+
if completed.returncode:
|
|
287
|
+
detail = completed.stderr.strip()
|
|
288
|
+
message = "cpb comment migration dialog failed"
|
|
289
|
+
if detail:
|
|
290
|
+
message += f": {detail}"
|
|
291
|
+
else:
|
|
292
|
+
message += "."
|
|
293
|
+
raise RuntimeError(message)
|
|
294
|
+
try:
|
|
295
|
+
return json.loads(completed.stdout)
|
|
296
|
+
except json.JSONDecodeError as exc:
|
|
297
|
+
raise RuntimeError(
|
|
298
|
+
"cpb comment migration dialog returned invalid data"
|
|
299
|
+
) from exc
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def prepare_comment_source(filename):
|
|
303
|
+
"""Migrate comment tags and author colors before a build, if requested."""
|
|
304
|
+
with open(filename, encoding="utf-8", newline="") as fp:
|
|
305
|
+
original = fp.read()
|
|
306
|
+
has_legacy = bool(
|
|
307
|
+
_LEGACY_TAG_RE.search(original) or _LEGACY_CLASS_RE.search(original)
|
|
308
|
+
)
|
|
309
|
+
if not (has_legacy or _AUTHOR_TAG_RE.search(original)
|
|
310
|
+
or _AUTHOR_CLASS_RE.search(original)):
|
|
311
|
+
return original
|
|
312
|
+
_, _, colors = _front_matter(original)
|
|
313
|
+
authors = {
|
|
314
|
+
initials.upper()
|
|
315
|
+
for initials in _AUTHOR_TAG_RE.findall(original)
|
|
316
|
+
} | {
|
|
317
|
+
initials.upper()
|
|
318
|
+
for initials in _AUTHOR_CLASS_RE.findall(original)
|
|
319
|
+
}
|
|
320
|
+
if not has_legacy and authors.issubset(colors):
|
|
321
|
+
return original
|
|
322
|
+
decisions = _migration_dialog(original, colors, has_legacy)
|
|
323
|
+
if not decisions.get("accepted"):
|
|
324
|
+
raise RuntimeError("cpb comment migration was canceled")
|
|
325
|
+
updated = original
|
|
326
|
+
for tag_author, target in decisions.get("replacements", {}).items():
|
|
327
|
+
if tag_author == "legacy":
|
|
328
|
+
updated = _LEGACY_TAG_RE.sub(
|
|
329
|
+
lambda found: (
|
|
330
|
+
found.group("closing") + target + "com"
|
|
331
|
+
+ (found.group("side") or "") + ">"
|
|
332
|
+
),
|
|
333
|
+
updated,
|
|
334
|
+
)
|
|
335
|
+
updated = _LEGACY_CLASS_RE.sub(
|
|
336
|
+
lambda found: target + "com-" + found.group(1), updated
|
|
337
|
+
)
|
|
338
|
+
else:
|
|
339
|
+
pattern = re.compile(
|
|
340
|
+
r"(?P<closing></?)" + re.escape(tag_author)
|
|
341
|
+
+ r"com(?P<side>-(?:left|right))?>",
|
|
342
|
+
re.I,
|
|
343
|
+
)
|
|
344
|
+
updated = pattern.sub(
|
|
345
|
+
lambda found: (
|
|
346
|
+
found.group("closing") + target + "com"
|
|
347
|
+
+ (found.group("side") or "") + ">"
|
|
348
|
+
),
|
|
349
|
+
updated,
|
|
350
|
+
)
|
|
351
|
+
class_pattern = re.compile(
|
|
352
|
+
r"\." + re.escape(tag_author)
|
|
353
|
+
+ r"com(?P<side>-(?:left|right))?(?=[\s}])", re.I
|
|
354
|
+
)
|
|
355
|
+
updated = class_pattern.sub(
|
|
356
|
+
lambda found: "." + target + "com"
|
|
357
|
+
+ (found.group("side") or ""), updated
|
|
358
|
+
)
|
|
359
|
+
additions = decisions.get("colors", {})
|
|
360
|
+
if additions:
|
|
361
|
+
# {{{ add chosen author colors to the YAML front matter
|
|
362
|
+
match, _, _ = _front_matter(updated)
|
|
363
|
+
color_lines = [
|
|
364
|
+
f'{author}color: "{color}"'
|
|
365
|
+
for author, color in sorted(additions.items())
|
|
366
|
+
]
|
|
367
|
+
if match:
|
|
368
|
+
body = match.group(2)
|
|
369
|
+
newline = "\r\n" if "\r\n" in updated else "\n"
|
|
370
|
+
if body and not body.endswith("\n"):
|
|
371
|
+
body += newline
|
|
372
|
+
body += newline.join(color_lines)
|
|
373
|
+
updated = (
|
|
374
|
+
updated[:match.start(2)] + body + updated[match.end(2):]
|
|
375
|
+
)
|
|
376
|
+
else:
|
|
377
|
+
newline = "\r\n" if "\r\n" in updated else "\n"
|
|
378
|
+
header = (
|
|
379
|
+
"---" + newline + newline.join(color_lines)
|
|
380
|
+
+ newline + "---" + newline
|
|
381
|
+
)
|
|
382
|
+
updated = header + updated
|
|
383
|
+
# }}}
|
|
384
|
+
if updated != original:
|
|
385
|
+
directory = os.path.dirname(os.path.abspath(filename))
|
|
386
|
+
fd, temp_path = tempfile.mkstemp(
|
|
387
|
+
prefix=".cpb-comments-", dir=directory
|
|
388
|
+
)
|
|
389
|
+
try:
|
|
390
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="") as fp:
|
|
391
|
+
fp.write(updated)
|
|
392
|
+
os.chmod(temp_path, os.stat(filename).st_mode)
|
|
393
|
+
os.replace(temp_path, filename)
|
|
394
|
+
finally:
|
|
395
|
+
if os.path.exists(temp_path):
|
|
396
|
+
os.unlink(temp_path)
|
|
397
|
+
return updated
|
|
@@ -32,6 +32,7 @@ local OPENERS = {
|
|
|
32
32
|
["<comment-right>"] = { side = "comment-right", close = "</comment-right>" },
|
|
33
33
|
["<comment-left>"] = { side = "comment-left", close = "</comment-left>" },
|
|
34
34
|
}
|
|
35
|
+
local AUTHOR_COLORS = {}
|
|
35
36
|
|
|
36
37
|
local comment_id = 0
|
|
37
38
|
local function next_id()
|
|
@@ -39,8 +40,15 @@ local function next_id()
|
|
|
39
40
|
return "c" .. tostring(comment_id)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
local function make_inline_comment(side, content_inlines)
|
|
43
|
-
local
|
|
43
|
+
local function make_inline_comment(side, content_inlines, spec)
|
|
44
|
+
local attributes = {}
|
|
45
|
+
if spec and spec.color then
|
|
46
|
+
attributes["style"] = "--comment-accent-color: " .. spec.color .. ";"
|
|
47
|
+
attributes["data-comment-author"] = spec.author
|
|
48
|
+
end
|
|
49
|
+
local inner = pandoc.Span(
|
|
50
|
+
content_inlines, pandoc.Attr("", { side }, attributes)
|
|
51
|
+
)
|
|
44
52
|
return pandoc.Span(pandoc.List({ inner }), pandoc.Attr("", { "comment-pin" }, {}))
|
|
45
53
|
end
|
|
46
54
|
|
|
@@ -50,11 +58,16 @@ local function make_block_anchor(id)
|
|
|
50
58
|
'<span class="comment-pin comment-pin-block" data-comment-id="' .. id .. '"></span>')
|
|
51
59
|
end
|
|
52
60
|
|
|
53
|
-
local function make_block_overlay(side, id, content_blocks)
|
|
61
|
+
local function make_block_overlay(side, id, content_blocks, spec)
|
|
54
62
|
-- Div can contain BulletList/Para/etc. JS will position this overlay.
|
|
55
63
|
return pandoc.Div(
|
|
56
64
|
content_blocks,
|
|
57
|
-
pandoc.Attr("", { "comment-overlay", side }, {
|
|
65
|
+
pandoc.Attr("", { "comment-overlay", side }, {
|
|
66
|
+
["data-comment-id"] = id,
|
|
67
|
+
["data-comment-author"] = spec and spec.author or nil,
|
|
68
|
+
["style"] = spec and spec.color
|
|
69
|
+
and ("--comment-accent-color: " .. spec.color .. ";") or nil,
|
|
70
|
+
})
|
|
58
71
|
)
|
|
59
72
|
end
|
|
60
73
|
|
|
@@ -263,7 +276,7 @@ function Inlines(inlines)
|
|
|
263
276
|
end
|
|
264
277
|
|
|
265
278
|
if found then
|
|
266
|
-
out:insert(make_inline_comment(spec.side, buf))
|
|
279
|
+
out:insert(make_inline_comment(spec.side, buf, spec))
|
|
267
280
|
i = j + 1
|
|
268
281
|
else
|
|
269
282
|
-- unmatched: leave literal
|
|
@@ -353,7 +366,7 @@ function Blocks(blocks)
|
|
|
353
366
|
if not merged_with_inline_anchor then
|
|
354
367
|
out:insert(make_block_anchor(id))
|
|
355
368
|
end
|
|
356
|
-
out:insert(make_block_overlay(spec.side, id, buf))
|
|
369
|
+
out:insert(make_block_overlay(spec.side, id, buf, spec))
|
|
357
370
|
|
|
358
371
|
-- Keep trailing content after the closer in main flow.
|
|
359
372
|
for after_index = 1, #after_close_blocks do
|
|
@@ -370,3 +383,54 @@ function Blocks(blocks)
|
|
|
370
383
|
|
|
371
384
|
return out
|
|
372
385
|
end
|
|
386
|
+
|
|
387
|
+
function Div(el)
|
|
388
|
+
for index, class_name in ipairs(el.classes) do
|
|
389
|
+
local author, side = class_name:match("^([A-Za-z][A-Za-z])com%-(left)$")
|
|
390
|
+
if not author then
|
|
391
|
+
author, side = class_name:match("^([A-Za-z][A-Za-z])com%-(right)$")
|
|
392
|
+
end
|
|
393
|
+
if not author then
|
|
394
|
+
author = class_name:match("^([A-Za-z][A-Za-z])com$")
|
|
395
|
+
side = "right"
|
|
396
|
+
end
|
|
397
|
+
author = author and author:upper() or nil
|
|
398
|
+
local color = author and AUTHOR_COLORS[author] or nil
|
|
399
|
+
if color then
|
|
400
|
+
el.classes[index] = "comment-" .. side
|
|
401
|
+
local style = el.attributes.style or ""
|
|
402
|
+
if #style > 0 and not style:match(";%s*$") then
|
|
403
|
+
style = style .. ";"
|
|
404
|
+
end
|
|
405
|
+
el.attributes.style = style .. "--comment-accent-color: " .. color .. ";"
|
|
406
|
+
el.attributes["data-comment-author"] = author
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
return el
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
function Pandoc(doc)
|
|
413
|
+
for key, value in pairs(doc.meta) do
|
|
414
|
+
local initials = tostring(key):match("^([A-Za-z][A-Za-z])color$")
|
|
415
|
+
local color = pandoc.utils.stringify(value)
|
|
416
|
+
if initials and color:match("^#%x%x%x%x%x%x$") then
|
|
417
|
+
initials = initials:upper()
|
|
418
|
+
color = color:lower()
|
|
419
|
+
AUTHOR_COLORS[initials] = color
|
|
420
|
+
for suffix, side in pairs({
|
|
421
|
+
["com"] = "comment-right",
|
|
422
|
+
["com-left"] = "comment-left",
|
|
423
|
+
["com-right"] = "comment-right",
|
|
424
|
+
}) do
|
|
425
|
+
local tag = "<" .. initials:lower() .. suffix .. ">"
|
|
426
|
+
OPENERS[tag] = {
|
|
427
|
+
side = side,
|
|
428
|
+
close = "</" .. initials:lower() .. suffix .. ">",
|
|
429
|
+
color = color,
|
|
430
|
+
author = initials,
|
|
431
|
+
}
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
return doc:walk({ Inlines = Inlines, Blocks = Blocks, Div = Div })
|
|
436
|
+
end
|