pyDiffTools 0.1.33__tar.gz → 0.1.35__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.33/pyDiffTools.egg-info → pydifftools-0.1.35}/PKG-INFO +42 -1
- {pydifftools-0.1.33 → pydifftools-0.1.35}/README.rst +41 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35/pyDiffTools.egg-info}/PKG-INFO +42 -1
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pyDiffTools.egg-info/SOURCES.txt +6 -1
- pydifftools-0.1.35/pydifftools/bibliography.py +305 -0
- pydifftools-0.1.35/pydifftools/citation_dialog.py +124 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/continuous.py +229 -16
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/wrap_sentences.py +70 -66
- pydifftools-0.1.35/pydifftools/zotero.py +278 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pyproject.toml +1 -1
- pydifftools-0.1.35/tests/test_bibliography.py +281 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_continuous_shutdown.py +25 -0
- pydifftools-0.1.35/tests/test_zotero.py +354 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/LICENSE.md +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/MANIFEST.in +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/browser_lifecycle.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/comment_tags_margin.lua +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/comment_tags_no_comments.lua +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/flowchart/comparison.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/flowchart/graph.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/flowchart/watch_graph.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/flowchart/wgrph_view.js +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/git_gd_history.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/setup.cfg +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_check_single_use.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_forward_search.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_git_gd_history.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_wrap_check.py +0 -0
- {pydifftools-0.1.33 → pydifftools-0.1.35}/tests/test_wrapping.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.35
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -96,6 +96,47 @@ included are (listed in order of fun/utility):
|
|
|
96
96
|
(currently tested on windows with *version 3.5* of the former,
|
|
97
97
|
*not the latest installer*,
|
|
98
98
|
since crossref isn't built with the most recent version).
|
|
99
|
+
|
|
100
|
+
When Pandoc reports missing citations, ``cpb`` retrieves their BibTeX
|
|
101
|
+
entries from the running local Zotero application, updates the active
|
|
102
|
+
bibliography, and rebuilds once. Install and enable
|
|
103
|
+
`Better BibTeX <https://retorque.re/zotero-better-bibtex/installation/>`_
|
|
104
|
+
in Zotero. No API key or Python dependency is needed. The integration uses
|
|
105
|
+
Better BibTeX's documented
|
|
106
|
+
`local JSON-RPC API <https://retorque.re/zotero-better-bibtex/exporting/json-rpc/>`_
|
|
107
|
+
at ``127.0.0.1:23119`` and searches the default personal library.
|
|
108
|
+
Zotero supports Windows, macOS, and Linux; this integration uses portable
|
|
109
|
+
Python file operations and Qt dialogs. Its automated runtime checks have
|
|
110
|
+
been performed on Linux, not on Windows.
|
|
111
|
+
|
|
112
|
+
Declare a single local ``.bib`` file with ``bibliography: references.bib``
|
|
113
|
+
in the Markdown YAML header; relative paths are resolved beside the
|
|
114
|
+
Markdown file. Without that declaration, ``cpb`` uses a single adjacent
|
|
115
|
+
``.bib`` file. Multiple or unsupported bibliographies are rendered normally
|
|
116
|
+
but are not automatically edited. Missing keys, unavailable Zotero, and
|
|
117
|
+
failed updates produce warnings; rendering continues. If Zotero or Better
|
|
118
|
+
BibTeX is unavailable at startup, an informational Qt window explains the
|
|
119
|
+
feature without blocking the preview.
|
|
120
|
+
|
|
121
|
+
Before importing a possible duplicate, a Qt dialog offers keeping the
|
|
122
|
+
existing entry, keeping the Zotero entry, or merging selected fields.
|
|
123
|
+
Matching uses DOI, book ISBN/edition, or a similar title with the same
|
|
124
|
+
first author and year. Conflicting DOIs are not treated as duplicates.
|
|
125
|
+
This follows the identifier and field comparison approach used by
|
|
126
|
+
`JabRef <https://docs.jabref.org/finding-sorting-and-cleaning-entries/mergeentries>`_.
|
|
127
|
+
The merge defaults to existing values and the existing key, filling absent
|
|
128
|
+
fields from Zotero. You can also keep both entries or skip an import.
|
|
129
|
+
After resolving a duplicate, ``@UNCHOSEN`` becomes ``@CHOSEN`` in the active
|
|
130
|
+
Markdown document. Code, links, ordinary text, and other documents are not
|
|
131
|
+
rewritten. Untouched bibliography entries retain their original text.
|
|
132
|
+
Edits made while a lookup or dialog is in progress abort the update.
|
|
133
|
+
|
|
134
|
+
Tests use recorded Zotero responses and real Pandoc. For the optional local
|
|
135
|
+
smoke tests, run ``PYDIFFTOOLS_ZOTERO_LIVE=1 python -m pytest -q
|
|
136
|
+
tests/test_zotero.py``. This additionally checks the live Hoult export and
|
|
137
|
+
builds a temporary copy of ``~/notebook/papers/eigenmode``, verifying that
|
|
138
|
+
the original files are unchanged. The ordinary suite requires neither
|
|
139
|
+
Zotero nor this notebook directory.
|
|
99
140
|
- `pydifft wgrph <graph.yaml>` watches a YAML flowchart description,
|
|
100
141
|
rebuilds the DOT/SVG output using GraphViz, and keeps a browser window
|
|
101
142
|
refreshed as you edit the file. This wraps the former
|
|
@@ -41,6 +41,47 @@ included are (listed in order of fun/utility):
|
|
|
41
41
|
(currently tested on windows with *version 3.5* of the former,
|
|
42
42
|
*not the latest installer*,
|
|
43
43
|
since crossref isn't built with the most recent version).
|
|
44
|
+
|
|
45
|
+
When Pandoc reports missing citations, ``cpb`` retrieves their BibTeX
|
|
46
|
+
entries from the running local Zotero application, updates the active
|
|
47
|
+
bibliography, and rebuilds once. Install and enable
|
|
48
|
+
`Better BibTeX <https://retorque.re/zotero-better-bibtex/installation/>`_
|
|
49
|
+
in Zotero. No API key or Python dependency is needed. The integration uses
|
|
50
|
+
Better BibTeX's documented
|
|
51
|
+
`local JSON-RPC API <https://retorque.re/zotero-better-bibtex/exporting/json-rpc/>`_
|
|
52
|
+
at ``127.0.0.1:23119`` and searches the default personal library.
|
|
53
|
+
Zotero supports Windows, macOS, and Linux; this integration uses portable
|
|
54
|
+
Python file operations and Qt dialogs. Its automated runtime checks have
|
|
55
|
+
been performed on Linux, not on Windows.
|
|
56
|
+
|
|
57
|
+
Declare a single local ``.bib`` file with ``bibliography: references.bib``
|
|
58
|
+
in the Markdown YAML header; relative paths are resolved beside the
|
|
59
|
+
Markdown file. Without that declaration, ``cpb`` uses a single adjacent
|
|
60
|
+
``.bib`` file. Multiple or unsupported bibliographies are rendered normally
|
|
61
|
+
but are not automatically edited. Missing keys, unavailable Zotero, and
|
|
62
|
+
failed updates produce warnings; rendering continues. If Zotero or Better
|
|
63
|
+
BibTeX is unavailable at startup, an informational Qt window explains the
|
|
64
|
+
feature without blocking the preview.
|
|
65
|
+
|
|
66
|
+
Before importing a possible duplicate, a Qt dialog offers keeping the
|
|
67
|
+
existing entry, keeping the Zotero entry, or merging selected fields.
|
|
68
|
+
Matching uses DOI, book ISBN/edition, or a similar title with the same
|
|
69
|
+
first author and year. Conflicting DOIs are not treated as duplicates.
|
|
70
|
+
This follows the identifier and field comparison approach used by
|
|
71
|
+
`JabRef <https://docs.jabref.org/finding-sorting-and-cleaning-entries/mergeentries>`_.
|
|
72
|
+
The merge defaults to existing values and the existing key, filling absent
|
|
73
|
+
fields from Zotero. You can also keep both entries or skip an import.
|
|
74
|
+
After resolving a duplicate, ``@UNCHOSEN`` becomes ``@CHOSEN`` in the active
|
|
75
|
+
Markdown document. Code, links, ordinary text, and other documents are not
|
|
76
|
+
rewritten. Untouched bibliography entries retain their original text.
|
|
77
|
+
Edits made while a lookup or dialog is in progress abort the update.
|
|
78
|
+
|
|
79
|
+
Tests use recorded Zotero responses and real Pandoc. For the optional local
|
|
80
|
+
smoke tests, run ``PYDIFFTOOLS_ZOTERO_LIVE=1 python -m pytest -q
|
|
81
|
+
tests/test_zotero.py``. This additionally checks the live Hoult export and
|
|
82
|
+
builds a temporary copy of ``~/notebook/papers/eigenmode``, verifying that
|
|
83
|
+
the original files are unchanged. The ordinary suite requires neither
|
|
84
|
+
Zotero nor this notebook directory.
|
|
44
85
|
- `pydifft wgrph <graph.yaml>` watches a YAML flowchart description,
|
|
45
86
|
rebuilds the DOT/SVG output using GraphViz, and keeps a browser window
|
|
46
87
|
refreshed as you edit the file. This wraps the former
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyDiffTools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.35
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -96,6 +96,47 @@ included are (listed in order of fun/utility):
|
|
|
96
96
|
(currently tested on windows with *version 3.5* of the former,
|
|
97
97
|
*not the latest installer*,
|
|
98
98
|
since crossref isn't built with the most recent version).
|
|
99
|
+
|
|
100
|
+
When Pandoc reports missing citations, ``cpb`` retrieves their BibTeX
|
|
101
|
+
entries from the running local Zotero application, updates the active
|
|
102
|
+
bibliography, and rebuilds once. Install and enable
|
|
103
|
+
`Better BibTeX <https://retorque.re/zotero-better-bibtex/installation/>`_
|
|
104
|
+
in Zotero. No API key or Python dependency is needed. The integration uses
|
|
105
|
+
Better BibTeX's documented
|
|
106
|
+
`local JSON-RPC API <https://retorque.re/zotero-better-bibtex/exporting/json-rpc/>`_
|
|
107
|
+
at ``127.0.0.1:23119`` and searches the default personal library.
|
|
108
|
+
Zotero supports Windows, macOS, and Linux; this integration uses portable
|
|
109
|
+
Python file operations and Qt dialogs. Its automated runtime checks have
|
|
110
|
+
been performed on Linux, not on Windows.
|
|
111
|
+
|
|
112
|
+
Declare a single local ``.bib`` file with ``bibliography: references.bib``
|
|
113
|
+
in the Markdown YAML header; relative paths are resolved beside the
|
|
114
|
+
Markdown file. Without that declaration, ``cpb`` uses a single adjacent
|
|
115
|
+
``.bib`` file. Multiple or unsupported bibliographies are rendered normally
|
|
116
|
+
but are not automatically edited. Missing keys, unavailable Zotero, and
|
|
117
|
+
failed updates produce warnings; rendering continues. If Zotero or Better
|
|
118
|
+
BibTeX is unavailable at startup, an informational Qt window explains the
|
|
119
|
+
feature without blocking the preview.
|
|
120
|
+
|
|
121
|
+
Before importing a possible duplicate, a Qt dialog offers keeping the
|
|
122
|
+
existing entry, keeping the Zotero entry, or merging selected fields.
|
|
123
|
+
Matching uses DOI, book ISBN/edition, or a similar title with the same
|
|
124
|
+
first author and year. Conflicting DOIs are not treated as duplicates.
|
|
125
|
+
This follows the identifier and field comparison approach used by
|
|
126
|
+
`JabRef <https://docs.jabref.org/finding-sorting-and-cleaning-entries/mergeentries>`_.
|
|
127
|
+
The merge defaults to existing values and the existing key, filling absent
|
|
128
|
+
fields from Zotero. You can also keep both entries or skip an import.
|
|
129
|
+
After resolving a duplicate, ``@UNCHOSEN`` becomes ``@CHOSEN`` in the active
|
|
130
|
+
Markdown document. Code, links, ordinary text, and other documents are not
|
|
131
|
+
rewritten. Untouched bibliography entries retain their original text.
|
|
132
|
+
Edits made while a lookup or dialog is in progress abort the update.
|
|
133
|
+
|
|
134
|
+
Tests use recorded Zotero responses and real Pandoc. For the optional local
|
|
135
|
+
smoke tests, run ``PYDIFFTOOLS_ZOTERO_LIVE=1 python -m pytest -q
|
|
136
|
+
tests/test_zotero.py``. This additionally checks the live Hoult export and
|
|
137
|
+
builds a temporary copy of ``~/notebook/papers/eigenmode``, verifying that
|
|
138
|
+
the original files are unchanged. The ordinary suite requires neither
|
|
139
|
+
Zotero nor this notebook directory.
|
|
99
140
|
- `pydifft wgrph <graph.yaml>` watches a YAML flowchart description,
|
|
100
141
|
rebuilds the DOT/SVG output using GraphViz, and keeps a browser window
|
|
101
142
|
refreshed as you edit the file. This wraps the former
|
|
@@ -45,8 +45,10 @@ pyDiffTools.egg-info/entry_points.txt
|
|
|
45
45
|
pyDiffTools.egg-info/requires.txt
|
|
46
46
|
pyDiffTools.egg-info/top_level.txt
|
|
47
47
|
pydifftools/__init__.py
|
|
48
|
+
pydifftools/bibliography.py
|
|
48
49
|
pydifftools/browser_lifecycle.py
|
|
49
50
|
pydifftools/check_numbers.py
|
|
51
|
+
pydifftools/citation_dialog.py
|
|
50
52
|
pydifftools/command_line.py
|
|
51
53
|
pydifftools/command_registry.py
|
|
52
54
|
pydifftools/comment_functions.py
|
|
@@ -79,6 +81,7 @@ pydifftools/unseparate_comments.py
|
|
|
79
81
|
pydifftools/update_check.py
|
|
80
82
|
pydifftools/wrap_sentences.py
|
|
81
83
|
pydifftools/xml2xlsx.vbs
|
|
84
|
+
pydifftools/zotero.py
|
|
82
85
|
pydifftools/flowchart/__init__.py
|
|
83
86
|
pydifftools/flowchart/comparison.py
|
|
84
87
|
pydifftools/flowchart/dot_to_yaml.py
|
|
@@ -90,6 +93,7 @@ pydifftools/notebook/__init__.py
|
|
|
90
93
|
pydifftools/notebook/display.py
|
|
91
94
|
pydifftools/notebook/fast_build.py
|
|
92
95
|
pydifftools/notebook/tex_to_qmd.py
|
|
96
|
+
tests/test_bibliography.py
|
|
93
97
|
tests/test_browser_lifecycle.py
|
|
94
98
|
tests/test_check_single_use.py
|
|
95
99
|
tests/test_command_line_help.py
|
|
@@ -102,4 +106,5 @@ tests/test_rrng.py
|
|
|
102
106
|
tests/test_tex_to_qmd.py
|
|
103
107
|
tests/test_update_check.py
|
|
104
108
|
tests/test_wrap_check.py
|
|
105
|
-
tests/test_wrapping.py
|
|
109
|
+
tests/test_wrapping.py
|
|
110
|
+
tests/test_zotero.py
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Lossless BibTeX edits and citation-aware Markdown key changes.
|
|
2
|
+
|
|
3
|
+
Pandoc remains the semantic parser. The small scanner here only locates raw
|
|
4
|
+
entry/field boundaries, preserving macros, custom fields and untouched text.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from difflib import SequenceMatcher
|
|
9
|
+
import html
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import re
|
|
14
|
+
import stat
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
import unicodedata
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def scan_bibtex(text, start, stops):
|
|
22
|
+
"""Locate an unquoted delimiter outside balanced braces."""
|
|
23
|
+
depth = 0
|
|
24
|
+
quoted = False
|
|
25
|
+
position = start
|
|
26
|
+
while position < len(text):
|
|
27
|
+
char = text[position]
|
|
28
|
+
if char == "\\":
|
|
29
|
+
position += 2
|
|
30
|
+
continue
|
|
31
|
+
if char == "%" and depth == 0 and not quoted:
|
|
32
|
+
newline = text.find("\n", position)
|
|
33
|
+
position = len(text) if newline < 0 else newline + 1
|
|
34
|
+
continue
|
|
35
|
+
if depth == 0 and not quoted and char in stops:
|
|
36
|
+
return position
|
|
37
|
+
if char == '"' and depth == 0:
|
|
38
|
+
quoted = not quoted
|
|
39
|
+
elif char == "{":
|
|
40
|
+
depth += 1
|
|
41
|
+
elif char == "}":
|
|
42
|
+
depth -= 1
|
|
43
|
+
if depth < 0:
|
|
44
|
+
raise ValueError("unbalanced BibTeX braces")
|
|
45
|
+
position += 1
|
|
46
|
+
if depth or quoted:
|
|
47
|
+
raise ValueError("unterminated BibTeX value")
|
|
48
|
+
return position
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class BibEntry:
|
|
53
|
+
start: int
|
|
54
|
+
end: int
|
|
55
|
+
kind: str
|
|
56
|
+
key: str
|
|
57
|
+
fields: dict
|
|
58
|
+
|
|
59
|
+
def render(self, key=None, fields=None):
|
|
60
|
+
selected = self.fields if fields is None else fields
|
|
61
|
+
body = ",\n".join(
|
|
62
|
+
f" {name} = {value}" for name, value in selected.items()
|
|
63
|
+
)
|
|
64
|
+
return f"@{self.kind}{{{key or self.key},\n{body}\n}}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def bib_entries(text):
|
|
68
|
+
"""Index entry spans and raw fields without reserializing them."""
|
|
69
|
+
entries = {}
|
|
70
|
+
position = 0
|
|
71
|
+
while position < len(text):
|
|
72
|
+
position = scan_bibtex(text, position, "@")
|
|
73
|
+
if position == len(text):
|
|
74
|
+
break
|
|
75
|
+
header = re.match(r"@([\w-]+)\s*([{(])", text[position:])
|
|
76
|
+
if not header:
|
|
77
|
+
position += 1
|
|
78
|
+
continue
|
|
79
|
+
start = position
|
|
80
|
+
kind = header[1].lower()
|
|
81
|
+
position += header.end()
|
|
82
|
+
closing = "}" if header[2] == "{" else ")"
|
|
83
|
+
end = scan_bibtex(text, position, closing)
|
|
84
|
+
if end == len(text):
|
|
85
|
+
raise ValueError("unterminated BibTeX entry")
|
|
86
|
+
body = text[position:end]
|
|
87
|
+
position = end + 1
|
|
88
|
+
if kind in {"comment", "preamble", "string"}:
|
|
89
|
+
continue
|
|
90
|
+
key_end = scan_bibtex(body, 0, ",")
|
|
91
|
+
key = body[:key_end].strip()
|
|
92
|
+
fields = {}
|
|
93
|
+
offset = key_end + 1
|
|
94
|
+
while offset < len(body):
|
|
95
|
+
whitespace = re.match(r"(?:\s|%[^\n]*(?:\n|$))*", body[offset:])
|
|
96
|
+
offset += whitespace.end()
|
|
97
|
+
if offset == len(body):
|
|
98
|
+
break
|
|
99
|
+
field = re.match(r"([\w-]+)\s*=\s*", body[offset:])
|
|
100
|
+
if not field:
|
|
101
|
+
raise ValueError(f"cannot safely edit BibTeX fields in {key}")
|
|
102
|
+
offset += field.end()
|
|
103
|
+
boundary = scan_bibtex(body, offset, ",")
|
|
104
|
+
name = field[1].lower()
|
|
105
|
+
if name in fields:
|
|
106
|
+
raise ValueError(f"duplicate BibTeX field {name} in {key}")
|
|
107
|
+
fields[name] = body[offset:boundary].strip()
|
|
108
|
+
offset = boundary + 1
|
|
109
|
+
if key in entries:
|
|
110
|
+
raise ValueError(f"duplicate BibTeX key {key}")
|
|
111
|
+
entries[key] = BibEntry(start, end + 1, kind, key, fields)
|
|
112
|
+
return entries
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def normalized_text(value):
|
|
116
|
+
text = html.unescape(re.sub(r"<[^>]+>", "", str(value)))
|
|
117
|
+
text = unicodedata.normalize("NFKD", text).casefold()
|
|
118
|
+
return "".join(char for char in text if char.isalnum())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def duplicate_reason(existing, incoming):
|
|
122
|
+
"""Identify likely duplicates for human review using Pandoc CSL records.
|
|
123
|
+
|
|
124
|
+
Inspired by JabRef's identifier-first, bibliographic-field comparison:
|
|
125
|
+
https://github.com/JabRef/jabref/blob/main/jablib/src/main/java/org/jabref/
|
|
126
|
+
logic/database/DuplicateCheck.java. No Java code is copied.
|
|
127
|
+
"""
|
|
128
|
+
identifiers = []
|
|
129
|
+
for record in (existing, incoming):
|
|
130
|
+
doi = str(record.get("DOI", "")).strip().casefold()
|
|
131
|
+
identifiers.append(
|
|
132
|
+
re.sub(r"^(?:https?://(?:dx\.)?doi.org/|doi:\s*)", "", doi)
|
|
133
|
+
)
|
|
134
|
+
if all(identifiers):
|
|
135
|
+
return "Same DOI" if identifiers[0] == identifiers[1] else None
|
|
136
|
+
if existing.get("type") != incoming.get("type"):
|
|
137
|
+
return None
|
|
138
|
+
for field in ("edition", "chapter-number"):
|
|
139
|
+
if existing.get(field) and incoming.get(field):
|
|
140
|
+
if normalized_text(existing[field]) != normalized_text(
|
|
141
|
+
incoming[field]
|
|
142
|
+
):
|
|
143
|
+
return None
|
|
144
|
+
if existing.get("type") == "chapter":
|
|
145
|
+
if normalized_text(existing.get("page")) != normalized_text(
|
|
146
|
+
incoming.get("page")
|
|
147
|
+
):
|
|
148
|
+
return None
|
|
149
|
+
if existing.get("type") == "book":
|
|
150
|
+
isbn = normalized_text(existing.get("ISBN", ""))
|
|
151
|
+
if isbn and isbn == normalized_text(incoming.get("ISBN", "")):
|
|
152
|
+
return "Same ISBN and edition"
|
|
153
|
+
titles = [
|
|
154
|
+
normalized_text(record.get("title", ""))
|
|
155
|
+
for record in (existing, incoming)
|
|
156
|
+
]
|
|
157
|
+
authors = [
|
|
158
|
+
record.get("author", record.get("editor", []))
|
|
159
|
+
for record in (existing, incoming)
|
|
160
|
+
]
|
|
161
|
+
years = [
|
|
162
|
+
record.get("issued", {}).get("date-parts", [[]])[0][:1]
|
|
163
|
+
for record in (existing, incoming)
|
|
164
|
+
]
|
|
165
|
+
if not all(titles) or not all(authors) or not all(years):
|
|
166
|
+
return None
|
|
167
|
+
surnames = [
|
|
168
|
+
normalized_text(names[0].get("family", names[0].get("literal", "")))
|
|
169
|
+
for names in authors
|
|
170
|
+
]
|
|
171
|
+
similar_title = titles[0] == titles[1] or (
|
|
172
|
+
min(map(len, titles)) >= 20
|
|
173
|
+
and SequenceMatcher(None, *titles).ratio() >= 0.92
|
|
174
|
+
)
|
|
175
|
+
if (
|
|
176
|
+
similar_title
|
|
177
|
+
and surnames[0]
|
|
178
|
+
and surnames[0] == surnames[1]
|
|
179
|
+
and years[0] == years[1]
|
|
180
|
+
):
|
|
181
|
+
return "Similar title, same first author and year"
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def markdown_ast(text, renames=None):
|
|
186
|
+
"""Parse Markdown, optionally normalizing citation IDs for comparison."""
|
|
187
|
+
completed = subprocess.run(
|
|
188
|
+
["pandoc", "--from=markdown-auto_identifiers", "--to=json"],
|
|
189
|
+
input=text.encode("utf-8"),
|
|
190
|
+
capture_output=True,
|
|
191
|
+
)
|
|
192
|
+
if completed.returncode:
|
|
193
|
+
raise ValueError("cannot parse Markdown to update citation keys")
|
|
194
|
+
tree = json.loads(completed.stdout)
|
|
195
|
+
if renames is not None:
|
|
196
|
+
pending = [tree]
|
|
197
|
+
while pending:
|
|
198
|
+
node = pending.pop()
|
|
199
|
+
if isinstance(node, dict):
|
|
200
|
+
if node.get("t") == "Cite":
|
|
201
|
+
# Pandoc stores both parsed citations and their source
|
|
202
|
+
# spelling; spelling changes when a key is renamed.
|
|
203
|
+
node["c"][1] = []
|
|
204
|
+
for citation in node["c"][0]:
|
|
205
|
+
old = citation["citationId"]
|
|
206
|
+
citation["citationId"] = renames.get(old, old)
|
|
207
|
+
pending.extend(node.values())
|
|
208
|
+
elif isinstance(node, list):
|
|
209
|
+
pending.extend(node)
|
|
210
|
+
return tree
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def rewrite_citations(markdown, renames):
|
|
214
|
+
"""Replace citation tokens, accepting edits only when Pandoc confirms them.
|
|
215
|
+
|
|
216
|
+
AST comparison protects code, links, email addresses, comments, ordinary
|
|
217
|
+
text and keys with a shared prefix, while retaining original formatting.
|
|
218
|
+
"""
|
|
219
|
+
text = markdown.decode("utf-8")
|
|
220
|
+
if not renames:
|
|
221
|
+
return markdown
|
|
222
|
+
expected = markdown_ast(text, renames)
|
|
223
|
+
keys = sorted(renames, key=len, reverse=True)
|
|
224
|
+
pattern = re.compile(
|
|
225
|
+
r"@(?:\{("
|
|
226
|
+
+ "|".join(map(re.escape, keys))
|
|
227
|
+
+ r")\}|("
|
|
228
|
+
+ "|".join(map(re.escape, keys))
|
|
229
|
+
+ r"))"
|
|
230
|
+
)
|
|
231
|
+
for match in reversed(list(pattern.finditer(text))):
|
|
232
|
+
old = match[1] or match[2]
|
|
233
|
+
replacement = (
|
|
234
|
+
"@{" + renames[old] + "}" if match[1] else "@" + renames[old]
|
|
235
|
+
)
|
|
236
|
+
candidate = text[: match.start()] + replacement + text[match.end() :]
|
|
237
|
+
if markdown_ast(candidate, renames) == expected:
|
|
238
|
+
text = candidate
|
|
239
|
+
if markdown_ast(text, {}) != expected:
|
|
240
|
+
raise ValueError("could not safely update all Markdown citation keys")
|
|
241
|
+
return text.encode("utf-8")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def replace_bibliography_files(changes):
|
|
245
|
+
"""Stage validated replacements and roll back if a later replacement fails.
|
|
246
|
+
|
|
247
|
+
Changes map paths to (original bytes, replacement bytes). Files are closed
|
|
248
|
+
before replacement for Windows. A crash across two replacements cannot be
|
|
249
|
+
atomic; ordinary write failures are rolled back without overwriting edits.
|
|
250
|
+
"""
|
|
251
|
+
staged = {}
|
|
252
|
+
backups = {}
|
|
253
|
+
committed = []
|
|
254
|
+
temporaries = []
|
|
255
|
+
try:
|
|
256
|
+
for path, (original, replacement) in changes.items():
|
|
257
|
+
path = Path(path)
|
|
258
|
+
if replacement == original:
|
|
259
|
+
continue
|
|
260
|
+
mode = stat.S_IMODE(path.stat().st_mode)
|
|
261
|
+
if not mode & 0o222 or not os.access(path, os.W_OK):
|
|
262
|
+
raise PermissionError(f"file is not writable: {path}")
|
|
263
|
+
for content, targets in [
|
|
264
|
+
(original, backups),
|
|
265
|
+
(replacement, staged),
|
|
266
|
+
]:
|
|
267
|
+
with tempfile.NamedTemporaryFile(
|
|
268
|
+
dir=path.parent, prefix=f".{path.name}.", delete=False
|
|
269
|
+
) as output:
|
|
270
|
+
temporary = Path(output.name)
|
|
271
|
+
temporaries.append(temporary)
|
|
272
|
+
output.write(content)
|
|
273
|
+
output.flush()
|
|
274
|
+
os.fsync(output.fileno())
|
|
275
|
+
os.chmod(temporary, mode)
|
|
276
|
+
targets[path] = temporary
|
|
277
|
+
for path, (original, _) in changes.items():
|
|
278
|
+
if Path(path).read_bytes() != original:
|
|
279
|
+
raise ValueError(f"file changed during Zotero lookup: {path}")
|
|
280
|
+
for path in staged:
|
|
281
|
+
if path.read_bytes() != changes[path][0]:
|
|
282
|
+
raise ValueError(f"file changed during Zotero lookup: {path}")
|
|
283
|
+
os.replace(staged[path], path)
|
|
284
|
+
committed.append(path)
|
|
285
|
+
except (OSError, ValueError):
|
|
286
|
+
for path in reversed(committed):
|
|
287
|
+
if path.read_bytes() == changes[path][1]:
|
|
288
|
+
try:
|
|
289
|
+
os.replace(backups[path], path)
|
|
290
|
+
except OSError as exc:
|
|
291
|
+
# Keep a recovery copy if even rollback is prevented.
|
|
292
|
+
temporaries.remove(backups[path])
|
|
293
|
+
raise OSError(
|
|
294
|
+
f"could not restore {path}; original saved at "
|
|
295
|
+
f"{backups[path]}"
|
|
296
|
+
) from exc
|
|
297
|
+
raise
|
|
298
|
+
finally:
|
|
299
|
+
for temporary in temporaries:
|
|
300
|
+
try:
|
|
301
|
+
temporary.unlink(missing_ok=True)
|
|
302
|
+
except OSError as exc:
|
|
303
|
+
print(
|
|
304
|
+
f"cpb: cannot remove {temporary}: {exc}", file=sys.stderr
|
|
305
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Qt review of a possible duplicate citation, run in a separate process."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from PySide6.QtCore import Qt
|
|
7
|
+
from PySide6.QtWidgets import (
|
|
8
|
+
QApplication,
|
|
9
|
+
QComboBox,
|
|
10
|
+
QDialog,
|
|
11
|
+
QHBoxLayout,
|
|
12
|
+
QHeaderView,
|
|
13
|
+
QLabel,
|
|
14
|
+
QPushButton,
|
|
15
|
+
QTableWidget,
|
|
16
|
+
QTableWidgetItem,
|
|
17
|
+
QVBoxLayout,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DuplicateDialog(QDialog):
|
|
22
|
+
"""Choose an entry or merge fields; default to existing values and key."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, matches, incoming, source):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.matches = matches
|
|
27
|
+
self.incoming = incoming
|
|
28
|
+
self.decision = None
|
|
29
|
+
self.setWindowTitle("pydifft cpb — Possible duplicate citation")
|
|
30
|
+
self.resize(1000, 650)
|
|
31
|
+
layout = QVBoxLayout(self)
|
|
32
|
+
explanation = QLabel(
|
|
33
|
+
f"Review the imported citation for {source}.\n"
|
|
34
|
+
"Keeping or merging a duplicate also replaces @UNCHOSEN with "
|
|
35
|
+
"@CHOSEN in this Markdown file. Other documents are not edited.\n"
|
|
36
|
+
"For a merge, choose each field below, including the citation key."
|
|
37
|
+
)
|
|
38
|
+
explanation.setWordWrap(True)
|
|
39
|
+
layout.addWidget(explanation)
|
|
40
|
+
self.match_selector = QComboBox()
|
|
41
|
+
for match in matches:
|
|
42
|
+
self.match_selector.addItem(f"{match['key']} — {match['reason']}")
|
|
43
|
+
layout.addWidget(self.match_selector)
|
|
44
|
+
self.table = QTableWidget()
|
|
45
|
+
self.table.setColumnCount(4)
|
|
46
|
+
self.table.setHorizontalHeaderLabels(
|
|
47
|
+
["Field", "Existing bibliography", "Zotero", "Merge uses"]
|
|
48
|
+
)
|
|
49
|
+
self.table.horizontalHeader().setSectionResizeMode(
|
|
50
|
+
QHeaderView.ResizeMode.Stretch
|
|
51
|
+
)
|
|
52
|
+
layout.addWidget(self.table)
|
|
53
|
+
buttons = QHBoxLayout()
|
|
54
|
+
self.buttons = {}
|
|
55
|
+
for label, action in [
|
|
56
|
+
("Keep existing", "existing"),
|
|
57
|
+
("Keep Zotero", "incoming"),
|
|
58
|
+
("Merge selected fields", "merge"),
|
|
59
|
+
("Not a duplicate: keep both", "both"),
|
|
60
|
+
("Skip this import", "skip"),
|
|
61
|
+
]:
|
|
62
|
+
button = QPushButton(label)
|
|
63
|
+
button.setAutoDefault(False)
|
|
64
|
+
button.clicked.connect(
|
|
65
|
+
lambda _checked=False, choice=action: self.choose(choice)
|
|
66
|
+
)
|
|
67
|
+
self.buttons[action] = button
|
|
68
|
+
buttons.addWidget(button)
|
|
69
|
+
layout.addLayout(buttons)
|
|
70
|
+
self.match_selector.currentIndexChanged.connect(self.populate)
|
|
71
|
+
self.populate()
|
|
72
|
+
|
|
73
|
+
def populate(self):
|
|
74
|
+
existing = self.matches[self.match_selector.currentIndex()]
|
|
75
|
+
self.left = {
|
|
76
|
+
"citation key": existing["key"],
|
|
77
|
+
"entry type": existing["kind"],
|
|
78
|
+
**existing["fields"],
|
|
79
|
+
}
|
|
80
|
+
self.right = {
|
|
81
|
+
"citation key": self.incoming["key"],
|
|
82
|
+
"entry type": self.incoming["kind"],
|
|
83
|
+
**self.incoming["fields"],
|
|
84
|
+
}
|
|
85
|
+
self.names = list(dict.fromkeys([*self.left, *self.right]))
|
|
86
|
+
self.table.setRowCount(len(self.names))
|
|
87
|
+
self.selectors = {}
|
|
88
|
+
for row, name in enumerate(self.names):
|
|
89
|
+
for column, value in enumerate(
|
|
90
|
+
[name, self.left.get(name, ""), self.right.get(name, "")]
|
|
91
|
+
):
|
|
92
|
+
cell = QTableWidgetItem(value)
|
|
93
|
+
cell.setFlags(cell.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
94
|
+
self.table.setItem(row, column, cell)
|
|
95
|
+
selector = QComboBox()
|
|
96
|
+
if name in self.left:
|
|
97
|
+
selector.addItem("Existing", self.left[name])
|
|
98
|
+
if name in self.right:
|
|
99
|
+
selector.addItem("Zotero", self.right[name])
|
|
100
|
+
self.table.setCellWidget(row, 3, selector)
|
|
101
|
+
self.selectors[name] = selector
|
|
102
|
+
self.table.resizeRowsToContents()
|
|
103
|
+
|
|
104
|
+
def choose(self, action):
|
|
105
|
+
existing = self.matches[self.match_selector.currentIndex()]
|
|
106
|
+
self.decision = {"action": action, "existing_key": existing["key"]}
|
|
107
|
+
if action == "merge":
|
|
108
|
+
values = {
|
|
109
|
+
name: selector.currentData()
|
|
110
|
+
for name, selector in self.selectors.items()
|
|
111
|
+
}
|
|
112
|
+
self.decision.update(
|
|
113
|
+
key=values.pop("citation key"),
|
|
114
|
+
kind=values.pop("entry type"),
|
|
115
|
+
fields=values,
|
|
116
|
+
)
|
|
117
|
+
self.accept()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
app = QApplication(sys.argv[:1])
|
|
122
|
+
dialog = DuplicateDialog(**json.load(sys.stdin))
|
|
123
|
+
dialog.exec()
|
|
124
|
+
print(json.dumps(dialog.decision))
|