openprocess 0.7.0__py3-none-any.whl
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.
- cpnpy/__init__.py +46 -0
- openprocess/__init__.py +57 -0
- openprocess/analysis/__init__.py +0 -0
- openprocess/analysis/state_space.py +521 -0
- openprocess/analysis/state_space_process.py +251 -0
- openprocess/cli.py +742 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
- openprocess/exercises/pack.md +14 -0
- openprocess/flow/__init__.py +50 -0
- openprocess/flow/box.py +466 -0
- openprocess/flow/boxes/__init__.py +7 -0
- openprocess/flow/boxes/check.py +119 -0
- openprocess/flow/boxes/compare.py +16 -0
- openprocess/flow/boxes/cpn.py +53 -0
- openprocess/flow/boxes/discover.py +124 -0
- openprocess/flow/boxes/filter.py +80 -0
- openprocess/flow/boxes/input.py +124 -0
- openprocess/flow/boxes/output.py +52 -0
- openprocess/flow/boxes/predict.py +186 -0
- openprocess/flow/boxes/science.py +159 -0
- openprocess/flow/boxes/sweeps.py +18 -0
- openprocess/flow/convert.py +187 -0
- openprocess/flow/datasets.py +198 -0
- openprocess/flow/explain.py +115 -0
- openprocess/flow/library.py +222 -0
- openprocess/flow/record.py +385 -0
- openprocess/flow/runner.py +357 -0
- openprocess/flow/sweep.py +92 -0
- openprocess/flow/types.py +290 -0
- openprocess/flow/workflow.py +628 -0
- openprocess/gui/__init__.py +0 -0
- openprocess/gui/app.py +90 -0
- openprocess/gui/arc_editing.py +295 -0
- openprocess/gui/canvas.py +1414 -0
- openprocess/gui/flow/__init__.py +8 -0
- openprocess/gui/flow/canvas.py +854 -0
- openprocess/gui/flow/page.py +972 -0
- openprocess/gui/flow/templates.py +131 -0
- openprocess/gui/flow/viewers.py +665 -0
- openprocess/gui/items.py +1275 -0
- openprocess/gui/learn/answer_boxes.py +978 -0
- openprocess/gui/learn/concealment.py +91 -0
- openprocess/gui/learn/mode.py +1181 -0
- openprocess/gui/panning.py +241 -0
- openprocess/gui/resources/openprocess-icon.png +0 -0
- openprocess/gui/studio/__init__.py +1 -0
- openprocess/gui/studio/__main__.py +3 -0
- openprocess/gui/studio/app.py +4031 -0
- openprocess/gui/studio/charts.py +115 -0
- openprocess/gui/studio/compare_page.py +487 -0
- openprocess/gui/studio/cpn_page.py +1858 -0
- openprocess/gui/studio/definition_view.py +284 -0
- openprocess/gui/studio/derivation_view.py +421 -0
- openprocess/gui/studio/documents.py +152 -0
- openprocess/gui/studio/dotted_chart.py +1401 -0
- openprocess/gui/studio/file_dialogs.py +143 -0
- openprocess/gui/studio/filter_dialog.py +247 -0
- openprocess/gui/studio/graph_builders.py +176 -0
- openprocess/gui/studio/graph_view.py +682 -0
- openprocess/gui/studio/instances.py +413 -0
- openprocess/gui/studio/log_editor.py +675 -0
- openprocess/gui/studio/log_page.py +800 -0
- openprocess/gui/studio/markdown_view.py +127 -0
- openprocess/gui/studio/mathtext.py +260 -0
- openprocess/gui/studio/ml_highlighter.py +75 -0
- openprocess/gui/studio/model_page.py +760 -0
- openprocess/gui/studio/net_comparison.py +124 -0
- openprocess/gui/studio/notes_overlay.py +275 -0
- openprocess/gui/studio/petri_page.py +844 -0
- openprocess/gui/studio/regions_view.py +502 -0
- openprocess/gui/studio/sidebar.py +149 -0
- openprocess/gui/studio/style.py +503 -0
- openprocess/gui/studio/tool_icons.py +134 -0
- openprocess/gui/studio/updates.py +439 -0
- openprocess/gui/studio/widgets.py +899 -0
- openprocess/gui/studio/workers.py +60 -0
- openprocess/gui/studio/workspace.py +447 -0
- openprocess/gui/theme.py +394 -0
- openprocess/gui/tidy.py +86 -0
- openprocess/io/__init__.py +0 -0
- openprocess/io/cpn_reader.py +389 -0
- openprocess/io/cpn_writer.py +357 -0
- openprocess/learn/__init__.py +23 -0
- openprocess/learn/answers.py +188 -0
- openprocess/learn/checks.py +953 -0
- openprocess/learn/computed.py +1180 -0
- openprocess/learn/context.py +145 -0
- openprocess/learn/exam.py +169 -0
- openprocess/learn/exercise-packs.md +325 -0
- openprocess/learn/importer.py +216 -0
- openprocess/learn/notation.py +474 -0
- openprocess/learn/pack.py +511 -0
- openprocess/learn/sheet.py +296 -0
- openprocess/mining/__init__.py +73 -0
- openprocess/mining/analysis.py +689 -0
- openprocess/mining/columns.py +282 -0
- openprocess/mining/compare_nets.py +246 -0
- openprocess/mining/conformance/__init__.py +0 -0
- openprocess/mining/conformance/alignments.py +263 -0
- openprocess/mining/conformance/quality.py +145 -0
- openprocess/mining/conformance/token_replay.py +252 -0
- openprocess/mining/csv_import.py +222 -0
- openprocess/mining/definitions.py +584 -0
- openprocess/mining/dfg.py +187 -0
- openprocess/mining/discovery/__init__.py +0 -0
- openprocess/mining/discovery/alpha.py +168 -0
- openprocess/mining/discovery/heuristics.py +332 -0
- openprocess/mining/discovery/inductive.py +477 -0
- openprocess/mining/discovery/state_regions.py +62 -0
- openprocess/mining/filtering.py +237 -0
- openprocess/mining/footprint.py +183 -0
- openprocess/mining/invariants.py +191 -0
- openprocess/mining/layout.py +279 -0
- openprocess/mining/log.py +364 -0
- openprocess/mining/petrinet.py +354 -0
- openprocess/mining/playout.py +75 -0
- openprocess/mining/pm4py_bridge.py +82 -0
- openprocess/mining/pnml.py +223 -0
- openprocess/mining/processtree.py +216 -0
- openprocess/mining/regions.py +476 -0
- openprocess/mining/stats.py +160 -0
- openprocess/mining/structure.py +374 -0
- openprocess/mining/transition_system.py +409 -0
- openprocess/mining/xes.py +399 -0
- openprocess/ml/__init__.py +0 -0
- openprocess/ml/ast_nodes.py +332 -0
- openprocess/ml/builtins.py +364 -0
- openprocess/ml/colorsets.py +522 -0
- openprocess/ml/errors.py +60 -0
- openprocess/ml/evaluator.py +754 -0
- openprocess/ml/lexer.py +277 -0
- openprocess/ml/multiset.py +417 -0
- openprocess/ml/parser.py +737 -0
- openprocess/ml/values.py +319 -0
- openprocess/model/__init__.py +0 -0
- openprocess/model/declarations.py +617 -0
- openprocess/model/examples.py +98 -0
- openprocess/model/net.py +701 -0
- openprocess/model/plain.py +192 -0
- openprocess/references.py +280 -0
- openprocess/sim/__init__.py +0 -0
- openprocess/sim/binding.py +620 -0
- openprocess/sim/export.py +66 -0
- openprocess/sim/simulator.py +315 -0
- openprocess/teaching/__init__.py +4 -0
- openprocess/teaching/answers.py +4 -0
- openprocess/teaching/checks.py +5 -0
- openprocess/teaching/pack.py +4 -0
- openprocess/teaching/sheet.py +4 -0
- openprocess-0.7.0.dist-info/METADATA +927 -0
- openprocess-0.7.0.dist-info/RECORD +173 -0
- openprocess-0.7.0.dist-info/WHEEL +5 -0
- openprocess-0.7.0.dist-info/entry_points.txt +6 -0
- openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
- openprocess-0.7.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Markdown with maths, as the exercises and the definitions show it.
|
|
2
|
+
|
|
3
|
+
``markdown_html`` turns GitHub-flavoured Markdown (tables, lists, emphasis)
|
|
4
|
+
with maths between ``$…$`` or ``$$…$$`` (typeset by :mod:`.mathtext`) into
|
|
5
|
+
Qt rich text; :class:`MarkdownLabel` shows it in a label that grows with its
|
|
6
|
+
text, so a worksheet reads as one page.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from html import escape
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from PySide6.QtCore import QUrl, Qt
|
|
16
|
+
from PySide6.QtGui import QTextDocument
|
|
17
|
+
from PySide6.QtWidgets import QFrame, QLabel, QSizePolicy, QTextBrowser
|
|
18
|
+
|
|
19
|
+
from .mathtext import MATH_FONT, render
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Markdown with maths
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
_DISPLAY = re.compile(r"\$\$(.+?)\$\$", re.S)
|
|
26
|
+
_INLINE = re.compile(r"(?<![\\$])\$([^$\n]+?)\$")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _maths(source: str, size: int) -> str:
|
|
30
|
+
try:
|
|
31
|
+
body = render(source.strip())
|
|
32
|
+
except ValueError: # not in the supported subset: show it as written
|
|
33
|
+
body = escape(source.strip())
|
|
34
|
+
return f"<span style='font-family: {MATH_FONT}; font-size: {size}px'>{body}</span>"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def markdown_html(text: str) -> str:
|
|
38
|
+
"""Markdown (GitHub flavour: tables, lists, emphasis) with ``$…$`` maths, as HTML."""
|
|
39
|
+
formulas: list[str] = []
|
|
40
|
+
|
|
41
|
+
def stash(match, size: int) -> str:
|
|
42
|
+
formulas.append(_maths(match.group(1), size))
|
|
43
|
+
return f"MATHXPLACEHOLDER{len(formulas) - 1}X"
|
|
44
|
+
|
|
45
|
+
text = _DISPLAY.sub(lambda m: "\n\n" + stash(m, 17) + "\n\n", text)
|
|
46
|
+
text = _INLINE.sub(lambda m: stash(m, 15), text)
|
|
47
|
+
document = QTextDocument()
|
|
48
|
+
document.setMarkdown(text, QTextDocument.MarkdownDialectGitHub)
|
|
49
|
+
html = document.toHtml()
|
|
50
|
+
return re.sub(r"MATHXPLACEHOLDER(\d+)X", lambda m: formulas[int(m.group(1))], html)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def plain_browser() -> QTextBrowser:
|
|
54
|
+
browser = QTextBrowser()
|
|
55
|
+
browser.setObjectName("plainBrowser") # borderless inside its card
|
|
56
|
+
browser.setOpenExternalLinks(True)
|
|
57
|
+
browser.setFrameShape(QFrame.NoFrame)
|
|
58
|
+
return browser
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def show_document(browser: QTextBrowser, path: Path) -> None:
|
|
62
|
+
"""A Markdown (or plain text) file in ``browser``, images relative to its folder."""
|
|
63
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
64
|
+
browser.document().setBaseUrl(QUrl.fromLocalFile(str(path.parent) + "/"))
|
|
65
|
+
browser.setSearchPaths([str(path.parent)])
|
|
66
|
+
if path.suffix.lower() == ".md":
|
|
67
|
+
browser.setHtml(markdown_html(text))
|
|
68
|
+
else:
|
|
69
|
+
browser.setPlainText(text)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
#: Pictures wider than this are shown scaled down to it (the worksheet's width).
|
|
74
|
+
IMAGE_WIDTH = 640
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _absolute_images(html: str, folder: Path) -> str:
|
|
78
|
+
"""``<img src="figure.png">`` relative to the question's folder, scaled
|
|
79
|
+
down when wider than the worksheet."""
|
|
80
|
+
from PySide6.QtGui import QImageReader
|
|
81
|
+
|
|
82
|
+
def absolute(match) -> str:
|
|
83
|
+
source = match.group(2)
|
|
84
|
+
if re.match(r"^[a-z]+:", source):
|
|
85
|
+
return match.group(0)
|
|
86
|
+
path = folder / source
|
|
87
|
+
width = QImageReader(str(path)).size().width()
|
|
88
|
+
scaled = f' width="{IMAGE_WIDTH}"' if width > IMAGE_WIDTH else ""
|
|
89
|
+
return f"{match.group(1)}{QUrl.fromLocalFile(str(path)).toString()}{match.group(3)}{scaled}"
|
|
90
|
+
return re.sub(r'(<img[^>]*src=")([^"]+)(")', absolute, html)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MarkdownLabel(QLabel):
|
|
94
|
+
"""Markdown in a label: wraps, grows with its text, links open in the browser."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, text: str = "", folder: Path | None = None, size: int | None = None,
|
|
97
|
+
parent=None) -> None:
|
|
98
|
+
super().__init__(parent)
|
|
99
|
+
self.setObjectName("markdown")
|
|
100
|
+
self.setTextFormat(Qt.RichText)
|
|
101
|
+
self.setWordWrap(True)
|
|
102
|
+
self.setOpenExternalLinks(True)
|
|
103
|
+
self.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
|
104
|
+
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
|
|
105
|
+
self.folder = folder
|
|
106
|
+
if size:
|
|
107
|
+
self.setStyleSheet(f"font-size: {size}px;")
|
|
108
|
+
self.set_markdown(text)
|
|
109
|
+
|
|
110
|
+
def set_markdown(self, text: str) -> None:
|
|
111
|
+
self.source = text
|
|
112
|
+
html = markdown_html(text)
|
|
113
|
+
# The label's own font (the document pins one of its own), and room
|
|
114
|
+
# between paragraphs so a worksheet reads easily.
|
|
115
|
+
html = re.sub(r"<body style=\"[^\"]*\">", "<body>", html)
|
|
116
|
+
html = re.sub(r"<p style=\" margin-top:\d+px; margin-bottom:\d+px;",
|
|
117
|
+
"<p style=\" margin-top:4px; margin-bottom:10px;", html)
|
|
118
|
+
html = re.sub(r"<li style=\" margin-top:\d+px; margin-bottom:\d+px;",
|
|
119
|
+
"<li style=\" margin-top:2px; margin-bottom:3px;", html)
|
|
120
|
+
if self.folder is not None:
|
|
121
|
+
html = _absolute_images(html, self.folder)
|
|
122
|
+
self.setText(html)
|
|
123
|
+
|
|
124
|
+
def plain_text(self) -> str:
|
|
125
|
+
document = QTextDocument()
|
|
126
|
+
document.setHtml(self.text())
|
|
127
|
+
return document.toPlainText()
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Typeset a small subset of LaTeX maths as Qt rich text (HTML).
|
|
2
|
+
|
|
3
|
+
Qt's rich-text engine has no maths support, but formulas about Petri nets need
|
|
4
|
+
little: italic symbols, sub- and superscripts, an overline, and Unicode for
|
|
5
|
+
∀ ∃ ∈ ⇒ •. This module turns the LaTeX used in
|
|
6
|
+
:mod:`openprocess.mining.definitions` (and in formulas the analysis builds for a
|
|
7
|
+
concrete net) into that HTML, so one source serves both GitHub, where MathJax
|
|
8
|
+
renders ``docs/definitions.md``, and the app.
|
|
9
|
+
|
|
10
|
+
Spacing follows TeX's idea rather than the source text: spaces in the source
|
|
11
|
+
are ignored, relations (= ∈ ⇒ …) and binary operators (∩ ∧ + …) get space on
|
|
12
|
+
both sides, and ``\\;``, ``\\quad`` and ``\\text{ … }`` add space explicitly.
|
|
13
|
+
|
|
14
|
+
Supported: letters (italic), digits, ``( ) [ ] , . | ! ' * + - < > = /``,
|
|
15
|
+
groups ``{…}``, ``_`` and ``^``, and the commands in :data:`SYMBOLS`,
|
|
16
|
+
:data:`RELATIONS`, :data:`BINARY` plus ``\\text``, ``\\mathrm``,
|
|
17
|
+
``\\mathit``, ``\\mathbb``, ``\\overline``, ``\\xrightarrow`` and ``\\not``.
|
|
18
|
+
Anything else raises :class:`ValueError`, so a typo shows up in the tests
|
|
19
|
+
instead of as a stray backslash on screen.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from html import escape
|
|
25
|
+
|
|
26
|
+
#: The font stack for formulas (serif, like a textbook).
|
|
27
|
+
MATH_FONT = ("'STIX Two Math', 'STIX Two Text', 'Cambria Math', 'Latin Modern Math', "
|
|
28
|
+
"'Times New Roman', serif")
|
|
29
|
+
|
|
30
|
+
#: Ordinary symbols: no extra space around them.
|
|
31
|
+
SYMBOLS = {
|
|
32
|
+
"forall": "∀", "exists": "∃", "neg": "¬", "bullet": "•", "emptyset": "∅",
|
|
33
|
+
"langle": "⟨", "rangle": "⟩", "lbrace": "{", "rbrace": "}", "{": "{", "}": "}",
|
|
34
|
+
"ldots": "…", "cdots": "⋯", "infty": "∞", "omega": "ω", "sigma": "σ",
|
|
35
|
+
"tau": "τ", "alpha": "α", "star": "∗", "max": "max", "bigcap": "⋂", "bigcup": "⋃",
|
|
36
|
+
}
|
|
37
|
+
#: Relations: a thick space on both sides.
|
|
38
|
+
RELATIONS = {
|
|
39
|
+
"in": "∈", "notin": "∉", "subseteq": "⊆", "neq": "≠", "leq": "≤", "geq": "≥",
|
|
40
|
+
"Rightarrow": "⇒", "iff": "⟺", "to": "→", "rightarrow": "→", "leftarrow": "←",
|
|
41
|
+
"mid": "|", "parallel": "‖", "colon": ":", "#": "#", "subsetneq": "⊊",
|
|
42
|
+
"Leftrightarrow": "⇔", "cong": "≅",
|
|
43
|
+
}
|
|
44
|
+
#: Binary operators: a medium space on both sides.
|
|
45
|
+
BINARY = {"cap": "∩", "cup": "∪", "times": "×", "land": "∧", "lor": "∨", "setminus": "∖",
|
|
46
|
+
"cdot": "·"}
|
|
47
|
+
#: Explicit spaces.
|
|
48
|
+
SPACES = {",": " ", ";": " ", "quad": " ", "qquad": "  ", " ": " ",
|
|
49
|
+
"!": ""}
|
|
50
|
+
BLACKBOARD = {"N": "ℕ", "Z": "ℤ", "R": "ℝ", "B": "𝔹"}
|
|
51
|
+
|
|
52
|
+
_ASCII_RELATIONS = {"=": "=", "<": "<", ">": ">"}
|
|
53
|
+
_ASCII_BINARY = {"+": "+", "-": "−"}
|
|
54
|
+
#: Commands that are negated by \not, and what the negation looks like.
|
|
55
|
+
_NEGATED = {"in": "∉", "=": "≠", "subseteq": "⊈", "exists": "∄"}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _Parser:
|
|
59
|
+
def __init__(self, source: str) -> None:
|
|
60
|
+
self.text = source
|
|
61
|
+
self.index = 0
|
|
62
|
+
|
|
63
|
+
# -- low level ---------------------------------------------------------------------
|
|
64
|
+
def peek(self) -> str:
|
|
65
|
+
return self.text[self.index] if self.index < len(self.text) else ""
|
|
66
|
+
|
|
67
|
+
def take(self) -> str:
|
|
68
|
+
char = self.peek()
|
|
69
|
+
self.index += 1
|
|
70
|
+
return char
|
|
71
|
+
|
|
72
|
+
def skip_spaces(self) -> None:
|
|
73
|
+
while self.peek().isspace():
|
|
74
|
+
self.index += 1
|
|
75
|
+
|
|
76
|
+
def command(self) -> str:
|
|
77
|
+
"""The name after a backslash (letters, or one other character)."""
|
|
78
|
+
start = self.index
|
|
79
|
+
if self.peek().isalpha():
|
|
80
|
+
while self.peek().isalpha():
|
|
81
|
+
self.index += 1
|
|
82
|
+
return self.text[start:self.index]
|
|
83
|
+
return self.take()
|
|
84
|
+
|
|
85
|
+
def group_source(self) -> str:
|
|
86
|
+
"""The raw text of a ``{…}`` argument (nested braces allowed)."""
|
|
87
|
+
self.skip_spaces()
|
|
88
|
+
if self.take() != "{":
|
|
89
|
+
raise ValueError(f"expected '{{' at {self.index} in {self.text!r}")
|
|
90
|
+
depth, start = 1, self.index
|
|
91
|
+
while depth:
|
|
92
|
+
char = self.take()
|
|
93
|
+
if not char:
|
|
94
|
+
raise ValueError(f"unbalanced braces in {self.text!r}")
|
|
95
|
+
if char == "\\":
|
|
96
|
+
self.take()
|
|
97
|
+
elif char == "{":
|
|
98
|
+
depth += 1
|
|
99
|
+
elif char == "}":
|
|
100
|
+
depth -= 1
|
|
101
|
+
return self.text[start:self.index - 1]
|
|
102
|
+
|
|
103
|
+
# -- rendering ---------------------------------------------------------------------
|
|
104
|
+
def argument(self) -> str:
|
|
105
|
+
"""One token or group, rendered (for ``_``, ``^`` and commands)."""
|
|
106
|
+
self.skip_spaces()
|
|
107
|
+
if self.peek() == "{":
|
|
108
|
+
return render(self.group_source())
|
|
109
|
+
return self.atom()
|
|
110
|
+
|
|
111
|
+
def atom(self) -> str:
|
|
112
|
+
char = self.take()
|
|
113
|
+
if char == "\\":
|
|
114
|
+
return self.backslash()
|
|
115
|
+
if char.isalpha():
|
|
116
|
+
letters = char
|
|
117
|
+
while self.peek().isalpha():
|
|
118
|
+
letters += self.take()
|
|
119
|
+
return f"<i>{letters}</i>"
|
|
120
|
+
if char.isdigit():
|
|
121
|
+
digits = char
|
|
122
|
+
while self.peek().isdigit():
|
|
123
|
+
digits += self.take()
|
|
124
|
+
return digits
|
|
125
|
+
if char == "{":
|
|
126
|
+
self.index -= 1
|
|
127
|
+
return render(self.group_source())
|
|
128
|
+
if char in _ASCII_RELATIONS:
|
|
129
|
+
return f" {_ASCII_RELATIONS[char]} "
|
|
130
|
+
if char in _ASCII_BINARY:
|
|
131
|
+
return f" {_ASCII_BINARY[char]} "
|
|
132
|
+
if char == "'":
|
|
133
|
+
return "′"
|
|
134
|
+
if char == ",":
|
|
135
|
+
return ", "
|
|
136
|
+
if char == "*":
|
|
137
|
+
return "∗" # the asterisk operator sits at maths height
|
|
138
|
+
if char in "()[]|.!/;:":
|
|
139
|
+
return escape(char)
|
|
140
|
+
raise ValueError(f"unsupported character {char!r} in {self.text!r}")
|
|
141
|
+
|
|
142
|
+
def backslash(self) -> str:
|
|
143
|
+
name = self.command()
|
|
144
|
+
if name in SYMBOLS:
|
|
145
|
+
return SYMBOLS[name]
|
|
146
|
+
if name in RELATIONS:
|
|
147
|
+
symbol = RELATIONS[name]
|
|
148
|
+
return f"{symbol} " if name == "colon" else f" {symbol} "
|
|
149
|
+
if name in BINARY:
|
|
150
|
+
return f" {BINARY[name]} "
|
|
151
|
+
if name in SPACES:
|
|
152
|
+
return SPACES[name]
|
|
153
|
+
if name in ("text", "mathrm"):
|
|
154
|
+
return _text(self.group_source())
|
|
155
|
+
if name == "mathit":
|
|
156
|
+
return f"<i>{_text(self.group_source())}</i>"
|
|
157
|
+
if name == "mathbb":
|
|
158
|
+
letter = self.group_source().strip()
|
|
159
|
+
return BLACKBOARD.get(letter, letter)
|
|
160
|
+
if name == "overline":
|
|
161
|
+
return f"<span style='text-decoration: overline'>{self.argument()}</span>"
|
|
162
|
+
if name == "xrightarrow":
|
|
163
|
+
return f" →<sup>{render(self.group_source())}</sup> "
|
|
164
|
+
if name == "not":
|
|
165
|
+
self.skip_spaces()
|
|
166
|
+
if self.peek() == "\\":
|
|
167
|
+
self.take()
|
|
168
|
+
negated = self.command()
|
|
169
|
+
if negated == "xrightarrow":
|
|
170
|
+
return f" ↛<sup>{render(self.group_source())}</sup> "
|
|
171
|
+
if negated in _NEGATED:
|
|
172
|
+
return f" {_NEGATED[negated]} "
|
|
173
|
+
raise ValueError(f"cannot negate \\{negated}")
|
|
174
|
+
if self.peek() == "=":
|
|
175
|
+
self.take()
|
|
176
|
+
return " ≠ "
|
|
177
|
+
raise ValueError(f"cannot negate {self.peek()!r}")
|
|
178
|
+
raise ValueError(f"unsupported command \\{name} in {self.text!r}")
|
|
179
|
+
|
|
180
|
+
def run(self) -> str:
|
|
181
|
+
parts: list[str] = []
|
|
182
|
+
while True:
|
|
183
|
+
self.skip_spaces()
|
|
184
|
+
if not self.peek():
|
|
185
|
+
break
|
|
186
|
+
char = self.peek()
|
|
187
|
+
if char in "_^":
|
|
188
|
+
self.take()
|
|
189
|
+
tag = "sub" if char == "_" else "sup"
|
|
190
|
+
script = f"<{tag}>{self.argument().strip()}</{tag}>"
|
|
191
|
+
# Attach to the symbol before it; a relation keeps its space after.
|
|
192
|
+
previous = parts.pop() if parts else ""
|
|
193
|
+
spaced = previous.endswith(" ")
|
|
194
|
+
parts.append(previous.rstrip() + script + (" " if spaced else ""))
|
|
195
|
+
elif char == "}":
|
|
196
|
+
raise ValueError(f"unbalanced '}}' in {self.text!r}")
|
|
197
|
+
else:
|
|
198
|
+
parts.append(self.atom())
|
|
199
|
+
return _tidy("".join(parts))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _text(source: str) -> str:
|
|
203
|
+
"""``\\text{…}``: upright words; ``\\{ \\}`` are literal braces, edge spaces kept."""
|
|
204
|
+
plain = source.replace("\\{", "{").replace("\\}", "}").replace("\\_", "_")
|
|
205
|
+
body = escape(plain.strip())
|
|
206
|
+
lead = " " if plain[:1].isspace() else ""
|
|
207
|
+
trail = " " if plain[-1:].isspace() else ""
|
|
208
|
+
return f"{lead}<span style='font-family: sans-serif'>{body}</span>{trail}"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _tidy(html: str) -> str:
|
|
212
|
+
"""Collapse the spaces operators added next to each other and at the ends."""
|
|
213
|
+
while " " in html:
|
|
214
|
+
html = html.replace(" ", " ")
|
|
215
|
+
return html.replace("( ", "(").replace(" )", ")").strip()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def render(source: str) -> str:
|
|
219
|
+
"""HTML for the LaTeX formula ``source`` (without the ``$`` delimiters)."""
|
|
220
|
+
return _Parser(source).run()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def render_text(text: str) -> str:
|
|
224
|
+
"""HTML for prose with inline ``$…$`` maths and ``*emphasis*``."""
|
|
225
|
+
out: list[str] = []
|
|
226
|
+
for index, chunk in enumerate(text.split("$")):
|
|
227
|
+
if index % 2:
|
|
228
|
+
out.append(f"<span style='font-family: {MATH_FONT}'>{render(chunk)}</span>")
|
|
229
|
+
else:
|
|
230
|
+
words = escape(chunk).split("*")
|
|
231
|
+
out.append("".join(f"<i>{w}</i>" if n % 2 else w for n, w in enumerate(words)))
|
|
232
|
+
return "".join(out)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def display(source: str, size: int = 15) -> str:
|
|
236
|
+
"""A formula on its own line, centred like display maths."""
|
|
237
|
+
return (f"<p align='center' style='font-family: {MATH_FONT}; font-size: {size}px; "
|
|
238
|
+
f"margin: 4px 0'>{render(source)}</p>")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# Writing formulas about a concrete net
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
def name(text: str) -> str:
|
|
245
|
+
"""A node name for use inside a formula (``\\text{…}`` with braces escaped)."""
|
|
246
|
+
cleaned = text.replace("\\", "").replace("{", "\\{").replace("}", "\\}")
|
|
247
|
+
return "\\text{" + cleaned + "}"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def name_set(names) -> str:
|
|
251
|
+
items = list(names)
|
|
252
|
+
return "\\emptyset" if not items else \
|
|
253
|
+
"\\lbrace " + ", ".join(name(n) for n in items) + " \\rbrace"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def sequence(names, limit: int = 8) -> str:
|
|
257
|
+
items = list(names)
|
|
258
|
+
if len(items) > limit:
|
|
259
|
+
items = items[:limit - 1] + ["…"]
|
|
260
|
+
return "\\langle " + ", ".join(name(n) for n in items) + " \\rangle"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Syntax highlighting for CPN ML (declarations and inscriptions).
|
|
2
|
+
|
|
3
|
+
A small regular-expression highlighter, enough to make declarations easy to
|
|
4
|
+
scan: keywords, colour-set and function names, strings, numbers and
|
|
5
|
+
``(* comments *)`` -- including comments that span several lines.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from PySide6.QtGui import QColor, QFont, QSyntaxHighlighter, QTextCharFormat
|
|
13
|
+
|
|
14
|
+
from . import style
|
|
15
|
+
|
|
16
|
+
KEYWORDS = (
|
|
17
|
+
"colset var val fun fn let in end if then else case of andalso orelse with and "
|
|
18
|
+
"globref timed product record list index subset union by declare ms op not"
|
|
19
|
+
).split()
|
|
20
|
+
TYPES = "int bool string unit real time intinf".split()
|
|
21
|
+
|
|
22
|
+
_WORD = re.compile(r"\b(" + "|".join(KEYWORDS) + r")\b")
|
|
23
|
+
_TYPE = re.compile(r"\b(" + "|".join(TYPES) + r")\b")
|
|
24
|
+
_DEFINED = re.compile(r"\b(?:colset|fun|val|var|globref)\s+([A-Za-z_][\w']*(?:\s*,\s*[A-Za-z_][\w']*)*)")
|
|
25
|
+
_NUMBER = re.compile(r"(?<![\w.])~?\d+(\.\d+)?")
|
|
26
|
+
_STRING = re.compile(r'"(?:[^"\\]|\\.)*"')
|
|
27
|
+
_OPERATOR = re.compile(r"``|`|\+\+|--|@\+|::|\^\^|<>|<=|>=|=>")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _format(colour: str, bold: bool = False, italic: bool = False) -> QTextCharFormat:
|
|
31
|
+
fmt = QTextCharFormat()
|
|
32
|
+
fmt.setForeground(QColor(colour))
|
|
33
|
+
if bold:
|
|
34
|
+
fmt.setFontWeight(QFont.DemiBold)
|
|
35
|
+
fmt.setFontItalic(italic)
|
|
36
|
+
return fmt
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MlHighlighter(QSyntaxHighlighter):
|
|
40
|
+
IN_COMMENT = 1
|
|
41
|
+
|
|
42
|
+
def highlightBlock(self, text: str) -> None: # noqa: N802 - Qt naming
|
|
43
|
+
t = style.tokens()
|
|
44
|
+
keyword = _format(style.categorical(6), bold=True)
|
|
45
|
+
type_ = _format(style.categorical(0))
|
|
46
|
+
defined = _format(t.text, bold=True)
|
|
47
|
+
number = _format(style.categorical(1))
|
|
48
|
+
string = _format(style.categorical(2))
|
|
49
|
+
operator = _format(t.text_secondary, bold=True)
|
|
50
|
+
comment = _format(t.text_muted, italic=True)
|
|
51
|
+
|
|
52
|
+
for pattern, fmt in ((_TYPE, type_), (_WORD, keyword), (_NUMBER, number),
|
|
53
|
+
(_OPERATOR, operator)):
|
|
54
|
+
for match in pattern.finditer(text):
|
|
55
|
+
self.setFormat(match.start(), match.end() - match.start(), fmt)
|
|
56
|
+
for match in _DEFINED.finditer(text):
|
|
57
|
+
self.setFormat(match.start(1), match.end(1) - match.start(1), defined)
|
|
58
|
+
for match in _STRING.finditer(text):
|
|
59
|
+
self.setFormat(match.start(), match.end() - match.start(), string)
|
|
60
|
+
|
|
61
|
+
# (* comments *), possibly spanning lines.
|
|
62
|
+
self.setCurrentBlockState(0)
|
|
63
|
+
start = 0
|
|
64
|
+
if self.previousBlockState() != self.IN_COMMENT:
|
|
65
|
+
start = text.find("(*")
|
|
66
|
+
while start >= 0:
|
|
67
|
+
end = text.find("*)", start + 2 if self.previousBlockState() != self.IN_COMMENT
|
|
68
|
+
or start > 0 else 0)
|
|
69
|
+
if end < 0:
|
|
70
|
+
self.setCurrentBlockState(self.IN_COMMENT)
|
|
71
|
+
length = len(text) - start
|
|
72
|
+
else:
|
|
73
|
+
length = end - start + 2
|
|
74
|
+
self.setFormat(start, length, comment)
|
|
75
|
+
start = text.find("(*", start + length)
|