From 3f4019d3a1615f6aa7e78e016b75647dda0e9721 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Thu, 6 Feb 2025 22:03:45 +0100 Subject: [PATCH 01/62] add missing .xbb --- coverArt/calculus1Cover/starsRed.xbb | 6 ++++++ coverArt/calculus2Cover/starsBlue.xbb | 6 ++++++ coverArt/calculus3Cover/starsTurquoise.xbb | 6 ++++++ coverArt/calculusA2Cover/starsGreen.xbb | 6 ++++++ coverArt/calculusECover/starsAmber.xbb | 6 ++++++ 5 files changed, 30 insertions(+) create mode 100644 coverArt/calculus1Cover/starsRed.xbb create mode 100644 coverArt/calculus2Cover/starsBlue.xbb create mode 100644 coverArt/calculus3Cover/starsTurquoise.xbb create mode 100644 coverArt/calculusA2Cover/starsGreen.xbb create mode 100644 coverArt/calculusECover/starsAmber.xbb diff --git a/coverArt/calculus1Cover/starsRed.xbb b/coverArt/calculus1Cover/starsRed.xbb new file mode 100644 index 000000000..8f46052ef --- /dev/null +++ b/coverArt/calculus1Cover/starsRed.xbb @@ -0,0 +1,6 @@ +%%Title: coverArt/calculus1Cover/starsRed.jpg +%%Creator: extractbb 20240407 +%%BoundingBox: 0 0 5184 3456 +%%HiResBoundingBox: 0.000000 0.000000 5184.000000 3456.000000 +%%CreationDate: Thu Feb 6 21:02:28 2025 + diff --git a/coverArt/calculus2Cover/starsBlue.xbb b/coverArt/calculus2Cover/starsBlue.xbb new file mode 100644 index 000000000..eaed206bc --- /dev/null +++ b/coverArt/calculus2Cover/starsBlue.xbb @@ -0,0 +1,6 @@ +%%Title: coverArt/calculus2Cover/starsBlue.jpg +%%Creator: extractbb 20240407 +%%BoundingBox: 0 0 5184 3456 +%%HiResBoundingBox: 0.000000 0.000000 5184.000000 3456.000000 +%%CreationDate: Thu Feb 6 21:02:28 2025 + diff --git a/coverArt/calculus3Cover/starsTurquoise.xbb b/coverArt/calculus3Cover/starsTurquoise.xbb new file mode 100644 index 000000000..43e4bc60c --- /dev/null +++ b/coverArt/calculus3Cover/starsTurquoise.xbb @@ -0,0 +1,6 @@ +%%Title: coverArt/calculus3Cover/starsTurquoise.jpg +%%Creator: extractbb 20240407 +%%BoundingBox: 0 0 5184 3456 +%%HiResBoundingBox: 0.000000 0.000000 5184.000000 3456.000000 +%%CreationDate: Thu Feb 6 21:02:28 2025 + diff --git a/coverArt/calculusA2Cover/starsGreen.xbb b/coverArt/calculusA2Cover/starsGreen.xbb new file mode 100644 index 000000000..9c2bb3027 --- /dev/null +++ b/coverArt/calculusA2Cover/starsGreen.xbb @@ -0,0 +1,6 @@ +%%Title: coverArt/calculusA2Cover/starsGreen.jpg +%%Creator: extractbb 20240407 +%%BoundingBox: 0 0 5184 3456 +%%HiResBoundingBox: 0.000000 0.000000 5184.000000 3456.000000 +%%CreationDate: Thu Feb 6 21:02:28 2025 + diff --git a/coverArt/calculusECover/starsAmber.xbb b/coverArt/calculusECover/starsAmber.xbb new file mode 100644 index 000000000..ef0a7ffa0 --- /dev/null +++ b/coverArt/calculusECover/starsAmber.xbb @@ -0,0 +1,6 @@ +%%Title: coverArt/calculusECover/starsAmber.jpg +%%Creator: extractbb 20240407 +%%BoundingBox: 0 0 5184 3456 +%%HiResBoundingBox: 0.000000 0.000000 5184.000000 3456.000000 +%%CreationDate: Thu Feb 6 21:02:28 2025 + From 99eae4c54f7bf6eb71f5734d1cade673920ca653 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Fri, 7 Feb 2025 09:01:53 +0100 Subject: [PATCH 02/62] add workflow (test) --- .github/workflows/serve-ximera.yml | 94 ++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 .github/workflows/serve-ximera.yml diff --git a/.github/workflows/serve-ximera.yml b/.github/workflows/serve-ximera.yml new file mode 100644 index 000000000..0dd08ca34 --- /dev/null +++ b/.github/workflows/serve-ximera.yml @@ -0,0 +1,94 @@ +name: Ximera Workflow + +on: + push: + +jobs: + build-ximera: + name: Build and preview Ximera Courses + runs-on: ubuntu-latest + + permissions: + actions: read + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # full + + - name: Setup cache + uses: actions/cache@v4 + with: + path: | + **/*.html + **/*.pdf + **/*.svg + **/*.aux + **/*.xref + **/*.toc + !xmPictures/** + !.git/** + !.github/** + key: ximera-${{ github.ref_name }}-${{ github.sha }} + restore-keys: | + ximera-${{ github.ref_name }}- + ximera- + + + - name: Build and publish Ximera courses + env: + GPG_KEY: ${{ secrets.GPG_KEY }} + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + XIMERA_URL: ${{ vars.XIMERA_URL }} + XIMERA_NAME: ${{ vars.XIMERA_NAME }}${{ github.ref_name }} + run: | + ls -alrt + # git config --global --add safe.directory "$(pwd)" + # export COMMAND=./xmScripts/xmlatex.test + ./xmScripts/xmlatex ghaction calculus1TextbookBySection/anApplicationOfLimits.tex || echo NOK + echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY + + - name: Setup ximera serve cache (only .git) + id: serve-cache + uses: actions/cache/save@v4 + with: + path: | + xmScripts/** + .git/** + key: ximeraSERVE-${{ github.ref_name }}-${{ github.sha }} + + + publish-ximera: + name: Publish Ximera Courses + needs: build-ximera # Waits for the build job to complete + runs-on: ubuntu-latest + environment: production + steps: + + + - name: Restore ximera serve cache (only .git) + id: serve-cache-restore + uses: actions/cache/restore@v4 + with: + fail-on-cache-miss: true + path: | + xmScripts/** + .git/** + key: ximeraSERVE-${{ github.ref_name }}-${{ github.sha }} + + - name: Serve + env: + GPG_KEY: ${{ secrets.GPG_KEY }} + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + XIMERA_URL: ${{ vars.XIMERA_URL }} + XIMERA_NAME: ${{ vars.XIMERA_NAME }} + run: | + ./xmScripts/xmlatex name + ./xmScripts/xmlatex serve -f # NOTE: -f should not be needed ... + echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY + + + From e9eeffcdf6e51ce127c38b46bb9ef4515af86613 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Fri, 7 Feb 2025 09:42:23 +0100 Subject: [PATCH 03/62] with -d ... --- .github/workflows/serve-ximera.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/serve-ximera.yml b/.github/workflows/serve-ximera.yml index 0dd08ca34..317c37c33 100644 --- a/.github/workflows/serve-ximera.yml +++ b/.github/workflows/serve-ximera.yml @@ -48,7 +48,7 @@ jobs: ls -alrt # git config --global --add safe.directory "$(pwd)" # export COMMAND=./xmScripts/xmlatex.test - ./xmScripts/xmlatex ghaction calculus1TextbookBySection/anApplicationOfLimits.tex || echo NOK + ./xmScripts/xmlatex -d ghaction calculus1TextbookBySection/anApplicationOfLimits.tex echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY - name: Setup ximera serve cache (only .git) From c299a04d1f514897570062dfa7182afc804a7c48 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Fri, 7 Feb 2025 10:15:13 +0100 Subject: [PATCH 04/62] serve calculus1 --- .github/workflows/serve-ximera.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/serve-ximera.yml b/.github/workflows/serve-ximera.yml index 317c37c33..623600f33 100644 --- a/.github/workflows/serve-ximera.yml +++ b/.github/workflows/serve-ximera.yml @@ -48,7 +48,7 @@ jobs: ls -alrt # git config --global --add safe.directory "$(pwd)" # export COMMAND=./xmScripts/xmlatex.test - ./xmScripts/xmlatex -d ghaction calculus1TextbookBySection/anApplicationOfLimits.tex + ./xmScripts/xmlatex -d ghaction calculus1.tex echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY - name: Setup ximera serve cache (only .git) @@ -87,7 +87,7 @@ jobs: XIMERA_NAME: ${{ vars.XIMERA_NAME }} run: | ./xmScripts/xmlatex name - ./xmScripts/xmlatex serve -f # NOTE: -f should not be needed ... + ./xmScripts/xmlatex serve calculus1.tex -f # NOTE: -f should not be needed ... echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY From f454731be95f8d7be4b7b7592944313b10a5e8a4 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Fri, 7 Feb 2025 10:44:34 +0100 Subject: [PATCH 05/62] semi-fix expandable --- antiderivatives/digInBasicAntiderivatives.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/antiderivatives/digInBasicAntiderivatives.tex b/antiderivatives/digInBasicAntiderivatives.tex index 9fe8af5a3..b6312ab6f 100644 --- a/antiderivatives/digInBasicAntiderivatives.tex +++ b/antiderivatives/digInBasicAntiderivatives.tex @@ -559,7 +559,7 @@ \section{Differential equations} \end{question} \begin{remark} We can directly check that any function $f(x)=Ce^x$ is a solution to our differential equation $f'(x)=f(x)$. Could there be any others? It turns out that these are the \textit{only} solutions. But showing that we didn't miss any is a bit tricky. - \begin{expandable} + \begin{expandable}{explanation}{} \begin{explanation} Well, suppose we have some mysterious function $f$ and all we know is that $f'(x)=f(x)$. Let's define a new function $g(x)=f(x)/e^x$. Since our denominator is never 0, the quotient rule tells us that \[ From d785f805d132a532246883a58e71199e7a01306b Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Fri, 7 Feb 2025 10:44:48 +0100 Subject: [PATCH 06/62] dubious \item --- computationsForGraphingFunctions/breakGround.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/computationsForGraphingFunctions/breakGround.tex b/computationsForGraphingFunctions/breakGround.tex index 96be4694f..402ad5c97 100644 --- a/computationsForGraphingFunctions/breakGround.tex +++ b/computationsForGraphingFunctions/breakGround.tex @@ -56,7 +56,7 @@ \item Compute $f'$ and $f''$. \item Analyze end behavior: as $x \to \pm \infty$, what happens to the graph of $f$? Does it have horizontal asymptotes, increase or decrease without bound, or have some other kind of behavior? \end{enumerate} - \item Use either the first or second derivative test to identify local extrema and/or + Use either the first or second derivative test to identify local extrema and/or find the intervals where your function is increasing/decreasing. In what order should we take these steps? For example, one must compute $f'$ before computing $f''$. Also, one must compute $f'$ before From e2904c2dd02feb3c86fb5b856ca631a9ab5e74fa Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Fri, 7 Feb 2025 14:00:55 +0100 Subject: [PATCH 07/62] add more xourses to action --- .github/workflows/serve-ximera.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/serve-ximera.yml b/.github/workflows/serve-ximera.yml index 623600f33..5fc85c521 100644 --- a/.github/workflows/serve-ximera.yml +++ b/.github/workflows/serve-ximera.yml @@ -48,7 +48,7 @@ jobs: ls -alrt # git config --global --add safe.directory "$(pwd)" # export COMMAND=./xmScripts/xmlatex.test - ./xmScripts/xmlatex -d ghaction calculus1.tex + ./xmScripts/xmlatex -d ghaction calculus?.tex echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY - name: Setup ximera serve cache (only .git) @@ -87,7 +87,7 @@ jobs: XIMERA_NAME: ${{ vars.XIMERA_NAME }} run: | ./xmScripts/xmlatex name - ./xmScripts/xmlatex serve calculus1.tex -f # NOTE: -f should not be needed ... + ./xmScripts/xmlatex serve calculus?.tex -f # NOTE: -f should not be needed ... echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY From df21b0d5ee889605df9009d9c9e9064aa5b98ef4 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Sun, 9 Feb 2025 13:48:32 +0100 Subject: [PATCH 08/62] some fixes --- .github/workflows/serve-ximera.yml | 2 +- coverArt/calculus3Cover/frontCover.tex | 3 +++ greensTheorem/digInCurlAndGreensTheorem.tex | 2 ++ xmPreamble.tex | 2 ++ 4 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 xmPreamble.tex diff --git a/.github/workflows/serve-ximera.yml b/.github/workflows/serve-ximera.yml index 5fc85c521..33acb02e6 100644 --- a/.github/workflows/serve-ximera.yml +++ b/.github/workflows/serve-ximera.yml @@ -48,7 +48,7 @@ jobs: ls -alrt # git config --global --add safe.directory "$(pwd)" # export COMMAND=./xmScripts/xmlatex.test - ./xmScripts/xmlatex -d ghaction calculus?.tex + ./xmScripts/xmlatex ghaction calculus?.tex || echo NOK echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY - name: Setup ximera serve cache (only .git) diff --git a/coverArt/calculus3Cover/frontCover.tex b/coverArt/calculus3Cover/frontCover.tex index f927712b3..3d449121b 100644 --- a/coverArt/calculus3Cover/frontCover.tex +++ b/coverArt/calculus3Cover/frontCover.tex @@ -27,6 +27,9 @@ \definecolor{scarlet}{RGB}{187,0,0} \begin{document} + +\renewcommand{\d}{\mathop{}\!d} + \pagenumbering{gobble} diff --git a/greensTheorem/digInCurlAndGreensTheorem.tex b/greensTheorem/digInCurlAndGreensTheorem.tex index 33ca5d7d0..0b529a8df 100644 --- a/greensTheorem/digInCurlAndGreensTheorem.tex +++ b/greensTheorem/digInCurlAndGreensTheorem.tex @@ -18,6 +18,8 @@ \end{abstract} \maketitle +\renewcommand{\d}{\,d} + %\begin{strip} A fundamental object in calculus is the derivative. However, there are different derivatives for different types of functions, an in each diff --git a/xmPreamble.tex b/xmPreamble.tex new file mode 100644 index 000000000..448a26d16 --- /dev/null +++ b/xmPreamble.tex @@ -0,0 +1,2 @@ +\def\xmNotExpandableAsAccordion{true} +\def\xmNotHintAsExpandable{true} \ No newline at end of file From 1c2768566b750a07ce8ac86c9fa38151a1d0fe6b Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Sun, 9 Feb 2025 15:33:24 +0100 Subject: [PATCH 09/62] extra small fixes --- continuityOfFunctionsOfSeveralVariables/digInContinuity.tex | 2 +- shapeOfThingsToCome/digInStokesTheorem.tex | 2 ++ shapeOfThingsToCome/digInSurfaceIntegrals.tex | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/continuityOfFunctionsOfSeveralVariables/digInContinuity.tex b/continuityOfFunctionsOfSeveralVariables/digInContinuity.tex index adf607d3a..80d2d35d0 100644 --- a/continuityOfFunctionsOfSeveralVariables/digInContinuity.tex +++ b/continuityOfFunctionsOfSeveralVariables/digInContinuity.tex @@ -103,7 +103,7 @@ \] A set that is not bounded is \dfn{unbounded}. \end{itemize} - \item Given a set $S$, we denote the \dfn{boundary} of $S$ by + Given a set $S$, we denote the \dfn{boundary} of $S$ by $\partial S$. \end{definition} diff --git a/shapeOfThingsToCome/digInStokesTheorem.tex b/shapeOfThingsToCome/digInStokesTheorem.tex index d1f39df3c..91ab85302 100644 --- a/shapeOfThingsToCome/digInStokesTheorem.tex +++ b/shapeOfThingsToCome/digInStokesTheorem.tex @@ -12,6 +12,8 @@ \end{abstract} \maketitle +\renewcommand{\d}{\,d} + Our final fundamental theorem of calculus is Stokes' theorem. Historically speaking, Stokes' theorem was discovered after both Green's theorem and the divergence theorem. Its application is diff --git a/shapeOfThingsToCome/digInSurfaceIntegrals.tex b/shapeOfThingsToCome/digInSurfaceIntegrals.tex index 9d46f451f..fe49796db 100644 --- a/shapeOfThingsToCome/digInSurfaceIntegrals.tex +++ b/shapeOfThingsToCome/digInSurfaceIntegrals.tex @@ -400,8 +400,8 @@ \section{Flux: The flow across a surface} domain = 0:360, y domain =-0.5:0.5 ] ( - {(1+0.5*y*cos(x/2)))*cos(x)}, - {(1+0.5*y*cos(x/2)))*sin(x)}, + {(1+0.5*y*cos(x/2))*cos(x)}, + {(1+0.5*y*cos(x/2))*sin(x)}, {0.5*y*sin(x/2)} ); \end{axis} From 79733d4a81e90087c5067f0a9a8cc370452300ba Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Sun, 9 Feb 2025 16:26:35 +0100 Subject: [PATCH 10/62] comment 'shader': fails with the selected driver `pgfsys-dvisvgm4ht.def') --- shapeOfThingsToCome/digInSurfaceIntegrals.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shapeOfThingsToCome/digInSurfaceIntegrals.tex b/shapeOfThingsToCome/digInSurfaceIntegrals.tex index fe49796db..502f3f5cf 100644 --- a/shapeOfThingsToCome/digInSurfaceIntegrals.tex +++ b/shapeOfThingsToCome/digInSurfaceIntegrals.tex @@ -392,7 +392,7 @@ \section{Flux: The flow across a surface} \addplot3 [ surf, colormap/cool, - shader = faceted interp, + %%%% shader = faceted interp, point meta = x, samples = 40, samples y = 5, From 9afb5b6ff09f3e6c9189445c772a7271b0b45a2e Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Mon, 10 Feb 2025 07:52:38 +0100 Subject: [PATCH 11/62] serve calculus*.tex --- .github/workflows/serve-ximera.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/serve-ximera.yml b/.github/workflows/serve-ximera.yml index 33acb02e6..1bfa60d2d 100644 --- a/.github/workflows/serve-ximera.yml +++ b/.github/workflows/serve-ximera.yml @@ -48,7 +48,7 @@ jobs: ls -alrt # git config --global --add safe.directory "$(pwd)" # export COMMAND=./xmScripts/xmlatex.test - ./xmScripts/xmlatex ghaction calculus?.tex || echo NOK + ./xmScripts/xmlatex ghaction calculus*.tex || echo NOK echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY - name: Setup ximera serve cache (only .git) @@ -87,7 +87,7 @@ jobs: XIMERA_NAME: ${{ vars.XIMERA_NAME }} run: | ./xmScripts/xmlatex name - ./xmScripts/xmlatex serve calculus?.tex -f # NOTE: -f should not be needed ... + ./xmScripts/xmlatex serve calculus*.tex -f # NOTE: -f should not be needed ... echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY From cf424040f0ed62e97e5cc695ce0c9aac92283203 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Mon, 10 Feb 2025 09:11:58 +0100 Subject: [PATCH 12/62] compile all ... --- .github/workflows/serve-ximera.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/serve-ximera.yml b/.github/workflows/serve-ximera.yml index 1bfa60d2d..987b41a2d 100644 --- a/.github/workflows/serve-ximera.yml +++ b/.github/workflows/serve-ximera.yml @@ -48,7 +48,8 @@ jobs: ls -alrt # git config --global --add safe.directory "$(pwd)" # export COMMAND=./xmScripts/xmlatex.test - ./xmScripts/xmlatex ghaction calculus*.tex || echo NOK + # ./xmScripts/xmlatex ghaction calculus*.tex || echo NOK + ./xmScripts/xmlatex ghaction || echo NOK echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY - name: Setup ximera serve cache (only .git) @@ -87,7 +88,8 @@ jobs: XIMERA_NAME: ${{ vars.XIMERA_NAME }} run: | ./xmScripts/xmlatex name - ./xmScripts/xmlatex serve calculus*.tex -f # NOTE: -f should not be needed ... + ./xmScripts/xmlatex serve -f # NOTE: -f should not be needed ... + # ./xmScripts/xmlatex serve calculus*.tex -f # NOTE: -f should not be needed ... echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY From c92354dea675352c732f515de8f56c5b82e5ab68 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Mon, 10 Feb 2025 13:06:09 +0100 Subject: [PATCH 13/62] HACK: \[ ... \] does not work ... ? --- .../exercises/secantLineTangentLine1.tex | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/definitionOfTheDerivative/exercises/secantLineTangentLine1.tex b/definitionOfTheDerivative/exercises/secantLineTangentLine1.tex index 080255757..5644bc28c 100644 --- a/definitionOfTheDerivative/exercises/secantLineTangentLine1.tex +++ b/definitionOfTheDerivative/exercises/secantLineTangentLine1.tex @@ -11,9 +11,9 @@ \begin{exercise} An object is moving along a horizontal line. Its position in feet is given by -\[ +$$ s(t) = t^2 - 2 -\] +$$ where $0\leq t\leq 5$ is in seconds. Consider the points on the line below. @@ -47,26 +47,26 @@ \begin{exercise} The average velocity of the object on the interval $[1,3]$ is -\[ +$$ v_{\text{av}} = \answer{4}\text{ft/s.} -\] +$$ The average velocity of the object on the interval $[1,t]$ for $t>1$ is -\[ +$$ v_{\text{av}} = \answer{t+1}\text{ft/s.} -\] +$$ The average velocity of the object on the interval $[t,1]$ for $0 Date: Mon, 10 Feb 2025 13:06:31 +0100 Subject: [PATCH 14/62] missing \begin{itemize} --- .../trigonometricSubstitution/trigonometricSubstitution.tex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/recitation/calculus2/trigonometricSubstitution/trigonometricSubstitution.tex b/recitation/calculus2/trigonometricSubstitution/trigonometricSubstitution.tex index 085cb038a..08f2d9b89 100644 --- a/recitation/calculus2/trigonometricSubstitution/trigonometricSubstitution.tex +++ b/recitation/calculus2/trigonometricSubstitution/trigonometricSubstitution.tex @@ -92,6 +92,7 @@ \section{Discussion Questions} \begin{problem} Two students are asked to evaluate $\int \sqrt{x^2-4} \d x$. +\begin{itemize} \item[I.] One student claims that \[ \int \sqrt{x^2-4} \d x = \int x-2 \d x = \frac{1}{2}x^2 - 2x + C. @@ -103,6 +104,7 @@ \section{Discussion Questions} \int \sqrt{x^2-4} \d x =\int \sqrt{4 \sec^2(\theta)-4} \d \theta = \int \sqrt{4 \tan^2(\theta)} \d \theta = \int \tan(\theta) \d \theta \] Is this student's solution correct so far? If not, determine a likely error that the student made in the calculation. +\end{itemize} \end{problem} \begin{freeResponse} From 3280297569d15b5362a8497e47ba1b9b25a7fc97 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Mon, 10 Feb 2025 13:07:26 +0100 Subject: [PATCH 15/62] no PDF's for now .... --- .github/workflows/serve-ximera.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/serve-ximera.yml b/.github/workflows/serve-ximera.yml index 987b41a2d..a653be30a 100644 --- a/.github/workflows/serve-ximera.yml +++ b/.github/workflows/serve-ximera.yml @@ -49,7 +49,7 @@ jobs: # git config --global --add safe.directory "$(pwd)" # export COMMAND=./xmScripts/xmlatex.test # ./xmScripts/xmlatex ghaction calculus*.tex || echo NOK - ./xmScripts/xmlatex ghaction || echo NOK + ./xmScripts/xmlatex ghaction --compile draft.htm|| echo NOK echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY - name: Setup ximera serve cache (only .git) @@ -88,7 +88,7 @@ jobs: XIMERA_NAME: ${{ vars.XIMERA_NAME }} run: | ./xmScripts/xmlatex name - ./xmScripts/xmlatex serve -f # NOTE: -f should not be needed ... + ./xmScripts/xmlatex serve -f --compile draft.htm # NOTE: -f should not be needed ... # ./xmScripts/xmlatex serve calculus*.tex -f # NOTE: -f should not be needed ... echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY From fc2fa3ed434a7846483ca217e612bc39527b4c6d Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Mon, 10 Feb 2025 13:10:50 +0100 Subject: [PATCH 16/62] typo --- .github/workflows/serve-ximera.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/serve-ximera.yml b/.github/workflows/serve-ximera.yml index a653be30a..e3be5dfdc 100644 --- a/.github/workflows/serve-ximera.yml +++ b/.github/workflows/serve-ximera.yml @@ -49,7 +49,7 @@ jobs: # git config --global --add safe.directory "$(pwd)" # export COMMAND=./xmScripts/xmlatex.test # ./xmScripts/xmlatex ghaction calculus*.tex || echo NOK - ./xmScripts/xmlatex ghaction --compile draft.htm|| echo NOK + ./xmScripts/xmlatex ghaction --compile draft.html || echo NOK echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY - name: Setup ximera serve cache (only .git) @@ -88,7 +88,7 @@ jobs: XIMERA_NAME: ${{ vars.XIMERA_NAME }} run: | ./xmScripts/xmlatex name - ./xmScripts/xmlatex serve -f --compile draft.htm # NOTE: -f should not be needed ... + ./xmScripts/xmlatex serve -f --compile draft.html # NOTE: -f should not be needed ... # ./xmScripts/xmlatex serve calculus*.tex -f # NOTE: -f should not be needed ... echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY From 78a5415794b05fbebc1f8a1b87bd452335e3c8c9 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Mon, 10 Feb 2025 13:35:22 +0100 Subject: [PATCH 17/62] HACK: \[ ... \] does not work ... ? --- .../exercises/averageVelocityInterval1.tex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/definitionOfTheDerivative/exercises/averageVelocityInterval1.tex b/definitionOfTheDerivative/exercises/averageVelocityInterval1.tex index c3b9096e4..19cba85b9 100644 --- a/definitionOfTheDerivative/exercises/averageVelocityInterval1.tex +++ b/definitionOfTheDerivative/exercises/averageVelocityInterval1.tex @@ -50,9 +50,9 @@ \begin{exercise} The average velocity of the object on the interval $[0,2]$ is -\[ +$$ v_{\text{av}} = \answer{0}\text{m/s.} -\] +$$ \end{exercise} \end{exercise} From 75294161fa2dc5fdb1b4d5969667bc12093fe5a8 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Mon, 10 Feb 2025 13:48:00 +0100 Subject: [PATCH 18/62] fix missing \fullwidth --- outcomesList/outcomesBySection.tex | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/outcomesList/outcomesBySection.tex b/outcomesList/outcomesBySection.tex index b54179202..026cb6227 100644 --- a/outcomesList/outcomesBySection.tex +++ b/outcomesList/outcomesBySection.tex @@ -41,6 +41,8 @@ %% Allows for a wide text/regular text +\providecommand{\fullwidth}{} +\providecommand{\normalwidth}{} \renewcommand{\fullwidth}{\newgeometry{textwidth=10cm,textheight=10cm}} \renewcommand{\normalwidth}{\restoregeometry} %% End @@ -174,8 +176,9 @@ \masterinput{../appliedOptimization/titlePage.tex} -%% Derivatives of trigonometric functions -\masterinput{../trigonometricDerivatives/titlePage.tex} +% 20250210: DOES NOT EXIST ??? +% %% Derivatives of trigonometric functions +% \masterinput{../trigonometricDerivatives/titlePage.tex} %% Implicit differentiation \masterinput{../implicitDifferentiation/titlePage.tex} From 7f8ac60fff5c98d11b9ea10ea2d6b3624eab6a9c Mon Sep 17 00:00:00 2001 From: Bart Snapp Date: Mon, 10 Feb 2025 14:51:37 -0500 Subject: [PATCH 19/62] fixed front cover (though I'm not sure) --- coverArt/calculus3Cover/frontCover.tex | 1 + 1 file changed, 1 insertion(+) diff --git a/coverArt/calculus3Cover/frontCover.tex b/coverArt/calculus3Cover/frontCover.tex index f927712b3..a76238cbc 100644 --- a/coverArt/calculus3Cover/frontCover.tex +++ b/coverArt/calculus3Cover/frontCover.tex @@ -27,6 +27,7 @@ \definecolor{scarlet}{RGB}{187,0,0} \begin{document} +\renewcommand{\d}{\mathop{}\!d} \pagenumbering{gobble} From 6ada256cb4c2f6dfadd487693dd2bbcf57bf7e84 Mon Sep 17 00:00:00 2001 From: Bart Snapp Date: Mon, 10 Feb 2025 14:56:32 -0500 Subject: [PATCH 20/62] fixed typo --- improperIntegrals/digInImproperIntegrals.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/improperIntegrals/digInImproperIntegrals.tex b/improperIntegrals/digInImproperIntegrals.tex index cbd9880a5..a4107ac83 100644 --- a/improperIntegrals/digInImproperIntegrals.tex +++ b/improperIntegrals/digInImproperIntegrals.tex @@ -380,7 +380,7 @@ \section{Unbounded intervals} \begin{align*} \int_{-\infty}^{\infty} \frac{1}{1+x^2} \d x&= \int_{-\infty}^{0} \frac{1}{1+x^2} \d x + \int_{0}^{\infty} \frac{1}{1+x^2} \d x \\ -& =\lim_{a \to -\infty} \int_{a}^{0} \frac{1}{1+x^2} \d x + \lim_{b \to \infty} \int_{0}^{\infty} \frac{1}{1+x^2} \d x +& =\lim_{a \to -\infty} \int_{a}^{0} \frac{1}{1+x^2} \d x + \lim_{b \to \infty} \int_{0}^{b} \frac{1}{1+x^2} \d x \end{align*} Now we must look at each integral separately. From 39b07aab34d29aee2f8f1041c35bb45216323961 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Mon, 10 Feb 2025 21:19:28 +0100 Subject: [PATCH 21/62] add .xbb --- logos/authorGuideLogo/bwstars.xbb | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 logos/authorGuideLogo/bwstars.xbb diff --git a/logos/authorGuideLogo/bwstars.xbb b/logos/authorGuideLogo/bwstars.xbb new file mode 100644 index 000000000..1ef0b2b31 --- /dev/null +++ b/logos/authorGuideLogo/bwstars.xbb @@ -0,0 +1,6 @@ +%%Title: logos/authorGuideLogo/bwstars.jpg +%%Creator: extractbb 20240407 +%%BoundingBox: 0 0 5184 3456 +%%HiResBoundingBox: 0.000000 0.000000 5184.000000 3456.000000 +%%CreationDate: Thu Feb 6 21:02:28 2025 + From 45eb306621cf48525eac71912f5ad043c3457ab4 Mon Sep 17 00:00:00 2001 From: Bart Snapp Date: Mon, 10 Feb 2025 15:42:48 -0500 Subject: [PATCH 22/62] Update serve-ximera.yml start with calc 1 --- .github/workflows/serve-ximera.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/serve-ximera.yml b/.github/workflows/serve-ximera.yml index e3be5dfdc..c18e07aa6 100644 --- a/.github/workflows/serve-ximera.yml +++ b/.github/workflows/serve-ximera.yml @@ -48,8 +48,8 @@ jobs: ls -alrt # git config --global --add safe.directory "$(pwd)" # export COMMAND=./xmScripts/xmlatex.test - # ./xmScripts/xmlatex ghaction calculus*.tex || echo NOK - ./xmScripts/xmlatex ghaction --compile draft.html || echo NOK + ./xmScripts/xmlatex ghaction calculus1.tex || echo NOK + # ./xmScripts/xmlatex ghaction --compile draft.html || echo NOK echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY - name: Setup ximera serve cache (only .git) From 0693af006b47f0e30185e2d98b2736c60e4d8862 Mon Sep 17 00:00:00 2001 From: Bart Snapp Date: Mon, 10 Feb 2025 15:44:09 -0500 Subject: [PATCH 23/62] Update serve-ximera.yml calculus* to calculus1 --- .github/workflows/serve-ximera.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/serve-ximera.yml b/.github/workflows/serve-ximera.yml index c18e07aa6..5bf3a545d 100644 --- a/.github/workflows/serve-ximera.yml +++ b/.github/workflows/serve-ximera.yml @@ -88,8 +88,8 @@ jobs: XIMERA_NAME: ${{ vars.XIMERA_NAME }} run: | ./xmScripts/xmlatex name - ./xmScripts/xmlatex serve -f --compile draft.html # NOTE: -f should not be needed ... - # ./xmScripts/xmlatex serve calculus*.tex -f # NOTE: -f should not be needed ... + # ./xmScripts/xmlatex serve -f --compile draft.html # NOTE: -f should not be needed ... + ./xmScripts/xmlatex serve calculus1.tex -f # NOTE: -f should not be needed ... echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY From 2c76ba4d1a092863146616b300b1c25809226802 Mon Sep 17 00:00:00 2001 From: Bart Snapp Date: Tue, 11 Feb 2025 09:36:00 -0500 Subject: [PATCH 24/62] added config --- xmScripts/config.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xmScripts/config.txt b/xmScripts/config.txt index 98eee6202..a8a97d6c9 100644 --- a/xmScripts/config.txt +++ b/xmScripts/config.txt @@ -7,8 +7,8 @@ # # For publishing you'll need a GPG Key # To generate a key-pair, fill in your name and email, and run 'xmlatex genKey' -YOUR_NAME="" -YOUR_EMAIL="" +YOUR_NAME="Bart Snapp" +YOUR_EMAIL="snapp.14@osu.edu--tempMooc" # # Current 'xmlatex genKey' logic: # if there is no .xmKeyFile, and it is properly .gitignored, From 04b7241e137e1fcecd5750b1c7584344c9a7b8a3 Mon Sep 17 00:00:00 2001 From: Wim Obbels Date: Tue, 11 Feb 2025 18:14:02 +0100 Subject: [PATCH 25/62] test parallel --- .github/workflows/serve-ximera.yml | 2 +- .ximera_local/LICENSE | 415 ++++++ .ximera_local/Makefile | 64 + .ximera_local/README | 34 + .ximera_local/README.md | 81 ++ .ximera_local/docker/Dockerfile | 127 ++ .ximera_local/docker/Dockerfile.full | 31 + .../docker/hashcash_1.22-1_amd64.deb | Bin 0 -> 132804 bytes .ximera_local/installingLocally.md | 91 ++ .ximera_local/luaxake/README.md | 164 +++ .ximera_local/luaxake/dkjson.lua | 752 +++++++++++ .ximera_local/luaxake/luaxake | 936 +++++++++++++ .ximera_local/luaxake/luaxake-NT | 820 +++++++++++ .ximera_local/luaxake/luaxake-compile.lua | 294 ++++ .ximera_local/luaxake/luaxake-config.lua | 59 + .ximera_local/luaxake/luaxake-files.lua | 522 +++++++ .ximera_local/luaxake/luaxake-frost.lua | 385 ++++++ .ximera_local/luaxake/luaxake-graph.lua | 125 ++ .ximera_local/luaxake/luaxake-logging.lua | 154 +++ .../luaxake/luaxake-transform-html.lua | 585 ++++++++ .../luaxake/make4ht-errorlogparser.lua | 163 +++ .ximera_local/luaxake/test-bake.lua | 231 ++++ .ximera_local/pgfsys-dvisvgm4ht.def | 75 ++ .ximera_local/sagetex.sty | 316 +++++ .ximera_local/src/abstract.dtx | 24 + .ximera_local/src/accordion.dtx | 72 + .ximera_local/src/activity.dtx | 158 +++ .ximera_local/src/answer.dtx | 111 ++ .ximera_local/src/banner.dtx | 21 + .ximera_local/src/choice.dtx | 226 ++++ .ximera_local/src/clearEnv.dtx | 18 + .ximera_local/src/dialogue.dtx | 28 + .ximera_local/src/ending.dtx | 20 + .ximera_local/src/enumerate.dtx | 10 + .ximera_local/src/feedback.dtx | 72 + .ximera_local/src/foldable.dtx | 79 ++ .ximera_local/src/footnotes.dtx | 63 + .ximera_local/src/freeresponse.dtx | 60 + .ximera_local/src/graded.dtx | 20 + .ximera_local/src/hideEnv.dtx | 36 + .ximera_local/src/hints.dtx | 53 + .ximera_local/src/html.dtx | 20 + .ximera_local/src/hyperref.dtx | 12 + .ximera_local/src/image.dtx | 71 + .ximera_local/src/instructornotes.dtx | 101 ++ .ximera_local/src/interactives/desmos.dtx | 16 + .ximera_local/src/interactives/geogebra.dtx | 27 + .ximera_local/src/interactives/google.dtx | 26 + .ximera_local/src/interactives/graph.dtx | 14 + .ximera_local/src/interactives/include.dtx | 22 + .ximera_local/src/interactives/javascript.dtx | 31 + .ximera_local/src/interactives/sagemath.dtx | 45 + .ximera_local/src/interactives/video.dtx | 24 + .ximera_local/src/jax.dtx | 71 + .ximera_local/src/labels.dtx | 15 + .ximera_local/src/leash.dtx | 18 + .ximera_local/src/link.dtx | 31 + .ximera_local/src/logo.dtx | 30 + .ximera_local/src/macros.dtx | 14 + .ximera_local/src/makeCounter.dtx | 37 + .ximera_local/src/metadata.dtx | 32 + .ximera_local/src/only.dtx | 90 ++ .ximera_local/src/options.dtx | 123 ++ .ximera_local/src/optionsxourse.dtx | 27 + .ximera_local/src/outcomes.dtx | 34 + .ximera_local/src/packages.dtx | 54 + .ximera_local/src/pagesetup.dtx | 73 + .ximera_local/src/preamble.dtx | 31 + .ximera_local/src/problem.dtx | 203 +++ .ximera_local/src/proof.dtx | 20 + .ximera_local/src/sectioning.dtx | 49 + .ximera_local/src/solution.dtx | 30 + .ximera_local/src/suppress.dtx | 33 + .ximera_local/src/textcolor.dtx | 20 + .ximera_local/src/theorems.dtx | 173 +++ .ximera_local/src/tikzexport.dtx | 45 + .ximera_local/src/title.dtx | 122 ++ .ximera_local/src/ungraded.dtx | 22 + .ximera_local/src/utilitymacros.dtx | 45 + .ximera_local/src/verbatim.dtx | 36 + .ximera_local/src/xkcd.dtx | 13 + .ximera_local/ximera.4ht | 408 ++++++ .ximera_local/ximera.cfg | 174 +++ .ximera_local/ximera.cls | 1198 +++++++++++++++++ .ximera_local/ximera.dtx | 384 ++++++ .ximera_local/ximera.hd | 0 .ximera_local/ximera.ins | 216 +++ .ximera_local/xmScripts/config.txt | 41 + .ximera_local/xmScripts/xmlatex | 781 +++++++++++ .ximera_local/xmScripts/xmlatex.base | 125 ++ .ximera_local/xourse.4ht | 100 ++ .ximera_local/xourse.cls | 154 +++ motionAndPathsInSpace/exercises/Curv1.pdf | Bin 9000 -> 0 bytes 93 files changed, 12977 insertions(+), 1 deletion(-) create mode 100644 .ximera_local/LICENSE create mode 100644 .ximera_local/Makefile create mode 100644 .ximera_local/README create mode 100644 .ximera_local/README.md create mode 100644 .ximera_local/docker/Dockerfile create mode 100644 .ximera_local/docker/Dockerfile.full create mode 100644 .ximera_local/docker/hashcash_1.22-1_amd64.deb create mode 100644 .ximera_local/installingLocally.md create mode 100644 .ximera_local/luaxake/README.md create mode 100644 .ximera_local/luaxake/dkjson.lua create mode 100755 .ximera_local/luaxake/luaxake create mode 100644 .ximera_local/luaxake/luaxake-NT create mode 100644 .ximera_local/luaxake/luaxake-compile.lua create mode 100644 .ximera_local/luaxake/luaxake-config.lua create mode 100644 .ximera_local/luaxake/luaxake-files.lua create mode 100644 .ximera_local/luaxake/luaxake-frost.lua create mode 100644 .ximera_local/luaxake/luaxake-graph.lua create mode 100644 .ximera_local/luaxake/luaxake-logging.lua create mode 100644 .ximera_local/luaxake/luaxake-transform-html.lua create mode 100755 .ximera_local/luaxake/make4ht-errorlogparser.lua create mode 100644 .ximera_local/luaxake/test-bake.lua create mode 100644 .ximera_local/pgfsys-dvisvgm4ht.def create mode 100644 .ximera_local/sagetex.sty create mode 100644 .ximera_local/src/abstract.dtx create mode 100644 .ximera_local/src/accordion.dtx create mode 100644 .ximera_local/src/activity.dtx create mode 100644 .ximera_local/src/answer.dtx create mode 100644 .ximera_local/src/banner.dtx create mode 100644 .ximera_local/src/choice.dtx create mode 100644 .ximera_local/src/clearEnv.dtx create mode 100644 .ximera_local/src/dialogue.dtx create mode 100644 .ximera_local/src/ending.dtx create mode 100644 .ximera_local/src/enumerate.dtx create mode 100644 .ximera_local/src/feedback.dtx create mode 100644 .ximera_local/src/foldable.dtx create mode 100644 .ximera_local/src/footnotes.dtx create mode 100644 .ximera_local/src/freeresponse.dtx create mode 100644 .ximera_local/src/graded.dtx create mode 100644 .ximera_local/src/hideEnv.dtx create mode 100644 .ximera_local/src/hints.dtx create mode 100644 .ximera_local/src/html.dtx create mode 100644 .ximera_local/src/hyperref.dtx create mode 100644 .ximera_local/src/image.dtx create mode 100644 .ximera_local/src/instructornotes.dtx create mode 100644 .ximera_local/src/interactives/desmos.dtx create mode 100644 .ximera_local/src/interactives/geogebra.dtx create mode 100644 .ximera_local/src/interactives/google.dtx create mode 100644 .ximera_local/src/interactives/graph.dtx create mode 100644 .ximera_local/src/interactives/include.dtx create mode 100644 .ximera_local/src/interactives/javascript.dtx create mode 100644 .ximera_local/src/interactives/sagemath.dtx create mode 100644 .ximera_local/src/interactives/video.dtx create mode 100644 .ximera_local/src/jax.dtx create mode 100644 .ximera_local/src/labels.dtx create mode 100644 .ximera_local/src/leash.dtx create mode 100644 .ximera_local/src/link.dtx create mode 100644 .ximera_local/src/logo.dtx create mode 100644 .ximera_local/src/macros.dtx create mode 100644 .ximera_local/src/makeCounter.dtx create mode 100644 .ximera_local/src/metadata.dtx create mode 100644 .ximera_local/src/only.dtx create mode 100644 .ximera_local/src/options.dtx create mode 100644 .ximera_local/src/optionsxourse.dtx create mode 100644 .ximera_local/src/outcomes.dtx create mode 100644 .ximera_local/src/packages.dtx create mode 100644 .ximera_local/src/pagesetup.dtx create mode 100644 .ximera_local/src/preamble.dtx create mode 100644 .ximera_local/src/problem.dtx create mode 100644 .ximera_local/src/proof.dtx create mode 100644 .ximera_local/src/sectioning.dtx create mode 100644 .ximera_local/src/solution.dtx create mode 100644 .ximera_local/src/suppress.dtx create mode 100644 .ximera_local/src/textcolor.dtx create mode 100644 .ximera_local/src/theorems.dtx create mode 100644 .ximera_local/src/tikzexport.dtx create mode 100644 .ximera_local/src/title.dtx create mode 100644 .ximera_local/src/ungraded.dtx create mode 100644 .ximera_local/src/utilitymacros.dtx create mode 100644 .ximera_local/src/verbatim.dtx create mode 100644 .ximera_local/src/xkcd.dtx create mode 100644 .ximera_local/ximera.4ht create mode 100644 .ximera_local/ximera.cfg create mode 100644 .ximera_local/ximera.cls create mode 100644 .ximera_local/ximera.dtx create mode 100644 .ximera_local/ximera.hd create mode 100644 .ximera_local/ximera.ins create mode 100644 .ximera_local/xmScripts/config.txt create mode 100755 .ximera_local/xmScripts/xmlatex create mode 100755 .ximera_local/xmScripts/xmlatex.base create mode 100644 .ximera_local/xourse.4ht create mode 100644 .ximera_local/xourse.cls delete mode 100644 motionAndPathsInSpace/exercises/Curv1.pdf diff --git a/.github/workflows/serve-ximera.yml b/.github/workflows/serve-ximera.yml index 5bf3a545d..1ed534544 100644 --- a/.github/workflows/serve-ximera.yml +++ b/.github/workflows/serve-ximera.yml @@ -48,7 +48,7 @@ jobs: ls -alrt # git config --global --add safe.directory "$(pwd)" # export COMMAND=./xmScripts/xmlatex.test - ./xmScripts/xmlatex ghaction calculus1.tex || echo NOK + ./xmScripts/xmlatex ghaction calculus1.tex -j 4 || echo NOK # ./xmScripts/xmlatex ghaction --compile draft.html || echo NOK echo "✅ Published to $XIMERA_URL$XIMERA_NAME " >> $GITHUB_STEP_SUMMARY diff --git a/.ximera_local/LICENSE b/.ximera_local/LICENSE new file mode 100644 index 000000000..4db9b5af2 --- /dev/null +++ b/.ximera_local/LICENSE @@ -0,0 +1,415 @@ +The LaTeX Project Public License +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +LPPL Version 1.3c 2008-05-04 + +Copyright 1999 2002-2008 LaTeX3 Project + Everyone is allowed to distribute verbatim copies of this + license document, but modification of it is not allowed. + + +PREAMBLE +======== + +The LaTeX Project Public License (LPPL) is the primary license under +which the LaTeX kernel and the base LaTeX packages are distributed. + +You may use this license for any work of which you hold the copyright +and which you wish to distribute. This license may be particularly +suitable if your work is TeX-related (such as a LaTeX package), but +it is written in such a way that you can use it even if your work is +unrelated to TeX. + +The section `WHETHER AND HOW TO DISTRIBUTE WORKS UNDER THIS LICENSE', +below, gives instructions, examples, and recommendations for authors +who are considering distributing their works under this license. + +This license gives conditions under which a work may be distributed +and modified, as well as conditions under which modified versions of +that work may be distributed. + +We, the LaTeX3 Project, believe that the conditions below give you +the freedom to make and distribute modified versions of your work +that conform with whatever technical specifications you wish while +maintaining the availability, integrity, and reliability of +that work. If you do not see how to achieve your goal while +meeting these conditions, then read the document `cfgguide.tex' +and `modguide.tex' in the base LaTeX distribution for suggestions. + + +DEFINITIONS +=========== + +In this license document the following terms are used: + + `Work' + Any work being distributed under this License. + + `Derived Work' + Any work that under any applicable law is derived from the Work. + + `Modification' + Any procedure that produces a Derived Work under any applicable + law -- for example, the production of a file containing an + original file associated with the Work or a significant portion of + such a file, either verbatim or with modifications and/or + translated into another language. + + `Modify' + To apply any procedure that produces a Derived Work under any + applicable law. + + `Distribution' + Making copies of the Work available from one person to another, in + whole or in part. Distribution includes (but is not limited to) + making any electronic components of the Work accessible by + file transfer protocols such as FTP or HTTP or by shared file + systems such as Sun's Network File System (NFS). + + `Compiled Work' + A version of the Work that has been processed into a form where it + is directly usable on a computer system. This processing may + include using installation facilities provided by the Work, + transformations of the Work, copying of components of the Work, or + other activities. Note that modification of any installation + facilities provided by the Work constitutes modification of the Work. + + `Current Maintainer' + A person or persons nominated as such within the Work. If there is + no such explicit nomination then it is the `Copyright Holder' under + any applicable law. + + `Base Interpreter' + A program or process that is normally needed for running or + interpreting a part or the whole of the Work. + + A Base Interpreter may depend on external components but these + are not considered part of the Base Interpreter provided that each + external component clearly identifies itself whenever it is used + interactively. Unless explicitly specified when applying the + license to the Work, the only applicable Base Interpreter is a + `LaTeX-Format' or in the case of files belonging to the + `LaTeX-format' a program implementing the `TeX language'. + + + +CONDITIONS ON DISTRIBUTION AND MODIFICATION +=========================================== + +1. Activities other than distribution and/or modification of the Work +are not covered by this license; they are outside its scope. In +particular, the act of running the Work is not restricted and no +requirements are made concerning any offers of support for the Work. + +2. You may distribute a complete, unmodified copy of the Work as you +received it. Distribution of only part of the Work is considered +modification of the Work, and no right to distribute such a Derived +Work may be assumed under the terms of this clause. + +3. You may distribute a Compiled Work that has been generated from a +complete, unmodified copy of the Work as distributed under Clause 2 +above, as long as that Compiled Work is distributed in such a way that +the recipients may install the Compiled Work on their system exactly +as it would have been installed if they generated a Compiled Work +directly from the Work. + +4. If you are the Current Maintainer of the Work, you may, without +restriction, modify the Work, thus creating a Derived Work. You may +also distribute the Derived Work without restriction, including +Compiled Works generated from the Derived Work. Derived Works +distributed in this manner by the Current Maintainer are considered to +be updated versions of the Work. + +5. If you are not the Current Maintainer of the Work, you may modify +your copy of the Work, thus creating a Derived Work based on the Work, +and compile this Derived Work, thus creating a Compiled Work based on +the Derived Work. + +6. If you are not the Current Maintainer of the Work, you may +distribute a Derived Work provided the following conditions are met +for every component of the Work unless that component clearly states +in the copyright notice that it is exempt from that condition. Only +the Current Maintainer is allowed to add such statements of exemption +to a component of the Work. + + a. If a component of this Derived Work can be a direct replacement + for a component of the Work when that component is used with the + Base Interpreter, then, wherever this component of the Work + identifies itself to the user when used interactively with that + Base Interpreter, the replacement component of this Derived Work + clearly and unambiguously identifies itself as a modified version + of this component to the user when used interactively with that + Base Interpreter. + + b. Every component of the Derived Work contains prominent notices + detailing the nature of the changes to that component, or a + prominent reference to another file that is distributed as part + of the Derived Work and that contains a complete and accurate log + of the changes. + + c. No information in the Derived Work implies that any persons, + including (but not limited to) the authors of the original version + of the Work, provide any support, including (but not limited to) + the reporting and handling of errors, to recipients of the + Derived Work unless those persons have stated explicitly that + they do provide such support for the Derived Work. + + d. You distribute at least one of the following with the Derived Work: + + 1. A complete, unmodified copy of the Work; + if your distribution of a modified component is made by + offering access to copy the modified component from a + designated place, then offering equivalent access to copy + the Work from the same or some similar place meets this + condition, even though third parties are not compelled to + copy the Work along with the modified component; + + 2. Information that is sufficient to obtain a complete, + unmodified copy of the Work. + +7. If you are not the Current Maintainer of the Work, you may +distribute a Compiled Work generated from a Derived Work, as long as +the Derived Work is distributed to all recipients of the Compiled +Work, and as long as the conditions of Clause 6, above, are met with +regard to the Derived Work. + +8. The conditions above are not intended to prohibit, and hence do not +apply to, the modification, by any method, of any component so that it +becomes identical to an updated version of that component of the Work as +it is distributed by the Current Maintainer under Clause 4, above. + +9. Distribution of the Work or any Derived Work in an alternative +format, where the Work or that Derived Work (in whole or in part) is +then produced by applying some process to that format, does not relax or +nullify any sections of this license as they pertain to the results of +applying that process. + +10. a. A Derived Work may be distributed under a different license + provided that license itself honors the conditions listed in + Clause 6 above, in regard to the Work, though it does not have + to honor the rest of the conditions in this license. + + b. If a Derived Work is distributed under a different license, that + Derived Work must provide sufficient documentation as part of + itself to allow each recipient of that Derived Work to honor the + restrictions in Clause 6 above, concerning changes from the Work. + +11. This license places no restrictions on works that are unrelated to +the Work, nor does this license place any restrictions on aggregating +such works with the Work by any means. + +12. Nothing in this license is intended to, or may be used to, prevent +complete compliance by all parties with all applicable laws. + + +NO WARRANTY +=========== + +There is no warranty for the Work. Except when otherwise stated in +writing, the Copyright Holder provides the Work `as is', without +warranty of any kind, either expressed or implied, including, but not +limited to, the implied warranties of merchantability and fitness for a +particular purpose. The entire risk as to the quality and performance +of the Work is with you. Should the Work prove defective, you assume +the cost of all necessary servicing, repair, or correction. + +In no event unless required by applicable law or agreed to in writing +will The Copyright Holder, or any author named in the components of the +Work, or any other party who may distribute and/or modify the Work as +permitted above, be liable to you for damages, including any general, +special, incidental or consequential damages arising out of any use of +the Work or out of inability to use the Work (including, but not limited +to, loss of data, data being rendered inaccurate, or losses sustained by +anyone as a result of any failure of the Work to operate with any other +programs), even if the Copyright Holder or said author or said other +party has been advised of the possibility of such damages. + + +MAINTENANCE OF THE WORK +======================= + +The Work has the status `author-maintained' if the Copyright Holder +explicitly and prominently states near the primary copyright notice in +the Work that the Work can only be maintained by the Copyright Holder +or simply that it is `author-maintained'. + +The Work has the status `maintained' if there is a Current Maintainer +who has indicated in the Work that they are willing to receive error +reports for the Work (for example, by supplying a valid e-mail +address). It is not required for the Current Maintainer to acknowledge +or act upon these error reports. + +The Work changes from status `maintained' to `unmaintained' if there +is no Current Maintainer, or the person stated to be Current +Maintainer of the work cannot be reached through the indicated means +of communication for a period of six months, and there are no other +significant signs of active maintenance. + +You can become the Current Maintainer of the Work by agreement with +any existing Current Maintainer to take over this role. + +If the Work is unmaintained, you can become the Current Maintainer of +the Work through the following steps: + + 1. Make a reasonable attempt to trace the Current Maintainer (and + the Copyright Holder, if the two differ) through the means of + an Internet or similar search. + + 2. If this search is successful, then enquire whether the Work + is still maintained. + + a. If it is being maintained, then ask the Current Maintainer + to update their communication data within one month. + + b. If the search is unsuccessful or no action to resume active + maintenance is taken by the Current Maintainer, then announce + within the pertinent community your intention to take over + maintenance. (If the Work is a LaTeX work, this could be + done, for example, by posting to comp.text.tex.) + + 3a. If the Current Maintainer is reachable and agrees to pass + maintenance of the Work to you, then this takes effect + immediately upon announcement. + + b. If the Current Maintainer is not reachable and the Copyright + Holder agrees that maintenance of the Work be passed to you, + then this takes effect immediately upon announcement. + + 4. If you make an `intention announcement' as described in 2b. above + and after three months your intention is challenged neither by + the Current Maintainer nor by the Copyright Holder nor by other + people, then you may arrange for the Work to be changed so as + to name you as the (new) Current Maintainer. + + 5. If the previously unreachable Current Maintainer becomes + reachable once more within three months of a change completed + under the terms of 3b) or 4), then that Current Maintainer must + become or remain the Current Maintainer upon request provided + they then update their communication data within one month. + +A change in the Current Maintainer does not, of itself, alter the fact +that the Work is distributed under the LPPL license. + +If you become the Current Maintainer of the Work, you should +immediately provide, within the Work, a prominent and unambiguous +statement of your status as Current Maintainer. You should also +announce your new status to the same pertinent community as +in 2b) above. + + +WHETHER AND HOW TO DISTRIBUTE WORKS UNDER THIS LICENSE +====================================================== + +This section contains important instructions, examples, and +recommendations for authors who are considering distributing their +works under this license. These authors are addressed as `you' in +this section. + +Choosing This License or Another License +---------------------------------------- + +If for any part of your work you want or need to use *distribution* +conditions that differ significantly from those in this license, then +do not refer to this license anywhere in your work but, instead, +distribute your work under a different license. You may use the text +of this license as a model for your own license, but your license +should not refer to the LPPL or otherwise give the impression that +your work is distributed under the LPPL. + +The document `modguide.tex' in the base LaTeX distribution explains +the motivation behind the conditions of this license. It explains, +for example, why distributing LaTeX under the GNU General Public +License (GPL) was considered inappropriate. Even if your work is +unrelated to LaTeX, the discussion in `modguide.tex' may still be +relevant, and authors intending to distribute their works under any +license are encouraged to read it. + +A Recommendation on Modification Without Distribution +----------------------------------------------------- + +It is wise never to modify a component of the Work, even for your own +personal use, without also meeting the above conditions for +distributing the modified component. While you might intend that such +modifications will never be distributed, often this will happen by +accident -- you may forget that you have modified that component; or +it may not occur to you when allowing others to access the modified +version that you are thus distributing it and violating the conditions +of this license in ways that could have legal implications and, worse, +cause problems for the community. It is therefore usually in your +best interest to keep your copy of the Work identical with the public +one. Many works provide ways to control the behavior of that work +without altering any of its licensed components. + +How to Use This License +----------------------- + +To use this license, place in each of the components of your work both +an explicit copyright notice including your name and the year the work +was authored and/or last substantially modified. Include also a +statement that the distribution and/or modification of that +component is constrained by the conditions in this license. + +Here is an example of such a notice and statement: + + %% pig.dtx + %% Copyright 2005 M. Y. Name + % + % This work may be distributed and/or modified under the + % conditions of the LaTeX Project Public License, either version 1.3 + % of this license or (at your option) any later version. + % The latest version of this license is in + % http://www.latex-project.org/lppl.txt + % and version 1.3 or later is part of all distributions of LaTeX + % version 2005/12/01 or later. + % + % This work has the LPPL maintenance status `maintained'. + % + % The Current Maintainer of this work is M. Y. Name. + % + % This work consists of the files pig.dtx and pig.ins + % and the derived file pig.sty. + +Given such a notice and statement in a file, the conditions +given in this license document would apply, with the `Work' referring +to the three files `pig.dtx', `pig.ins', and `pig.sty' (the last being +generated from `pig.dtx' using `pig.ins'), the `Base Interpreter' +referring to any `LaTeX-Format', and both `Copyright Holder' and +`Current Maintainer' referring to the person `M. Y. Name'. + +If you do not want the Maintenance section of LPPL to apply to your +Work, change `maintained' above into `author-maintained'. +However, we recommend that you use `maintained', as the Maintenance +section was added in order to ensure that your Work remains useful to +the community even when you can no longer maintain and support it +yourself. + +Derived Works That Are Not Replacements +--------------------------------------- + +Several clauses of the LPPL specify means to provide reliability and +stability for the user community. They therefore concern themselves +with the case that a Derived Work is intended to be used as a +(compatible or incompatible) replacement of the original Work. If +this is not the case (e.g., if a few lines of code are reused for a +completely different task), then clauses 6b and 6d shall not apply. + + +Important Recommendations +------------------------- + + Defining What Constitutes the Work + + The LPPL requires that distributions of the Work contain all the + files of the Work. It is therefore important that you provide a + way for the licensee to determine which files constitute the Work. + This could, for example, be achieved by explicitly listing all the + files of the Work near the copyright notice of each file or by + using a line such as: + + % This work consists of all files listed in manifest.txt. + + in that place. In the absence of an unequivocal list it might be + impossible for the licensee to determine what is considered by you + to comprise the Work and, in such a case, the licensee would be + entitled to make reasonable conjectures as to which files comprise + the Work. diff --git a/.ximera_local/Makefile b/.ximera_local/Makefile new file mode 100644 index 000000000..d198e0181 --- /dev/null +++ b/.ximera_local/Makefile @@ -0,0 +1,64 @@ +NAME = ximera +SHELL = bash +PWD = $(shell pwd) +VERS = $(shell ltxfileinfo -v $(NAME).dtx|sed -e 's/^v//') +LOCAL = $(shell kpsewhich --var-value TEXMFLOCAL) +UTREE = $(shell kpsewhich --var-value TEXMFHOME) +INPUTS = $(wildcard src/*.dtx) $(wildcard src/interactives/*.dtx) $(NAME).dtx +OUTPUTS = ximera.cls xourse.cls ximera.4ht xourse.4ht ximera.cfg + +# based on +# +# https://stackoverflow.com/questions/2973445/gnu-makefile-rule-generating-a-few-targets-from-a-single-source-file +# +# I use a silly pattern rule to convince GNU make that multiple +# outputs are created with a single invocation + +all: $(NAME).pdf $(OUTPUTS) + test -e README.txt && mv README.txt README || exit 0 + +$(NAME)%pdf ximera%cls xourse%cls ximera%4ht xourse%4ht ximera%cfg: $(INPUTS) + pdflatex -shell-escape -recorder -interaction=batchmode $(NAME).dtx >/dev/null + if [ -f $(NAME).glo ]; then makeindex -q -s gglo.ist -o $(NAME).gls $(NAME).glo; fi + if [ -f $(NAME).idx ]; then makeindex -q -s gind.ist -o $(NAME).ind $(NAME).idx; fi + pdflatex --recorder --interaction=nonstopmode $(NAME).dtx > /dev/null + pdflatex --recorder --interaction=nonstopmode $(NAME).dtx > /dev/null + +clean: + rm -f $(NAME).{aux,fls,glo,gls,hd,idx,ilg,ind,ins,log,out} + +distclean: clean + rm -f ximera.pdf README $(OUTPUTS) + +# BADBAD: The code below still needs to be fixed + + +ctan: all + mkdir -p ./ctan/ximera/src/interactives/ # create a directory structure + touch ./ctan/ximera.zip + rm ./ctan/ximera.zip # remove old zip file + cp ximera.dtx ./ctan/ximera/ # copy files + cp ximera.pdf ./ctan/ximera/ + cp ximera.ins ./ctan/ximera/ + cp ./src/*.dtx ./ctan/ximera/src # only copy the dtx files! + cp ./src/interactives/*.dtx ./ctan/ximera/src/interactives/ # only copy the dtx files! + cp LICENSE ./ctan/ximera/ + cp README ./ctan/ximera/ + zip -r ./ctan/ximera.zip ./ctan/ximera + +inst: all + mkdir -p $(UTREE)/{tex,source,doc}/latexn/$(NAME) + cp $(NAME).dtx $(UTREE)/source/latex/$(NAME) + cp $(NAME).cls $(UTREE)/tex/latex/$(NAME) + cp $(NAME).pdf $(UTREE)/doc/latex/$(NAME) + +install: all + sudo mkdir -p $(LOCAL)/{tex,source,doc}/latex/$(NAME) + sudo cp $(NAME).dtx $(LOCAL)/source/latex/$(NAME) + sudo cp $(NAME).cls $(LOCAL)/tex/latex/$(NAME) + sudo cp $(NAME).pdf $(LOCAL)/doc/latex/$(NAME) + +zip: all + ln -sf . $(NAME) + zip -Drq $(PWD)/$(NAME)-$(VERS).zip $(NAME)/{README,$(NAME).{pdf,dtx}} + rm $(NAME) diff --git a/.ximera_local/README b/.ximera_local/README new file mode 100644 index 000000000..8e1fd7001 --- /dev/null +++ b/.ximera_local/README @@ -0,0 +1,34 @@ +------------:| ------------------------------------------------------------ + ximera:| Simultaneously write print and online interactive materials + Author:| Jim Fowler and Oscar Levin and Jason Nowell and Wim Obbels and Hans Parshall and Bart Snapp + E-mail:| bart.snapp@gmail.com + License:| Released under the LaTeX Project Public License v1.3c or later + See:| http://www.latex-project.org/lppl.txt + +Ximera, pronounced "chimera," (Ximera: Interactive, Mathematics, +Education, Resources, for All) is an open-source platform that +provides tools for authoring and publishing (PDF and Online), +open-source, interactive educational content, such as textbooks, +assessments, and online courses. + +The Ximera document class is designed to support all syntax used to +create interactive online content. This means that by default, we try +to display all authoring content. This includes links to online +interactives, notifications when something differs in the PDF vs the +online incarnation. By using various options in the documentclass, +notably `handout` reasonable PDF handouts and full books can be +created along side the document class. + +While one can produce high-quality PDF using Ximera, authors need to +decide exactly how Ximera documents fit-in to their work and use them +consistently. + +Please reach out to the developers with any questions. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + diff --git a/.ximera_local/README.md b/.ximera_local/README.md new file mode 100644 index 000000000..1dd1892c7 --- /dev/null +++ b/.ximera_local/README.md @@ -0,0 +1,81 @@ + +This repository contains the source for the `ximera` LaTeX package. + +Since the summer of 2024, the Ximera document class is available in [CTAN](https://ctan.org/pkg/ximera?lang=en). Moreover it is strongly advised to use Ximera with [docker](https://github.com/XimeraProject/ximeraFirstSteps). +This repo should therefore not be directly relevant for (prospective) Ximera authors or endusers. +They should consult the [Ximera Manual](https://ximera.osu.edu/xman) or [Example Ximera Xourse](https://go.osu.edu/ximera-examples) for more info on Ximera. + + +This repo also contains the optional (but strongly recommended) build (lua-) script `luaxake` and a wrapper (bash-) script `xmlatex` with some extra functionality but espaccially to transparently run `luaxake` in a docker container. + +This repo also contain the `Dockerfile(s)` to build docker images which provide the most typical way to use Ximera. + +Manual local installation of the Ximera LaTeX package is normally never needed, but (somewhat old) documentation remains [available](./installingLocally.md). + + +# Contents of the repository + +* This README.md file. + +* The LPPL 1.3c license. + +* The Ximera documented LaTeX file type, ximera.dtx. This file + generates ximera.cls, xourse.cls, and ximeraLaTeX.pdf, as well as a + few other files. + +* In the `src` folder the ximeraLatex source files (as used by ximera.dtx) + +* In the `luaxake` folder the LUA code of the `luaxake` build script. This has its own [README](luaxake/README.md). + +* In the `xmScripts` folder, the (wrapper-) script `xmlatex`. One version goes into the docker image, a simplified 'header' part should go in each ximera-repo (or just once somewhere in the PATH) oo your PC. + +* In the `docker` folder build files for docker images. Images are automatically build for each tag of this repo, and released versions are available from [github](https://github.com/orgs/XimeraProject/packages) + +# Advanced: building (local) docker images + +Check out this repo and run (from the root folder) +``` +docker buildx build --tag ghcr.io/ximeraproject/ximeralatex:latest --file docker/Dockerfile.full . +``` + +and test or use the newly build image eg with +``` +XAKE_VERSION=latest xmlatex bake mytestfile.tex +``` +To further develop, test or manipulate Ximera, you can work inside the container with +``` +XAKE_VERSION=latest xmlatex bash +``` +It is possible to extract this package from the container into a .ximera_local folder, and develop from there with +``` +XAKE_VERSION=latest xmlatex copySettingsLocal +``` + +The default XAKE_VERSION (and thus the container to be used) can also be set in xmScripts/config.txt. + +# Advanced: compiling the `ximera` LaTeX package + +Running `make` generates the derived files README, ximera.pdf, ximera.cls, xourse.cls, ximera.cfg, ximera.4ht, xourse.4ht. + +Running `make ctan` generates a submission suitable for CTAN + +(OBSOLETE) Running `make inst` installs the files in the user's TeX tree. + +(OBSOLETE) Running `make install` installs the files in the local TeX tree. + +All this can (optionally) done INSIDE a ximeralatex docker container, ie after running +``` +xmlatex bash +``` +in the root folder of this repo. + + +# A Non-Official List Of Possible Future Features + +- Ability to include \activities and \practice within a ximera document + - when adding Xourses, the path to the xourse/file needs to be given +in some way. We have an example of this in the preamble of +examples/exerciseCollection/exerciseCollection.tex This enables an +author to print the file and know where to find the parts. + - Perhaps by default, all Xourse files appear on the top page, but if modified with `\documentclass[hidden]{xourse}, they would no longer appear + - A separate, perhaps password protected page with ALL content on it. \ No newline at end of file diff --git a/.ximera_local/docker/Dockerfile b/.ximera_local/docker/Dockerfile new file mode 100644 index 000000000..4d7c958ad --- /dev/null +++ b/.ximera_local/docker/Dockerfile @@ -0,0 +1,127 @@ +FROM registry.gitlab.com/islandoftex/images/texlive:TL2024-2024-11-17-medium + +RUN apt-get update -y; \ + apt-get install -y \ + git git-restore-mtime \ + # mupdf-tools \ + pdf2svg \ + gnupg \ + jq \ + curl \ + wget \ + dos2unix \ + vim-tiny strace less \ + nano file + +# hashcash needed for setting up communication to ximeraServer; seems unfortunately not (easily?) installable with apt.. +ADD docker/hashcash_1.22-1_amd64.deb . +RUN dpkg -i hashcash_1.22-1_amd64.deb + + +# Extra packages (on top of 'medium') +RUN tlmgr update --self; \ + tlmgr install \ + # afterpage \ + amsmath \ + # amssymb \ + # amsthm \ + # bm \ + booktabs \ + cancel \ + caption \ + ccicons \ + changebar \ + chemfig \ + chemgreek \ + chemmacros \ + circuitikz \ + # color \ + comment \ + currfile \ + datetime \ + enumitem \ + environ \ + epstopdf \ + esint \ + eurosym \ + # extramarks \ + fancyhdr \ + fancyvrb \ + float \ + # fontenc \ + forloop \ + geometry \ + gettitlestring \ + # graphicx \ + hyperref \ + hypdoc \ + # ifpdf \ + # ifthen \ + # ifxetex \ + # inputenc \ + listings \ + makecell \ + makecmds \ + marginnote \ + mhchem \ + morewrites \ + # multicol \ + multido \ + multirow \ + # nameref \ + pdflscape \ + pgfplots \ + polynom \ + qrcode \ + relsize \ + # sagetex \ + silence \ + signchart \ + siunitx \ + tabto-ltx \ + # tabularx \ + tcolorbox \ + # textcomp \ + textpos \ + # tikz \ + tikz-3dplot \ + tikzsymbols \ + titlesec \ + # titletoc \ + titling \ + url \ + # verbatim \ + xcolor \ + xifthen \ + xkeyval \ + xstring \ + tikz-cd tkz-euclide \ + ifmtarg \ + etoolbox translations mathtools xfrac filehook \ + doclicense \ + simplekv fmtcount pdfcol \ + tikzfill \ + tkz-tab \ + csquotes \ + babel-dutch \ + microtype \ + tex4ht make4ht \ + luaxml \ + fontawesome stix \ + latexindent \ + wallpaper + +# make4ht is not in /usr/local/bin if installed with tlmgr!!! +ENV PATH="$PATH:/usr/local/texlive/2024/bin/x86_64-linux" + +# Add this complete ximeraLatex repo (is overkill, but allows bugfixing/editing inside the docker image! +# It automatically adds the luaxake and xmlatex scripts +RUN mkdir -p /root/texmf/tex/latex +ADD . /root/texmf/tex/latex/ximeraLatex + +# Add some needed/convenience setup +RUN ln -s /root/texmf/tex/latex/ximeraLatex/luaxake/luaxake /usr/local/bin/luaxake; \ + ln -s /root/texmf/tex/latex/ximeraLatex/xmScripts/xmlatex /usr/local/bin/xmlatex; \ + git config --global --add safe.directory /code + +WORKDIR /code \ No newline at end of file diff --git a/.ximera_local/docker/Dockerfile.full b/.ximera_local/docker/Dockerfile.full new file mode 100644 index 000000000..37e8b9f81 --- /dev/null +++ b/.ximera_local/docker/Dockerfile.full @@ -0,0 +1,31 @@ +FROM registry.gitlab.com/islandoftex/images/texlive:TL2024-2024-11-17-full +# FROM registry.gitlab.com/islandoftex/images/texlive:TL2024-2024-12-15-full + +RUN apt-get update -y; \ + apt-get install -y \ + curl \ + git git-restore-mtime \ + mupdf-tools pdf2svg imagemagick \ + gnupg \ + jq \ + dos2unix \ + vim wget strace less + +# RUN apt-get install -u hashcash + +# hashcash needed for setting up communication to ximeraServer; seems unfortunately not (easily?) installable with apt.. +ADD docker/hashcash_1.22-1_amd64.deb . +RUN dpkg -i hashcash_1.22-1_amd64.deb + +# Add this complete ximeraLatex repo (is overkill, but allows bugfixing/editing inside the docker image! +# It automatically adds the luaxake and xmlatex scripts +RUN mkdir -p /root/texmf/tex/latex +ADD . /root/texmf/tex/latex/ximeraLatex + +# Add some needed/convenience setup +RUN ln -s /root/texmf/tex/latex/ximeraLatex/luaxake/luaxake /usr/local/bin/luaxake; \ + ln -s /root/texmf/tex/latex/ximeraLatex/xmScripts/xmlatex /usr/local/bin/xmlatex; \ + git config --global --add safe.directory /code ; \ + apt-get update && apt-get install -y nano file + +WORKDIR /code \ No newline at end of file diff --git a/.ximera_local/docker/hashcash_1.22-1_amd64.deb b/.ximera_local/docker/hashcash_1.22-1_amd64.deb new file mode 100644 index 0000000000000000000000000000000000000000..fffc3d31fab3f9351fc320ef42bba0f7f54d5992 GIT binary patch literal 132804 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�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 +``` + + +Possible commands are `bake`, `frost`, `serve` and a number of variants/alternatives/extensions. + +One or more directories and/or TeX files can be given as argument. + + +## Options + +For a full list of options, see `luaxake -h` or the `luaxake` source code. Main options are + +- `-l`,`--loglevel` level : set level of messages printed to the terminal. Possible + values: `debug`, `info`, `status`, `warning`, `error`, `fatal`. Default value is `status`, + which prints warnings, errors and status messages. + +- `-s` : silent, synonym of `-l status` +- `-v` : verbose, synonym of `-l info` +- `-d` : debug, synonym of `-l debug` +- `-t` : debug, synonym of `-l trace` + +- `-f` : force, rebuild even when a file is considered uptodate, and/or git-push-force to serve +- `--check` : do not do the actual compilations. (But as of 2025-01, the POST-PROCESSING is done!) +- `--noclean` : by deafult, temporary files that are not needed for subsequent compilations are automatically deleted. This option keeps them available. + +- `--compile` target_list: overwrite the compilations to be done. targetlist can be a list of `pdf`, `draft.html`, `handout.pdf`. E.g `--compile pdf,handout.pdf,draft.html` generates three output files. + +- `--settings` -- Lua script that can change Luaxake configuration settings. + +Extra options for developers or advanced use cases: + +- `-c`,`--config` -- name of TeX4ht config file. It can be full path to the + config file, or just the name. If you pass just the filename, Luaxake will + search first in the directory with the current TeX file, to support different + config files for different projects, then in the current working directory, + project root and local TEXMF tree. + +NOTE 2025-01: there is currently some confusion with passing options from `xmlatex` to `lualatex`: +- general rule: options and commands that are not processed by `xmlatex` are passed to `luaxake` +- use the standard linux `--` trick to explicitly pass things to `luaxake`: xmlatex -- -t bake test.tex` +- suggest improvements via Issues/Pull requests. + + +# Lua settings (implementation to be documented/improved) + +You can set settings using a Lua script with the `-s` option. The script should +only set the configuration values. For example, to change the command for HTML +conversion, you can use the following settings file: + +```Lua +compilers.html.command = "make4ht -c @{config_file} @{filename} 'options'" +``` + +NOTE: this setup might still change, as it should be integrated with the (bash-syntax) config.txt of `xmlatex` ! + + +## Available configuration settings + +- `output_formats` -- list of extensions of output formats + +```Lua +output_formats = {"html", "pdf", "sagetex.sage"}, +``` + +OBSOLETE: this is calculated by `luaxake` now, based on the `compile_sequence` + +NOTE 2025-01: there is currently NO sage support + +- `compile_sequence` -- sequence of compilers to be called on each TeX file (cfr `--compile` option) + +```Lua +compile_sequence = {"pdf", "sagetex.sage", "pdf", "html"}, +``` + +- `clean` -- list of extensions to be removed after compilation + +```Lua +clean = { "aux", "4ct", "4tc", "oc", "md5", "dpth", "out", "jax", "idv", "lg", "tmp", "xref", "log", "auxlock", "dvi", "scmd", "sout" } +``` + +## Compilers + +- `compilers` -- settings for compiler commands. Each compiler contains table with additional settings. + +There are several available compilers, and more can be added in the settings: + +- `pdf` -- command used for the PDF generation +- `html` -- (OBSOLETE) command used for the HTML generation +- `draft.html` -- command used for the HTML generation +- `sagetex.sage` -- command used for the `sagetex.sage` generation (NOT YET IMPLEMENTED) + +```Lua +compilers = { + html = { + command = "make4ht -f html5+dvisvgm_hashes -c @{config_file} -sm draft @{filename}", + check_log = true, -- check log + status = 0 -- check that the latex command return 0 + }, +} +``` + +### Settings available in the `compiler` table: + +- `check_log` -- should we check the log file for errors? +- `check_file` -- check if the file exists before compilation. It is used by `sage`, which must be executed only if `filename.sagetex.sage` exists. +- `status` -- expected status code from the command. +- `process_html` -- run HTML post-processing. +- `command` -- template for the command to be executed. `@{variable}` tag will be replaced with the content of variable. + +### Variables available in command templates: + + - `dir` -- relative directory path of the file + - `absolute_dir` -- absolute directory path of the file + - `filename` -- filename of the file + - `basename` -- filename without extension + - `extension` -- file extension + - `relative_path` -- relative path of the file + - `absolute_path` -- absolute path of the file + - `exists` -- boolean, true if file exists + - `config_file` -- TeX4ht config file diff --git a/.ximera_local/luaxake/dkjson.lua b/.ximera_local/luaxake/dkjson.lua new file mode 100644 index 000000000..3bfbec2e7 --- /dev/null +++ b/.ximera_local/luaxake/dkjson.lua @@ -0,0 +1,752 @@ +-- Module options: +local always_use_lpeg = false +local register_global_module_table = false +local global_module_name = 'json' + +--[==[ + +David Kolf's JSON module for Lua 5.1 - 5.4 + +Version 2.8 + + +For the documentation see the corresponding readme.txt or visit +. + +You can contact the author by sending an e-mail to 'david' at the +domain 'dkolf.de'. + + +Copyright (C) 2010-2024 David Heiko Kolf + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--]==] + +-- global dependencies: +local pairs, type, tostring, tonumber, getmetatable, setmetatable = + pairs, type, tostring, tonumber, getmetatable, setmetatable +local error, require, pcall, select = error, require, pcall, select +local floor, huge = math.floor, math.huge +local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat = + string.rep, string.gsub, string.sub, string.byte, string.char, + string.find, string.len, string.format +local strmatch = string.match +local concat = table.concat + +local json = { version = "dkjson 2.8" } + +local jsonlpeg = {} + +if register_global_module_table then + if always_use_lpeg then + _G[global_module_name] = jsonlpeg + else + _G[global_module_name] = json + end +end + +local _ENV = nil -- blocking globals in Lua 5.2 and later + +pcall (function() + -- Enable access to blocked metatables. + -- Don't worry, this module doesn't change anything in them. + local debmeta = require "debug".getmetatable + if debmeta then getmetatable = debmeta end +end) + +json.null = setmetatable ({}, { + __tojson = function () return "null" end +}) + +local function isarray (tbl) + local max, n, arraylen = 0, 0, 0 + for k,v in pairs (tbl) do + if k == 'n' and type(v) == 'number' then + arraylen = v + if v > max then + max = v + end + else + if type(k) ~= 'number' or k < 1 or floor(k) ~= k then + return false + end + if k > max then + max = k + end + n = n + 1 + end + end + if max > 10 and max > arraylen and max > n * 2 then + return false -- don't create an array with too many holes + end + return true, max +end + +local escapecodes = { + ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", + ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t" +} + +local function escapeutf8 (uchar) + local value = escapecodes[uchar] + if value then + return value + end + local a, b, c, d = strbyte (uchar, 1, 4) + a, b, c, d = a or 0, b or 0, c or 0, d or 0 + if a <= 0x7f then + value = a + elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then + value = (a - 0xc0) * 0x40 + b - 0x80 + elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then + value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80 + elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then + value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80 + else + return "" + end + if value <= 0xffff then + return strformat ("\\u%.4x", value) + elseif value <= 0x10ffff then + -- encode as UTF-16 surrogate pair + value = value - 0x10000 + local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400) + return strformat ("\\u%.4x\\u%.4x", highsur, lowsur) + else + return "" + end +end + +local function fsub (str, pattern, repl) + -- gsub always builds a new string in a buffer, even when no match + -- exists. First using find should be more efficient when most strings + -- don't contain the pattern. + if strfind (str, pattern) then + return gsub (str, pattern, repl) + else + return str + end +end + +local function quotestring (value) + -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js + value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8) + if strfind (value, "[\194\216\220\225\226\239]") then + value = fsub (value, "\194[\128-\159\173]", escapeutf8) + value = fsub (value, "\216[\128-\132]", escapeutf8) + value = fsub (value, "\220\143", escapeutf8) + value = fsub (value, "\225\158[\180\181]", escapeutf8) + value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8) + value = fsub (value, "\226\129[\160-\175]", escapeutf8) + value = fsub (value, "\239\187\191", escapeutf8) + value = fsub (value, "\239\191[\176-\191]", escapeutf8) + end + return "\"" .. value .. "\"" +end +json.quotestring = quotestring + +local function replace(str, o, n) + local i, j = strfind (str, o, 1, true) + if i then + return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1) + else + return str + end +end + +-- locale independent num2str and str2num functions +local decpoint, numfilter + +local function updatedecpoint () + decpoint = strmatch(tostring(0.5), "([^05+])") + -- build a filter that can be used to remove group separators + numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+" +end + +updatedecpoint() + +local function num2str (num) + return replace(fsub(tostring(num), numfilter, ""), decpoint, ".") +end + +local function str2num (str) + local num = tonumber(replace(str, ".", decpoint)) + if not num then + updatedecpoint() + num = tonumber(replace(str, ".", decpoint)) + end + return num +end + +local function addnewline2 (level, buffer, buflen) + buffer[buflen+1] = "\n" + buffer[buflen+2] = strrep (" ", level) + buflen = buflen + 2 + return buflen +end + +function json.addnewline (state) + if state.indent then + state.bufferlen = addnewline2 (state.level or 0, + state.buffer, state.bufferlen or #(state.buffer)) + end +end + +local encode2 -- forward declaration + +local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state) + local kt = type (key) + if kt ~= 'string' and kt ~= 'number' then + return nil, "type '" .. kt .. "' is not supported as a key by JSON." + end + if prev then + buflen = buflen + 1 + buffer[buflen] = "," + end + if indent then + buflen = addnewline2 (level, buffer, buflen) + end + -- When Lua is compiled with LUA_NOCVTN2S this will fail when + -- numbers are mixed into the keys of the table. JSON keys are always + -- strings, so this would be an implicit conversion too and the failure + -- is intentional. + buffer[buflen+1] = quotestring (key) + buffer[buflen+2] = ":" + return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state) +end + +local function appendcustom(res, buffer, state) + local buflen = state.bufferlen + if type (res) == 'string' then + buflen = buflen + 1 + buffer[buflen] = res + end + return buflen +end + +local function exception(reason, value, state, buffer, buflen, defaultmessage) + defaultmessage = defaultmessage or reason + local handler = state.exception + if not handler then + return nil, defaultmessage + else + state.bufferlen = buflen + local ret, msg = handler (reason, value, state, defaultmessage) + if not ret then return nil, msg or defaultmessage end + return appendcustom(ret, buffer, state) + end +end + +function json.encodeexception(reason, value, state, defaultmessage) + return quotestring("<" .. defaultmessage .. ">") +end + +encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state) + local valtype = type (value) + local valmeta = getmetatable (value) + valmeta = type (valmeta) == 'table' and valmeta -- only tables + local valtojson = valmeta and valmeta.__tojson + if valtojson then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + state.bufferlen = buflen + local ret, msg = valtojson (value, state) + if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end + tables[value] = nil + buflen = appendcustom(ret, buffer, state) + elseif value == nil then + buflen = buflen + 1 + buffer[buflen] = "null" + elseif valtype == 'number' then + local s + if value ~= value or value >= huge or -value >= huge then + -- This is the behaviour of the original JSON implementation. + s = "null" + else + s = num2str (value) + end + buflen = buflen + 1 + buffer[buflen] = s + elseif valtype == 'boolean' then + buflen = buflen + 1 + buffer[buflen] = value and "true" or "false" + elseif valtype == 'string' then + buflen = buflen + 1 + buffer[buflen] = quotestring (value) + elseif valtype == 'table' then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + level = level + 1 + local isa, n = isarray (value) + if n == 0 and valmeta and valmeta.__jsontype == 'object' then + isa = false + end + local msg + if isa then -- JSON array + buflen = buflen + 1 + buffer[buflen] = "[" + for i = 1, n do + buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + if i < n then + buflen = buflen + 1 + buffer[buflen] = "," + end + end + buflen = buflen + 1 + buffer[buflen] = "]" + else -- JSON object + local prev = false + buflen = buflen + 1 + buffer[buflen] = "{" + local order = valmeta and valmeta.__jsonorder or globalorder + if order then + local used = {} + n = #order + for i = 1, n do + local k = order[i] + local v = value[k] + if v ~= nil then + used[k] = true + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + for k,v in pairs (value) do + if not used[k] then + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + else -- unordered + for k,v in pairs (value) do + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + if indent then + buflen = addnewline2 (level - 1, buffer, buflen) + end + buflen = buflen + 1 + buffer[buflen] = "}" + end + tables[value] = nil + else + return exception ('unsupported type', value, state, buffer, buflen, + "type '" .. valtype .. "' is not supported by JSON.") + end + return buflen +end + +function json.encode (value, state) + state = state or {} + local oldbuffer = state.buffer + local buffer = oldbuffer or {} + state.buffer = buffer + updatedecpoint() + local ret, msg = encode2 (value, state.indent, state.level or 0, + buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state) + if not ret then + error (msg, 2) + elseif oldbuffer == buffer then + state.bufferlen = ret + return true + else + state.bufferlen = nil + state.buffer = nil + return concat (buffer) + end +end + +local function loc (str, where) + local line, pos, linepos = 1, 1, 0 + while true do + pos = strfind (str, "\n", pos, true) + if pos and pos < where then + line = line + 1 + linepos = pos + pos = pos + 1 + else + break + end + end + return strformat ("line %d, column %d", line, where - linepos) +end + +local function unterminated (str, what, where) + return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where) +end + +local function scanwhite (str, pos) + while true do + pos = strfind (str, "%S", pos) + if not pos then return nil end + local sub2 = strsub (str, pos, pos + 1) + if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then + -- UTF-8 Byte Order Mark + pos = pos + 3 + elseif sub2 == "//" then + pos = strfind (str, "[\n\r]", pos + 2) + if not pos then return nil end + elseif sub2 == "/*" then + pos = strfind (str, "*/", pos + 2) + if not pos then return nil end + pos = pos + 2 + else + return pos + end + end +end + +local escapechars = { + ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f", + ["n"] = "\n", ["r"] = "\r", ["t"] = "\t" +} + +local function unichar (value) + if value < 0 then + return nil + elseif value <= 0x007f then + return strchar (value) + elseif value <= 0x07ff then + return strchar (0xc0 + floor(value/0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0xffff then + return strchar (0xe0 + floor(value/0x1000), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0x10ffff then + return strchar (0xf0 + floor(value/0x40000), + 0x80 + (floor(value/0x1000) % 0x40), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + else + return nil + end +end + +local function scanstring (str, pos) + local lastpos = pos + 1 + local buffer, n = {}, 0 + while true do + local nextpos = strfind (str, "[\"\\]", lastpos) + if not nextpos then + return unterminated (str, "string", pos) + end + if nextpos > lastpos then + n = n + 1 + buffer[n] = strsub (str, lastpos, nextpos - 1) + end + if strsub (str, nextpos, nextpos) == "\"" then + lastpos = nextpos + 1 + break + else + local escchar = strsub (str, nextpos + 1, nextpos + 1) + local value + if escchar == "u" then + value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16) + if value then + local value2 + if 0xD800 <= value and value <= 0xDBff then + -- we have the high surrogate of UTF-16. Check if there is a + -- low surrogate escaped nearby to combine them. + if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then + value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16) + if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then + value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000 + else + value2 = nil -- in case it was out of range for a low surrogate + end + end + end + value = value and unichar (value) + if value then + if value2 then + lastpos = nextpos + 12 + else + lastpos = nextpos + 6 + end + end + end + end + if not value then + value = escapechars[escchar] or escchar + lastpos = nextpos + 2 + end + n = n + 1 + buffer[n] = value + end + end + if n == 1 then + return buffer[1], lastpos + elseif n > 1 then + return concat (buffer), lastpos + else + return "", lastpos + end +end + +local scanvalue -- forward declaration + +local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta) + local tbl, n = {}, 0 + local pos = startpos + 1 + if what == 'object' then + setmetatable (tbl, objectmeta) + else + setmetatable (tbl, arraymeta) + end + while true do + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + local char = strsub (str, pos, pos) + if char == closechar then + return tbl, pos + 1 + end + local val1, err + val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + if char == ":" then + if val1 == nil then + return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")" + end + pos = scanwhite (str, pos + 1) + if not pos then return unterminated (str, what, startpos) end + local val2 + val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + tbl[val1] = val2 + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + else + n = n + 1 + tbl[n] = val1 + end + if char == "," then + pos = pos + 1 + end + end +end + +scanvalue = function (str, pos, nullval, objectmeta, arraymeta) + pos = pos or 1 + pos = scanwhite (str, pos) + if not pos then + return nil, strlen (str) + 1, "no valid JSON value (reached the end)" + end + local char = strsub (str, pos, pos) + if char == "{" then + return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta) + elseif char == "[" then + return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta) + elseif char == "\"" then + return scanstring (str, pos) + else + local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos) + if pstart then + local number = str2num (strsub (str, pstart, pend)) + if number then + return number, pend + 1 + end + end + pstart, pend = strfind (str, "^%a%w*", pos) + if pstart then + local name = strsub (str, pstart, pend) + if name == "true" then + return true, pend + 1 + elseif name == "false" then + return false, pend + 1 + elseif name == "null" then + return nullval, pend + 1 + end + end + return nil, pos, "no valid JSON value at " .. loc (str, pos) + end +end + +local function optionalmetatables(...) + if select("#", ...) > 0 then + return ... + else + return {__jsontype = 'object'}, {__jsontype = 'array'} + end +end + +function json.decode (str, pos, nullval, ...) + local objectmeta, arraymeta = optionalmetatables(...) + return scanvalue (str, pos, nullval, objectmeta, arraymeta) +end + +function json.use_lpeg () + local g = require ("lpeg") + + if type(g.version) == 'function' and g.version() == "0.11" then + error "due to a bug in LPeg 0.11, it cannot be used for JSON matching" + end + + local pegmatch = g.match + local P, S, R = g.P, g.S, g.R + + local function ErrorCall (str, pos, msg, state) + if not state.msg then + state.msg = msg .. " at " .. loc (str, pos) + state.pos = pos + end + return false + end + + local function Err (msg) + return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall) + end + + local function ErrorUnterminatedCall (str, pos, what, state) + return ErrorCall (str, pos - 1, "unterminated " .. what, state) + end + + local SingleLineComment = P"//" * (1 - S"\n\r")^0 + local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/" + local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0 + + local function ErrUnterminated (what) + return g.Cmt (g.Cc (what) * g.Carg (2), ErrorUnterminatedCall) + end + + local PlainChar = 1 - S"\"\\\n\r" + local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars + local HexDigit = R("09", "af", "AF") + local function UTF16Surrogate (match, pos, high, low) + high, low = tonumber (high, 16), tonumber (low, 16) + if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then + return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000) + else + return false + end + end + local function UTF16BMP (hex) + return unichar (tonumber (hex, 16)) + end + local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit)) + local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP + local Char = UnicodeEscape + EscapeSequence + PlainChar + local String = P"\"" * (g.Cs (Char ^ 0) * P"\"" + ErrUnterminated "string") + local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0)) + local Fractal = P"." * R"09"^0 + local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1 + local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num + local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1) + local SimpleValue = Number + String + Constant + local ArrayContent, ObjectContent + + -- The functions parsearray and parseobject parse only a single value/pair + -- at a time and store them directly to avoid hitting the LPeg limits. + local function parsearray (str, pos, nullval, state) + local obj, cont + local start = pos + local npos + local t, nt = {}, 0 + repeat + obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state) + if cont == 'end' then + return ErrorUnterminatedCall (str, start, "array", state) + end + pos = npos + if cont == 'cont' or cont == 'last' then + nt = nt + 1 + t[nt] = obj + end + until cont ~= 'cont' + return pos, setmetatable (t, state.arraymeta) + end + + local function parseobject (str, pos, nullval, state) + local obj, key, cont + local start = pos + local npos + local t = {} + repeat + key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state) + if cont == 'end' then + return ErrorUnterminatedCall (str, start, "object", state) + end + pos = npos + if cont == 'cont' or cont == 'last' then + t[key] = obj + end + until cont ~= 'cont' + return pos, setmetatable (t, state.objectmeta) + end + + local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) + local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) + local Value = Space * (Array + Object + SimpleValue) + local ExpectedValue = Value + Space * Err "value expected" + local ExpectedKey = String + Err "key expected" + local End = P(-1) * g.Cc'end' + local ErrInvalid = Err "invalid JSON" + ArrayContent = (Value * Space * (P"," * g.Cc'cont' + P"]" * g.Cc'last'+ End + ErrInvalid) + g.Cc(nil) * (P"]" * g.Cc'empty' + End + ErrInvalid)) * g.Cp() + local Pair = g.Cg (Space * ExpectedKey * Space * (P":" + Err "colon expected") * ExpectedValue) + ObjectContent = (g.Cc(nil) * g.Cc(nil) * P"}" * g.Cc'empty' + End + (Pair * Space * (P"," * g.Cc'cont' + P"}" * g.Cc'last' + End + ErrInvalid) + ErrInvalid)) * g.Cp() + local DecodeValue = ExpectedValue * g.Cp () + + jsonlpeg.version = json.version + jsonlpeg.encode = json.encode + jsonlpeg.null = json.null + jsonlpeg.quotestring = json.quotestring + jsonlpeg.addnewline = json.addnewline + jsonlpeg.encodeexception = json.encodeexception + jsonlpeg.using_lpeg = true + + function jsonlpeg.decode (str, pos, nullval, ...) + local state = {} + state.objectmeta, state.arraymeta = optionalmetatables(...) + local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state) + if state.msg then + return nil, state.pos, state.msg + else + return obj, retpos + end + end + + -- cache result of this function: + json.use_lpeg = function () return jsonlpeg end + jsonlpeg.use_lpeg = json.use_lpeg + + return jsonlpeg +end + +if always_use_lpeg then + return json.use_lpeg() +end + +return json + diff --git a/.ximera_local/luaxake/luaxake b/.ximera_local/luaxake/luaxake new file mode 100755 index 000000000..f41717291 --- /dev/null +++ b/.ximera_local/luaxake/luaxake @@ -0,0 +1,936 @@ +#!/usr/bin/env texlua +kpse.set_program_name "luatex" + +local pl = require "penlight" +local utils = require "pl.utils" +local tablex = require("pl.tablex") +local path = pl.path +local lapp = require "pl.lapp" +-- local lapp = require "lapp-mk4" -- the above is 'better'? + +local ffi = require "ffi" + +logging = require("luaxake-logging") +-- better make a logfile per day ... ? +-- NOTE: after a chdir (as in compile...), the logfile would change without the abspath!! +logging.set_outfile(path.abspath("luaxake.log")) + +local log = logging.new("luaxake") +local version = "{{version}}" + +local files = require "luaxake-files" +local compile = require "luaxake-compile" +local frost = require "luaxake-frost" +local html = require "luaxake-transform-html" + + +-- TODO: fix syntax with [command dirs]: should be 1 obligatory, then many optional args +local option_text = [[ +Luaxake: build system for Ximera documents +Usage: +$ luaxake [command dirs] + +Options: +-h,--help Print help message +-l,--loglevel Set log level: trace, debug, info, status, warning, error, fatal +-s,--silent Set log level to 'status' +-v,--verbose Set log level to 'info' +-d,--debug Set log level to 'debug' +-t,--trace Set log level to 'trace' +--version Version info +-f,--force Recompile anyway +--check Only check, no actual compiling/cleaning +--noclean Keep all temp files +--nodependencies Do not (re-)compile potential dependencies +--compile (default none) Compile sequence (default 'pdf,html', or as set in settings) +--settingsfile (default none) Luaxake settings script +--configfile (default none) TeX4ht config file +-j,--jobs (optional number) Number of compile jobs to run in parallel + +Possible commands: + bake + name -- NOT (YET) IMPLEMENTED HERE: see xmlatex !!! + frost + serve + clean / veryclean -- to be changed/improved + info -- not yet very useful.. + ]] + +-- REMOVED: (table) Document root directory +--- @class args +--- @field config string TeX4ht config file +--- @field help boolean Print help message +--- @field settings string Luaxake settings script +--- @field loglevel string Logging level +--- @field version boolean Print version +--- @field command string Command to execute +--- @field dir table Document root directory +local args = lapp(option_text) + +if args.version then + print("Luaxake version: " .. (version == "{{version}}" and "devel" or version)) + os.exit() +end + +-- Highest level of -v, -d, -t wins, except that -l would overwrite it +logging.set_level("info") -- default +if args.silent then logging.set_level("status") end +if args.verbose then logging.set_level("info") end +if args.debug then logging.set_level("debug") end +if args.trace then logging.set_level("trace") end + +if args.loglevel then logging.set_level(args.loglevel) end + + +GLOB_root_dir = path.abspath(".") -- TODO: find git folder ...? +-- log:tracef("Setting GLOB_root_dir=%s",GLOB_root_dir) + + +-- +-- FOR REFERENCE: (and to be checked ...?) +-- + +--- @class DOM_Object +--- Dummy type declaration for LuaXML DOM object, to prevent error messages from language server +--- @field query_selector function get all elements that match a given CSS selector +--- @field get_children function +--- @field get_text function +--- @field get_attribute function +--- @field remove_node function + +--- @class compiler +--- @field command string command template +--- @field check_log? boolean should we check the log file for errors? +--- @field check_file? boolean execute command only if the output file exists +--- @field status? number expected status code from the command +--- @field process_html? boolean run HTML post-processing + +--- @class config +--- @field compile_sequence [string] sequence of compiler names to be executed +--- @field output_formats [string] list of output format extensions, calculated from compile_sequence +--- @field compilers {string: compiler} table with available 'compilers' +--- @field clean [string] list of extensions of temp files to be removed after the compilation +--- @field configfile string TeX4ht config file +--- @field documentclass_lines number on how many lines in TeX files we should try to look for \documentclass + + +-- a GLOBAL config class +-- The values here are defaults, that can be overwritten with +-- - values in a settingsfile (the file itself can be given with --settingsfile or as environment variable) +-- - environment variables (see infra) +-- - optional arguments (--clean, etc) +-- - explicit constructs infra (ef default_dependencies, ...) +-- General rule: +-- an explicitly give argument always wins, over +-- an environment variables, that wins over +-- an setting in the settings file, that wins over +-- a default value provided here. + +config = { + --- will be set infra --- compile_sequence = {"pdf", "draft.html"}, + -- compile_sequence = {"pdf", "make4ht.html", "handout.pdf"}, + -- compile_sequence = {"pdf", "sagetex.sage", "pdf", "html"}, + -- see infra -- default_dependencies = { "xmPreamble.tex" }, -- add here e.g. xmPreamble, ximera.cls, ... + compilers = { + pdf = { + -- this doesn't work well + -- command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape "\\PassOptionsToClass{tikzexport}{ximera}\\PassOptionsToClass{xake}{ximera}\\PassOptionsToClass{xake}{xourse}\\nonstopmode\\input{@{filename}}"', + -- command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape "\\PassOptionsToClass{xake}{ximera}\\PassOptionsToClass{xake}{xourse}\\input{@{filename}}"', + command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape "\\input{@{filename}}"', -- mmm, this increases the .jax file !!! + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + infix = "" , -- used for .handout, and .make4k4 + extension = "pdf", -- not used ???? + post_command = 'post_process_pdf', + download_folder = 'ximera-downloads/with-answers', + }, + ["handout.pdf"] = { + command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape -jobname @{basename}.handout "\\PassOptionsToClass{handout}{ximera}\\PassOptionsToClass{handout}{xourse}\\input{@{filename}}"', + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + extension = "handout.pdf", + infix = "handout" , + post_command = 'post_process_pdf', + download_folder = 'ximera-downloads/handouts', + }, + -- 20241217: no longet use "html", but eg draft.html (this keeps logfiles etc from being overwritten ...)! + ["draft.html"] = { + command = "make4ht -l -c @{configfile} -f html5+dvisvgm_hashes -m draft -j @{basename}.draft -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'post_process_html', + extension = "html", + infix = "draft" , + }, + -- alternatives, use at your own risk + ["make4ht.html"] = { + command = "make4ht -l -c @{configfile} -f html5+dvisvgm_hashes -j @{basename}.make4ht -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'post_process_html', + extension = "html", + infix = "make4ht" , + }, + -- test: use 'tikz+' option (FAILS for some tikzpictures, eg with shading/patterns) + ["tikz.html"] = { + command = "make4ht -l -c @{configfile} -f html5+dvisvgm_hashes -m draft -j @{basename}.draft -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'post_process_html', + extension = "html", + infix = "draft" , + }, + ["test.html"] = { + -- command = "make4ht -f html5+dvisvgm_hashes -c @{configfile} -sm draft @{filename}", + -- command = "make4ht -c @{configfile} -f html5+dvisvgm_hashes -s @{make4ht_mode} -a debug @{filename} 'svg,htex4ht,mathjax,-css,info,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + command = "make4ht -l -c @{configfile} -f html5+dvisvgm_hashes -j @{basename}.make4ht -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css,info,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'post_process_html', + extension = "html", + infix = "make4ht" , + }, + + -- sage not tested/implemented !!!! + ["sagetex.sage"] = { + command = "sage @{output_file}", + check_log = true, -- check log + check_file = true, -- check if the sagetex.sage file exists + status = 0, -- check that the latex command return 0 + extension = "sage", -- ? + }, + -- a dummy test: create .ddd files that contain the date .. + ddd = { + command = 'date >@{basename}.ddd', + status = 0, -- check that the command returns 0 + }, + }, + -- TeX macro's to use for dependency-checking in .tex files + input_commands = { + input=true, + include=true, + includeonly=true, + activity=true, + practice=true, + activitychapter=true, + activitysection=true, + practicechapter=true, + practicesection=true, + }, + -- extensions to be kept by get_files (and thus for which fileinfo is collected) + keep_extensions = { + tex = true, + html = true, + sty = true, + }, + -- list of 'infixes' to be cleaned by default + clean_infixes = { + "", + ".make4ht", + ".draft", + ".handout", + }, + -- automatically clean files immediately after each compilation + -- the commented extensions might cause issues when automatically cleaned, as they may be needed for the next compilation + clean_extensions = { + -- "aux", + "4ct", + "4tc", + "oc", + "md5", + "dpth", + "out", + -- "jax", + "idv", + "lg", + "tmp", + -- "xref", + -- "log", + "auxlock", + "dvi", + "scmd", + "sout", + "ids", + "mw", + "cb", + "cb2", + }, + documentclass_lines = 30, + -- for debugging: dumps the 'fileinfo' of matching files + -- make4ht_loglevel = "", + make4ht_extraoptions= "", + -- number of lines in tex files where we should look for \documentclass + -- dump_fileinfo = "aFirstXourse.tex", + jobs = 2, +} + + +-- first real argument is the command (bake/frost/serve/...) +local command = table.remove(args, 1) + +if not command then + log:error("Usage: script [command] ") + os.exit(1) +end + +log:debug("command: "..command) + +-- all further arguments are considered dirs/files to be processed +local dirs = {} + +for i, value in ipairs(args) do + log:trace("Args: adding file/dir "..value) + table.insert(dirs, value) +end + +if #dirs == 0 then + dirlist = os.getenv("XM_TO_PROCESS") or "." + log:debugf("No files/directories given as argument, using default: %s", dirlist) + dirs = utils.split(dirlist,',') +end + +-- Store all to_be_processed files/folders +config.dirs = dirs + + +if args.settingsfile ~= "none" then + config.settingsfile = args.settingsfile -- explicitly give as option +else + config.settingsfile = os.getenv("XM_SETTINGSFILE") or "none" +end + +-- Set/overwrite config +if config.settingsfile ~= "none" then + -- config file can be a Lua script, which should only set properties for the config table + local configlib = require "luaxake-config" + log:info("Using settings file: " .. config.settingsfile) + configlib.update_config(config.settingsfile, config) +end + +config.check = args.check or os.getenv("XM_ONLY_CHECK") or config.check +config.noclean = args.noclean or os.getenv("XM_NOCLEAN") or config.noclean +config.nodependencies = args.nodependencies or os.getenv("XM_NODEPENDENCIES") or config.nodependencies +config.force = args.force or os.getenv("XM_FORCE") or config.force +config.jobs = args.jobs or os.getenv("XM_JOBS") or config.jobs + +if args.configfile ~= "none" then + config.configfile = args.configfile +else + config.configfile = os.getenv("XM_CONFIGFILE") or "ximera.cfg" +end + +if config.check then log:info("Running with (only) 'check'") end +if config.noclean then log:info("Running with 'noclean'") end +if config.nodependencies then log:info("Running with 'nodependencies'") end +if config.force then log:info("Running with 'force' (compile/serve)") end + +if config.configfile ~= "ximera.cfg" then + log:warning("Using non-default config file " .. config.configfile) +end + + +-- set/add potential default dependencies +if not config.default_dependencies and path.exists("xmPreamble.tex") then + log:info("Adding default dependency xmPreamble.tex") + config.default_dependencies = { "xmPreamble.tex" } +else + config.default_dependencies = {} +end + +-- Function to get extensions for array of compilers +local function get_extensions_for_compilers(tbl, kys) + local extensions = {} + for _, key in ipairs(kys) do + if tbl[key] and tbl[key].extension then + table.insert(extensions, tbl[key].extension) + end + end + return extensions +end +-- Function to get infixes for array of compilers +local function get_infixes_for_compilers(tbl, kys) + local infixes = {} + for _, key in ipairs(kys) do + if tbl[key] and tbl[key].infix then + if tbl[key].infix == "" then + table.insert(infixes, tbl[key].infix) + else -- prepend the . !!! + table.insert(infixes, "."..tbl[key].infix) + end + end + end + return infixes +end + + +-- shope adhoc 'shortcuts', mainly because used in .vscode/tasks.json ... +if command == "compilePdf" then + log:info("Compile only PDF") + config.compile_sequence = { "pdf" } + config.output_formats = get_extensions_for_compilers(config.compilers, config.compile_sequence) +end +if command == "compile" then + log:info("Compile only HTML") + config.compile_sequence = { "draft.html" } + config.output_formats = get_extensions_for_compilers(config.compilers, config.compile_sequence) +end + + +if args.compile ~= "none" then + config.compile_sequence = utils.split(args.compile,',') +else + local compileseq = os.getenv("XM_COMPILE_SEQUENCE") or "pdf,draft.html" + config.compile_sequence = utils.split(compileseq,',') + +end + +for _, compiler in ipairs(config.compile_sequence) do + if not config.compilers[compiler] then + log:fatalf("Unknown compiler %s (should be in %s)", compiler, table.concat(tablex.keys(config.compilers),', ')) + os.exit(1) + end +end + + +config.clean_infixes = get_infixes_for_compilers(config.compilers, config.compile_sequence) +config.output_formats = get_extensions_for_compilers(config.compilers, config.compile_sequence) +log:infof("Set compile_sequence=%s (and output_formats=%s)", table.concat(config.compile_sequence,','), table.concat(config.output_formats,',')) + +-- +-- Prepare processing: collect all metadata / to_be_compiled etc +-- +local tex_fileinfos = {} + +for i,nextarg in ipairs(config.dirs) do + log:infof("Processing argument %d: %s", i,nextarg) + nextarg = nextarg:gsub("^.//*", "") -- remove potential leading './' -- vscode-settings sometimes even add .//myfile.tex; it also might confuse skipping hidden .xxx files/folders + + --if path.isdir(nextarg) or path.isfile(nextarg) then + + if not path.isdir(nextarg) then + local ext = nextarg:match(("%.([^%.]+)$")) + if ext ~= "tex" and path.isfile(nextarg..".tex") then + log:trace("Adding .tex to " .. nextarg) + nextarg = nextarg..".tex" + elseif ext ~= "tex" then + log:warningf("Argument %s not folder nor tex-file (%s): SKIPPING", nextarg, ext) + goto nextarg -- continue ... + end + end + + -- some files might need compilation even if they are not in the args, e.g. _pdf.tex files, or other dependent .tex files + local all_involved_texfiles = files.get_tex_files_with_status(nextarg, config.output_formats, config.compile_sequence) + + if path.isfile(nextarg) then + if config.force then + log:infof("Explicitly force (re-) compilation of %s", nextarg) + all_involved_texfiles[nextarg].needs_compilation = true + end + + -- Function to filter key-value tables + local function filter_needs_compilation(tbl) + local filtered = {} + for key, entry in pairs(tbl) do + if entry.needs_compilation then filtered[key] = entry end + end + return filtered + end + + local extra_need_compilation = filter_needs_compilation(all_involved_texfiles) + extra_need_compilation[nextarg] = nil + + log:debugf("%d DEPENDENT file(s) need compilation: %s", tablex.size(extra_need_compilation), table.concat(tablex.keys(extra_need_compilation),', ')) + + if config.nodependencies and pl.tablex.size(extra_need_compilation) > 0 then + -- log:debugf("Compiling %s, without the %d dependencies", nextarg, pl.tablex.size(all_involved_texfiles) - 1) + log:infof("Compiling %s, without dependencies %s", nextarg, table.concat(tablex.keys(extra_need_compilation),', ')) + tex_fileinfos[nextarg] = all_involved_texfiles[nextarg] + else + for fname, finfo in pairs(all_involved_texfiles) do + log:tracef("File %s: collecting %-30s into tex_fileinfos", nextarg, fname) + tex_fileinfos[fname] = finfo + end + end + else -- nextarg is a folder + for fname, finfo in pairs(all_involved_texfiles) do + log:tracef("Dir %s: collecting %-30s into tex_fileinfos", nextarg, fname) + tex_fileinfos[fname] = finfo + end + end + + ::nextarg:: +end + +-- add extra files we depend on to the potentially to-be-compiled tex_files +local tex_files = tablex.values(tex_fileinfos) + + +log:debugf("Finding and sorting dependencies for %d tex_files", #tex_files) +local to_be_compiled = files.sort_dependencies(tex_files) + +function filter_main_tex_files(tbl) + local result = {} + for _, entry in pairs(tbl) do + if not entry.tex_documentclass then + log:debugf("Removing non-document file %s", entry.relative_path) + else + table.insert(result, entry) + end + end + return result +end + +to_be_compiled = filter_main_tex_files(to_be_compiled) +if #to_be_compiled == 1 then + log:statusf("%d file needs compiling: %s", #to_be_compiled, to_be_compiled[1].relative_path) +elseif #to_be_compiled > 0 then + log:statusf("%d files need compiling", #to_be_compiled) + for i,file in ipairs(to_be_compiled) do + log:infof("%3d %s",i,file.relative_path) + end +else + log:status("No files need compiling") +end + + -- + -- Start processing further commands + -- + + if command == "info" then + log:infof("Got %d tex files:", #tex_files) + for _,file in ipairs(tex_files) do + print(file.relative_path) + --print(file.absolute_path) + end + end + + if command == "clean" or command == "veryclean" then + local n_files_deleted = 0 + local to_be_cleaned_extensions = tablex.copy(config.clean_extensions) + local to_be_cleaned_infixes = tablex.copy(config.clean_infixes) + + -- if args.compile ~= "none" then -- if explicitly given compilers, only clean those infixes + -- -- to_be_cleaned_infixes = utils.split(args.compile,',') + -- config.clean_infixes = get_infixes_for_keys(config.compilers, config.compile_sequence) + -- log:info("Infixes to be cleaned: " .. args.compile) + -- end + + if command == "veryclean" then + -- log:debug("Appending extra extensions") + tablex.insertvalues(to_be_cleaned_extensions, config.output_formats) -- append arrays ... + tablex.insertvalues(to_be_cleaned_extensions, { "aux", "toc", "log", "dlog", "xref", "jax", "xmjax", "sagetex.sage", "html.failed", "pdf.failed", "svg" }) -- append arrays ... + log:debugf("Appended extra extensions %s", table.concat(to_be_cleaned_extensions, ', ')) + end + + log:statusf("Cleaning %s files with extensions %s", table.concat(to_be_cleaned_infixes, ', '),table.concat(to_be_cleaned_extensions, ', ')) + + for _,file in ipairs(tex_files) do + log:tracef("Removing temp files for %s", file.relative_path) + n_files_deleted = n_files_deleted + compile.clean(file, to_be_cleaned_extensions, to_be_cleaned_infixes, config.check) + end + + -- require 'pl.pretty'.dump(config) + -- require 'pl.pretty'.dump(to_be_cleaned_extensions) + + log:infof("Cleaned %d files", n_files_deleted) + end + +-- POSIX system calls we need for non-blocking popen +-- Note this makes us POSIX dependent, but we could add Windows system calls to support Windows if we ever needed +ffi.cdef([[ + void* popen(const char* cmd, const char* mode); + int pclose(void* stream); + int fileno(void* stream); + int fcntl(int fd, int cmd, int arg); + int *__errno_location (); + ssize_t read(int fd, void* buf, size_t count); +]]) + +-- I think these are technically system dependent +local F_SETFL = 4 +local O_NONBLOCK = 2048 +local EAGAIN = 11 + +function do_command_start(cmd) + + local file=cmd.file + local command=cmd.command + local folder=file.absolute_dir + + log:tracef("Starting process in %s with command %s " , folder, command) + + local process = {} + process.cmd = cmd + process.file_name = file.absolute_path + + process.start_time =socket.gettime() + + if cmd.only_check then + log:info("Running in check-modus: SKIPPING " .. command ) + command = "echo SKIPPED " .. command + end + + log:statusf("Command %3d started for %s", cmd.job_nr, cmd.id ) + + + -- Start process with "command" + process.handle = ffi.C.popen("cd "..folder.."; ".. command, "r") + if process.handle == nil then + log:warningf("ffi.popen returns %s", popen) + + local err_code = _errno[0] + return tonumber(err_code), "Error trying to popen command: " .. tostring(command) .. "Error code: " .. tostring(err_code) + end + + -- Get file descriptor of pipe + process.fd = ffi.C.fileno(process.handle) + log:tracef("ffi.fileno returns %s", process.fd) + + if process.fd == -1 then + local err_code = _errno[0] + local err_msg = "Failed to get file descriptor for command: " .. tostring(command) .. ". Error code: " .. tostring(err_code) + + local status_code = ffi.C.plcose(process.handle) + + if status_code -1 then + err_code = _errno[0] + err_msg = err_msg .. ". Failed to pclose process. Error code: " .. tostring(err_code) + end + + return tonumber(err_code), err_msg + end + + -- Set non-blocking mode for pipe + local status_code = ffi.C.fcntl(process.fd, F_SETFL, O_NONBLOCK) + if status_code ~= 0 then + err_code = _errno(0) + log:info("failed fcntl. status_code: " .. tostring(status_code) .. ". error: " .. tostring(_errno[0])) + return tonumber(err_code), "Failed to set non-blocking reads for pipe to command: " .. tostring(command) .. ". Error code: " .. tostring(err_code) + end + + return 0, process +end + + + +function bake(to_be_compiled, n_jobs) + local commands_to_run = {} + local commands_that_ran = {} + + + log:tracef("Collecting all needed compile commands for %s to be compiled files", #to_be_compiled) + for i, file in ipairs(to_be_compiled) do + + local extra_run_commands = compile.get_commands_to_run(file, config.compilers, config.compile_sequence, config.check) + tablex.insertvalues(commands_to_run, extra_run_commands) + + log:infof("Added %d compile commands for file %3d/%d: %s", #extra_run_commands, i, #to_be_compiled, file.relative_path) + end + + local job_total = #commands_to_run + log:statusf("There are %d commands to run for %d files", job_total, #to_be_compiled) + + + local _errno = ffi.C.__errno_location() -- Get pointer to errno location + local buffer_size = 2025 + local read_buffer = ffi.new("char[?]", buffer_size) + + local current_processes = {} + local current_workers = 1 + local job_nr = 1 + + local total_start_time = socket.gettime() + + log:tracef("Starting up to %d processes", n_jobs) + while current_workers <= n_jobs do + -- Take first command (and remove it from the list of commands_to_run) + local cmd = table.remove(commands_to_run,1) + + if not cmd then + log:tracef("No more commands to compile, not all %d processes were needed", n_jobs) + break + end + + -- Give this command a 'job_nr' for logging/follow-up + cmd.job_nr = job_nr + job_nr = job_nr + 1 + + log:debugf("Starting process %d for command %s", current_workers, cmd.command) + local ret, process = do_command_start(cmd) + + if ret > 0 then + return ret,process + else + log:tracef("Added process %d to current_processes",current_workers) + table.insert(current_processes, process) + current_workers = current_workers + 1 + end + end + -- (at most) n_jobs processes have been started; now start collecting results, and restart processes as long as needed + + log:tracef("Starting main processing loop (for %d processes)", #current_processes) + while #current_processes > 0 do + local j = 1 + while j <= #current_processes do + local process = current_processes[j] + log:tracef("Checking process %d of %s (fd=%d)", j , #current_processes, process.fd) + + -- log:tracef("Read up to 2024 bytes from fd %d",process.fd) + local bytes_read = ffi.C.read(process.fd, read_buffer, 2024) -- Read 2024 bytes at a time (We currently don't do anything with what we read) + -- log:tracef("ffi.read returns %s (%s)", bytes_read, read_buffer) + + if (bytes_read == -1) and (_errno[0] ~= EAGAIN) then -- There was some unexpected error + log:errorf("Reading 2024 bytes from fd %d returns %s",process.fd, _errno[0]) + -- TODO: We might be able to handle some of these errors instead of just failing + return tonumber(_errno[0]), "Failed to read pipe for compilation process for file: " .. tostring(process.file_name) .. ". Error code: " .. tostring(_errno[0]) + + elseif bytes_read == -1 then + j = j+1 -- TODO: check ...! + elseif bytes_read > 0 then + local read_string = string.sub(string.gsub(ffi.string(read_buffer),"[\r\n]",""), 1, math.min(bytes_read,30)+1) + log:tracef("Read from fd %d returns %s: %s...",process.fd, bytes_read, read_string) + j = j+1 + else -- bytes_read == 0 then -- End of file + log:tracef("Zero bytes read from fd %d",process.fd) + + local ret_code = ffi.C.pclose(process.handle) + + + if ret_code == -1 then -- TODO: Do we have to do anything more in this case? + log:error("Failed to get return code for compilation process for file: " .. tostring(process.file_name)) + elseif ret_code == 0 then + log:tracef("Got returncode %s for file %s", ret_code, process.file_name) + else + log:warningf("Got returncode %s for file %s", ret_code, process.file_name) + -- ret_code = ret_code / 256 -- Discard last 8 bits + end + + process.cmd.status_command = ret_code + + -- TODO: add error handling based off of this return code + log:trace("Return code for compilation process for file: " .. tostring(process.file_name) .. " is ret_code: " .. tostring(ret_code)) + + local compilation_time = socket.gettime() - process.start_time + + process.cmd.compilation_time = compilation_time + + + local cmd = compile.do_command_handle(process.cmd) + + log:statusf("Command %3d/%d returns %s (process %d, %.1f seconds) for %s of %s", process.cmd.job_nr, job_total, process.cmd.status, j, process.cmd.compilation_time, process.cmd.extension, process.cmd.file.relative_path) + + table.insert(commands_that_ran, cmd) + + -- Now start the next command_to_run + local next_cmd + if cmd.status_post_command == "RETRY_COMPILATION" then + next_cmd = cmd -- RETRY !! + next_cmd.this_is_a_retry = true + log:infof("Restarting %s", next_cmd.id) + else + -- Start the next compile, if there are any left + log:tracef("Selecting next command to run") + next_cmd = table.remove(commands_to_run,1) + + -- -- TODO : check if dependencies are successfully compiled + -- -- (unless you don't want to ...) + -- if not config.nodependencies then + -- for fname, ffile in pairs(next_cmd.file.depends_on_files) do + -- if ffile.needs_compilation then + -- log:errorf("SKIPPING %s: dependent file %s not (yet) compiled.", next_cmd.file.relative_path, fname) + -- goto uptonextcompilation + -- end + -- end + -- end + end + + if next_cmd then + log:debugf("Starting worker %d for %s", j, next_cmd.command) + + next_cmd.job_nr = job_nr + job_nr = job_nr + 1 + local ret, process = do_command_start(next_cmd) + + if ret > 0 then + log:errorf("Problem starting current_processes %d for %s: %s", j, next_cmd.id, process) + return ret,process + else + log:debugf("Updating current_processes %d for %s", j, next_cmd.id) + current_processes[j] = process + end + + + j = j+1 + else + log:debugf("No more work, removing worker %d", j) + table.remove(current_processes, j) -- THIS SHIFTS THE REMAINING ELEMENTS, so no increase of j !!! + end + end + end + local sleep_time = 0.25 + log:tracef("sleep %f", sleep_time) + socket.sleep(sleep_time) + end + + + -- if config.noclean then + -- log:debugf("Skipping cleaning temp files") + -- else + -- compile.clean(file, config.clean_extensions,config.clean_infixes) + -- end + local total_end_time = socket.gettime() + + log:statusf("Finished compiling %d files in %.1f seconds", #to_be_compiled, total_end_time - total_start_time) + + + -- collect and print all errors + local failed_commands = {} + for _, cmd in ipairs(commands_that_ran) do + log:trace("File "..(cmd.output_file or "UNKNOWN??") .." got status " .. (cmd.status or 'NIL??') ) + + if cmd.status ~= "OK" then + failed_commands[cmd.id] = #(cmd.errors) + log:debugf("Found %d errors: %s", #(cmd.errors), cmd.errors) + + for _, err in ipairs(cmd.errors or {}) do + -- log:errorf("[%10s] %s:%s %s [%s]", compile_info.compiler, compile_info.source_file, err.line, err.context,err.error) + log:errorf("[%-10s] %s:%s", cmd.extension, cmd.file.relative_path, err.constructed_errormessage) + + end + if cmd.post_processing_error then + log:errorf("[%-10s] %s: %s", "post_command", cmd.file.relative_path, cmd.post_processing_error) + local _, n_errors = cmd.post_processing_error:gsub("\n", "") -- HACK: number of lines equals number of errors ... + failed_commands[cmd.id] = ( failed_commands[cmd.id] or 0 ) + n_errors + 1 + end + end + end + -- + -- all compilations done; process/summarize errors + -- + if tablex.size(failed_commands) == 0 then + -- log:statusf("Baked %d files, no errors found", #to_be_compiled) + return 0, string.format("Baked %d files, no errors found", #to_be_compiled) + else + log:warningf("Baked %d files, but %d compilation%s failed", #to_be_compiled, tablex.size(failed_commands), tablex.size(failed_commands) == 1 and "" or "s") + + for filename, errs in pairs(failed_commands) do + log:debugf("Found %2d errors in %s", errs, filename) + end + -- log:statusf("Baking resulted in %d errors", tablex.size(failed_files) ) + return 1, string.format("Baking resulted in %d errors", tablex.size(failed_commands) ) + end +end + + -- TODO: fix commands (cfr xmlatex/xmlatex/lua) +if command == "bake" or command == "compilePdf" or command == "compile" then + log:status("Start "..command) + + local ret, msg + if #to_be_compiled == 0 then + ret, msg = 0, "Nothing to be baked" + else + ret, msg = bake(to_be_compiled, config.jobs) + end -- end baking + + if ret > 0 then + log:error("Bake failed: " .. msg) + os.exit(ret) + else + log:statusf("Bake succeeded: " .. msg) + end +end + +if command == "frost" then + log:status("Start " .. command) + local ret, msg = frost.frost(tex_files, to_be_compiled) + if ret > 0 then + log:error("Frost failed: " .. msg) + os.exit(ret) + else + log:statusf("Frost succeeded: " .. msg) + end +end + +if command == "serve" then + log:status("Start " .. command) + local ret, msg = frost.serve(config.force) + if ret > 0 then + log:error("Serve failed: " .. msg) + os.exit(ret) + else + log:statusf("Serve succeeded: " .. msg) + end +end + +if command == "ghaction" then + + local ret, msg + if #to_be_compiled == 0 then + log:status("Nothing to be baked") + msg={} + else + log:status("Start bake") + ret, msg = bake(to_be_compiled, config.jobs) + + if ret > 0 then + log:error("Bake failed: " .. msg) + log:warning("But, I'll continue, and hope for the best") + -- os.exit(ret) + else + log:statusf("Bake succeeded: " .. msg) + end + end + + -- Hack: update to_be_compiled for usage in frost (uncompiled files might/should genetate a warning or error ...) + local still_to_be_compiled = {} + for _, lfile in ipairs(to_be_compiled) do + if lfile.needs_compilation then + log:infof("Mmm, file %s still needs compilation.", lfile.relative_path) + table.insert(still_to_be_compiled, lfile) + end + end + + if #still_to_be_compiled > 0 then + log:infof("Still %d files to be compiled after bake.", #still_to_be_compiled) + end + to_be_compiled = still_to_be_compiled + -- end-of-hack to_be_compiled + + log:status("Start frost") + local ret, msg = frost.frost(tex_files, to_be_compiled) + if ret > 0 then + log:error("Frost failed: " .. msg) + os.exit(ret) + else + log:statusf("Frost succeeded: " .. msg) + end + log:status("Start serve") + local ret, msg = frost.serve(true) -- WITH FORCE (until push/depth issue fixed) + if ret > 0 then + log:error("Serve failed: " .. msg) + os.exit(ret) + else + log:statusf("Serve succeeded: " .. msg) + end +end + + +if command == "extrainfo" then + all_labels = {} + -- TEST: to be implemented ...? + for i, file in ipairs(tex_files) do + log:infof("Info for %s (%s)",file.filename,file.relative_path) + + local html_file = files.get_fileinfo(file.relative_path:gsub("tex$", "html")) + local ret, msg = html.update_html_fileinfo(html_file) + + for k,v in pairs(html_file.labels or {}) do + log:infof("Adding %s to %s",v,k) + all_labels[k] = ( all_labels[k] or 0) + v + end + end + if all_labels ~= {} then + log:infof("Potentially duplicate labels:") + require 'pl.pretty'.dump(all_labels) + end +end \ No newline at end of file diff --git a/.ximera_local/luaxake/luaxake-NT b/.ximera_local/luaxake/luaxake-NT new file mode 100644 index 000000000..ac6bb077d --- /dev/null +++ b/.ximera_local/luaxake/luaxake-NT @@ -0,0 +1,820 @@ +#!/usr/bin/env texlua +kpse.set_program_name "luatex" + +local pl = require "penlight" +local utils = require "pl.utils" +local tablex = require("pl.tablex") +local path = pl.path +local lapp = require "pl.lapp" +local ffi = require "ffi" +-- local lapp = require "lapp-mk4" -- the above is 'better'? + +logging = require("luaxake-logging") +-- better make a logfile per day ... ? +-- NOTE: after a chdir (as in compile...), the logfile would change without the abspath!! +logging.set_outfile(path.abspath("luaxake.log")) + +local log = logging.new("luaxake") +local version = "{{version}}" + +local files = require "luaxake-files" +local compile = require "luaxake-compile" +local frost = require "luaxake-frost" +local html = require "luaxake-transform-html" + + +-- TODO: fix syntax with [command dirs]: should be 1 obligatory, then many optional args +local option_text = [[ +Luaxake: build system for Ximera documents +Usage: +$ luaxake [command dirs] + +Options: +-h,--help Print help message +-l,--loglevel Set log level: trace, debug, info, status, warning, error, fatal +-s,--silent Set log level to 'status' +-v,--verbose Set log level to 'info' +-d,--debug Set log level to 'debug' +-t,--trace Set log level to 'trace' +--version Version info +-f,--force Recompile anyway +--check Only check, no actual compiling/cleaning +--noclean Keep all temp files +--nodependencies Do not (re-)compile potential dependencies +--compile (default none) Compile sequence (default 'pdf,html', or as set in settings) +--settingsfile (default none) Luaxake settings script +--configfile (default none) TeX4ht config file + +Possible commands: + bake + name -- NOT (YET) IMPLEMENTED HERE: see xmlatex !!! + frost + serve + clean / veryclean -- to be changed/improved + info -- not yet very useful.. + ]] + +-- REMOVED: (table) Document root directory +--- @class args +--- @field config string TeX4ht config file +--- @field help boolean Print help message +--- @field settings string Luaxake settings script +--- @field loglevel string Logging level +--- @field version boolean Print version +--- @field command string Command to execute +--- @field dir table Document root directory +local args = lapp(option_text) + +if args.version then + print("Luaxake version: " .. (version == "{{version}}" and "devel" or version)) + os.exit() +end + +-- Highest level of -v, -d, -t wins, except that -l would overwrite it +logging.set_level("info") -- default +if args.silent then logging.set_level("status") end +if args.verbose then logging.set_level("info") end +if args.debug then logging.set_level("debug") end +if args.trace then logging.set_level("trace") end + +if args.loglevel then logging.set_level(args.loglevel) end + + +GLOB_root_dir = path.abspath(".") -- TODO: find git folder ...? +-- log:tracef("Setting GLOB_root_dir=%s",GLOB_root_dir) + + +-- +-- FOR REFERENCE: (and to be checked ...?) +-- + +--- @class DOM_Object +--- Dummy type declaration for LuaXML DOM object, to prevent error messages from language server +--- @field query_selector function get all elements that match a given CSS selector +--- @field get_children function +--- @field get_text function +--- @field get_attribute function +--- @field remove_node function + +--- @class compiler +--- @field command string command template +--- @field check_log? boolean should we check the log file for errors? +--- @field check_file? boolean execute command only if the output file exists +--- @field status? number expected status code from the command +--- @field process_html? boolean run HTML post-processing + +--- @class config +--- @field compile_sequence [string] sequence of compiler names to be executed +--- @field output_formats [string] list of output format extensions, calculated from compile_sequence +--- @field compilers {string: compiler} table with available 'compilers' +--- @field clean [string] list of extensions of temp files to be removed after the compilation +--- @field configfile string TeX4ht config file +--- @field documentclass_lines number on how many lines in TeX files we should try to look for \documentclass + + +-- a GLOBAL config class +-- The values here are defaults, that can be overwritten with +-- - values in a settingsfile (the file itself can be given with --settingsfile or as environment variable) +-- - environment variables (see infra) +-- - optional arguments (--clean, etc) +-- - explicit constructs infra (ef default_dependencies, ...) +-- General rule: +-- an explicitly give argument always wins, over +-- an environment variables, that wins over +-- an setting in the settings file, that wins over +-- a default value provided here. + +config = { + --- will be set infra --- compile_sequence = {"pdf", "draft.html"}, + -- compile_sequence = {"pdf", "make4ht.html", "handout.pdf"}, + -- compile_sequence = {"pdf", "sagetex.sage", "pdf", "html"}, + -- see infra -- default_dependencies = { "xmPreamble.tex" }, -- add here e.g. xmPreamble, ximera.cls, ... + compilers = { + pdf = { + -- this doesn't work well + -- command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape "\\PassOptionsToClass{tikzexport}{ximera}\\PassOptionsToClass{xake}{ximera}\\PassOptionsToClass{xake}{xourse}\\nonstopmode\\input{@{filename}}"', + -- command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape "\\PassOptionsToClass{xake}{ximera}\\PassOptionsToClass{xake}{xourse}\\input{@{filename}}"', + command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape "\\input{@{filename}}"', -- mmm, this increases the .jax file !!! + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + infix = "" , -- used for .handout, and .make4k4 + extension = "pdf", -- not used ???? + post_command = 'post_process_pdf', + download_folder = 'ximera-downloads/with-answers', + }, + ["handout.pdf"] = { + command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape -jobname @{basename}.handout "\\PassOptionsToClass{handout}{ximera}\\PassOptionsToClass{handout}{xourse}\\input{@{filename}}"', + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + extension = "handout.pdf", + infix = "handout" , + post_command = 'post_process_pdf', + download_folder = 'ximera-downloads/handouts', + }, + -- 20241217: no longet use "html", but eg draft.html (this keeps logfiles etc from being overwritten ...)! + ["draft.html"] = { + command = "make4ht -l -c @{configfile} -f html5+dvisvgm_hashes -m draft -j @{basename}.draft -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'post_process_html', + extension = "html", + infix = "draft" , + }, + -- alternatives, use at your own risk + ["make4ht.html"] = { + command = "make4ht -l -c @{configfile} -f html5+dvisvgm_hashes -j @{basename}.make4ht -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'post_process_html', + extension = "html", + infix = "make4ht" , + }, + -- test: use 'tikz+' option (FAILS for some tikzpictures, eg with shading/patterns) + ["tikz.html"] = { + command = "make4ht -l -c @{configfile} -f html5+dvisvgm_hashes -m draft -j @{basename}.draft -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'post_process_html', + extension = "html", + infix = "draft" , + }, + ["test.html"] = { + -- command = "make4ht -f html5+dvisvgm_hashes -c @{configfile} -sm draft @{filename}", + -- command = "make4ht -c @{configfile} -f html5+dvisvgm_hashes -s @{make4ht_mode} -a debug @{filename} 'svg,htex4ht,mathjax,-css,info,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + command = "make4ht -l -c @{configfile} -f html5+dvisvgm_hashes -j @{basename}.make4ht -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css,info,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'post_process_html', + extension = "html", + infix = "make4ht" , + }, + + -- sage not tested/implemented !!!! + ["sagetex.sage"] = { + command = "sage @{output_file}", + check_log = true, -- check log + check_file = true, -- check if the sagetex.sage file exists + status = 0, -- check that the latex command return 0 + extension = "sage", -- ? + }, + -- a dummy test: create .ddd files that contain the date .. + ddd = { + command = 'date >@{basename}.ddd', + status = 0, -- check that the command returns 0 + }, + }, + -- TeX macro's to use for dependency-checking in .tex files + input_commands = { + input=true, + include=true, + includeonly=true, + activity=true, + practice=true, + activitychapter=true, + activitysection=true, + practicechapter=true, + practicesection=true, + }, + -- extensions to be kept by get_files (and thus for which fileinfo is collected) + keep_extensions = { + tex = true, + html = true, + sty = true, + }, + -- list of 'infixes' to be cleaned by default + clean_infixes = { + "", + ".make4ht", + ".draft", + ".handout", + }, + -- automatically clean files immediately after each compilation + -- the commented extensions might cause issues when automatically cleaned, as they may be needed for the next compilation + clean_extensions = { + -- "aux", + "4ct", + "4tc", + "oc", + "md5", + "dpth", + "out", + -- "jax", + "idv", + "lg", + "tmp", + -- "xref", + -- "log", + "auxlock", + "dvi", + "scmd", + "sout", + "ids", + "mw", + "cb", + "cb2", + }, + documentclass_lines = 30, + -- for debugging: dumps the 'fileinfo' of matching files + -- make4ht_loglevel = "", + make4ht_extraoptions= "", + -- number of lines in tex files where we should look for \documentclass + -- dump_fileinfo = "aFirstXourse.tex", +} + + + +-- first real argument is the command (bake/frost/serve/...) +local command = table.remove(args, 1) + +if not command then + log:error("Usage: script [command] ") + os.exit(1) +end + +log:debug("command: "..command) + + +-- all further arguments are considered dirs/files to be processed +local dirs = {} + +for i, value in ipairs(args) do + log:trace("Args: adding file/dir "..value) + table.insert(dirs, value) +end + +if #dirs == 0 then + dirlist = os.getenv("XM_TO_PROCESS") or "." + log:debugf("No files/directories given as argument, using default: %s", dirlist) + dirs = utils.split(dirlist,',') +end + +-- Store all to_be_processed files/folders +config.dirs = dirs + + +if args.settingsfile ~= "none" then + config.settingsfile = args.settingsfile -- explicitly give as option +else + config.settingsfile = os.getenv("XM_SETTINGSFILE") or "none" +end + +-- Set/overwrite config +if config.settingsfile ~= "none" then + -- config file can be a Lua script, which should only set properties for the config table + local configlib = require "luaxake-config" + log:info("Using settings file: " .. config.settingsfile) + configlib.update_config(config.settingsfile, config) +end + +config.check = args.check or os.getenv("XM_ONLY_CHECK") or config.check +config.noclean = args.noclean or os.getenv("XM_NOCLEAN") or config.noclean +config.nodependencies = args.nodependencies or os.getenv("XM_NODEPENDENCIES") or config.nodependencies +config.force = args.force or os.getenv("XM_FORCE") or config.force + +if args.configfile ~= "none" then + config.configfile = args.configfile +else + config.configfile = os.getenv("XM_CONFIGFILE") or "ximera.cfg" +end + +if config.check then log:info("Running with (only) 'check'") end +if config.noclean then log:info("Running with 'noclean'") end +if config.nodependencies then log:info("Running with 'nodependencies'") end +if config.force then log:info("Running with 'force' (compile/serve)") end + +if config.configfile ~= "ximera.cfg" then + log:warning("Using non-default config file " .. config.configfile) +end + + +-- set/add potential default dependencies +if not config.default_dependencies and path.exists("xmPreamble.tex") then + log:info("Adding default dependency xmPreamble.tex") + config.default_dependencies = { "xmPreamble.tex" } +else + config.default_dependencies = {} +end + +-- Function to get extensions for array of compilers +local function get_extensions_for_compilers(tbl, kys) + local extensions = {} + for _, key in ipairs(kys) do + if tbl[key] and tbl[key].extension then + table.insert(extensions, tbl[key].extension) + end + end + return extensions +end +-- Function to get infixes for array of compilers +local function get_infixes_for_compilers(tbl, kys) + local infixes = {} + for _, key in ipairs(kys) do + if tbl[key] and tbl[key].infix then + if tbl[key].infix == "" then + table.insert(infixes, tbl[key].infix) + else -- prepend the . !!! + table.insert(infixes, "."..tbl[key].infix) + end + end + end + return infixes +end + + +-- shope adhoc 'shortcuts', mainly because used in .vscode/tasks.json ... +if command == "compilePdf" then + log:info("Compile only PDF") + config.compile_sequence = { "pdf" } + config.output_formats = get_extensions_for_compilers(config.compilers, config.compile_sequence) +end +if command == "compile" then + log:info("Compile only HTML") + config.compile_sequence = { "draft.html" } + config.output_formats = get_extensions_for_compilers(config.compilers, config.compile_sequence) +end + + +if args.compile ~= "none" then + config.compile_sequence = utils.split(args.compile,',') +else + local compileseq = os.getenv("XM_COMPILE_SEQUENCE") or "pdf,draft.html" + config.compile_sequence = utils.split(compileseq,',') + +end + +for _, compiler in ipairs(config.compile_sequence) do + if not config.compilers[compiler] then + log:fatalf("Unknown compiler %s (should be in %s)", compiler, table.concat(tablex.keys(config.compilers),', ')) + os.exit(1) + end +end + + +config.clean_infixes = get_infixes_for_compilers(config.compilers, config.compile_sequence) +config.output_formats = get_extensions_for_compilers(config.compilers, config.compile_sequence) +log:infof("Set compile_sequence=%s (and output_formats=%s)", table.concat(config.compile_sequence,','), table.concat(config.output_formats,',')) + +-- +-- Prepare processing: collect all metadata / to_be_compiled etc +-- +local tex_fileinfos = {} + +for i,nextarg in ipairs(config.dirs) do + log:infof("Processing argument %d: %s", i,nextarg) + nextarg = nextarg:gsub("^.//*", "") -- remove potential leading './' -- vscode-settings sometimes even add .//myfile.tex; it also might confuse skipping hidden .xxx files/folders + + --if path.isdir(nextarg) or path.isfile(nextarg) then + + if not path.isdir(nextarg) then + local ext = nextarg:match(("%.([^%.]+)$")) + if ext ~= "tex" and path.isfile(nextarg..".tex") then + log:trace("Adding .tex to " .. nextarg) + nextarg = nextarg..".tex" + elseif ext ~= "tex" then + log:warningf("Argument %s not folder nor tex-file (%s): SKIPPING", nextarg, ext) + goto nextarg -- continue ... + end + end + + -- some files might need compilation even if they are not in the args, e.g. _pdf.tex files, or other dependent .tex files + local all_involved_texfiles = files.get_tex_files_with_status(nextarg, config.output_formats, config.compile_sequence) + + if path.isfile(nextarg) then + if config.force then + log:infof("Explicitly force (re-) compilation of %s", nextarg) + all_involved_texfiles[nextarg].needs_compilation = true + end + + -- Function to filter key-value tables + local function filter_needs_compilation(tbl) + local filtered = {} + for key, entry in pairs(tbl) do + if entry.needs_compilation then filtered[key] = entry end + end + return filtered + end + + local extra_need_compilation = filter_needs_compilation(all_involved_texfiles) + extra_need_compilation[nextarg] = nil + + log:debugf("%d DEPENDENT file(s) need compilation: %s", tablex.size(extra_need_compilation), table.concat(tablex.keys(extra_need_compilation),', ')) + + if config.nodependencies and pl.tablex.size(extra_need_compilation) > 0 then + -- log:debugf("Compiling %s, without the %d dependencies", nextarg, pl.tablex.size(all_involved_texfiles) - 1) + log:infof("Compiling %s, without dependencies %s", nextarg, table.concat(tablex.keys(extra_need_compilation),', ')) + tex_fileinfos[nextarg] = all_involved_texfiles[nextarg] + else + for fname, finfo in pairs(all_involved_texfiles) do + log:tracef("File %s: collecting %-30s into tex_fileinfos", nextarg, fname) + tex_fileinfos[fname] = finfo + end + end + else -- nextarg is a folder + for fname, finfo in pairs(all_involved_texfiles) do + log:tracef("Dir %s: collecting %-30s into tex_fileinfos", nextarg, fname) + tex_fileinfos[fname] = finfo + end + end + + ::nextarg:: +end + +-- add extra files we depend on to the potentially to-be-compiled tex_files +local tex_files = tablex.values(tex_fileinfos) + + +log:debugf("Finding and sorting dependencies for %d tex_files", #tex_files) +local to_be_compiled = files.sort_dependencies(tex_files) + +function filter_main_tex_files(tbl) + local result = {} + for _, entry in pairs(tbl) do + if not entry.tex_documentclass then + log:debugf("Removing non-document file %s", entry.relative_path) + else + table.insert(result, entry) + end + end + return result +end + +to_be_compiled = filter_main_tex_files(to_be_compiled) +if #to_be_compiled == 1 then + log:statusf("%d file needs compiling: %s", #to_be_compiled, to_be_compiled[1].relative_path) +elseif #to_be_compiled > 0 then + log:statusf("%d files need compiling", #to_be_compiled) + for i,file in ipairs(to_be_compiled) do + log:infof("%3d %s",i,file.relative_path) + end +else + log:status("No files need compiling") +end + + -- + -- Start processing further commands + -- + + if command == "info" then + log:infof("Got %d tex files:", #tex_files) + for _,file in ipairs(tex_files) do + print(file.relative_path) + --print(file.absolute_path) + end + end + + if command == "clean" or command == "veryclean" then + local n_files_deleted = 0 + local to_be_cleaned_extensions = tablex.copy(config.clean_extensions) + local to_be_cleaned_infixes = tablex.copy(config.clean_infixes) + + -- if args.compile ~= "none" then -- if explicitly given compilers, only clean those infixes + -- -- to_be_cleaned_infixes = utils.split(args.compile,',') + -- config.clean_infixes = get_infixes_for_keys(config.compilers, config.compile_sequence) + -- log:info("Infixes to be cleaned: " .. args.compile) + -- end + + if command == "veryclean" then + -- log:debug("Appending extra extensions") + tablex.insertvalues(to_be_cleaned_extensions, config.output_formats) -- append arrays ... + tablex.insertvalues(to_be_cleaned_extensions, { "aux", "toc", "log", "dlog", "xref", "jax", "xmjax", "sagetex.sage", "html.failed", "pdf.failed", "svg" }) -- append arrays ... + log:debugf("Appended extra extensions %s", table.concat(to_be_cleaned_extensions, ', ')) + end + + log:statusf("Cleaning %s files with extensions %s", table.concat(to_be_cleaned_infixes, ', '),table.concat(to_be_cleaned_extensions, ', ')) + + for _,file in ipairs(tex_files) do + log:tracef("Removing temp files for %s", file.relative_path) + n_files_deleted = n_files_deleted + compile.clean(file, to_be_cleaned_extensions, to_be_cleaned_infixes, config.check) + end + + -- require 'pl.pretty'.dump(config) + -- require 'pl.pretty'.dump(to_be_cleaned_extensions) + + log:infof("Cleaned %d files", n_files_deleted) + end + + +-- POSIX system calls we need for non-blocking popen +-- Note this makes us POSIX dependent, but we could add Windows system calls to support Windows if we ever needed +ffi.cdef([[ + void* popen(const char* cmd, const char* mode); + int pclose(void* stream); + int fileno(void* stream); + int fcntl(int fd, int cmd, int arg); + int *__errno_location (); + ssize_t read(int fd, void* buf, size_t count); +]]) + +-- I think these are technically system dependent +local F_SETFL = 4 +local O_NONBLOCK = 2048 +local EAGAIN = 11 +local max_workers = 2 + +function bake(to_be_compiled) + log:debug("Baking") + + local all_statuses = {} + + local _errno = ffi.C.__errno_location() -- Get pointer to errno location + local current_processes = {} + local current_workers = 0 + + local i = 1 + local total_to_compile = #to_be_compiled + + while i <= total_to_compile or current_workers > 0 do -- TODO: Look into making this cleaner + if current_workers < max_workers and i <= total_to_compile then -- We're ready to start more compiling processes + local file = to_be_compiled[i] + -- Start ugly passing of info to test-bake.lua + local command = "luatex --luaonly /root/texmf/tex/latex/ximeraLatex/luaxake/test-bake.lua" + -- local command = "./xmScripts/bake" + command = command .. " :" .. tostring(file.relative_path) + command = command .. " :" .. tostring(file.absolute_path) + command = command .. " :" .. tostring(file.absolute_dir) + command = command .. " :" .. tostring(file.relative_dir) + command = command .. " :" .. tostring(file.filename) + command = command .. " :" .. tostring(file.basename) + command = command .. " :" .. tostring(file.extension) + command = command .. " :" .. tostring(file.exists) + command = command .. " :" .. tostring(file.modified) + command = command .. " :" .. tostring(file.needs_compilation) + command = command .. " :" .. tostring(file.tex_documentclass) + + for _, value in pairs(config.compile_sequence) do + command = command .. " :" .. tostring(value) + end + + command = command .. " " .. "BREAK" -- and it only gets uglier + + for _, value in pairs(config.output_formats) do + command = command .. " :" .. tostring(value) + end + + -- End ugly passing of info to test-bake.lua + + log:info("Starting process with command: " .. tostring(command)) + local process = {} + process.file_name = file.absolute_path + + -- Start process with "command" + process.handle = ffi.C.popen(command, "r") + if process.handle == nil then + local err_code = _errno[0] + return tonumber(err_code), "Error trying to popen command: " .. tostring(command) .. "Error code: " .. tostring(err_code) + end + + -- Get file descriptor of pipe + process.fd = ffi.C.fileno(process.handle) + if process.fd == -1 then + local err_code = _errno[0] + local err_msg = "Failed to get file descriptor for command: " .. tostring(command) .. ". Error code: " .. tostring(err_code) + + local status_code = ffi.C.plcose(process.handle) + if status_code -1 then + err_code = _errno[0] + err_msg = err_msg .. ". Failed to pclose process. Error code: " .. tostring(err_code) + end + + return tonumber(err_code), err_msg + end + + -- Set non-blocking mode for pipe + local status_code = ffi.C.fcntl(process.fd, F_SETFL, O_NONBLOCK) + if status_code ~= 0 then + err_code = _errno(0) + log:info("failed fcntl. status_code: " .. tostring(status_code) .. ". error: " .. tostring(_errno[0])) + return tonumber(err_code), "Failed to set non-blocking reads for pipe to command: " .. tostring(command) .. ". Error code: " .. tostring(err_code) + end + + table.insert(current_processes, process) + current_workers = current_workers + 1 + i = i + 1 + + else -- Check to see if any processes have ended + for j, process in ipairs(current_processes) do + local read_buffer = "" + local bytes_read = ffi.C.read(process.fd, read_buffer, 2024) -- Read 2024 bytes at a time (We currently don't do anything with what we read) + if (bytes_read == -1) and (_errno[0] ~= EAGAIN) then -- There was some unexpected error + + -- TODO: We might be able to handle some of these errors instead of just failing + return tonumber(_errno[0]), "Failed to read pipe for compilation process for file: " .. tostring(process.file_name) .. ". Error code: " .. tostring(_errno[0]) + + elseif bytes_read == 0 then -- End of file + + local ret_code = ffi.C.pclose(process.handle) + if ret_code == -1 then -- TODO: Do we have to do anything more in this case? + log:error("Failed to get return code for compilation process for file: " .. tostring(process.file_name)) + else + ret_code = ret_code / 256 -- Discard last 8 bits + end + + -- TODO: add error handling based off of this return code + log:info("Return code for compilation process for file: " .. tostring(process.file_name) .. " is ret_code: " .. tostring(ret_code)) + + current_workers = current_workers - 1 + table.remove(current_processes, j) + end + end + end + end + + return 0, string.format("Baking resulted in unknown errors") + + -- collect and print all errors + -- local failed_files = {} + -- for _, entry in ipairs(all_statuses) do + -- for _, compile_info in ipairs(entry) do + -- log:debug("File "..(compile_info.output_file or "UNKNOWN??") .." got status " .. (compile_info.status or 'NIL??') ) + + -- if (compile_info.status or 0) > 0 then + -- -- require 'pl.pretty'.dump(compile_info) + -- failed_files[compile_info.source_file] = ( failed_files[compile_info.source_file] or 0 ) + #(compile_info.errors) + + -- for _, err in ipairs(compile_info.errors) do + -- -- log:errorf("[%10s] %s:%s %s [%s]", compile_info.compiler, compile_info.source_file, err.line, err.context,err.error) + -- log:errorf("[%-10s] %s:%s", compile_info.compiler, compile_info.source_file, err.constructed_errormessage) + + -- -- require 'pl.pretty'.dump(err) + -- end + -- else + -- if compile_info.post_processing_error then + -- log:errorf("[%-10s] %s: %s", "post_command", compile_info.source_file, compile_info.post_processing_error) + -- local _, n_errors = compile_info.post_processing_error:gsub("\n", "") -- HACK: number of lines equals number of errors ... + -- failed_files[compile_info.source_file] = ( failed_files[compile_info.source_file] or 0 ) + n_errors + 1 + -- end + -- end + -- end + -- end + + + -- -- + -- -- all compilations done; process/summarize errors + -- -- + -- if tablex.size(failed_files) == 0 then + -- -- log:statusf("Baked %d files, no errors found", #to_be_compiled) + -- return 0, string.format("Baked %d files, no errors found", #to_be_compiled) + -- else + -- log:warningf("Baked %d files, but %d compilation%s failed", #to_be_compiled, tablex.size(failed_files), tablex.size(failed_files) == 1 and "" or "s") + + -- for filename, errs in pairs(failed_files) do + -- log:errorf("%2d errors in %s", errs, filename) + -- end + -- -- log:statusf("Baking resulted in %d errors", tablex.size(failed_files) ) + -- return 1, string.format("Baking resulted in %d errors", tablex.size(failed_files) ) + -- end +end + + -- TODO: fix commands (cfr xmlatex/xmlatex/lua) +if command == "bake" or command == "compilePdf" or command == "compile" then + log:status("Start "..command) + + local ret, msg + if #to_be_compiled == 0 then + ret, msg = 0, "Nothing to be baked" + else + ret, msg = bake(to_be_compiled) + end -- end baking + + if ret > 0 then + log:error("Bake failed: " .. msg) + os.exit(ret) + else + log:statusf("Bake succeeded: " .. msg) + end +end + +if command == "frost" then + log:status("Start " .. command) + local ret, msg = frost.frost(tex_files, to_be_compiled) + if ret > 0 then + log:error("Frost failed: " .. msg) + os.exit(ret) + else + log:statusf("Frost succeeded: " .. msg) + end +end + +if command == "serve" then + log:status("Start " .. command) + local ret, msg = frost.serve(config.force) + if ret > 0 then + log:error("Serve failed: " .. msg) + os.exit(ret) + else + log:statusf("Serve succeeded: " .. msg) + end +end + +if command == "ghaction" then + + local ret, msg + if #to_be_compiled == 0 then + log:status("Nothing to be baked") + msg={} + else + log:status("Start bake") + ret, msg = bake(to_be_compiled) + + if ret > 0 then + log:error("Bake failed: " .. msg) + log:warning("But, I'll continue, and hope for the best") + -- os.exit(ret) + else + log:statusf("Bake succeeded: " .. msg) + end + end + + -- Hack: update to_be_compiled for usage in frost (uncompiled files might/should genetate a warning or error ...) + local still_to_be_compiled = {} + for _, lfile in ipairs(to_be_compiled) do + if lfile.needs_compilation then + log:infof("Mmm, file %s still needs compilation.", lfile.relative_path) + table.insert(still_to_be_compiled, lfile) + end + end + + if #still_to_be_compiled > 0 then + log:infof("Still %d files to be compiled after bake.", #still_to_be_compiled) + end + to_be_compiled = still_to_be_compiled + -- end-of-hack to_be_compiled + + log:status("Start frost") + local ret, msg = frost.frost(tex_files, to_be_compiled) + if ret > 0 then + log:error("Frost failed: " .. msg) + os.exit(ret) + else + log:statusf("Frost succeeded: " .. msg) + end + log:status("Start serve") + local ret, msg = frost.serve(true) -- WITH FORCE (until push/depth issue fixed) + if ret > 0 then + log:error("Serve failed: " .. msg) + os.exit(ret) + else + log:statusf("Serve succeeded: " .. msg) + end +end + + +if command == "extrainfo" then + all_labels = {} + -- TEST: to be implemented ...? + for i, file in ipairs(tex_files) do + log:infof("Info for %s (%s)",file.filename,file.relative_path) + + local html_file = files.get_fileinfo(file.relative_path:gsub("tex$", "html")) + local ret, msg = html.update_html_fileinfo(html_file) + + for k,v in pairs(html_file.labels or {}) do + log:infof("Adding %s to %s",v,k) + all_labels[k] = ( all_labels[k] or 0) + v + end + end + if all_labels ~= {} then + log:infof("Potentially duplicate labels:") + require 'pl.pretty'.dump(all_labels) + end +end \ No newline at end of file diff --git a/.ximera_local/luaxake/luaxake-compile.lua b/.ximera_local/luaxake/luaxake-compile.lua new file mode 100644 index 000000000..7dc10027c --- /dev/null +++ b/.ximera_local/luaxake/luaxake-compile.lua @@ -0,0 +1,294 @@ +local M = {} +local lfs = require "lfs" +local error_logparser = require("make4ht-errorlogparser") +local pl = require "penlight" +local path = pl.path +local pldir = pl.dir +local plfile = pl.file +local html = require "luaxake-transform-html" +local files = require "luaxake-files" -- for get_fileinfo +local frost = require "luaxake-frost" -- for osExecute +local socket = require "socket" + +local log = logging.new("compile") + + +local function parse_log_file(filename) + -- log:errorf("PARSING %s", filename) + local f = io.open(filename, "r") + if not f then + log:warningf("Cannot open log file %s; SKIPPING parsing logfile for errors ", filename) + return nil + end + local content = f:read("*a") + f:close() + local result = error_logparser.parse(content) + log:tracef("PARSING got %s", result) + return result +end + +-- +-- These next functions are/can be called by post_command in config.commands +-- HACK: these currently need to be global; TODO: fix! +-- The functions should return an updated 'cmd' structure, with eg +-- cmd.final_output_file = +-- cmd.status_post_command = "OK" +function post_process_html(cmd) + -- simple wrapper to make it work in post_command + -- + return html.post_process_html(cmd) +end + +function post_process_pdf(cmd) + -- move the pdf to a corresponding folder under root_dir (presumably ximera-downloads, with different path/name!) + -- + -- use absolute paths when running in chdir-context during compilation .... + local file = cmd.file + local src_filename = cmd.output_file + local absfolder = path.join(GLOB_root_dir, cmd.command_metadata.download_folder, file.relative_dir) + local relfolder = path.join( cmd.command_metadata.download_folder, file.relative_dir) + local abstgt = path.join(absfolder, file.basename ..".pdf") + local reltgt = path.join(relfolder, file.basename ..".pdf") + -- require 'pl.pretty'.dump(src) + if not path.exists(src_filename) then + log:warningf("Output file %s does not exists (for %s)",src_filename, file.relative_path) + else + log:infof("Moving %s to %s", src_filename, reltgt) + pldir.makepath(absfolder) + plfile.copy(src_filename, abstgt) + end + + if file.relative_path:match("_pdf.tex$" ) then + log:infof("Convert _pdf.pdf file to svg for %s",file.relative_path) + -- Mmm, osExecute should better not be in module 'frost' + frost.osExecute("pdf2svg " .. file.absolute_path:gsub(".tex",".pdf") .. " " .. file.absolute_path:gsub(".tex",".svg")) + end + + cmd.final_output_file = reltgt + cmd.status_post_command = "OK" + return cmd +end + +--- run a complete compile-cycle on a given file +--- +--- SIDE-EFFECT: adds output_files to the file argument !!! +--- +--- @param file fileinfo file on which the command should be run +--- @param compilers [compiler] list of compilers +--- @param compile_sequence table sequence of keys from the compilers table to be executed +--- @return [compile_info] statuses information from the commands +local function get_commands_to_run(file, compilers, compile_sequence, only_check) + only_check = only_check or false + local compile_commands = {} + + -- Collect ALL needed compilations for this file + -- NOTE: extension is a bad name, it's rather 'compiler' + for _, extension in ipairs(compile_sequence) do + log:tracef("Collecting %s compilation of %s (%s)", extension, file.relative_path, file.tex_documentclass) + local command_metadata = compilers[extension] + + if not command_metadata then + log:errorf("No compiler defined for %s (%s); SKIPPING",extension,file.relative_path) + goto uptonextcompilation -- nice: a goto-statement !!! + end + -- This could/should perhaps be handled higher up? Compilation of e.g. preamble.tex does not make sense ... + if not file.tex_documentclass then + log:infof("Skipping %s compilation of non-tex-document %s",extension, file.relative_path) + goto uptonextcompilation + end + if file.tex_documentclass ~= "ximera" and file.tex_documentclass ~= "xourse" and string.match(extension,"html") then + log:infof("Compiling a non-ximera %s file %s with 'xhtml' (and thus not ximera.cfg)", file.tex_documentclass, file.relative_path) + file.configfile = "xhtml" + end + + if file.extension ~= "tex" then + log:errorf("Can't compile non-tex file %s; SKIPPING, SHOULD PROBABLY NOT HAVE HAPPENED",file.relative_path) + goto uptonextcompilation + end + + + -- HACK: _pdf.tex and _beamer.tex files should by convention NOT generate HTML (as they typically would contain non-TeX4ht-compatible constructs) + if extension:match("html$") and ( file.relative_path:match("_pdf.tex$") or file.relative_path:match("_beamer.tex$") ) then + log:infof("Skipping HTML compilation of pdf-only file %s",file.relative_path) + + -- create/update a dummy outputfile to mark this file 'uptodate' + local filename = file.absolute_path:gsub(".tex$",".html") + local file, err = io.open(filename, "r") + + if file then + -- File exists, update modification time + file:close() + lfs.touch(filename) + else + -- File doesn't exist, create a new one + file, err = io.open(filename, "w") + if file then file:close() + else log:infof("Failed to fix dummy htmlfile %s: %s",filename,err) + end + end + goto uptonextcompilation + end + + -- Construct the expected names of the generated output and logfiles + local infix = "" -- used for compilation-variations, eg 'handout' of 'make4ht'/'draft' + if command_metadata.infix and command_metadata.infix ~= "" then + infix = command_metadata.infix.."." + end + local output_file = file.absolute_path:gsub("tex$", extension) -- to be generated by compile + local log_file = file.absolute_path:gsub("tex$", infix.."log") -- hopefully this is where the logs go + + -- sometimes compiler wants to check for the output file (like for sagetex.sage), + if command_metadata.check_file and not path.exists(output_file) then + log:debugf("Skipping compilation because of 'check_file', and file %s does not exist",output_file) + goto uptonextcompilation -- TODO: CHECK (for sagetex.sage ...) + end + + if output_file.exists and not output_file.needs_compilation then + log:debugf("Mmm, compiling file %s which was registered as not needing compilation.",output_file) + end + + -- replace placeholders like @{filename} with the corresponding keys (from the metadata table, or config) + local command = command_metadata.command + command = command:gsub("@{(.-)}", file) + command = command:gsub("@{(.-)}", { output_file = output_file }) -- used for sage ... + command = command:gsub("@{(.-)}", config) + + log:debug("Adding command " .. command ) + + local cmd = { + id=extension.."|"..file.relative_path, + file=file, + extension=extension, + command=command, + command_metadata=command_metadata, + output_file = output_file, + log_file=log_file, + only_check=only_check, + } + table.insert(compile_commands, cmd) + + log:tracef("ADDED %s for %s of %s", cmd.id, cmd.extention, file.relative_path) + + + ::uptonextcompilation:: + end + + return compile_commands + +end + + +local function do_command_handle(cmd) + + -- log:info("START do_command_handle") + + local file=cmd.file + local extension=cmd.extension + + log:debugf("Handling return of %s for %s: returns %s (expected %d) after %.3f seconds", cmd.extension, file.relative_path, cmd.status_command or 42, cmd.command_metadata.status or 42, cmd.compilation_time or 42) + + if cmd.command_metadata.check_log then + + log:tracef("Checking logfile %s", cmd.log_file) + local errors = parse_log_file(cmd.log_file) -- gets errors the make4ht-way ! + cmd.errors = errors -- keep them around + + -- Show nicely formatted errors + local err_context = "" + local err_line = "" + for i, err in ipairs(errors or {}) do + + -- Format errormessage a bit, and store it in err.constructed_errormessage + err_context = "at "..err.context + err_line = "" + if err.line then err_line = "[l." .. err.line .. "]" end + + -- remove useless context ... + if err.context:match('See the LaTeX manual or LaTeX Companion for explanation') + or err.context:match('^ <-') then + err_context = "" + end + + err.constructed_errormessage = string.format("%s %-30s %s", err_line, err.error, err_context) + + if i<10 then + log:errorf("%-20s:%s", cmd.log_file, err.constructed_errormessage) + elseif i == 10 then + log:warningf("... skipping further errorlog; %d errors found", #errors) + end + end + end + + if cmd.status_command ~= cmd.command_metadata.status then + -- log:errorf("Compilation of %s for %s failed: returns %d (not %d) after %3f seconds", extension, file.relative_path, cmd.status_command, cmd.status_expected, compilation_time) + log:errorf("Compilation of %s for %s failed: returns %d (not %d)", extension, file.relative_path, cmd.status_command, cmd.command_metadata.status) + if path.exists(cmd.output_file) then + -- prevent trailing non-correct files, as they prevent automatic re-compilation ! + log:infof("Moving output of failed compilation to %s", cmd.output_file..".failed") + pl.file.move(cmd.output_file, cmd.output_file..".failed") + end + cmd.status = "COMMAND_FAILED" -- adhoc errorstatus + return cmd + end + + -- The 'output_file' might need to be post-processed into a 'final_output_file' + -- ( eg html manipulation, or moving a pdf to a downloads folder) + -- in case no postprocessing: + local final_output_file = cmd.output_file + if cmd.command_metadata.post_command then + local post_command = cmd.command_metadata.post_command + log:debugf("Postprocessing %s: %s",cmd.id, post_command) + -- call the post_command + cmd = _G[post_command](cmd) -- lua way of calling the function whose name is in 'post_command' + + -- The post_command might/should have created a new ('final') output file + final_output_file = cmd.output_file_postprocessed + end + + if final_output_file then + log:debugf("Adding outputfile %s for %s ", final_output_file, file.relative_path) + if not file.output_files_made then file.output_files_made = {} end + file.output_files_made[final_output_file] = files.get_fileinfo(final_output_file, true) -- never used? + end + + cmd.status = "OK" + -- log:trace("DONE do_command_handle") + + -- -- Update 'needs_compilation' ... (BADBAD: should probably be done in a better way ...) + -- files.update_output_files(file, output_formats) + -- log:infof("Updated status of %s:%s uptodate", file.relative_path, file.needs_compilation and ' NOT' or '' ) + + -- files.dump_fileinfo(file) -- only for debugging + + return cmd +end + +--- remove temporary files +---@param basefile fileinfo +---@param extensions table list of extensions of files to be removed +---@return number nfiles number of files removed +local function clean(basefile, extensions, infixes, only_check) + only_check = only_check or false + local nfiles = 0 + local basename = path.splitext(basefile.absolute_path) + log:tracef("%s the temp files for %s (%s)", (only_check and "Would remove" or "Removing"), basename, basefile.absolute_path) + + for _, infix in ipairs(infixes) do + for _, ext in ipairs(extensions) do + local filename = basename .. infix .. "." .. ext + if path.exists(filename) then + log:debugf("%s %-14s file %s", (only_check and "Would remove" or "Removing") ,infix.."."..ext, filename) + if not only_check then os.remove(filename); nfiles = nfiles + 1 end + -- else + -- log:tracef("No file %s present", filename) + end + end + end + return nfiles +end + +M.get_commands_to_run = get_commands_to_run +M.do_command_handle = do_command_handle +M.clean = clean + +return M diff --git a/.ximera_local/luaxake/luaxake-config.lua b/.ximera_local/luaxake/luaxake-config.lua new file mode 100644 index 000000000..9db9d75db --- /dev/null +++ b/.ximera_local/luaxake/luaxake-config.lua @@ -0,0 +1,59 @@ +local M = {} +local log = logging.new("config") +local pretty = require("pl.pretty") +local path = require("pl.path") + +-- load and run a script in the provided environment +-- returns the modified environment table + + +-- https://stackoverflow.com/a/69910551 +--- run Lua file and return it's environment table +--- @param scriptfile string script file name +--- @param config config configuration table +--- @return table env script environment table +local function run_test_script(scriptfile, config) + local env = setmetatable({}, {__index=config}) + log:debugf("Loading settings %s",scriptfile) + if not path.isfile(scriptfile) then + log:errorf("File %s does not exists; SKIPPING LOADING SETTINGS",scriptfile) + return env + end + assert(pcall(loadfile(scriptfile,"run_test_script",env))) + setmetatable(env, nil) + return env +end + + +-- https://stackoverflow.com/a/7470789 +--- merge tables +---@param t1 table +---@param t2 table +---@return table merged +local function merge(t1, t2) + for k, v in pairs(t2) do + if (type(v) == "table") and (type(t1[k] or false) == "table") then + merge(t1[k], t2[k]) + else + log:debugf("Setting key '%s' to value '%s'", tostring(k), pretty.write(v)) + t1[k] = v + end + end + return t1 +end + + + +--- update config table from script +--- @param scriptfile string filename +--- @param config config table +--- @return config configuration table +local function update_config(scriptfile, config) + local env = run_test_script(scriptfile, config) + return merge(config, env) +end + + +M.update_config = update_config + +return M diff --git a/.ximera_local/luaxake/luaxake-files.lua b/.ximera_local/luaxake/luaxake-files.lua new file mode 100644 index 000000000..b022b7ebb --- /dev/null +++ b/.ximera_local/luaxake/luaxake-files.lua @@ -0,0 +1,522 @@ +local M = {} +local pl = require "penlight" +local graph = require "luaxake-graph" +local log = logging.new("files") +local lfs = require "lfs" + +local path = pl.path +local abspath = pl.path.abspath +local tablex = pl.tablex + +GLOB_files = {} -- global variable with all fileinfo collected with get_files (caching...) + + + +--- find TeX4ht config file +--- @param filename string name of the config file +--- @param directories [string] +--- @return string path of the config file +local function find_config(filename, directories) + -- the situation with the TeX4ht config file is a bit complicated + -- it can be placed in the current directory, in the document root directory, + -- or in the kpse path. if it cannot be found in any of these places, + -- we will set it to config.config_file (presumably ximera.cfg) + -- in any case, we must provide a full path to the config file, because it will + -- be used in different directories. + for _, dir in ipairs(directories) do + local lpath = dir .. "/" .. filename + if pl.path.exists(lpath) then + log:trace("find_config found "..filename.. " in ".. lpath.."( from "..table.concat(directories,', ')..")") + return lpath + end + end + -- if we cannot find the config file in any directory, try to find it using kpse + local lpath = kpse.find_file(filename, "texmfscripts") + if lpath then + log:trace("find_config found "..filename.. " in ".. lpath.."( from kpse texmfscripts)") + return lpath + end + -- lastly, test if it is a full path to the file + if pl.path.exists(filename) then + log:trace("find_config found "..filename.. " ( as this file happens to exist)") + return filename + end + -- xhtml is default TeX4th config file, use it if we cannot find a user config file + -- return "xhtml" + return config.config_file +end + + +--- get absolute and relative file path, as well as other file metadata +--- @param input_path string filename +--- @return fileinfo +local function dump_fileinfo(fileinfo) + + local filename = fileinfo.relative_path + if logging.show_level <= logging.levels["debug"] and config.dump_fileinfo and filename:match(config.dump_fileinfo) then + log:debugf("Dumping updated fileinfo for %s:", filename) + pl.pretty.dump(fileinfo) + end +end + + +--- get absolute and relative file path, as well as other file metadata +--- @param input_path string filename +--- @return fileinfo +local function get_fileinfo(input_path, use_no_cache) + + -- caching + if not use_no_cache and GLOB_files[input_path] then + log:tracef("Getting cached fileinfo for %s", input_path) + return GLOB_files[input_path] + end + + log:tracef("Getting fileinfo for %s (from folder %s)", input_path, lfs.currentdir()) + + -- if root_folder and string.match(input_path, "^"..root_folder) then + -- input_path = input_path:gsub("^"..root_folder, "") + -- end + + local relative_path = path.normpath(input_path) -- resolve potential ../ parts + + --- @class fileinfo + --- @field relative_path string relative path of the file (to the root_folder) + --- @field absolute_path string absolute path of the file + --- @field absolute_dir string absolute directory path of the file + --- @field filename string filename of the file + --- @field basename string filename without extension + --- @field extension string file extension + --- @field exists boolean true if file exists + --- @field modified number last modification time + --- @field needs_compilation boolean + --- @field depends_on_files fileinfo[] list of files the file depends on + --- @field output_files fileinfo[] + --- @field config_file? string TeX4ht config file + --- @field root_folder? string root folder + --- @ + local fileinfo = {} + + fileinfo.relative_path = relative_path + fileinfo.absolute_path = abspath(relative_path) + -- fileinfo.absolute_dir = abspath(dir) + + fileinfo.exists = path.exists(relative_path) + fileinfo.modified = path.getmtime(relative_path) + fileinfo.needs_compilation = false + -- fileinfo.config_file = config.config_file -- always the same, unless overwritten somewhere ? + + fileinfo.relative_dir, fileinfo.filename = path.splitpath(relative_path) + fileinfo.absolute_dir, _ = path.splitpath(fileinfo.absolute_path) + fileinfo.basename, fileinfo.extension = fileinfo.filename:match("(.*)%.([^%.]+)$") + fileinfo.basenameshort, fileinfo.extensionlong = fileinfo.filename:match("([^%.]*)%.(.+)$") + + + -- fileinfo.depends_on_files = {} + -- fileinfo.output_files = {} + + GLOB_files[fileinfo.relative_path] = fileinfo + + -- dump_fileinfo(fileinfo) -- for debugging + + return fileinfo +end + + +--- get fileinfo for all files in a directory and it's subdirectories +--- @param dir string path to the directory +--- @param files? table retrieved files +--- @return fileinfo[] +local function get_files(dir) + --dir = dir:gsub("/$", "") -- remove potential trailing '/' + dir = dir:gsub("^.//*", "") -- remove potential leading './' -- it confuses skipping hidden .xxx files/folders + dir = path.normpath(dir) + local all_filenames = {} + + if path.isfile(dir) then + all_filenames = { dir } + log:tracef("get_files: considering %s", dir) + elseif path.isdir(dir) then + all_filenames = pl.dir.getallfiles(dir) + log:tracef("get_files: considering %d files (for %s)", #all_filenames, dir) + else + all_filenames = { } + log:errorf("get_files %s: no such file or directory", dir) + return {} + end + + + local files = {} + + for _, filename in ipairs(all_filenames) do + -- ext = path.extension(filename) + + -- local basename = filename:match(".*/(.*)$") or filename -- Extract filename from path + if filename:match("/%.") then -- ignore every file/folder starting with a . (ie, containing xxx/.yyy ) + -- log:tracef("get_files skips %s", filename) + goto next_file + end + + ext = filename:match("%.([^%.]+)$") + + -- log:tracef("get_files adding %s (%s)", filename, ext) + if config.keep_extensions[ext] then + log:tracef("get_files adding %s", filename) + + finfo = get_fileinfo(filename) + files[finfo.relative_path] = finfo + end + + ::next_file:: + end + + return files + +end + + +function filter_tex_files(tbl) + local result = {} + for _, entry in pairs(tbl) do + if entry.extension == "tex" then + table.insert(result, entry) + end + end + return result +end + +--- Detect if the output file needs recompilation +---@param tex fileinfo metadata of the main TeX file to be compiled +---@param outfile fileinfo metadata of the output file +---@return boolean +local function needs_compiling(tex, outfile) + -- if the output file doesn't exist, it needs recompilation + log:tracef("Does %s need compilation? %s",outfile.relative_path, outfile.exists and "It exists." or "It doesn't exist.") + if not outfile.exists then return true end + -- test if the output file is older if the main file or any dependency + local status = tex.modified > outfile.modified + if status then + log:debugf("TeX file %s has changed since compilation of %s",tex.relative_path, outfile.relative_path) + end + + if not tex.depends_on_files then + log:warningf("File %s does not depend on any files ...?") + else + for filename, subfile in pairs(tex.depends_on_files or {}) do + log:tracef("Check modified of subfile %s", subfile.relative_path) + if not subfile.modified then + log:warningf("No modified info for dependency %s of %s",filename,tex.relative_path) + -- pl.pretty.dump(subfile) + elseif subfile.modified > outfile.modified then + log:tracef("Dependent file %s has changed since compilation of %s", subfile.relative_path, outfile.relative_path) + status = status or subfile.modified > outfile.modified + end + end + end + log:tracef("%s %s", outfile.relative_path, status and "needs compilation" or "does not need compilation") + return status +end + + +--- update the list of files included in the given TeX file +--- @param fileinfo fileinfo TeX file metadata +--- @return +local function update_depends_on_files(fileinfo) + local filename = fileinfo.absolute_path + local relfilename = fileinfo.relative_path -- for logging ... + local current_dir = fileinfo.absolute_dir + + if fileinfo.depends_on_files and tablex.size(fileinfo.depends_on_files) > 0 then + -- TODO: check this? Is it safe to skip? Should the duplicate work not be detected earlier? + log:debugf("Mmm, %s already has %d dependent files! Skip potential duplicate work...?", relfilename, tablex.size(fileinfo.depends_on_files)) + return + end + + fileinfo.depends_on_files = {} -- update has ran, no files found (yet) + + local f, msg = io.open(filename, "r") + if not f then + log:errorf("Could not open file %s: %s",filename, msg) + return + end + + local content = f:read("*a") + f:close() + -- + -- Warning: this matching of commands is definitely not completely correct !!! + -- + -- remove all comments (otherwise also commented commands would be processed!) + -- content = content:gsub("([^\\])%%.-\n", "%1\n") + content = content:gsub("%%[^\n]*", "") + -- remove all optional arguments (they break the matching infra!) + content = content:gsub("%[[^%]]*%]", "") + -- remove verbatim environments + content = content:gsub("\\begin{verbatim}.-\\end{verbatim}", "") + + fileinfo.tex_documentclass = false -- default, might be overwritten infra + -- loop over all LaTeX commands with arguments + for command, argument in content:gmatch("\\(%w+)%s*{([^%}]+)}") do + + -- log:tracef("MATCHED command=%s and argument=%s.", command, argument) + -- add dependency if the current command is \input like + --- local metadata = nil -- should be fileinfo ... + local included_file = nil + local wanted_extension = nil + if not fileinfo.tex_documentclass and command == "documentclass" then + -- only process the first documentclass found (poor-mans prevention against later \documentclass in \verb or so ... !) + log:debugf("%-40s has documentclass %s", relfilename, argument) + fileinfo.tex_documentclass = argument + elseif not fileinfo.has_title and (command == "title" or command == "xmtitle") then + -- only process the first title/xmtitle found (poor-mans prevention against later \documentclass in \verb or so ... !) + log:debugf("%-40s has title %s", relfilename, argument) + fileinfo.has_title = true + elseif command == "dependsonpdf" then + -- hack to include PDF (or SVG) eg of cheatsheets (that can/should not converted to HTML): a (dummy) \dependsonpdf{xxx} can explicitely mark the dependency of aaa.tex on aaa_pdf.tex) + included_file = fileinfo.relative_path:gsub(".tex","_pdf.tex") + log:debugf("%-40s explicitely dependsonpdf (%s, but use semi-hardcoded %s)", relfilename, argument, included_file) + wanted_extension = "pdf" + elseif fileinfo.tex_documentclass and config.input_commands[command] then -- only process inputs AFTER the documentclass (and thus not INSIDE preambles etc) !!! + -- log:tracef("Consider %s{%s}", command, argument) + included_file = path.normpath(current_dir.."/"..argument) -- make absolute dir, and remove potential ../ constructs + wanted_extension = "html" -- because the html will/might be read to get + if not path.isfile(included_file) then + if not path.isfile(included_file..".tex") then + if not path.isfile(included_file..".sty") then + log:warningf("%-40s includes %s, but this file nor variants with .sty or .tex seem to exits", relfilename, included_file) + else + included_file = included_file..".sty" + end + else + included_file = included_file..".tex" + end + end + log:tracef("%-40s considering included file %s (from %s)", relfilename, path.relpath(included_file, GLOB_root_dir), included_file) + included_file = path.relpath(included_file, GLOB_root_dir) -- make relative path + + -- else + -- log:tracef("Skipping command %s (arg=%s)", command, argument) -- would log all commands in the .tex file .... !!! + end + + if included_file then + + local included_fileinfo = get_fileinfo(included_file) + + log:debugf("%-40s depends on %s", relfilename, included_file) + fileinfo.depends_on_files[included_file] = included_fileinfo + + + -- log:tracef("Getting tex_file_with_status for included file %s", included_file) + update_status_tex_file(included_fileinfo, {wanted_extension}, {wanted_extension} ) + for fname, finfo in pairs(included_fileinfo.depends_on_files) do + if finfo.exists then + log:debugf("%-40s indirectly depends on %s", relfilename, finfo.relative_path) + fileinfo.depends_on_files[finfo.relative_path] = finfo + else + log:warningf("%-40s indirectly depends on non-existing file %s (%s); NOT ADDED TO DEPENDENT FILES", relfilename, finfo.relative_path, finfo.absolute_path) + end + + end + end -- included_file + end -- next command ... + + -- for ximera ducuments, add potential 'default dependensies', ie xmPreamble.tex, ximera.cfg, ... + -- ( only xmPreamble.tex implemented and tested ...!) + -- done here at the end, because tex_documentclass needs to be availbale ... + if fileinfo.tex_documentclass == "ximera" or fileinfo.tex_documentclass == "xourse" then + for _, dep in ipairs(config.default_dependencies or {}) do + local dep_fileinfo = get_fileinfo(dep) + dep_fileinfo.tex_documentclass = false -- HACK: prevent compilation later + if filename ~= dep_fileinfo.absolute_path then + log:tracef("%-40s Adding default dependency %s",filename, dep) + fileinfo.depends_on_files[dep] = dep_fileinfo + else + log:tracef("%-40s Skipping circular default dependency %s",relfilename, dep) + end + end + end + + + local deps = tablex.keys(fileinfo.depends_on_files) + table.sort(deps) + + -- HINT: pipe output/logfile to "grep dependenc" to get all relevant info ...! + if #deps == 0 then + log:debugf("%-40s has no dependencies" , relfilename) + elseif #deps == 1 then + log:debugf("%-40s has %2d dependency %s" , relfilename, #deps, deps[1]) + elseif #deps < 10 then + log:debugf("%-40s has %2d dependencies %s", relfilename, #deps, table.concat(deps,', ')) + else + log:debugf("%-40s has %3d dependencies, namely \ndependency %s", relfilename, #deps, table.concat(deps,",\ndependency ")) + end +end + + +--- sets metadata.output_files and metadata.needs_compilation (for given extensions as html, pdf, ..) +--- @param metadata metadata metadata of the TeX file +--- @param extensions table list of extensions +local function update_output_files(metadata, extensions) + metadata.output_files_needed = {} + local needs_compilation = false + for _, extension in ipairs(extensions) do + local out_file = get_fileinfo(metadata.relative_path:gsub("tex$", extension), true) -- do not use cached info + -- detect if the HTML file needs recompilation + local status = needs_compiling(metadata, out_file) + + -- for some extensions (like sagetex.sage), we need to check if the output file exists + -- and stop the compilation if it doesn't + -- TODO: check sagetex.sage stuff; + -- -- local compiler = compilers[extension] or {} + -- -- if compiler.check_file and not path.exists(out_file.absolute_path) then + if extension == "sagetex.sage" and not path.exists(out_file.absolute_path) then + log:debug("Ignored output file doesn't exist: " .. out_file.absolute_path) + status = false + end + + out_file.needs_compilation = status + out_file.extension = extension + + needs_compilation = needs_compilation or status + + log:debugf("Marked output %-12s %-18s for %s",extension, status and 'NEEDS_COMPILATIONS' or 'NO_COMPILATION', out_file.relative_path) + + -- output_files[#output_files+1] = out_file + metadata.output_files_needed[extension] = out_file + end + metadata.needs_compilation = needs_compilation + + log:infof( "Marked source %-12s %-18s for %s", metadata.extension, needs_compilation and 'NEEDS_COMPILATIONS' or 'NO_COMPILATION', metadata.relative_path) + + if metadata.tex_documentclass == "ximera" or metadata.tex_documentclass == "xourse" + then + metadata.config_file = config.config_file + end + + -- removed finding config-file + +end + +--- create sorted table of files that needs to be compiled +--- @param tex_files metadata[] list of TeX files metadata +--- @return metadata[] to_be_compiled list of files in order to be compiled +local function sort_dependencies(tex_files) + -- create a dependency graph for files that needs compilation + -- the files that include other courses needs to be compiled after changed courses + -- at least that is what the original Xake command did. I am not sure if it is really necessary. + log:tracef("Sorting dependencies") + + local Graph = graph:new() + local used = {} + local to_be_compiled = {} + -- first add all used files + for _, metadata in ipairs(tex_files) do + log:tracef("Consider %s", metadata.relative_path) + + if metadata.needs_compilation then + Graph:add_edge("root", metadata.relative_path) + used[metadata.relative_path] = metadata + end + end + + -- now add edges to included files which needs to be recompiled + for _, metadata in pairs(used or {}) do + local current_name = metadata.relative_path + log:tracef("Get used = %s (%s)",current_name, tablex.keys(metadata.depends_on_files)) + for filename, child in pairs(metadata.depends_on_files or {}) do + -- for _, child in ipairs(metadata.dependecies or {}) do + local name = child.relative_path + log:tracef("Get child = %s",name) + -- add edge only to files added in the first run, because only these needs compilation + if used[name] then + log:tracef("Added edge %s - %s", current_name, name) + Graph:add_edge(current_name, name) + end + end + end + log:tracef("Topographic sort") + + -- topographic sort of the graph to get dependency sequence + local sorted, msg = Graph:sort() + + if not sorted then + log:errorf("Could not sort dependency Graph: %s", msg) + log:errorf("RETURNING UNSORTED LIST") + return tex_files + end + + -- we need to save files in the reversed order, because these needs to be compiled first + -- and delete the dummy 'root' entry + + if not sorted[1] == "root" then + log:errorf("The first entry of the sorted dependency list is NOT the dummy 'root' entry, but %s. BADBAD, this should not have happened...") + else + table.remove(sorted,1) -- remove the dummy entry + end + for i = #sorted, 1, -1 do + local name = sorted[i] + log:tracef("Adding file to be compiled %2d: %-30s (%s)",i ,name, used[name].relative_path) + to_be_compiled[#to_be_compiled+1] = used[name] + end + return to_be_compiled +end + + +--- update the fileinfo ('metadata') of a tex-file, in particular the depends_on_files and output_files, for a given (set of) output_format(s) +--- @param metadata fileinfo fileinfo of TeX-file to be updated, +function update_status_tex_file(metadata, output_formats, compilers) + log:tracef("update_status_tex_file %s (for output_formats=%s and compilers=%s)", metadata.relative_path, table.concat(output_formats,', '), table.concat(compilers,', ')) + + local relfilename = metadata.relative_path + + if metadata.status_updated then + log:tracef("%-40s has already an updated status: SKIPPING" , relfilename) + return + end + + update_depends_on_files(metadata) + if not metadata.tex_documentclass then + log:tracef("%-40s has no documentclass; skipping output", metadata.relative_path) + + else + -- check for the need compilation + update_output_files(metadata, output_formats) + -- 20250109: SKIPPED finding config_file; now set above in update_output_files + end + + metadata.status_updated = true -- prevent reprocessing (e.g. for preambles ...) + + dump_fileinfo(metadata) -- only for debugging +end + +--- find TeX files that needs to be compiled in the directory tree +--- @param dir string root directory where we should find TeX files +--- @return metadata[] tex_files list of all these TeX files AND THE FILES THEY MIGHT DEPEND ON (potentially in OTHER directories) +function get_tex_files_with_status(dir, output_formats, compilers) + log:debugf("Getting tex files in %s (for output_formats=%s and compilers=%s)", dir, table.concat(output_formats,', '), table.concat(compilers,', ')) + local files = get_files(dir, {}) + local tex_files = filter_tex_files(files) + + local tex_fileinfos = {} + -- now check which output files needs a compilation + log:tracef("Start getting status of %d tex files", #tex_files) + for _, metadata in ipairs(tex_files) do + tex_fileinfos[metadata.relative_path] = metadata + log:tracef("Adding main file to tex_fileinfo: %s", metadata.relative_path) + update_status_tex_file(metadata, output_formats, compilers) -- collect depends_on_files ... + for fname, finfo in pairs(metadata.depends_on_files) do + log:tracef("Adding dependend file to tex_fileinfo: %s", fname) + tex_fileinfos[fname] = finfo + end + + end + + return tex_fileinfos +end + +M.get_tex_files_with_status = get_tex_files_with_status +M.update_output_files = update_output_files +M.sort_dependencies = sort_dependencies +M.get_fileinfo = get_fileinfo +M.dump_fileinfo = dump_fileinfo + + +return M diff --git a/.ximera_local/luaxake/luaxake-frost.lua b/.ximera_local/luaxake/luaxake-frost.lua new file mode 100644 index 000000000..6a33675b8 --- /dev/null +++ b/.ximera_local/luaxake/luaxake-frost.lua @@ -0,0 +1,385 @@ +local M = {} +local pl = require "penlight" +local path = require "pl.path" +local html = require "luaxake-transform-html" +local files = require "luaxake-files" +local log = logging.new("frost") + +local json = require("dkjson") + +--- save Ximera metadata.json file (with labels/xourses/...) +--- @param xmmetadata table ximera metadata table +--- @return boolean success +local function save_as_json(xmmetadata) + local file = io.open("metadata.json", "w") + + if file then + local contents = json.encode(xmmetadata) + file:write( contents ) + io.close( file ) + return true + else + return false + end + end + + + +local function osExecute(cmd, no_warnings) + log:debug("Exec: "..cmd) + local fileHandle = assert(io.popen(cmd .. " 2>&1", 'r')) + local commandOutput = assert(fileHandle:read('*a')) + local returnCode = fileHandle:close() and 0 or 1 + commandOutput = string.gsub(commandOutput, "\n$", "") + if returnCode > 0 and not no_warnings then + log:warningf("Command %s returns %d: %s", cmd, returnCode, commandOutput) + end + log:trace("returns "..returnCode..": "..commandOutput..".") + return returnCode, commandOutput +end + +local function get_output_files(file, extension) + local result = {} + for fname, entry in pairs(file.output_files_needed or {}) do + log:tracef("Getting %-14s entry: %s ", file.extension, entry.absolute_path) + + if entry.extension == extension then --and entry.info.type == targetType then + + if extension == "make4ht.html" then -- 20250121: does never occur ??? + local file = files.get_fileinfo(entry.relative_dir .."/" .. entry.basenameshort..".html") + -- require 'pl.pretty'.dump(entry) + -- require 'pl.pretty'.dump(file) + table.insert(result, file) + log:debug(string.format("Hacking %-14s outputfile: %s ", file.extension, file.absolute_path)) + elseif extension == "draft.html" then + local file = files.get_fileinfo(entry.relative_dir .. "/" .. entry.basenameshort..".html") + -- require 'pl.pretty'.dump(entry) + -- require 'pl.pretty'.dump(file) + table.insert(result, file) + log:debug(string.format("Hacking %-14s outputfile: %s ", file.extension, file.absolute_path)) + else + table.insert(result, entry) + log:debug(string.format("Adding %-14s outputfile: %s ", entry.extension, entry.absolute_path)) + end + else + log:tracef("Skipping %-14s outputfile: %s ", entry.extension, entry.absolute_path) + end + end + return result +end + +local function get_git_uncommitted_files() + -- local ret, out = osExecute("git ls-files --modified --other --exclude-standard") + local ret, out = osExecute("git status --porcelain") + if ret > 0 then + log:warningf("Could not get git info: %s",out) + out = "GIT ERROR" + end + local utils = require "pl.utils" + return utils.split(out,"\n") +end + + +--- Frosting: create a 'publications' commit-and-tag +---@param file metadata -- presumably only root-folder really makes sense for 'frosting' +---@return boolean status +---@return string? msg +local function frost(tex_files, to_be_compiled_files) + log:debug("frost") + + local uncommitted_files = get_git_uncommitted_files() + + if #uncommitted_files > 0 then + log:warningf("There are %d uncommitted files; should serve only to localhost", #uncommitted_files) + end + + if #to_be_compiled_files > 0 then + log:warningf("There are %d file to be compiled; should serve only to localhost", #to_be_compiled_files) + end + + local needing_publication = {} + local all_labels = {} + local tex_xourses = {} + for i, tex_file in ipairs(tex_files) do + log:debugf("Getting output for %s (%s)", tex_file.absolute_path, tex_file.relative_path) + -- add the .tex file itself (might not always be needed/wanted... ? It typically shows all solutions ...) + needing_publication[#needing_publication + 1] = tex_file.relative_path + + -- note: .pdf's are added through ximera-downloads for now ... ! + + -- add a .css if present + local css_file = tex_file.relative_path:gsub(".tex$",".css") + if path.exists(css_file) then + log:debugf("Added file-specific css file %s", css_file) + needing_publication[#needing_publication + 1] = css_file + end + + -- add the .html + local html_file + if tex_file.relative_path:match("_pdf.tex") or tex_file.relative_path:match("_beamer.tex") then + log:tracef("No html output for _pdf or _beamer files: skipping %s", tex_file.relative_path) + else + if not tex_file.output_files_needed then + log:debugf("No output_files_needed for %s, and thus no HTML",tex_file.relative_path) + else + html_file = tex_file.output_files_needed.html + + log:debugf("Processing %s", html_file.relative_path) + + needing_publication[#needing_publication + 1] = html_file.relative_path + + -- local html_name = html_file.absolute_path + if not html_file.associated_files then + log:debugf("Collecting extra fileinfo for %s", html_file.relative_path) + local ret, msg = html.update_html_fileinfo(html_file) -- get labels etc + if ret then + log:errorf("FAILED to get associated files etc. for %s", html_file.relative_path) + return ret, msg + end + end + -- now we have added labels, associated_files, title and abstract in html_file + + -- merge all labels in a big table, to be added to metadata.json + for k,v in pairs(html_file.labels) do + if all_labels[k] then + log:warningf("Label %s already used in %s; ignoring for %s",k, all_labels[k], html_file.relative_path) + else + all_labels[k] = html_file.relative_path + log:tracef("Label %s added for %s",k,html_file.relative_path) + end + end + + -- add all associated_files (images!) to needing_publication + table.move(html_file.associated_files, 1, #(html_file.associated_files), #needing_publication + 1, needing_publication) + + + log:debug(string.format("Added %4d files for new total of %4d for %s", #(html_file.associated_files)+2, #needing_publication, html_file.relative_path)) + -- require 'pl.pretty'.dump(to_be_compiled) + + -- Store xourses, they have to be added explicitly to metadata.json + if tex_file.tex_documentclass == "xourse" then + log:info("Adding XOURSE "..tex_file.relative_path.." ("..html_file.title..")") + tex_xourses[html_file.relative_path:gsub(".html","")] = { title = html_file.title, abstract = html_file.abstract } + end + end + end + end + + if path.exists("global.css") then + log:debugf("Added global.css file") + needing_publication[#needing_publication + 1] = "global.css" + end + + + -- TODO: check/fix use of 'github'; check use of labels + local xmmetadata={ + xakeVersion = "2.5", + labels = all_labels, + github = {}, + xourses = tex_xourses, + } + + save_as_json(xmmetadata) + -- require 'pl.pretty'.dump(tex_xourses) + needing_publication[#needing_publication + 1] = "metadata.json" + + -- + -- START ACTUAL FROSTING (ie, creating a 'publication tag') + -- + local _, head_oid = osExecute("git rev-parse HEAD") + if not head_oid then + log:error("No headid returned by git rev-parse HEAD") + end + + local publication_branch = "PUB_"..head_oid + + local ret, publication_oid = osExecute("git rev-parse --verify --quiet "..publication_branch) + if ret > 0 then -- publication_branch does noy (yet) exist: create it + osExecute("git branch "..publication_branch) + publication_oid = head_oid + end + log:debug("GOT publication_oid "..(publication_oid or "")) + + if path.exists("ximera-downloads") then + osExecute("git add -f ximera-downloads") + else + log:debug("No ximera-downloads folder, and thus no PDF files will be made available for download") + end + -- require 'pl.pretty'.dump(needing_publication) + + -- 'git add' the files in batches of 10 (risks line-too-long!) + -- local files_string = table.concat(needing_publication,",") + -- Execute the git add command + + -- local downloads = list_files("ximera-downloads") + -- table.move(downloads, 1, #downloads, #needing_publication + 1, needing_publication) + +-- if false then +-- local f = io.open(".xmgitindexfiles", "w") + +-- for _, line in ipairs(needing_publication) do +-- log:trace("ADDING "..line) +-- f:write(line .. "\n") +-- end +-- f:close() +-- -- Close the process to flush stdin and complete execution +-- local proc = io.popen("cat .xmgitindexfiles | git update-index --add --stdin") +-- local output = proc:read("*a") +-- local success, reason, exit_code = proc:close() + +-- if not success then +-- log:errorf("git update-index fails with %s (%d)",reason, exit_code) +-- else +-- log:debugf("Added %d files (%s)", #needing_publication,output) +-- end + +-- else + for _, line in ipairs(needing_publication) do + log:trace("ADDING "..line) + osExecute("git add -f "..line) + end +-- end + + + local _, new_tree = osExecute("git write-tree") + if not new_tree then + log:error("No tree returned by git write-tree") + end + log:debug("Made new tree ", new_tree) + + -- local tagName = "publications/"..head_oid + -- result, tag_oid = osExecute("git for-each-ref --sort=-creatordate --count=1 --format '%(refname:strip=2)' refs/tags/publications/*") + + local result, most_recent_publication = osExecute("git for-each-ref --sort=-creatordate --count=1 --format '%(tree) %(objectname) %(refname:strip=2)' refs/tags/publications/*") + + local tagtree_oid, tag_oid,tagName + if not most_recent_publication or most_recent_publication == "" then + log:info("No publication tag found") + else + log:debugf("Got publication: %s",most_recent_publication) + + tagtree_oid, tag_oid, tagName = most_recent_publication:match("([^%s]+) ([^%s]+) ([^%s]+)") + + log:infof("Found %s (tree:%s tag:%s) ", tagName, tagtree_oid, tag_oid) + end + + if tagtree_oid and tagtree_oid == new_tree then + log:statusf("Tag "..tagName.." already exists (for %s)",tag_oid) + return 0, 'Reusing '..tagName + end + + -- Give a dummy account to push/commit if none is available + ret, output = osExecute("git config --get user.name || { echo Setting container-global git user.name; git config --global user.name 'xmlatex Xake'; }") + ret, output = osExecute("git config --get user.email || { echo Setting container-global git user.email; git config --global user.email 'xmlatex@xakecontainer'; }") + + local ret, commit_oid = osExecute("git commit-tree -m "..publication_branch.." -p "..publication_oid.." "..new_tree) + if ret > 0 then + return ret, commit_oid -- this is the errormessage in this case! + end + log:debug("GOT commit "..(commit_oid or "")) + + if logging.show_level <= logging.levels["trace"] then + log:tracef("Committed files for %s:", commit_oid) + osExecute("git ls-tree -r --name-only "..commit_oid) + end + + local ret, output = osExecute("git reset") + + -- TODO: check this, we might be creating too many commits/.. + if false and tagtree_oid then + log:statusf("Updating tag %s for %s (was %s)", tagName, commit_oid, tag_oid) + ret, output = osExecute("git update-ref refs/tags/"..tagName.." "..commit_oid) + else + --local tagName = "publications/"..os.date("%Y%m%d_%H%M%S") + tagName = "publications/"..commit_oid + log:statusf("Creating tag %s for %s", tagName, commit_oid) + ret, output = osExecute("git tag "..tagName.." "..commit_oid) + -- if ret > 0 then + -- log:errorf("Created tag %s for %s: %s", tagName, commit_oid, output) + -- end + end + + -- restore ownership of files: committing INSIDE a container creates files owned by root ! + local testfile = ".git" -- file from which to get the original ownership; + local attributes = lfs.attributes(testfile) + + if not attributes then + log:warningf("Could not determine owner of .git folder....") + elseif attributes.uid == 0 then + log:warningf("BIZAR: .git folder owned by root ...? Skipping resetting ownership.") + else + local set_uidgid = attributes.uid ..":".. attributes.gid + log:debugf("Resetting ownership af all files to uid:gid %s", set_uidgid) + ret, output = osExecute("chown -R " .. set_uidgid .. " .") + end + + if ret > 0 then -- TODO: check/correct usage ret value(s) + return ret, output + else + return 0, "Created "..tagName + end +end + +local function serve(force_serving) + + local result, most_recent_publication = osExecute("git for-each-ref --sort=-creatordate --count=1 --format '%(tree) %(objectname) %(refname:strip=2)' refs/tags/publications/*") + + if not most_recent_publication or most_recent_publication == "" then + log:warning("No publication tags found. Need 'frost' first?") + return 1, 'No publications found' + end + + log:debugf("Got publication: %s",most_recent_publication) + + local ret, remote_ximera = osExecute("git remote get-url ximera") + + if ret > 0 then + log:warning("No remote 'ximera' found. Need 'name' first?") + return 1, "No remote 'ximera' found" + end + if remote_ximera:match("localhost") then + log:infof("Publishing to localhost: %s", remote_ximera) + force_serving = true + end + + local tree_oid, tag_oid, tagName = most_recent_publication:match("([^%s]+) ([^%s]+) ([^%s]+)") + + log:debugf("Publishing %s (tree:%s tag:%s) ", tagName, tree_oid, tag_oid) + + -- do not warn-on-error + local ret, output = osExecute("git push ximera "..tagName, true) + if ret > 0 then + log:tracef("Could not push to 'ximera' target: %s",output) + if not force_serving then + return ret, output + else + log:infof("Retrying push with more power (git push -f ...)") + ret, output = osExecute("git push -f ximera "..tagName) + if ret > 0 then + return ret,output + end + end + end + local ret, output = osExecute("git push ximera "..tag_oid..":refs/heads/master", true) -- HACK ??? + if ret > 0 then + log:tracef("Could not push refs to 'ximera' target: %s",output) + if not force_serving then + return ret,output + else + log:infof("Retrying push with more power (git push -f ximera "..tag_oid..":refs/heads/master)") + ret, output = osExecute("git push -f ximera "..tag_oid..":refs/heads/master") + if ret > 0 then + return ret,output + end + end + end + + log:debugf("Published %s to %s", tagName, remote_ximera) + return 0, "Published " .. tagName .. "to " .. remote_ximera:gsub(".git","") +end + +M.frost = frost +M.serve = serve +M.osExecute = osExecute + +return M diff --git a/.ximera_local/luaxake/luaxake-graph.lua b/.ximera_local/luaxake/luaxake-graph.lua new file mode 100644 index 000000000..38a94e24b --- /dev/null +++ b/.ximera_local/luaxake/luaxake-graph.lua @@ -0,0 +1,125 @@ +local Graph = { nodes = {} } + +function Graph:new() + setmetatable(Graph, self) + self.__index = self + return self +end + +--- initialize graph node +---@param a string +---@return table +function Graph:add_node(a) + local node = self.nodes[a] or { edges = {}, edges_from = {}} + self.nodes[a] = node + return node +end + +--- add edge between nodes +---@param a string parent node +---@param b string child node +---@param value? any +function Graph:add_edge(a,b, value) + -- get the parent edge + local parent = self:add_node(a) + -- initialize also the second node + local child = self:add_node(b) + -- we can either add a value, or just simply use true + value = value or true + parent.edges[b] = value + child.edges_from[a] = value +end + +--- get node +---@param a string node name +---@return table node +function Graph:get_node(a) + return self.nodes[a] +end + +--- count number of items in a hash table +---@param tbl table hash table to be counted +---@return number count +local function count_table(tbl) + local count = 0 + for _, _ in pairs(tbl) do count = count + 1 end + return count +end + +--- count number of edges pointing to the given node +---@param a string node +---@return number count of edges +function Graph:count_incoming_edges(a) + return count_table(self.nodes[a].edges_from) +end + + + +function Graph:sort() + -- based on Kahn's algorithm + local L = {} + local S = {} + local all_edges = {} + -- prepare table with all edges that points to the given node + for name, node in pairs(self.nodes) do + local t = {} + for edge, _ in pairs(node.edges_from) do t[edge] = true end + all_edges[name] = t + end + -- get list of nodes with no incoming edges + for name, _ in pairs(self.nodes) do + if self:count_incoming_edges(name) == 0 then S[#S+1] = name end + end + while #S > 0 do + -- remove first entry from the list of nodes with no incoming edges + local n = table.remove(S, 1) + L[#L+1] = n + local node = self:get_node(n) + -- find all nodes that are children of n + for m, _ in pairs(node.edges) do + -- get edges that point to m + local edges = all_edges[m] + if edges[n] then + -- remove edge from n to m + edges[n] = nil + -- if there are no other edges, we need to process this node in the next run of this loop + if count_table(edges) == 0 then + S[#S+1] = m + end + end + end + end + -- test if we removed all edges + local count = 0 + for _, tbl in pairs(all_edges) do + count = count + count_table(tbl) + end + if count > 0 then + return nil, "Graph has "..count.." cycles" + end + return L +end + +--------------------- +-- Example of use: -- +-- ------------------ +-- local graph = Graph:new() +-- +-- +-- graph:add_edge("a", "b") +-- graph:add_edge("a", "c") +-- graph:add_edge("b", "d") +-- graph:add_edge("c", "d") +-- graph:add_edge("d", "e") +-- graph:add_edge("b", "e") +-- +-- for _, name in ipairs(graph:sort()) do +-- print(name) +-- end +-- + +return Graph + + + + diff --git a/.ximera_local/luaxake/luaxake-logging.lua b/.ximera_local/luaxake/luaxake-logging.lua new file mode 100644 index 000000000..e10340996 --- /dev/null +++ b/.ximera_local/luaxake/luaxake-logging.lua @@ -0,0 +1,154 @@ +-- logging system for luaxake, minor adaptation from make4ht +-- inspired by https://github.com/rxi/log.lua +local logging = {} + +local levels = {} +-- level of bugs that should be shown +-- enable querying of current log level +logging.show_level = 1 +local max_width = 0 +local max_status = 0 + +logging.use_colors = true + +logging.modes = { + {name = "trace", color = 36}, + {name = "debug", color = 34}, + {name = "info", color = 37}, + {name = "status", color = 32}, + {name = "warning", color = 33}, + {name = "error", color = 31, status = 1}, + {name = "fatal", color = 35, status = 2} +} + +-- local posix = require("posix") -- not in tex ? +-- local ffi = require("ffi") +-- ffi.cdef[[ +-- int getpid(); +-- ]] +local pid = os.time() +-- Map the pid to a sting between AA - ZZ, eg as follows: +local firstLetter = string.char(65 + (pid % 26)) -- First letter (A-Z) +local secondLetter = string.char(65 + ((pid // 26) % 26)) -- Second letter (A-Z) + +local run_identifier = firstLetter .. secondLetter +-- logging.use_runidentifier = true +logging.dateformat = run_identifier.."_%Y%m%d %H:%M:%S" + + + +-- prepare table with mapping between mode names and corresponding levels + +function logging.prepare_levels(modes) + local modes = modes or logging.modes + logging.modes = modes + for level, mode in ipairs(modes) do + levels[mode.name] = level + mode.level = level + max_width = math.max(string.len(mode.name), max_width) + end + logging.levels = levels + +end + +-- the logging level is set once +function logging.set_level(name) + local level = levels[name] or 1 + logging.show_level = level +end + +function logging.set_outfile(name) + logging.outfile = name +end + +function logging.print_msg(header, message, color) + local color = color or 0 + -- use format for colors depending on the use_colors option +-- local header = "[" .. header .. "]" + local color_format = logging.use_colors and string.format("\27[%im%%s\27[0m%%s", color) or "%s%s" + -- the padding is maximal mode name width + brackets + space + local padded_header = string.format("[%-".. max_width .. "s] ", header) + local output= string.format(color_format, padded_header, message) + print(output) + if logging.outfile then + local output= string.format("%s%s", padded_header, message) -- no color ! + local fp = io.open(logging.outfile, "a") + local str = string.format("%s: %s\n", os.date(logging.dateformat), output) + fp:write(str) + fp:close() + end +end + +-- +function logging.new(module) + local obj = { + module = module, + output = function(self, output) + -- used for printing of output of commands + if logging.show_level <= (levels["debug"] or 1) then + print(output) + end + end + } + obj.__index = obj + -- make a function for each mode + for _, mode in ipairs(logging.modes) do + local name = mode.name + local color = mode.color + local status = mode.status or 0 + obj[name] = function(self, ...) + -- set make4ht exit status + max_status = math.max(status, max_status) + -- max width is saved in logging.prepare_levels + if mode.level >= logging.show_level then + -- support variable number of parameters + local table_with_holes = table.pack(...) + local table_without_holes = {} + -- trick used to support the nil values in the varargs + -- https://stackoverflow.com/a/7186820/2467963 + for i= 1, table_with_holes.n do + table.insert(table_without_holes, tostring(table_with_holes[i]) or "") + end + local msg = table.concat(table_without_holes, "\t") + logging.print_msg(string.upper(name), string.format("%7s: %s", self.module, msg), color) + end + end + obj[name.."f"] = function(self, ...) + -- set make4ht exit status + max_status = math.max(status, max_status) + -- max width is saved in logging.prepare_levels + if mode.level >= logging.show_level then + local msg = string.format(...) + logging.print_msg(string.upper(name), string.format("%7s: %s", self.module, msg), color) + end + end + end + return setmetatable({}, obj) + +end + +-- exit make4ht with maximal error status +function logging.exit_status() + os.exit(max_status) +end + + +-- prepare default levels +logging.prepare_levels() + +-- for _, mode in ipairs(logging.modes) do +-- logging.print_msg(mode.name,"xxxx", mode.color) +-- end + +-- local cls = logging.new("sample") +-- cls:warning("hello") +-- cls:error("world") +-- cls:info("set new level") +-- logging.set_level("error") +-- cls:info("level set") +-- cls:error("just print the error") +-- + + +return logging + diff --git a/.ximera_local/luaxake/luaxake-transform-html.lua b/.ximera_local/luaxake/luaxake-transform-html.lua new file mode 100644 index 000000000..c0af76b2c --- /dev/null +++ b/.ximera_local/luaxake/luaxake-transform-html.lua @@ -0,0 +1,585 @@ +-- post-process HTML files created by TeX4ht to a form suitable for Ximera +local M = {} +local log = logging.new("html") +local domobject = require "luaxml-domobject" +local pl = require "penlight" +local path = require "pl.path" + +local url = require("socket.url") +-- local url = require "lualibs-url" + +function getRelativePath(base, targ) + -- Normalize paths by ensuring both are absolute + local function normalize(path) + -- Remove trailing slashes for consistency + return path:gsub("/$", ""):gsub("\\$", "") + end + + base = normalize(base) + targ = normalize(targ) + + -- Split paths into components + local baseParts = {} + local targParts = {} + + for part in base:gmatch("[^/\\]+") do + table.insert(baseParts, part) + end + + for part in targ:gmatch("[^/\\]+") do + table.insert(targParts, part) + end + + -- Find the common prefix + local i = 1 + while i <= #baseParts and i <= #targParts and baseParts[i] == targParts[i] do + i = i + 1 + end + + -- Calculate the number of steps to go up from base + local upSteps = #baseParts - i + 1 + local relativeParts = {} + + -- Add `..` for each step up + for _ = 1, upSteps do + table.insert(relativeParts, "..") + end + + -- Add the remaining parts of the target path + for j = i, #targParts do + table.insert(relativeParts, targParts[j]) + end + + -- Join the relative parts into a path + return table.concat(relativeParts, "/") +end + + +local html_cache = {} + +--- load DOM from a HTML file +---@param filename string +---@return DOM_Object|nil dom +---@return string? error_message +local function load_html(filename) + -- cache DOM objects + if false and html_cache[filename] then -- does not work with RECOMPILE ... + log:trace("returning cached dom ") + -- require 'pl.pretty'.dump(domobject.html_parse(content)) + return html_cache[filename] + else + log:debug("Loading and parsing html for "..filename) + local f = io.open(filename, "r") + if not f then return nil, "Cannot open HTML file: " .. (filename or "") end + -- log:debug("Opened html for "..filename) + local content = f:read("*a") + f:close() + -- log:debug("Dumping html for "..filename..": "..content) + html_cache[filename] = domobject.html_parse(content) + log:trace("returning non-cached dom ") + return domobject.html_parse(content) + end +end + +--- detect if the HTML file is xourse +---@param dom DOM_Object +---@return boolean +local function is_xourse(dom, filename) + local metas = dom:query_selector("meta[name='description']") + if #metas == 0 then + log:debug("No meta[description] tags in " .. filename .. " (and thus not a xourse)") + end + for _, meta in ipairs(metas) do + if meta:get_attribute("content") == "xourse" then + log:debug("File "..filename.." is a xourse") + return true + else + log:debug("File "..filename.." has not-a-xourse description tag "..(meta.get_attribute("content") or "")) + end + end + -- log:debug("File "..filename.." is not a xourse ") + return false +end + +local function is_element_empty(element) + -- detect if element is empty or contains only blank spaces + local children = element:get_children() + if #children > 1 then return false + elseif #children == 1 then + if children[1]:is_text() then + if children[1]._text:match("^%s*$") then + return true + end + return false + end + return false + end + return true + +end + +--- Remove empty paragraphs +---@param dom DOM_Object +local function remove_empty_paragraphs(dom) + for _, par in ipairs(dom:query_selector("p")) do + if is_element_empty(par) then + log:trace("Removing empty par") + par:remove_node() + end + end +end + +local function read_title_and_abstract(activity_dom) + local title, abstract + local title_el = activity_dom:query_selector("title")[1] + if title_el then title = title_el:get_text() end + log:trace("Read title ", title) + local abstract_el = activity_dom:query_selector("div.abstract")[1] + if abstract_el then + abstract = abstract_el:get_text() + -- log:trace("Read abstract ", abstract) + end + return title, abstract +end + +local function get_labels(activity_dom) + local labels = {} + + if not activity_dom then + log:warning("Passed nil to get_labels...? No labels returned.") + return {} + end + for i, anchor in ipairs(activity_dom:query_selector("a.ximera-label")) do + -- require 'pl.pretty'.dump(anchor) + local label = anchor:get_attribute("id") + labels[label] = (labels[label] or 0) + 1 + log:tracef("Found label %s", label ) + if labels[label] > 1 then + log:warning("Duplicate label ",label) + end + end + return labels +end + +--- Transform Xourse files +---@param dom DOM_Object +---@param file fileinfo +---@return DOM_Object +local function transform_xourse(dom, file) + for _, activity in ipairs(dom:query_selector("a.activity")) do + local href = activity:get_attribute("href") + log:trace("Updating srv/title/abstract for activity", href) + if not href then + log:warningf("Bizar, an activity without href in %s? Nothing to process...", file.relative_path) + goto next_activity + end + -- some activity links don't have links to HTML files + -- remove the optional '.tex' + local newhref = href + if path.extension(href) == ".tex" then newhref, _ = path.splitext(href) end + -- add .html if no extension (anymore) + if path.extension(newhref) == "" then newhref = newhref .. ".html" end + + local relhref = file.relative_dir.."/"..newhref + relhref = relhref:gsub("^/","") -- remove leading / + if relhref:gsub(".html$","") ~= href then + -- The .html extension breaks the previous/next buttons in ximeraServer + log:debug("Resetting href to "..relhref:gsub(".html$","") .. "( from "..href..")") + activity:set_attribute("href",relhref:gsub(".html$","")) + end + + -- the absolute path to .html of the linked activity + local abshtmlpath = path.join(file.absolute_dir, newhref) + local relhtmlpath = path.join(file.relative_dir, newhref) + local title, abstract + + if not path.exists(abshtmlpath) then + log:errorf("File %s: html file for activity %s does not (yet?) exist; SKIPPING add/update title and abstract", file.relative_path, abshtmlpath) + goto next_activity + end + + -- add the title and abstract of the activity to the xourse file ... + -- TODO: these could already be in the fileinfo of abshtmlpath, which would prevent reading/parsing the .html here (again...) + + -- add titles and abstracts from linked activity HTML + local html_fileinfo = GLOB_files[relhtmlpath] + if false and html_fileinfo then -- TODO : DOES NOT WORK (file not (yet?) there + log:debug("Found cached fileinfo for "..abshtmlpath) + title = html_fileinfo.title + abstract = html_fileinfo.abstract + + else + log:debugf("File %s: no fileinfo yet for activity %-30s; Getting it now.", file.relative_path, relhtmlpath) + + local activity_dom, msg = load_html(abshtmlpath) + if not activity_dom then + log:error(msg) + goto next_activity + end + title, abstract = read_title_and_abstract(activity_dom) + end + + + if title and title ~= "" then + local h2 = activity:create_element("h2") + local h2_text = h2:create_text_node(title ) + log:trace("Adding title for "..href..": "..title) + h2:add_child_node(h2_text) + activity:add_child_node(h2,1) + else + log:debug("No title found for "..href) + end + -- the problem with abstract is that Ximera redefines \maketitle in TeX4ht to produce nothing, + -- abstract in Ximera is part of \maketitle, so abstracts are missing in the generated HTML + if abstract then + --require 'pl.pretty'.dump(abstract) + local h3 = activity:create_element("h3") + local h3_text = h3:create_text_node(abstract) + log:trace("Adding abstract (h3) for "..href..": "..abstract:gsub("\n","")) + h3:add_child_node(h3_text) + activity:add_child_node(h3,1) + end + ::next_activity:: + end + + return dom +end + +--- return sha256 digest of a file +---@param filename string +---@return string|nil hash +---@return unknown? error +local function hash_file(filename) + -- Xake used sha1, but we don't have it in Texlua. On the other hand, sha256 is built-in + local f = io.open(filename, "r") + if not f then return nil, "Cannot open TeX dependency for hashing: " .. (filename or "") end + local content = f:read("*a") + f:close() + -- the digest return binary code, we need to convert it to hexa code + local bincode = sha2.digest256(content) + local hexs = {} + for char in bincode:gmatch(".") do + hexs[#hexs+1] = string.format("%X", string.byte(char)) + end + return table.concat(hexs) +end + + + +--- Add fileinfo with TeX file dependencies to the HTML DOM +---@param dom DOM_Object +---@param file fileinfo +---@return DOM_Object +local function add_dependencies(dom, file) + -- we will add also TeX file of the current HTML file + local t = {file} + -- copy dependencies, as we have an extra entry of the current file + for _, x in ipairs(file.dependecies) do t[#t+1] = x end + local head = dom:query_selector("head")[1] + if not head then log:error("Cannot find head element " .. file.absolute_path:gsub("tex$", "html")) end + for _, dependency in ipairs(file.dependecies) do + log:debug("dependency", dependency.relative_path, dependency.filename, dependency.basename) + local hash, msg = hash_file(dependency.absolute_path) + if not hash then + log:warning(msg) + else + local content = hash .. " " .. dependency.filename + local meta = head:create_element("meta", {name = "dependency", content = content}) + local newline = head:create_text_node("\n") + head:add_child_node(meta) + head:add_child_node(newline) + end + + end + return dom +end + + + +--- get file extension +--- @param relative_path string file path +--- @return string extension +local function get_extension(relative_path) + return relative_path:match("%.([^%.]+)$") +end + + + +--- Get all files 'associated' with a given file (i.e. images) +---@param dom DOM_Object +---@param file fileinfo +---@return ret , table +local function get_associated_files(dom, file) + log:tracef("get_associated_files for %s", file.filename) + -- pl.pretty.dump(file) + + if not dom then + log:tracef("Passed nil to get_associated_files for %s...? No files returned.", file.filename or "") + return 1, "Passed nil to get_associated_files for " .. file.filename or "" + end + + local ass_files = {} + local ass_errors = {} + local isXimeraFile = dom:query_selector("meta[name='ximera']")[1] + if not isXimeraFile then + log:warning(file.filename.." is not a ximera file (no meta[name='ximera' tag])") + end + + -- Add images + for _, img_el in ipairs(dom:query_selector("img") ) do + local src = img_el:get_attribute("src") + -- log:tracef("Found img %s in %s (%s)", src, file.relative_dir, file.relative_path ) + -- src = (file.relative_dir or ".").."/"..src + src = path.join(file.relative_dir, src) + log:debugf("Found img %s in %s", src, file.absolute_path ) + + if not path.exists(path.join(GLOB_root_dir, src)) then -- BADBAD: this might got processed after chdir in compile ....! + log:errorf("Image file %s does not exist (%s)", src, path.join(GLOB_root_dir, src)) + ass_errors[#ass_errors+1] = src .. " does not exist" + goto next_image + + end + if path.getsize(path.join(GLOB_root_dir, src)) == 0 then + log:errorf("Image file %s has size zero", path.join(GLOB_root_dir, src)) + ass_errors[#ass_errors+1] = src .. " has size zero" + goto next_image + end + + ass_files[#ass_files+1] = src + + -- local u = url.parse(src) + -- if false and get_extension(u.path) == "svg" + -- then + -- local png = u.path:gsub(".svg$", ".png") + -- log:debug("also adding "..png) + -- ass_files[#ass_files+1] = png + -- end + + ::next_image:: + end + + if #ass_errors > 0 then + log:warningf("Got %d errors for associateds file (images) of %s", #ass_errors, file.relative_path) + table.insert(ass_errors,1,"") -- HACK to get also the first error on a newline ... + return 1, "Error(s):".. table.concat(ass_errors, "\n ERROR ->") + else + log:debugf("Got %d associated files (images) for %s", #ass_files, file.relative_path) + return nil, ass_files + end +end + + + +--- Update 'fileinfo' of html file +---@param fileinfo fileinfo +---@param dom? DOM_Object +---@return ret, msg +local function update_html_fileinfo(fileinfo, dom) + log:tracef("update_html_fileinfo for %s", fileinfo.filename) + + local html_file = fileinfo.relative_path + + local msg + + -- if dom not passed, get it ... + if not dom then + dom, msg = load_html(fileinfo.absolute_path) + if not dom then + log:tracef("No dom for %s (%s). SKIPPING", html_file, msg) + return 1, "No dom loaded for " .. html_file .. ": " .. msg + end + end + +-- collect info (for frosting...) + fileinfo.labels = get_labels(dom) + + -- check if e.g. images can be found... + local ret, associated_files = get_associated_files(dom, fileinfo) + + if ret then + log:tracef("ERROR: Could not get associated files (images) for %s: %s", html_file, associated_files ) + return ret, associated_files + end + + fileinfo.associated_files = associated_files + + local title, abstract = read_title_and_abstract(dom) + fileinfo.title = title or "" + fileinfo.abstract = abstract or "" + + return nil,"OK" +end + +--- Save DOM to file +---@param dom DOM_Object +---@param filename string +--- returns nil on success, errormessage on failure +local function save_html(dom, filename) + local f, err = io.open(filename, "w") + if not f then + return "Cannot save updated HTML to " .. (filename or "" .. ": ".. err) + end + f:write(dom:serialize()) + f:close() + return nil +end + + +--- Post-process HTML files +---@param cmd command +---@return string? name of post_processed file (could be same name/file is src, or not...) +---@return string? msg (if error: name in nil) +-- local function post_process_html(src, file, cmd_meta) +local function post_process_html(cmd) + local file = cmd.file + local src= cmd.output_file + local extension = cmd.command_metadata.extension + + log:tracef("post_process_html %s (%s)",src, file.relative_path) + + local dom, msg = load_html(src) + if not dom then + cmd.status_post_command = "NO_HTML_DOM_FOUND" + cmd.error = msg + return cmd + end + + + local ret, msg = update_html_fileinfo(file, dom) -- not really 'post-processing', but implicit checking-of-generated-images + -- BADBAD: inconsistent error-conventions .... ! + if ret then + cmd.status_post_command = ret + cmd.error = msg + return cmd + end + + if file.has_title and ( not file.title or file.title == "" ) then + if not cmd.this_is_a_retry then + log:warningf("No title found in %s; recompiling once more might solve this ...", file.relative_path) + cmd.status_post_command = "RETRY_COMPILATION" + else + log:warningf("No title found in %s; recompiling did not work to solve this ...", file.relative_path) + cmd.status_post_command = "NO_TITLE_FOUND" + cmd.error = msg + end + return cmd + end + + remove_empty_paragraphs(dom) + --add_dependencies(dom, file) -- IS THIS NEEDED??? + + log:debug("Remove blanks in '\\begin {' if present") + for _, mjax in ipairs(dom:query_selector(".mathjax-inline, .mathjax-block")) do + local mtext = mjax:get_text() + mtext = mtext:gsub("\\begin%s*{", "\\begin{") + mtext = mtext:gsub("\\end%s*{", "\\end{") + if mtext ~= mjax:get_text() then + log:tracef("Set mtext to %30.30s.", mtext:gsub("[\n \t]+"," ")) + mjax.textContent = mtext + end + end + + log:trace("Process .xmjax file if present") + local jax_file = src:gsub(".html$", ".xmjax") + if not path.exists(jax_file) then + log:warning("Strange: no JAX file with extra LaTeX commands for MathJAX") + jax_file = nil + end + + if jax_file then + + local preambles = dom:query_selector("div.preamble") + + if #preambles == 0 then + -- Should not happen ... + log:error("No div.preamble in html : please add one") + end + + local preamble = preambles[1] + local scrpt = preamble:create_element("script") + scrpt:set_attribute("type", "math/tex") + + + local f = io.open(jax_file, "r") + local cmds = f:read("*a") + f:close() + + -- Function to keep only lines starting with \new + local function filter_newcommands(text) + local result = {} + for line in text:gmatch("[^\r\n]+") do + if line:match("^\\newcommand {") or line:match("^\\DeclareMathOperator") or line:match("^\\newenvironment") then + table.insert(result, line) + end + end + return table.concat(result, "\n") + end + + local filtered_cmds = cmds + filtered_cmds= filtered_cmds:gsub("[^\n]*[:*@].-\n", "") -- remove all 'exotic' characters; _ must be kept... + filtered_cmds= filtered_cmds:gsub("[^\n]\\_.-\n", "") -- remove \_ (Mathax error) + filtered_cmds= filtered_cmds:gsub("[^\n]\\TU.-\n", "") -- remove \_ (Mathax error) + filtered_cmds= filter_newcommands(filtered_cmds) -- only keep newcommands and declaremathoperator + filtered_cmds= filtered_cmds:gsub("##(%d)", "#%1") -- replace ##1 with #1 + + local _, n_cmds = cmds:gsub("\n","") + local _, n_filtered_cmds = filtered_cmds:gsub("\n","") + + log:debugf("Adding %d newcommands (from %s, %d filtered)",n_filtered_cmds, jax_file, n_cmds - n_filtered_cmds) + + local scrpt_text = scrpt:create_text_node(filtered_cmds) + scrpt:add_child_node(scrpt_text) + preamble:add_child_node(scrpt) + + end + + + if is_xourse(dom, src) then -- not needed anymore, was already determened from .tex source ??? + -- if file.tex_documentclass == "xourse" then + transform_xourse(dom, file) + + + log:debug("Checking if a 'part' is present") + local part = dom:query_selector(".card.part") + + if #part == 0 then + log:info("No parts found, adding one, as this is needed in (some versions of) the ximeraServer") + + local body = dom:query_selector("body")[1] + local first_activity = dom:query_selector(".card.activity")[1] + if first_activity then + log:debug("Adding default card of type 'part' (HACK: needed by current preview server)") + local h1 = body:create_element("h1") + local h1_text = h1:create_text_node("Main Part") + h1:add_child_node(h1_text) + h1:set_attribute("class", "card part") + body:add_child_node(h1,6) -- the 3 is a guess + else + log:warning("BIZAR: No 'activity' card found to add a dummy 'part' to ??? ") + end + end + --

The First Topic of This Course

+ end + + -- save with absolute path to be independent of chdir's in compilation-step ... + local abstgt = path.join(file.absolute_dir, file.basename ..".".. extension) + local reltgt = path.join(file.relative_dir, file.basename ..".".. extension) + + log:infof("Adapted html being saved as %s (%s)", reltgt, abstgt) + + local msg = save_html(dom, abstgt) + if err then + cmd.status_post_command = "NO_COULD_NOT_SAVE" + cmd.error = msg + return cmd + end -- return failure + + cmd.output_file_final = reltgt + cmd.status_post_command = "OK" + return cmd -- return success: relative path of post_processed file +end + +M.post_process_html = post_process_html +M.update_html_fileinfo = update_html_fileinfo + +return M diff --git a/.ximera_local/luaxake/make4ht-errorlogparser.lua b/.ximera_local/luaxake/make4ht-errorlogparser.lua new file mode 100755 index 000000000..f26585a13 --- /dev/null +++ b/.ximera_local/luaxake/make4ht-errorlogparser.lua @@ -0,0 +1,163 @@ +local m = {} + +local function get_filename(chunk) + local filename = chunk:match("([^\n^%(]+)") + if not filename then + return false, "No filename detected" + end + local first = filename:match("^[%./\\]+") + if first then return filename end + return false +end + +local function get_chunks(text) + -- parse log for particular included files + local chunks = {} + -- each file is enclosed in matching () brackets + local newtext = text:gsub("(%b())", function(a) + local chunk = string.sub(a,2,-2) + -- if no filename had been found in the chunk, it is probably not file chunk + -- so just return the original text + local filename = get_filename(chunk) + if not filename then return a end + local children, text = get_chunks(chunk) + table.insert(chunks, {filename = filename, text = text, children = children}) + return "" + end) + return chunks, newtext +end + + +function print_chunks(chunks, level) + local level = level or 0 + local indent = string.rep(" ", level) + for k,v in ipairs(chunks) do + print(indent .. (v.filename or "?"), string.len(v.text)) + print_chunks(v.children, level + 1) + end +end + +local function parse_default_error(lines, i) + local line = lines[i] + -- get the error message "! msg text" + local err = line:match("^!(.+)") + -- the next line should contain line number where error happened + local next_line = lines[i+1] or "" + local msg = {} + -- get the line number and first line of the error context + local line_no, msg_start = next_line:match("^l%.(%d+)(.+)") + line_no = line_no or false + msg_start = msg_start or "" + msg[#msg+1] = msg_start .. " <-" + -- try to find rest of the error context. + for x = i+2, i+5 do + local next_line = lines[x] or "" + -- break on blank lines + if next_line:match("^%s*$") then break end + msg[#msg+1] = next_line:gsub("^%s*", ""):gsub("%s$", "") + end + return err, line_no, table.concat(msg, " ") +end + +local function parse_linenumber_error(lines, i) + -- parse errors from log created with the -file-line-number option + local line = lines[i] + local filename, line_no, err = line:match("^([^%:]+)%:(%d+)%:%s*(.*)") + local msg = {} + -- get error context + for x = i+1, i+2 do + local next_line = lines[x] or "" + -- break on blank lines + if next_line:match("^%s*$") then break end + msg[#msg+1] = next_line:gsub("^%s*", ""):gsub("%s$", "") + end + -- insert mark to the error + if #msg > 1 then + table.insert(msg, 2, "<-") + end + return err, line_no, table.concat(msg, " ") +end + +--- get error messages, linenumbers and contexts from a log file chunk +---@param text string chunk from the long file where we should find errors +---@return table errors error messages +---@return table error_lines error line number +---@return table error_messages error line contents +local function parse_errors(text) + local lines = {} + local errors = {} + local find_line_no = false + local error_lines = {} + local error_messages = {} + for line in text:gmatch("([^\n]+)") do + lines[#lines+1] = line + end + for i = 1, #lines do + local line = lines[i] + local err, line_no, msg + if line:match("^!(.+)") then + err, line_no, msg = parse_default_error(lines, i) + elseif line:match("^[^%:]+%:%d+%:.+") then + err, line_no, msg = parse_linenumber_error(lines, i) + end + if err then + errors[#errors+1] = err + error_lines[#errors] = line_no + error_messages[#errors] = msg + end + end + return errors, error_lines, error_messages +end + + +local function get_errors(chunks, errors) + local errors = errors or {} + for _, v in ipairs(chunks) do + local current_errors, error_lines, error_contexts = parse_errors(v.text) + for i, err in ipairs(current_errors) do + table.insert(errors, {filename = v.filename, error = err, line = error_lines[i], context = error_contexts[i] }) + end + errors = get_errors(v.children, errors) + end + return errors +end + +function m.get_missing_4ht_files(log) + local used_files = {} + local used_4ht_files = {} + local missing_4ht_files = {} + local pkg_names = {sty=true, cls=true} + for filename, ext in log:gmatch("[^%s]-([^%/^%\\^%.%s]+)%.([%w][%w]+)") do + -- break ak + if ext == "aux" then break end + if pkg_names[ext] then + used_files[filename .. "." .. ext] = true + elseif ext == "4ht" then + used_4ht_files[filename] = true + end + end + for filename, _ in pairs(used_files) do + if not used_4ht_files[mkutils.remove_extension(filename)] then + table.insert(missing_4ht_files, filename) + end + end + return missing_4ht_files +end + + +function m.parse(log) + local chunks, newtext = get_chunks(log) + -- save the unparsed text that contains system messages + table.insert(chunks, {text = newtext, children = {}}) + -- print_chunks(chunks) + local errors = get_errors(chunks) + -- for _,v in ipairs(errors) do + -- print("error", v.filename, v.line, v.error) + -- end + return errors, chunks +end + + +m.print_chunks = print_chunks + +return m diff --git a/.ximera_local/luaxake/test-bake.lua b/.ximera_local/luaxake/test-bake.lua new file mode 100644 index 000000000..178d70ca4 --- /dev/null +++ b/.ximera_local/luaxake/test-bake.lua @@ -0,0 +1,231 @@ + +kpse.set_program_name "luatex" + +local pl = require "penlight" +local path = pl.path + +logging = require("luaxake-logging") +local compile = require "luaxake-compile" + +GLOB_root_dir = path.abspath(".") + +-- Natee just copying this cause it's easier. Should pass needed stuff in through command line args probably +config = { + --- will be set infra --- compile_sequence = {"pdf", "draft.html"}, + -- compile_sequence = {"pdf", "make4ht.html", "handout.pdf"}, + -- compile_sequence = {"pdf", "sagetex.sage", "pdf", "html"}, + -- see infra -- default_dependencies = { "xmPreamble.tex" }, -- add here e.g. xmPreamble, ximera.cls, ... + compilers = { + pdf = { + -- this doesn't work well + -- command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape "\\PassOptionsToClass{tikzexport}{ximera}\\PassOptionsToClass{xake}{ximera}\\PassOptionsToClass{xake}{xourse}\\nonstopmode\\input{@{filename}}"', + -- command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape "\\PassOptionsToClass{xake}{ximera}\\PassOptionsToClass{xake}{xourse}\\input{@{filename}}"', + command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape "\\input{@{filename}}"', -- mmm, this increases the .jax file !!! + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + infix = "" , -- used for .handout, and .make4k4 + extension = "pdf", -- not used ???? + post_command = 'post_process_pdf', + download_folder = 'ximera-downloads/with-answers', + }, + ["handout.pdf"] = { + command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape -jobname @{basename}.handout "\\PassOptionsToClass{handout}{ximera}\\PassOptionsToClass{handout}{xourse}\\input{@{filename}}"', + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + extension = "handout.pdf", + infix = "handout" , + post_command = 'post_process_pdf', + download_folder = 'ximera-downloads/handouts', + }, + -- 20241217: no longet use "html", but eg draft.html (this keeps logfiles etc from being overwritten ...)! + ["draft.html"] = { + command = "make4ht -l -c @{configfile} -f html5+dvisvgm_hashes -m draft -j @{basename}.draft -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'post_process_html', + extension = "html", + infix = "draft" , + }, + -- alternatives, use at your own risk + ["make4ht.html"] = { + command = "make4ht -l -c @{configfile} -f html5+dvisvgm_hashes -j @{basename}.make4ht -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'post_process_html', + extension = "html", + infix = "make4ht" , + }, + -- test: use 'tikz+' option (FAILS for some tikzpictures, eg with shading/patterns) + ["tikz.html"] = { + command = "make4ht -l -c @{configfile} -f html5+dvisvgm_hashes -m draft -j @{basename}.draft -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'post_process_html', + extension = "html", + infix = "draft" , + }, + ["test.html"] = { + -- command = "make4ht -f html5+dvisvgm_hashes -c @{configfile} -sm draft @{filename}", + -- command = "make4ht -c @{configfile} -f html5+dvisvgm_hashes -s @{make4ht_mode} -a debug @{filename} 'svg,htex4ht,mathjax,-css,info,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + command = "make4ht -l -c @{configfile} -f html5+dvisvgm_hashes -j @{basename}.make4ht -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css,info,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'post_process_html', + extension = "html", + infix = "make4ht" , + }, + + -- sage not tested/implemented !!!! + ["sagetex.sage"] = { + command = "sage @{output_file}", + check_log = true, -- check log + check_file = true, -- check if the sagetex.sage file exists + status = 0, -- check that the latex command return 0 + extension = "sage", -- ? + }, + -- a dummy test: create .ddd files that contain the date .. + ddd = { + command = 'date >@{basename}.ddd', + status = 0, -- check that the command returns 0 + }, + }, + -- TeX macro's to use for dependency-checking in .tex files + input_commands = { + input=true, + include=true, + includeonly=true, + activity=true, + practice=true, + activitychapter=true, + activitysection=true, + practicechapter=true, + practicesection=true, + }, + -- extensions to be kept by get_files (and thus for which fileinfo is collected) + keep_extensions = { + tex = true, + html = true, + sty = true, + }, + -- list of 'infixes' to be cleaned by default + clean_infixes = { + "", + ".make4ht", + ".draft", + ".handout", + }, + -- automatically clean files immediately after each compilation + -- the commented extensions might cause issues when automatically cleaned, as they may be needed for the next compilation + clean_extensions = { + -- "aux", + "4ct", + "4tc", + "oc", + "md5", + "dpth", + "out", + -- "jax", + "idv", + "lg", + "tmp", + -- "xref", + -- "log", + "auxlock", + "dvi", + "scmd", + "sout", + "ids", + "mw", + "cb", + "cb2", + }, + documentclass_lines = 30, + -- for debugging: dumps the 'fileinfo' of matching files + -- make4ht_loglevel = "", + make4ht_extraoptions= "", + -- number of lines in tex files where we should look for \documentclass + -- dump_fileinfo = "aFirstXourse.tex", +} + +-- Start reading command line arguments for needed data +local file = {} +file.depends_on_files = {} -- Natee we can worry about dependencies later. hassle to pass them in command line rn +file.relative_path = arg[1]:sub(2) +file.absolute_path = arg[2]:sub(2) +file.absolute_dir = arg[3]:sub(2) +file.relative_dir = arg[4]:sub(2) +file.filename = arg[5]:sub(2) + +logging.set_outfile(path.abspath("bake-" .. tostring(file.filename) .. ".log")) +local log = logging.new("bake-" .. tostring(file.filename)) + +file.basename = arg[6]:sub(2) +file.extension = arg[7]:sub(2) +if arg[8] == ":true" then + file.exists = true +else + file.exists = false +end +file.modified = tonumber(arg[9]:sub(2)) +if arg[10] == ":true" then + file.needs_compilation = true +else + file.needs_compilation = false +end +file.tex_documentclass = arg[11]:sub(2) + +config.compile_sequence = {} +local i = 12 +while arg[i] ~= "BREAK" do + table.insert(config.compile_sequence, arg[i]:sub(2)) + i = i + 1 +end + +config.output_formats = {} +while arg[i] ~= nil do + table.insert(config.output_formats, arg[i]:sub(2)) + i = i + 1 +end + +config.configfile = "ximera.cfg" + +-- End reading command line arguments for needed data + +-- Start actual baking +log:info("Starting compilation of file: " .. tostring(file.absolute_path)) +local start_time = socket.gettime() +local compile_infos = compile.compile(file, config.compilers, config.compile_sequence, config.output_formats, config.check) + +if config.noclean then + log:info("Skipping cleaning temp files") +else + compile.clean(file, config.clean_extensions,config.clean_infixes) +end +local end_time = socket.gettime() + +log:statusf("Finished compiling " .. tostring(file.absolute_path) .. " in %.1f seconds", end_time - start_time) + +-- print errors +for _, compile_info in ipairs(compile_infos) do + log:info("File "..(compile_info.output_file or "UNKNOWN??") .." got status " .. (compile_info.status or 'NIL??') ) + + if (compile_info.status or 0) > 0 then + for _, err in ipairs(compile_info.errors) do + log:errorf("[%-10s] %s:%s", compile_info.compiler, compile_info.source_file, err.constructed_errormessage) + end + else + if compile_info.post_processing_error then + log:errorf("[%-10s] %s: %s", "post_command", compile_info.source_file, compile_info.post_processing_error) + end + end +end + +os.exit(0) + + + + + + + + diff --git a/.ximera_local/pgfsys-dvisvgm4ht.def b/.ximera_local/pgfsys-dvisvgm4ht.def new file mode 100644 index 000000000..4465cb06b --- /dev/null +++ b/.ximera_local/pgfsys-dvisvgm4ht.def @@ -0,0 +1,75 @@ +% Copyright 2021-2024 by Michal Hoftich +% Copyright 2006 by Till Tantau +% +% This file may be distributed and/or modified +% +% 1. under the LaTeX Project Public License and/or +% 2. under the GNU Public License. +% +% See the file doc/generic/pgf/licenses/LICENSE for more details. + +\ProvidesFileRCS{pgfsys-dvisvgm4ht.def} + +% Driver commands for tex4ht + +% +% Load common pdf commands: +% + +% we switched to dvisvgm driver by default. it supports patterns and other features +% dvips driver is available through the tikz+ option. It doesn't support everything, +% but it worked better with nested pictures in the past. +\ifdefined\ifOption +\ifOption{tikz+}{\input pgfsys-dvips.def}{\input pgfsys-dvisvgm.def} +\else +% load the dvips driver by default +\input pgfsys-dvisvgm.def +\fi + + +\catcode`\:=11% + +% we must call most of these redefinitions in \AtBeginDocument, because \HLet is available +% only at that moment +\AtBeginDocument{% + % configure the output picture format to svg, as it will require dvisvgm + % post processing. + \Configure{Picture}{.svg}% + + % insert picture hooks to pgfsys commands + % these redefinitions are usually called only with the \tikz command, + % they are ignored in tikzpicture environment + \def\:tempa#1{% + \texfourht@tikz@begin% + \csname o:pgfsys@typesetpicturebox:\endcsname{#1} + \texfourht@tikz@end% + } + \HLet\pgfsys@typesetpicturebox\:tempa + + % we must remove Picture-alt in \pgfsys@beginpicture, because it can result in alt text included in the image + \def\:tempa{\Configure{Picture-alt}{}\texfourht@tikz@begin\o:pgfsys@beginpicture:} + \HLet\pgfsys@beginpicture\:tempa + \let\o:pgfsys@endpicture:\pgfsys@endpicture + \def\:tempa{\o:pgfsys@endpicture:} + \HLet\pgfsys@endpicture\:tempa + + % start picture around TikZ and PGF environments + \ConfigureEnv{tikzpicture}{\texfourht@tikz@begin}{\texfourht@tikz@end}{}{}% + \ConfigureEnv{pgfpicture}{\texfourht@tikz@begin}{\texfourht@tikz@end}{}{}% +} + +\def\texfourht@tikz@begin{ + \protect\csname nested:math\endcsname% support display math + \Picture+[\csname a:Picture-alt\endcsname]{}% +} +\def\texfourht@tikz@end{\EndPicture} + +\catcode`\:=12% + + +\endinput + + +%%% Local Variables: +%%% mode: latex +%%% End: diff --git a/.ximera_local/sagetex.sty b/.ximera_local/sagetex.sty new file mode 100644 index 000000000..71d06bc64 --- /dev/null +++ b/.ximera_local/sagetex.sty @@ -0,0 +1,316 @@ +%% +%% This is file `sagetex.sty', +%% generated with the docstrip utility. +%% +%% The original source files were: +%% +%% sagetex.dtx (with options: `latex') +%% py-and-sty.dtx (with options: `latex') +%% +%% This is a generated file. It is part of the SageTeX package. +%% +%% Copyright (C) 2008--2015 by Dan Drake +%% +%% This program is free software: you can redistribute it and/or modify it +%% under the terms of the GNU General Public License as published by the +%% Free Software Foundation, either version 2 of the License, or (at your +%% option) any later version. +%% +%% This program is distributed in the hope that it will be useful, but +%% WITHOUT ANY WARRANTY; without even the implied warranty of +%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +%% Public License for more details. +%% +%% You should have received a copy of the GNU General Public License along +%% with this program. If not, see . +%% +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{sagetex} + [2020/08/12 v3.5 embedding Sage into LaTeX documents] +\newcommand{\ST@ver}{2020/08/12 v3.5} +\RequirePackage{verbatim} +\RequirePackage{fancyvrb} +\RequirePackage{listings} +\RequirePackage{color} +\lstdefinelanguage{Sage}[]{Python} + {morekeywords={False,sage,True},sensitive=true} +\lstdefinelanguage{SageOutput}[]{} + {morekeywords={False,True},sensitive=true} +\lstdefinestyle{DefaultSageInputOutput}{ + nolol, + identifierstyle=, + name=sagecommandline, + xleftmargin=5pt, + numbersep=5pt, + aboveskip=0pt, + belowskip=0pt, + breaklines=true, + numberstyle=\footnotesize, + numbers=right +} +\lstdefinestyle{DefaultSageInput}{ + language=Sage, + style=DefaultSageInputOutput, + basicstyle={\ttfamily\bfseries}, + commentstyle={\ttfamily\color{dgreencolor}}, + keywordstyle={\ttfamily\color{dbluecolor}\bfseries}, + stringstyle={\ttfamily\color{dgraycolor}\bfseries}, +} +\lstdefinestyle{DefaultSageOutput}{ + language=SageOutput, + style=DefaultSageInputOutput, + basicstyle={\ttfamily}, + commentstyle={\ttfamily\color{dgreencolor}}, + keywordstyle={\ttfamily\color{dbluecolor}}, + stringstyle={\ttfamily\color{dgraycolor}}, +} +\lstdefinestyle{SageInput}{ + style=DefaultSageInput, +} +\lstdefinestyle{SageOutput}{ + style=DefaultSageOutput, +} +\definecolor{dbluecolor}{rgb}{0.01,0.02,0.7} +\definecolor{dgreencolor}{rgb}{0.2,0.4,0.0} +\definecolor{dgraycolor}{rgb}{0.30,0.3,0.30} +\RequirePackage{graphicx} +\RequirePackage{makecmds} +\RequirePackage{ifpdf} +\RequirePackage{ifthen} +\IfFileExists{ifxetex.sty}{ + \RequirePackage{ifxetex} +}{ + \newboolean{xetex} + \setboolean{xetex}{false}} +\newcounter{ST@inline} +\newcounter{ST@plot} +\newcounter{ST@cmdline} +\setcounter{ST@inline}{0} +\setcounter{ST@plot}{0} +\setcounter{ST@cmdline}{0} +\newlength{\sagetexindent} +\setlength{\sagetexindent}{5ex} +\newif\ifST@paused +\ST@pausedfalse +\AtBeginDocument{\@ifundefined{ST@final}{% +\newwrite\ST@sf% +\immediate\openout\ST@sf=\jobname.sagetex.sage% +\newcommand{\ST@wsf}[1]{\immediate\write\ST@sf{#1}}% +\ST@wsf{% +# -*- encoding: utf-8 -*-^^J% +# This file (\jobname.sagetex.sage) was *autogenerated* from \jobname.tex with +sagetex.sty version \ST@ver.^^J% +import sagetex^^J% +_st_ = sagetex.SageTeXProcessor('\jobname', version='\ST@ver', version_check=\ST@versioncheck)}}% +{\newcommand{\ST@wsf}[1]{\relax}}} +\newcommand{\ST@dodfsetup}{% +\@ifundefined{ST@diddfsetup}{% +\newwrite\ST@df% +\immediate\openout\ST@df=\jobname_doctest.sage% +\immediate\write\ST@df{r"""^^J% +This file was *autogenerated* from \jobname.tex with sagetex.sty^^J% +version \ST@ver. It contains the contents of all the^^J% +sageexample environments from \jobname.tex. You should be able to^^J% +doctest this file with "sage -t \jobname_doctest.sage".^^J% +^^J% +It is always safe to delete this file; it is not used in typesetting your^^J% +document.^^J}% +\AtEndDocument{\immediate\write\ST@df{"""}}% +\gdef\ST@diddfsetup{x}}% +{\relax}} +\newcommand{\ST@wdf}[1]{\immediate\write\ST@df{#1}} +\DeclareOption{final}{% + \newcommand{\ST@final}{x}% + \IfFileExists{\jobname.sagetex.sout}{}{\AtEndDocument{\PackageWarningNoLine{sagetex}% + {`final' option provided, but \jobname.sagetex.sout^^Jdoesn't exist! No Sage + input will appear in your document. Remove the `final'^^Joption and + rerun LaTeX on your document}}}} +\DeclareOption{imagemagick}{% + \newcommand{\ST@useimagemagick}{x}% + \AtBeginDocument{% + \@ifundefined{ST@final}{% + \ST@wsf{_st_.useimagemagick = True}}{}}} +\DeclareOption{epstopdf}{% +\AtBeginDocument{% +\@ifundefined{ST@final}{% + \ST@wsf{_st_.useepstopdf = True}}{}}} +\newcommand{\ST@versioncheck}{True} +\DeclareOption{noversioncheck}{% + \renewcommand{\ST@versioncheck}{False}} +\ProcessOptions\relax +\InputIfFileExists{\jobname.sagetex.sout}{} +{\typeout{No file \jobname.sagetex.sout.}} +\AtBeginDocument{\provideenvironment{NoHyper}{}{}} +\newcommand{\ST@sage}[1]{\ST@wsf{% +try:^^J + _st_.current_tex_line = \the\inputlineno^^J + _st_.inline(\theST@inline, #1)^^J% +except:^^J + _st_.goboom(\the\inputlineno)}% +\ifST@paused + \mbox{(Sage\TeX{} is paused)}% +\else + \begin{NoHyper}\ref{@sageinline\theST@inline}\end{NoHyper}% + \@ifundefined{r@@sageinline\theST@inline}{\gdef\ST@rerun{x}}{}% +\fi +\stepcounter{ST@inline}} +\newcommand{\sage}[1]{\ST@sage{latex(#1)}} +\newcommand{\sagestr}[1]{\ST@sage{#1}} +\catcode`\%=12 +\newcommand{\percent}{%} +\catcode`\%=14 +\newcommand{\ST@plotdir}{sage-plots-for-\jobname.tex} +\newcommand{\ST@missingfilebox}{\framebox[2cm]{\rule[-1cm]{0cm}{2cm}\textbf{??}}} +\newcommand{\sageplot}[1][]{% + \@ifnextchar[{\ST@sageplot[#1]}{\ST@sageplot[#1][notprovided]}} +\def\ST@sageplot[#1][#2]#3{\ST@wsf{try:^^J + _st_.current_tex_line = \the\inputlineno^^J + _st_.plot(\theST@plot, format='#2', _p_=#3)^^Jexcept:^^J + _st_.goboom(\the\inputlineno)}% +\ifthenelse{\boolean{pdf} \or \boolean{xetex}}{ + \ifthenelse{\equal{#2}{notprovided}}% + {\ST@inclgrfx{#1}{pdf}}% + {\ST@inclgrfx{#1}{#2}}} +{ \ifthenelse{\equal{#2}{notprovided}}% + {\ST@inclgrfx{#1}{eps}}% + {\@ifundefined{ST@useimagemagick}% + {\IfFileExists{\ST@plotdir/plot-\theST@plot.#2}% + {\ST@missingfilebox% + \PackageWarning{sagetex}{Graphics file + \ST@plotdir/plot-\theST@plot.#2\space on page \thepage\space + cannot be used with DVI output. Use pdflatex or create an EPS + file. Plot command is}}% + {\ST@missingfilebox% + \PackageWarning{sagetex}{Graphics file + \ST@plotdir/plot-\theST@plot.#2\space on page \thepage\space + does not exist. Plot command is}% + \gdef\ST@rerun{x}}}% + {\ST@inclgrfx{#1}{eps}}}} +\stepcounter{ST@plot}} +\newcommand{\ST@inclgrfx}[2]{\ifST@paused + \fbox{\rule[-1cm]{0cm}{2cm}Sage\TeX{} is paused; no graphic} +\else + \IfFileExists{\ST@plotdir/plot-\theST@plot.#2}% + {\includegraphics[#1]{\ST@plotdir/plot-\theST@plot.#2}}% + {\IfFileExists{\ST@plotdir/plot-\theST@plot.png}% + {\ifpdf + \ST@inclgrfx{#1}{png} + \else + \PackageWarning{sagetex}{Graphics file + \ST@plotdir/plot-\theST@plot.png on page \thepage\space not + supported; try using pdflatex. Plot command is}% + \fi}% + {\ST@missingfilebox% + \PackageWarning{sagetex}{Graphics file + \ST@plotdir/plot-\theST@plot.#2\space on page \thepage\space does not + exist. Plot command is}% + \gdef\ST@rerun{x}}} +\fi} +\newcommand{\ST@beginsfbl}{% + \@bsphack\ST@wsf{% +_st_.current_tex_line = \the\inputlineno^^J% +_st_.blockbegin()^^Jtry:}% + \let\do\@makeother\dospecials\catcode`\^^M\active} +\newcommand{\ST@endsfbl}{% +\ST@wsf{except:^^J + _st_.goboom(\the\inputlineno)^^J_st_.blockend()}} +\newenvironment{sageblock}{\ST@beginsfbl% +\def\verbatim@processline{\ST@wsf{ \the\verbatim@line}% +\hspace{\sagetexindent}\the\verbatim@line\par}% +\verbatim}% +{\ST@endsfbl\endverbatim} +\newenvironment{sagesilent}{\ST@beginsfbl% +\def\verbatim@processline{\ST@wsf{ \the\verbatim@line}}% +\verbatim@start}% +{\ST@endsfbl\@esphack} +\newenvironment{sageverbatim}{% +\def\verbatim@processline{\hspace{\sagetexindent}\the\verbatim@line\par}% +\verbatim}% +{\endverbatim} +\newcommand{\sageexampleincludetextoutput}{False} +\newenvironment{sageexample}{% + \ST@wsf{% +try:^^J + _st_.current_tex_line = \the\inputlineno^^J + _st_.doctest(\theST@inline, r"""}% + \ST@dodfsetup% + \ST@wdf{Sage example, line \the\inputlineno::^^J}% + \begingroup% + \@bsphack% + \let\do\@makeother\dospecials% + \catcode`\^^M\active% + \def\verbatim@processline{% + \ST@wsf{\the\verbatim@line}% + \ST@wdf{\the\verbatim@line}% + }% + \verbatim@start% +} +{ + \@esphack% + \endgroup% + \ST@wsf{% + """, globals(), locals(), \sageexampleincludetextoutput)^^Jexcept:^^J + _st_.goboom(\the\inputlineno)}% + \ifST@paused% + \mbox{(Sage\TeX{} is paused)}% + \else% + \begin{NoHyper}\ref{@sageinline\theST@inline}\end{NoHyper}% + \@ifundefined{r@@sageinline\theST@inline}{\gdef\ST@rerun{x}}{}% + \fi% + \ST@wdf{}% + \stepcounter{ST@inline}} +\newcommand{\sagecommandlinetextoutput}{True} +\newlength{\sagecommandlineskip} +\setlength{\sagecommandlineskip}{8pt} +\newenvironment{sagecommandline}{% + \ST@wsf{% +try:^^J + _st_.current_tex_line = \the\inputlineno^^J + _st_.commandline(\theST@cmdline, r"""}% + \ST@dodfsetup% + \ST@wdf{Sage commandline, line \the\inputlineno::^^J}% + \begingroup% + \@bsphack% + \let\do\@makeother\dospecials% + \catcode`\^^M\active% + \def\verbatim@processline{% + \ST@wsf{\the\verbatim@line}% + \ST@wdf{\the\verbatim@line}% + }% + \verbatim@start% +} +{ + \@esphack% + \endgroup% + \ST@wsf{% + """, globals(), locals(), \sagecommandlinetextoutput)^^Jexcept:^^J + _st_.goboom(\the\inputlineno)}% + \ifST@paused% + \mbox{(Sage\TeX{} is paused)}% + \else% + \begin{NoHyper}\ref{@sagecmdline\theST@cmdline}\end{NoHyper}% + \@ifundefined{r@@sagecmdline\theST@cmdline}{\gdef\ST@rerun{x}}{}% + \fi% + \ST@wdf{}% + \stepcounter{ST@cmdline}} +\newcommand{\sagetexpause}{\ifST@paused\relax\else +\ST@wsf{print('SageTeX paused on \jobname.tex line \the\inputlineno')^^J"""} +\ST@pausedtrue +\fi} +\newcommand{\sagetexunpause}{\ifST@paused +\ST@wsf{"""^^Jprint('SageTeX unpaused on \jobname.tex line \the\inputlineno')} +\ST@pausedfalse +\fi} +\AtEndDocument{\ifST@paused +\ST@wsf{"""^^Jprint('SageTeX unpaused at end of \jobname.tex')} +\fi +\ST@wsf{_st_.endofdoc()}% +\@ifundefined{ST@rerun}{}% +{\typeout{*********************************************************************} +\PackageWarningNoLine{sagetex}{there were undefined Sage formulas and/or +plots.^^JRun Sage on \jobname.sagetex.sage, and then run LaTeX on \jobname.tex +again}} +\typeout{*********************************************************************}} +\endinput +%% +%% End of file `sagetex.sty'. diff --git a/.ximera_local/src/abstract.dtx b/.ximera_local/src/abstract.dtx new file mode 100644 index 000000000..e4f76baca --- /dev/null +++ b/.ximera_local/src/abstract.dtx @@ -0,0 +1,24 @@ +% \subsubsection{Abstract} +% \DescribeEnv{abstract}{Every activity should include a short abstract.} +% \begin{macrocode} +%<*classXimera> +\let\abstract\relax +\let\endabstract\relax +% Use of environ package, may want to find a better way. +% see the messing around with \theabstract in title.dtx ... Is this really needed/wanted? +\NewEnviron{abstract}{\protected@xdef\theabstract{\BODY}} +% +% \end{macrocode} +% The abstract has been stored in |\theabstract| and should be emitted +% as a div. The code below is required for the abstract to show online. +% \begin{macrocode} +%<*cfgXimera> +\ifvmode\IgnorePar\fi\EndP +\ConfigureEnv{abstract}{\ifvmode\IgnorePar\fi\EndP\HCode{\Hnewline
}\par}{\ifvmode\IgnorePar\fi\EndP\HCode{\Hnewline
}\par}{}{} +% +% \end{macrocode} +% \begin{macrocode} +%<*htXimera> +\RenewEnviron{abstract}{\BODY} +%<*htXimera> +% \end{macrocode} diff --git a/.ximera_local/src/accordion.dtx b/.ximera_local/src/accordion.dtx new file mode 100644 index 000000000..3c7777853 --- /dev/null +++ b/.ximera_local/src/accordion.dtx @@ -0,0 +1,72 @@ +% \subsubsection{Accordion} +% The package \verb|mdframed| is used to make pretty foldable, but the +% amsthm/mdframed conflict also messes up the .jax file so we don't +% load mdframed when performing the xake step. But even the below +% isn't enough to fix this. +% \begin{macrocode} +%\iftikzexport\else\RequirePackage[framemethod=TikZ]{mdframed}\fi +% \end{macrocode} +% \begin{macrocode} +%<*classXimera> + + +\colorlet{textColor}{black} % since textColor is referenced below +\colorlet{background}{white} % since background is referenced below + +% The core environments. Find results in 4ht file. +%% accordion container +\newenvironment{accordion}{% +}{% +} +%% pretty-accordion +\iftikzexport + \newenvironment{accordion-item}[1][]{% + }{% + } +\else + \newenvironment{accordion-item}[1][]{% + \paragraph{#1} + }{% + } +\fi + +% +% \end{macrocode} +% On the web, these accordions result in a jquery accordion with multiple accordion-items. +% \begin{macrocode} +%<*htXimera> +\renewenvironment{accordion}{\ifvmode \IgnorePar\fi \EndP\HCode{
} +}{\HCode{
}\IgnoreIndent} + +\renewenvironment{accordion-item}[1][]{\stepcounter{identification}\ifvmode \IgnorePar\fi \EndP\HCode{

}#1\HCode{

} +}{\HCode{
}\IgnoreIndent} + + +\ifdefined\xmNotHintAsExpandable\else +%\let\hint\relax +%\let\endhint\relax +\RenewEnviron{hint}{% + \begin{expandable}{xmhint}{}% + \BODY% + \end{expandable}% +} +\fi + +\ifdefined\xmNotExpandableAsAccordion\else +%% re-implement expandable (04/2023) +\let\expandable\relax +\let\endexpandable\relax +\newenvironment{expandable}[2]{% + \stepcounter{identification}% + \ifvmode \IgnorePar\fi \EndP% + \HCode{
\Hnewline}% + \HCode{

}#2\HCode{

\Hnewline}% + \HCode{
\Hnewline} +}% +{% + \HCode{
\Hnewline
\Hnewline}\IgnoreIndent +} +\fi + +% +% \end{macrocode} diff --git a/.ximera_local/src/activity.dtx b/.ximera_local/src/activity.dtx new file mode 100644 index 000000000..647be0565 --- /dev/null +++ b/.ximera_local/src/activity.dtx @@ -0,0 +1,158 @@ +% \subsection{Activities} +% The core of the |xourse| system. It works by redefining the +% |document| environment, thus making the |\begin| and +% |\end{document}| of the subfile `transparent' to the +% inclusion. The redefinition of |\documentclass| is analogous, just +% having a required and an optional arguments which mean nothing to +% |\subfile|. +% \begin{macrocode} +%<*classXourse> +\newcommand{\skip@preamble}{% + \let\document\relax\let\enddocument\relax% + \newenvironment{document}{\let\input\otherinput}{}% + \renewcommand{\documentclass}[2][subfiles]{}} +% \end{macrocode} +% Note that the new command |\subfile| calls for |\skip@preamble| \emph{within a group}. The changes to |document| and |\documentclass| are undone after the inclusion of the subfile. +% +% Numbering starts a page too soon without this: +% \begin{macrocode} +\let\otherinput\input +% \end{macrocode} +% Store usual |\maketitle| as |\othermaketitle| +% \begin{macrocode} +\let\othermaketitle\maketitle +% \end{macrocode} +% \DescribeMacro{\maketitle}{In a \textsf{xourse} file, |\maketitle| is redefined to give course packet title page and toc.} +% \begin{macrocode} +\renewcommand{\maketitle}{ % +\pagestyle{empty} +\begin{center} +~\\ %puts space at top of page to move title down. +\vskip .25\textheight +\hrulefill\\ +\vskip 1em +\bfseries{\Huge \@title} \\ +\hrulefill\\ +\vskip 3em +{\Large \@author} +\vskip 2em +{\large \@date} +\end{center} +\clearpage +% \end{macrocode} +% When |notoc| option is used, we do not include a table of contents. +% Otherwise we include a table of contents in every course packet. +% \begin{macrocode} +\ifnotoc +\else + \tableofcontents\clearpage + \clearpage +\fi +% \end{macrocode} +% Switch to main pagestyle, just like a document with documentclass \textsf{ximera}. +% \begin{macrocode} +\pagestyle{main} +% \end{macrocode} +% Renew maketitle to usual definition. +% \begin{macrocode} +\let\maketitle\othermaketitle +% \end{macrocode} +% And we finish with our redefinition of |\maketitle|. +% \begin{macrocode} +} +\relax +% +% \end{macrocode} + +% \subsubsection{Regular activities} +% \DescribeMacro{\activity}{Documents included with |\activity| will +% be included in the body of the xourse document. Any |\input| +% commands within included \textsf{ximera} documents will be ignored. +% Any |\usepackage| commands within included ximera documents will +% cause an error. Overlapping |\newcommand| definitions within +% multiple ximera documents included simultaneously will cause an +% error.} The |\activity| command inputs the file name provided +% without |\documentclass|, without +% |\begin{document}|/|\end{document}| and without any inputs in the +% preamble of the included file. +% \begin{macrocode} +%<*classXourse> +\ifnonewpage +\newcommand{\activity}[2][]{% + \setkeys{activity}{#1} + \renewcommand{\input}[1]{} + \begingroup\skip@preamble\otherinput{#2}\endgroup\par\vspace{\topsep} + \let\input\otherinput} +\else +\newcommand{\activity}[2][]{% + \setkeys{activity}{#1} + \renewcommand{\input}[1]{} + \begingroup\skip@preamble\otherinput{#2}\endgroup\clearpage + \let\input\otherinput} +\fi +\relax +% +% \end{macrocode} + +% \begin{macrocode} +%<*htXourse> +\renewcommand\activity[2][]{% +\ifvmode \IgnorePar\fi \EndP\HCode{
#2}\IgnoreIndent% +} +% +% \end{macrocode} + +% When running xake, we can just ignore activities +% \begin{macrocode} +%<*classXourse> +\ifxake +\renewcommand\activity[2][]{} +\fi +% +% \end{macrocode} + + +% \subsubsection{Practice activities} +% \DescribeMacro{\practice}{Like |\activity| but not expecting a title.} +% \begin{macrocode} +%<*classXourse> +\ifhandout +\newcommand{\practice}[2][]{ + \setkeys{practice}{#1}%!!!!! + \renewcommand{\input}[1]{} + \begingroup\skip@preamble\otherinput{#2}\endgroup + \let\input\otherinput} +\else +\newcommand{\practice}[2][]{\texttt{\detokenize{#2}}%% gives file name for practice + \setkeys{practice}{#1}%!!!!! + \renewcommand{\input}[1]{} + \begingroup\skip@preamble\otherinput{#2}\endgroup + \let\input\otherinput} +\fi +\relax +% +% \end{macrocode} + +% The practice environment does nothing, but will eventually produce +% exercises at the end of an activity +% \begin{macrocode} +%<*classXourse> +\ifxake +\renewcommand\practice[2][]{} +\fi +% +% \end{macrocode} + +% I suppose it is reasonable for practice cards to NOT have an activitystyle, since the activitystyle is basically PRACTICE. +% \begin{macrocode} +%<*htXourse> +\renewcommand\practice[2][]{% + \ifvmode\IgnorePar\fi\EndP% + \HCode{#2}% + \IgnoreIndent% +} +% +% \end{macrocode} + + + diff --git a/.ximera_local/src/answer.dtx b/.ximera_local/src/answer.dtx new file mode 100644 index 000000000..c6f5460d0 --- /dev/null +++ b/.ximera_local/src/answer.dtx @@ -0,0 +1,111 @@ +% \subsubsection{Answers} +% \DescribeMacro{\answer}{A math answer} +% \begin{macrocode} +%<*classXimera> + +\ifdefined\HCode +\newcommand{\recordvariable}[1]{} +\else +\newwrite\idfile +\immediate\openout\idfile=\jobname.ids +\newcommand{\recordvariable}[1]{\ifthenelse{\equal{#1}{}}{}{\immediate\write\idfile{var #1;}}} +\fi +% \end{macrocode} +% Determines if answer is shown in handout mode. when |given=true|, +% show answer in handout mode, show answer in ``given box'' outside +% handout mode. When |given=false|, do not show answer in handout +% mode, show answer outside handout mode +% \begin{macrocode} +\define@key{answer}{given}[true]{\def\ans@given{#1}} +% \end{macrocode} +% Used for setting numeric answer tolerance for online student input. +% \begin{macrocode} +\define@key{answer}{tolerance}{\def\ans@tol{#1}} +% \end{macrocode} +% Used to run dynamic js code on student provided answers. Note: +% currently pdf outputs the validator code itself. +% \begin{macrocode} +\define@key{answer}{validator}{} +% \end{macrocode} +% Used for assigning a js ID to answer for dynamic code (eg validators). +% \begin{macrocode} +\define@key{answer}{id}{\def\ans@id{#1}} +% \end{macrocode} +% Used to set anticipated input format; eg "string". +% \begin{macrocode} +\define@key{answer}{format}{} +% \end{macrocode} +% Used to hide the answer input box on the web. +% \begin{macrocode} +\define@key{answer}{onlinenoinput}[false]{} +% \end{macrocode} +% Used to add a `show answer' button to the answer blank. +% \begin{macrocode} +\define@key{answer}{onlineshowanswerbutton}[false]{} +% \end{macrocode} +% Set default values for |\answer| command |key=value| pairs. +% Default values are |given = false|. +% \begin{macrocode} +\setkeys{answer}{id=,given=false,onlinenoinput=false,onlineshowanswerbutton=false} +% \end{macrocode} +% Basic code for |\answer|. +% \begin{macrocode} + +% Options for handout +\newcommand{\answerFormatLength}{2cm} + +\newcommand{\answerFormatDots}[1]{\ldots\ldots} +\newcommand{\answerFormatLine}[1]{\protect\rule{\answerFormatLength}{0.4pt}} +\newcommand{\answerFormatFlexibleLine}[1]{\protect\rule{\widthof{$#1$}*2}{0.4pt}} +\newcommand{\answerFormatFlexibleBox}[1]{\fbox{\scalebox{2}{\phantom{$#1$}}}} + +% options for default (i.e with answers filled in) +\newcommand{\answerFormatPlain}[1]{\ensuremath{#1}} +\newcommand{\answerFormatBlue}[1]{\color{blue}\ensuremath{#1}} +\newcommand{\answerFormatBoxed}[1]{\fbox{\ensuremath{#1}}} +\newcommand{\answerFormatBoxedGiven}[1]{\underset{\scriptstyle\mathrm{given}}{\fbox{\ensuremath{#1}}}} + +% defaults for handout and default mode, and for \answer[given] +\let\handoutAnswerFormat\answerFormatDots +\let\defaultAnswerFormat\answerFormatBlue +\let\givenAnswerFormat\answerFormatBoxedGiven + +\newcommand{\answer}[2][]{% + \ifmmode% + \setkeys{answer}{#1}% + \recordvariable{\ans@id} + \ifthenelse{\boolean{\ans@given}} + {% Start then statement + \ifhandout + #2 + \else + \givenAnswerFormat{#2} %% in case the argument helps formatting + \fi + }% End then statement + {% Start else statement + \ifhandout + \handoutAnswerFormat{#2} %% in case the argument helps formatting + \else% show answer in box outside handout mode + \defaultAnswerFormat{#2} %% in case the argument helps formatting + \fi + }% End else statement + \else% + \GenericError{\space\space\space\space}% Throw an error based on... something? -- Jason + {Attempt to use \@backslashchar answer outside of math mode} + {See https://github.com/ximeraProject/ximeraLatex for explanation.} + {Need to use either inline or display math.}% + \fi +} +% +% \end{macrocode} +% On the HTML side, |\answer| emits spans---but it is usually just +% handled directly by MathJax. +% \begin{macrocode} +%<*htXimera> +\renewcommand{\answer}[2][false]{\HCode{}#2\HCode{}} + +\def\validator[#1]{\stepcounter{identification}\HCode{
}} +\def\endvalidator{\HCode{
}} + +% +% \end{macrocode} diff --git a/.ximera_local/src/banner.dtx b/.ximera_local/src/banner.dtx new file mode 100644 index 000000000..ec5d62b28 --- /dev/null +++ b/.ximera_local/src/banner.dtx @@ -0,0 +1,21 @@ +% \begin{macrocode} +%<*classXimera> +\let\m\message +\def\w{\m{::}}\def\t{\m{:::}}\def\s{\m{ }}\def\n{\m{^^J}}\def\u{::'}\def\d{'::}\def\v{:::::} +\n +\m{\d.}\s\m{.\u}\w\m{}\m{\v}\s\s\m{\v}\m{}\m{.\v\v\v::::.}\s\m{}\t\n +\s\m{\d.\u}\s\w\t\t\s\t\t\w\m{}\s\s\s\s\s\s\s\t\s\m{\u::}\n +\s\s\t\s\s\w\t\m{}\t\t\m{}\t\m{ ::::\v}\m{.\v:\u}\m{}\m{\u}\m{\d}\n +\s\m{.\u::.}\s\w\t\m{}\t\t\m{}\t\w\s\s\s\s\w\m{}\m{\d.}\s\m{\u}\s\m{\d}\n +\m{.\u}\s\m{\d.}\w\t\s\m{':\u}\s\t\m{\d\v::}\w\s\m{}\m{\d.\u}\s\s\m{\d}\n +\n +\let\w\undefined +\let\t\undefined +\let\s\undefined +\let\n\undefined +\let\u\undefined +\let\d\undefined +\let\v\undefined +\let\m\undefined +% +% \end{macrocode} diff --git a/.ximera_local/src/choice.dtx b/.ximera_local/src/choice.dtx new file mode 100644 index 000000000..70770cd24 --- /dev/null +++ b/.ximera_local/src/choice.dtx @@ -0,0 +1,226 @@ +% \subsubsection{Multiple choice and the like} +% \DescribeEnv{multipleChoice}{Multiple choice} +% \begin{macrocode} +%<*classXimera> +% Jim: Originally this was \renewcommand{\theenumi}{$(\mathrm{\alph{enumi}})$} +% but that breaks tex4ht because mathmode can only be processed by mathjax. +% so now I made this just italicized. +% \end{macrocode} +% \subsubsection{Options} +% \begin{macrocode} +\define@key{choice}{value}[]{\def\choice@value{#1}} +% \end{macrocode} +% This flags the answer as the correct answer +% \begin{macrocode} +\define@boolkey{choice}{correct}[true]{\def\choice@correct{#1}} +% \end{macrocode} +% Use an ID to refer to the choice. +% \begin{macrocode} +\define@key{multipleChoice}{id}{\def\mc@id{#1}} +% \end{macrocode} +% |\otherchoice| outputs the item if correct and nothing if incorrect. +% \begin{macrocode} +\define@key{otherchoice}{value}[]{\def\otherchoice@value{#1}} +\define@boolkey{otherchoice}{correct}[true]{\def\otherchoice@correct{#1}} +% \end{macrocode} +% Default key choices for multiple choice options. +% Default for choice pairs. +% Default: answers without the option "correct=true" is "incorrect". +% \begin{macrocode} +\setkeys{choice}{correct=false,value=} +% \end{macrocode} +% Defaults for multipleChoice pairs. +% Default to no id? -- Jason +% \begin{macrocode} +\setkeys{multipleChoice}{id=} +% \end{macrocode} +% Defaults for otherchoice pairs. +% Default "otherchoice" to behave like "choice" for error checking. +% \begin{macrocode} +\setkeys{otherchoice}{correct=false,value=} +% +% \end{macrocode} +% +% \subsubsection{Choices} +% \DescribeMacro{\choice}{Like |\item| but for choice environments.} +% choice command denotes a possible answer choice for the multiple choice question. +% \begin{macrocode} +%<*classXimera> +\newcommand{\choice}[2][]{% +\setkeys{choice}{#1}% +\item{#2} +\ifthenelse{\boolean{\choice@correct}} + {% Begin then result + \ifhandout% if it's a handout do nothing. + \else% otherwise place a checkmark when you select the "correct choice"... maybe? -- Jason + \,\checkmark\,\setkeys{choice}{correct=false} + \fi + }% End then result + {}% Begin/End else result. +} + +%Define an expandable version of choice Not really meant to be used outside this package (used in wordChoice) +% Is there a reason we can't just always use this as default? -- Jason +\newcommand{\choiceEXP}[2][]{% + \expandafter\setkeys\expandafter{choice}{#1}% + \item{#2} + \ifthenelse{\boolean{\choice@correct}} + {% Begin then result + \ifhandout + \else + \,\checkmark\,\setkeys{choice}{correct=false} + \fi + }% End then result + {}% Begin/End else result. + } %% note all the {} are needed in case the choice has [] in it. + +% \otherchoice is the \choice used in wordChoice command. +\newcommand{\otherchoice}[2][]{% +\ignorespaces% +\setkeys{otherchoice}{#1}% +\ifthenelse{\boolean{\otherchoice@correct}}% +{% Start then result +#2\ignorespaces\setkeys{otherchoice}{correct=false}\ignorespaces% +}% End then result +{}% Start/End else result +\ignorespaces% +}% +\newcommand{\inlinechoice}[2][]{% +\setkeys{choice}{#1}% +\iffirstinlinechoice +(\hspace{-.25em} +\firstinlinechoicefalse +\else +/ +\fi +#2 +\ifthenelse{\boolean{\choice@correct}}% +{% Start then result +\ifhandout\else\checkmark\ignorespaces\setkeys{choice}{correct=false}\ignorespaces\fi% +}% End then result +{}% Start/End else result +\hspace{-.25em}\ignorespaces% +} + +% +% \end{macrocode} +% On the HTML side, |\choice| emits ||s. +% \begin{macrocode} +%<*htXimera> +\newcounter{choiceId} +\renewcommand{\choice}[2][]{% +\setkeys{choice}{correct=false}% +\setkeys{choice}{#1}% +\stepcounter{choiceId}\IgnorePar% +\HCode{}% +#2\HCode{}} +\let\inlinechoice\choice +% +% \end{macrocode} + +% \subsubsection{Environment(s)} +% The environment |multipleChoice@| is for internal use only. +% \DescribeEnv{multipleChoice}{Wrap |\choice|s in a |multipleChoice| environment to make a multiple choice question.} +% \begin{macrocode} +%<*classXimera> +\newenvironment{multipleChoice}[1][] + {% Environment Start Code + \setkeys{multipleChoice}{#1}% + \recordvariable{\mc@id}% + \begin{trivlist} + \item[\hskip \labelsep\small\bfseries Multiple Choice:]\hfil + \begin{enumerate} + }% Note this means that \item has to be the first line after \begin{multipleChoice}. + {% Environment End Code + \end{enumerate} + \end{trivlist} + } + +%multipleChoice@ is for internal use only! (used in wordChoice) +%this is simply a wrapper for the sole showing (other)choice. +\newenvironment{multipleChoice@}[1][]{}{)} +% +% \end{macrocode} + + + +% On the web, you might also expect these to be "problem environments" +% but they aren't -- they're respondables. You might expect a +% |\setcounter{choiceId}{0}| here --- that would be wrong, because then +% the generated IDs would no longer be unique. +% \begin{macrocode} +%<*htXimera> +\renewenvironment{multipleChoice}[1][] +{\setkeys{multipleChoice}{#1}% +\stepcounter{identification}\ifvmode \IgnorePar\fi \EndP\HCode{
}% +}{\HCode{
}\IgnoreIndent} +\ConfigureEnv{multipleChoice}{}{}{}{} +% +% \end{macrocode} + +% \subsection{Word choice} +% \DescribeMacro{\wordChoice}{An in-line version of multipleChoice: +% uses enumitem package note, it is coded as a single line to avoid +% unwanted spaces in ``given'' mode.} +% \begin{macrocode} +%<*classXimera> +\newcommand{\wordChoice}[1]{% +\let\choicetemp\choice% Assign a "choicetemp" command to duplicate choice. +\ifwordchoicegiven% If wordchoice option is on, we need to juggle around some definitions. +\let\choice\otherchoice% +%\begin{multipleChoice@}% -unnecessary (REMOVE THIS LINE IF THE YEAR IS 2019 or Beyond) +#1 +%\end{multipleChoice@}% -unnecessary (REMOVE THIS LINE IF THE YEAR IS 2019 or Beyond) +\else% If it isn't the regular "choice" command should work. +\let\choice\inlinechoice% +\begin{multipleChoice@}% +#1% +\end{multipleChoice@}% +\fi% +\let\choice\choicetemp% Now that choicetmp has been manipulated to what we want, replace choice with it. +}% + + +% +% \end{macrocode} +% This is actually just word choice +% \begin{macrocode} +%<*htXimera> +\renewenvironment{multipleChoice@}{\refstepcounter{problem}}{}% +\ConfigureEnv{multipleChoice@}{\stepcounter{identification}\IgnorePar\HCode{}}{\HCode{}\IgnoreIndent}{}{} +% +% \end{macrocode} + +% \subsection{Select all} +% \DescribeEnv{selectAll}{A multiple-multiple choice question} +% \begin{macrocode} +%<*classXimera> +\newenvironment{selectAll}[1][] +{\begin{trivlist}\item[\hskip \labelsep\small\bfseries Select All Correct Answers:]\hfil\begin{enumerate}} + {\end{enumerate}\end{trivlist}} +% +% \end{macrocode} + +% In the future we need this to (optionally) be displayed in the +% problem, while the actual code lives in the solution. Here is how +% this could be implemented: Like the title/maketitle commands, the +% multiple-choice could be stored in |\themultiplechoice|, flip a +% boolean, and execute |\makemultiplechoice| at the |\end| of the +% problem. We should also make a command called |\showchoices| that +% will show choices in the handout. + +% On the web, |selectAll| is handled just like |multipleChoice|. +% \begin{macrocode} +%<*htXimera> +\renewenvironment{selectAll}{\refstepcounter{problem}}{}% +\ConfigureEnv{selectAll}{\stepcounter{identification}\ifvmode \IgnorePar\fi \EndP\HCode{
}}{\HCode{
}\IgnoreIndent}{}{} +% +% \end{macrocode} + + diff --git a/.ximera_local/src/clearEnv.dtx b/.ximera_local/src/clearEnv.dtx new file mode 100644 index 000000000..af906995d --- /dev/null +++ b/.ximera_local/src/clearEnv.dtx @@ -0,0 +1,18 @@ +% \subsubsection{clearEnv} +% \DescribeMacro{clearEnv}{envName} +% This macro allows you to delete an environment by redefining it as null. This also deletes any corresponding internal counter - such as those generated by "\newtheorem" command. +% #1 is envName is the name of the environment you want to clear. This will (re)define the +% begin and end environments, as well as the internal counter from \newtheoremenv to \relax. +% \begin{macrocode} +%<*classXimera> +\newcommand{\clearEnv}[1]{ + %% Syntax: \clearEnv{envName} + % #1 is envName is the name of the environment you want to clear. This will (re)define the + % begin and end environments, as well as the internal counter from \newtheoremenv to \relax. + \expandafter\let\csname #1\endcsname\relax + \expandafter\let\csname end#1\endcsname\relax + \expandafter\let\csname c@#1\endcsname\relax + } +% +% \end{macrocode} + diff --git a/.ximera_local/src/dialogue.dtx b/.ximera_local/src/dialogue.dtx new file mode 100644 index 000000000..b4b31580d --- /dev/null +++ b/.ximera_local/src/dialogue.dtx @@ -0,0 +1,28 @@ +% \subsubsection{Dialogues} +% \DescribeEnv{dialogue}{A dialogue between people.} +% \begin{macrocode} +%<*classXimera> +\newenvironment{dialogue}{% + \renewcommand\descriptionlabel[1]{\hspace{\labelsep}\textbf{##1:}} + \begin{description}% +}{% + \end{description}% +} +% +% \end{macrocode} +% On the web, the resulting |
| should have an appropriate |class| set. +% \begin{macrocode} +%<*htXimera> +\renewenvironment{dialogue}{\begin{description}}{\end{description}} + +\ConfigureList{dialogue}% + {\EndP\HCode{
}% + \PushMacro\end:itm +\global\let\end:itm=\empty} + {\PopMacro\end:itm \global\let\end:itm \end:itm +\EndP\HCode{
}\ShowPar} + {\end:itm \global\def\end:itm{\EndP\Tg}\HCode{
}\bgroup \bf} + {\egroup\EndP\HCode{
}} +% +% \end{macrocode} diff --git a/.ximera_local/src/ending.dtx b/.ximera_local/src/ending.dtx new file mode 100644 index 000000000..8f53a0fd2 --- /dev/null +++ b/.ximera_local/src/ending.dtx @@ -0,0 +1,20 @@ +% \subsubsection{The End} +% It seems that some of the files need to conclude with something or another. +% \begin{macrocode} +%<*htXimera> +\Hinput{ximera} +% +% \end{macrocode} + +% \begin{macrocode} +%<*htXourse> +\Hinput{xourse} +% +% \end{macrocode} + +% \begin{macrocode} +%<*cfgXimera> +\begin{document} +\EndPreamble +% +% \end{macrocode} diff --git a/.ximera_local/src/enumerate.dtx b/.ximera_local/src/enumerate.dtx new file mode 100644 index 000000000..7c37e9743 --- /dev/null +++ b/.ximera_local/src/enumerate.dtx @@ -0,0 +1,10 @@ +% \subsubsection{Enumerate fixes} +% Make enumerate use a letter +% \begin{macrocode} +%<*classXimera> +\renewcommand{\theenumi}{\textup{(\alph{enumi})}} +\renewcommand{\labelenumi}{\theenumi} +\renewcommand{\theenumii}{\textup{(\roman{enumii})}} +\renewcommand{\labelenumii}{\theenumii} +% +% \end{macrocode} diff --git a/.ximera_local/src/feedback.dtx b/.ximera_local/src/feedback.dtx new file mode 100644 index 000000000..03d09770b --- /dev/null +++ b/.ximera_local/src/feedback.dtx @@ -0,0 +1,72 @@ +% \subsubsection{Feedback} +% \DescribeEnv{feedback}{An initially hidden environment that uncovers itself at an appropriate time.} +% New Validator rewrite code added by Jason Nowell. Original code +% orovided by Jim Fowler Validator is an environment designed to run a +% custom check on answers (usually) using javascript code. +% +% Define a placeholder command for validator and feedback. +% \begin{macrocode} +%<*classXimera> +\newcommand{\PH@Command}{} +% \end{macrocode} +% Validator should take an argument and detokenize it and display it +% at the start of the environment. The original Validator environment +% had everything framed in an mbox; presumably to make the text look +% a bit nicer, although this seems redundant with |texttt|. It +% shouldn't cause any harm so I have left it in for now. +% \begin{macrocode} +\newenvironment{validator}[1][]{ + \def\PH@Command{#1}% Use PH@Command to hold the content and be a target for "\expandafter" to expand once. + \mbox{\texttt{\detokenize\expandafter{\PH@Command}}}% Now expand PH@Command once and then detokenize. + }{} +% \end{macrocode} +% First, if it's a handout, we want feedback to eat everything and +% then disappear entirely. So we do this: +% \begin{macrocode} +\ifhandout% +\newenvironment{feedback} + {% + \setbox0\vbox\bgroup + } + {% + \egroup + } +% \end{macrocode} +% If this isn't a handout, then we want to display the Feedback by +% using a label, positioned and formated as a |\item| in a +% trivlist. It is important that we also detokenize the content of +% the optional argument, as it is likely to contain javascript or +% other code that latex won't be able to make sense of. +% \begin{macrocode} +\else +% \end{macrocode} +% \begin{macrocode} +\newenvironment{feedback}[1][attempt]{ + + \def\PH@Command{#1}% Use PH@Command to hold the content and be a target for "\expandafter" to expand once. + + \begin{trivlist}% Begin the trivlist to use formating of the "Feedback" label. + \item[\hskip \labelsep\small\slshape\bfseries Feedback% Format the "Feedback" label. Don't forget the space. + (\texttt{\detokenize\expandafter{\PH@Command}}):% Format (and detokenize) the condition for feedback to trigger + \hspace{2ex}]\small\slshape% Insert some space before the actual feedback given. + }{ + \end{trivlist} + } + +\fi +% +% \end{macrocode} +% Feedback environments take an optional parameter (which describes +% when the feedback is to be provided) +% \begin{macrocode} +%<*htXimera> +\def\feedback{\@ifnextchar[{\@feedbackcode}{\@feedbackattempt}} +\def\@feedbackattempt{\@feedbackcode[attempt]} +\def\@feedbackcode[#1]{\stepcounter{identification}% +\ifvmode \IgnorePar\fi \EndP% +\ifthenelse{\equal{#1}{attempt}}{\HCode{