Merge branch 'main' into feature/question-search
This commit is contained in:
@@ -15,3 +15,4 @@ bd.build_edition("NEA")
|
|||||||
bd.build_assets()
|
bd.build_assets()
|
||||||
bd.build_solutions()
|
bd.build_solutions()
|
||||||
bd.build_question_index()
|
bd.build_question_index()
|
||||||
|
bd.build_zip()
|
||||||
|
|||||||
@@ -1087,6 +1087,28 @@
|
|||||||
\pgfcirc@activate@bipole@simple{l}{channelcoder}
|
\pgfcirc@activate@bipole@simple{l}{channelcoder}
|
||||||
\makeatother
|
\makeatother
|
||||||
|
|
||||||
|
% Channel Decoder
|
||||||
|
\makeatletter
|
||||||
|
\ctikzset{bipoles/channeldecoder/width/.initial=.7}
|
||||||
|
|
||||||
|
\pgfcirc@define@twoports{blocks}
|
||||||
|
{}
|
||||||
|
{\ctikzvalof{bipoles/channeldecoder/width}}
|
||||||
|
{channeldecoder}
|
||||||
|
{\ctikzvalof{bipoles/channeldecoder/width}}
|
||||||
|
{\ctikzvalof{bipoles/channeldecoder/width}}
|
||||||
|
{
|
||||||
|
\pgfcirc@twoport@draw@splitline
|
||||||
|
%
|
||||||
|
\pgf@circ@text@strokecolor
|
||||||
|
\pgftext[center,x=0.25\pgf@circ@res@left,y=0.6\pgf@circ@res@up]{\tiny 01+01}
|
||||||
|
\pgftext[center,x=0.55\pgf@circ@res@right,y=0.6\pgf@circ@res@down]{\tiny 01}
|
||||||
|
}
|
||||||
|
|
||||||
|
\pgfcirc@activate@bipole@simple{l}{channeldecoder}
|
||||||
|
\makeatother
|
||||||
|
|
||||||
|
|
||||||
% Source Coder
|
% Source Coder
|
||||||
\makeatletter
|
\makeatletter
|
||||||
\ctikzset{bipoles/sourcecoder/width/.initial=.7}
|
\ctikzset{bipoles/sourcecoder/width/.initial=.7}
|
||||||
@@ -1108,6 +1130,27 @@
|
|||||||
\pgfcirc@activate@bipole@simple{l}{sourcecoder}
|
\pgfcirc@activate@bipole@simple{l}{sourcecoder}
|
||||||
\makeatother
|
\makeatother
|
||||||
|
|
||||||
|
% Source Decoder
|
||||||
|
\makeatletter
|
||||||
|
\ctikzset{bipoles/sourcedecoder/width/.initial=.7}
|
||||||
|
|
||||||
|
\pgfcirc@define@twoports{blocks}
|
||||||
|
{}
|
||||||
|
{\ctikzvalof{bipoles/sourcedecoder/width}}
|
||||||
|
{sourcedecoder}
|
||||||
|
{\ctikzvalof{bipoles/sourcedecoder/width}}
|
||||||
|
{\ctikzvalof{bipoles/sourcedecoder/width}}
|
||||||
|
{
|
||||||
|
\pgfcirc@twoport@draw@splitline
|
||||||
|
%
|
||||||
|
\pgf@circ@text@strokecolor
|
||||||
|
\pgftext[center,x=0.55\pgf@circ@res@left,y=0.6\pgf@circ@res@up]{\tiny 01}
|
||||||
|
\pgftext[center,x=0.30\pgf@circ@res@right,y=0.6\pgf@circ@res@down]{\tiny 0101}
|
||||||
|
}
|
||||||
|
|
||||||
|
\pgfcirc@activate@bipole@simple{l}{sourcedecoder}
|
||||||
|
\makeatother
|
||||||
|
|
||||||
% Mapper
|
% Mapper
|
||||||
\makeatletter
|
\makeatletter
|
||||||
\ctikzset{bipoles/mapper/width/.initial=.7}
|
\ctikzset{bipoles/mapper/width/.initial=.7}
|
||||||
@@ -1147,4 +1190,45 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
\pgfcirc@activate@bipole@simple{l}{mapper}
|
\pgfcirc@activate@bipole@simple{l}{mapper}
|
||||||
|
\makeatother
|
||||||
|
|
||||||
|
% Demapper
|
||||||
|
\makeatletter
|
||||||
|
\ctikzset{bipoles/demapper/width/.initial=.7}
|
||||||
|
|
||||||
|
\pgfcirc@define@twoports{blocks}
|
||||||
|
{}
|
||||||
|
{\ctikzvalof{bipoles/demapper/width}}
|
||||||
|
{demapper}
|
||||||
|
{\ctikzvalof{bipoles/demapper/width}}
|
||||||
|
{\ctikzvalof{bipoles/demapper/width}}
|
||||||
|
{
|
||||||
|
\pgfcirc@twoport@draw@splitline
|
||||||
|
%
|
||||||
|
\pgf@circ@text@strokecolor
|
||||||
|
%
|
||||||
|
\pgfpathmoveto{\pgfpoint{0.50\pgf@circ@res@left}{+0.25\pgf@circ@res@up}}
|
||||||
|
\pgfpathlineto{\pgfpoint{0.50\pgf@circ@res@left}{+0.75\pgf@circ@res@up}}
|
||||||
|
\pgfusepath{draw}
|
||||||
|
%
|
||||||
|
\pgfpathmoveto{\pgfpoint{0.25\pgf@circ@res@left}{+0.50\pgf@circ@res@up}}
|
||||||
|
\pgfpathlineto{\pgfpoint{0.75\pgf@circ@res@left}{+0.50\pgf@circ@res@up}}
|
||||||
|
\pgfusepath{draw}
|
||||||
|
%
|
||||||
|
\pgfpathcircle{\pgfpoint{0.65\pgf@circ@res@left}{+0.65\pgf@circ@res@up}}{0.05\pgf@circ@res@up}
|
||||||
|
\pgfusepath{fill}
|
||||||
|
%
|
||||||
|
\pgfpathcircle{\pgfpoint{0.35\pgf@circ@res@left}{+0.65\pgf@circ@res@up}}{0.05\pgf@circ@res@up}
|
||||||
|
\pgfusepath{fill}
|
||||||
|
%
|
||||||
|
\pgfpathcircle{\pgfpoint{0.65\pgf@circ@res@left}{+0.35\pgf@circ@res@up}}{0.05\pgf@circ@res@up}
|
||||||
|
\pgfusepath{fill}
|
||||||
|
%
|
||||||
|
\pgfpathcircle{\pgfpoint{0.35\pgf@circ@res@left}{+0.35\pgf@circ@res@up}}{0.05\pgf@circ@res@up}
|
||||||
|
\pgfusepath{fill}
|
||||||
|
%
|
||||||
|
\pgftext[center,x=0.55\pgf@circ@res@right,y=-0.6\pgf@circ@res@up]{\tiny 01}
|
||||||
|
}
|
||||||
|
|
||||||
|
\pgfcirc@activate@bipole@simple{l}{demapper}
|
||||||
\makeatother
|
\makeatother
|
||||||
@@ -23,6 +23,7 @@ def main(
|
|||||||
output: Annotated[str | None, typer.Option("--output", "-o", help="Destination directory to build to.")] = None,
|
output: Annotated[str | None, typer.Option("--output", "-o", help="Destination directory to build to.")] = None,
|
||||||
render_editions: Annotated[bool, typer.Option(help="Skip building editions.")] = True,
|
render_editions: Annotated[bool, typer.Option(help="Skip building editions.")] = True,
|
||||||
render_solutions: Annotated[bool, typer.Option(help="Skip building solutions.")] = True,
|
render_solutions: Annotated[bool, typer.Option(help="Skip building solutions.")] = True,
|
||||||
|
build_zip: Annotated[bool, typer.Option(help="Whether to build a zip file of the output.")] = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
conf = config.Config(content_path=input, build_path=output)
|
conf = config.Config(content_path=input, build_path=output)
|
||||||
bd = build.Build(conf)
|
bd = build.Build(conf)
|
||||||
@@ -43,6 +44,10 @@ def main(
|
|||||||
bd.build_assets()
|
bd.build_assets()
|
||||||
bd.build_question_index()
|
bd.build_question_index()
|
||||||
|
|
||||||
|
if build_zip:
|
||||||
|
# Build zip file of output
|
||||||
|
bd.build_zip()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app()
|
app()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from .dash import Dash
|
|||||||
from .formula import Formula
|
from .formula import Formula
|
||||||
from .halfwidth_spaces import HalfwidthSpaces
|
from .halfwidth_spaces import HalfwidthSpaces
|
||||||
from .include import Include
|
from .include import Include
|
||||||
|
from .index import Index
|
||||||
from .morse import Morse
|
from .morse import Morse
|
||||||
from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots
|
from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots
|
||||||
from .photo import Photo
|
from .photo import Photo
|
||||||
@@ -64,6 +65,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
|||||||
Qso,
|
Qso,
|
||||||
Include,
|
Include,
|
||||||
Formula,
|
Formula,
|
||||||
|
Index,
|
||||||
*extras,
|
*extras,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
@@ -332,3 +334,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
|||||||
|
|
||||||
def render_formula(self, token):
|
def render_formula(self, token):
|
||||||
return f"\n$${token.formula}$$\n"
|
return f"\n$${token.formula}$$\n"
|
||||||
|
|
||||||
|
def render_index(self, token):
|
||||||
|
# For HTML just ignore the index
|
||||||
|
return ""
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from mistletoe.latex_renderer import LaTeXRenderer
|
|||||||
from .comment import BlockComment
|
from .comment import BlockComment
|
||||||
from .dash import Dash
|
from .dash import Dash
|
||||||
from .halfwidth_spaces import HalfwidthSpaces
|
from .halfwidth_spaces import HalfwidthSpaces
|
||||||
|
from .index import Index
|
||||||
from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots
|
from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots
|
||||||
from .photo import Photo
|
from .photo import Photo
|
||||||
from .picture import Picture
|
from .picture import Picture
|
||||||
@@ -32,6 +33,7 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer):
|
|||||||
TableRow,
|
TableRow,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableCell,
|
TableCell,
|
||||||
|
Index,
|
||||||
)
|
)
|
||||||
self.question_renderer = question_renderer
|
self.question_renderer = question_renderer
|
||||||
|
|
||||||
@@ -148,3 +150,9 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer):
|
|||||||
|
|
||||||
def render_table_cell(self, token: TableCell):
|
def render_table_cell(self, token: TableCell):
|
||||||
return self.render_inner(token)
|
return self.render_inner(token)
|
||||||
|
|
||||||
|
def render_index(self, token):
|
||||||
|
if token.second:
|
||||||
|
return f"\\index{{{token.first}!{token.second}}}"
|
||||||
|
else:
|
||||||
|
return f"\\index{{{token.first}}}"
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from mistletoe.span_token import SpanToken
|
||||||
|
|
||||||
|
|
||||||
|
class Index(SpanToken):
|
||||||
|
"""
|
||||||
|
Resolve index commands
|
||||||
|
Identifies "[index:term:subterm]" or "[index:term]"
|
||||||
|
"""
|
||||||
|
|
||||||
|
pattern = re.compile(r"[ \t]*\[index:([^:\]\s](?:[^:\]\s]| )*)(?::([^:\]\s](?:[^:\]\s]| )*))?\]")
|
||||||
|
|
||||||
|
def __init__(self, match_object):
|
||||||
|
self.first = match_object.group(1)
|
||||||
|
self.second = match_object.group(2)
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
@@ -561,3 +563,38 @@ class Build:
|
|||||||
with (self.config.p_build_assets / "question_index.json").open("w", encoding="utf-8") as file:
|
with (self.config.p_build_assets / "question_index.json").open("w", encoding="utf-8") as file:
|
||||||
json.dump(self.question_index, file, ensure_ascii=False, indent=2, sort_keys=True)
|
json.dump(self.question_index, file, ensure_ascii=False, indent=2, sort_keys=True)
|
||||||
file.write("\n")
|
file.write("\n")
|
||||||
|
def build_zip(self, zip_name: str | None = None) -> Path:
|
||||||
|
"""Create a zip archive of the complete build output directory.
|
||||||
|
|
||||||
|
The zip file is written into the build directory itself and is excluded
|
||||||
|
from the archive contents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
build_dir = self.config.p_build
|
||||||
|
build_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
zip_path = build_dir / (zip_name if zip_name is not None else f"{build_dir.name}.zip")
|
||||||
|
zip_path = zip_path.resolve()
|
||||||
|
|
||||||
|
# If a previous archive exists, remove it first to avoid zipping stale data.
|
||||||
|
if zip_path.exists():
|
||||||
|
zip_path.unlink()
|
||||||
|
|
||||||
|
tqdm.write(f"Creating zip archive: {zip_path}")
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
# os.walk includes dotfiles; sort for stable archives.
|
||||||
|
for root, dirs, files in os.walk(build_dir):
|
||||||
|
dirs.sort()
|
||||||
|
files.sort()
|
||||||
|
|
||||||
|
root_path = Path(root)
|
||||||
|
for filename in files:
|
||||||
|
file_path = (root_path / filename).resolve()
|
||||||
|
if file_path == zip_path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
arcname = file_path.relative_to(build_dir.resolve())
|
||||||
|
zf.write(file_path, arcname=arcname)
|
||||||
|
|
||||||
|
return zip_path
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import mistletoe
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
|
||||||
|
from renderer.fifty_ohm_latex_renderer import FiftyOhmLaTeXRenderer
|
||||||
|
from test.util import paragraph
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.html
|
||||||
|
def test_index_html():
|
||||||
|
assertions = {
|
||||||
|
"Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501
|
||||||
|
"Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501
|
||||||
|
"Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht",
|
||||||
|
"Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht",
|
||||||
|
"Im Betrieb werden Yagi-Antennen[index:Test Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen oft gedreht", # noqa: E501
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in assertions.items():
|
||||||
|
assert mistletoe.markdown(key, FiftyOhmHtmlRenderer) == paragraph(value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.latex
|
||||||
|
def test_index_latex():
|
||||||
|
assertions = {
|
||||||
|
"Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", # noqa: E501
|
||||||
|
"Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", # noqa: E501
|
||||||
|
"Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne} oft gedreht", # noqa: E501
|
||||||
|
"Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne} oft gedreht", # noqa: E501
|
||||||
|
"Im Betrieb werden Yagi-Antennen [index:Test Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Test Antenne} oft gedreht", # noqa: E501
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in assertions.items():
|
||||||
|
assert mistletoe.markdown(key, FiftyOhmLaTeXRenderer) == "\n" + value + "\n"
|
||||||
Reference in New Issue
Block a user