diff --git a/.cirrus.yml b/.cirrus.yml index cb2e32861..61d6ab423 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -41,11 +41,16 @@ task: - git fetch --prune --tags ||: - ./bootstrap.sh configure_script: | - ./configure MAKE=gmake \ - --enable-developer-mode LDOC=false LUAROCKS=false LUACHECK=false BUSTED=false DELTA=cat PDFINFO=false NIX=false NPM=false DOCKER=false STYLUA=false TYPOS=false \ + ./configure \ + DOCKER=false \ + MAKE=gmake \ + PDFINFO=false \ + --enable-developer-mode \ + --without-developer-tools \ --disable-font-variations \ --with-system-lua-sources \ --with-system-luarocks \ + --with-luarocks=luarocks51 \ --without-manual make_script: - gmake all diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 742ae8318..04a1a9f79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: ./bootstrap.sh ./configure \ --enable-developer-mode \ - BUSTED=false DELTA=false LDOC=false LUACHECK=false NIX=false STYLUA=false TYPOS=cat \ + --without-developer-tools \ --disable-font-variations \ --with-manual \ ${{ matrix.configuration[1] }} @@ -113,8 +113,11 @@ jobs: run: | ./bootstrap.sh ./configure \ + FCMATCH=true \ + PDFINFO=false \ --enable-developer-mode \ - BUSTED=false DELTA=false LDOC=false LUACHECK=false NIX=false STYLUA=false TYPOS=cat FCMATCH=true PDFINFO=false CARGO=true \ + --without-developer-tools \ + DOCKER=$(which docker) \ --disable-font-variations \ --with-system-lua-sources \ --without-manual @@ -144,7 +147,7 @@ jobs: .sources key: fonts-${{ hashFiles('Makefile-fonts') }} - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v15 + uses: DeterminateSystems/nix-installer-action@v16 - name: Cache Nix dependencies uses: DeterminateSystems/magic-nix-cache-action@v8 - name: Setup developer environment diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c13962ee0..9653ead3e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -60,7 +60,9 @@ jobs: run: | ./bootstrap.sh ./configure \ - --enable-developer-mode LDOC=false LUACHECK=false NIX=false DELTA=cat STYLUA=false TYPOS=false \ + --enable-developer-mode \ + --without-developer-tools \ + BUSTED=$(which busted) \ --disable-font-variations \ --without-manual - name: Make diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 120557708..26c664348 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,20 +30,14 @@ jobs: echo "REF=${GITHUB_REF##refs/*/}" >> $GITHUB_ENV ./bootstrap.sh ./configure \ + BSDTAR=false \ + FCMATCH=true \ + PDFINFO=false \ --enable-developer-mode \ + --without-developer-tools \ + DOCKER=$(which docker) \ --without-harfbuzz \ - --disable-font-variations \ - BSDTAR=false \ - BUSTED=false \ - DELTA=false \ - FCMATCH=true \ - LDOC=false \ - LUACHECK=false \ - LUAROCKS=false \ - NIX=false \ - PDFINFO=false \ - STYLUA=false \ - TYPOS=false + --disable-font-variations - name: Publish Docker Image to GH Container Registry run: | make docker-build-push diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index b3c0e2c2d..0d884eee4 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -16,7 +16,7 @@ jobs: with: fetch-depth: 0 - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v15 + uses: DeterminateSystems/nix-installer-action@v16 - name: Cache Nix dependencies uses: DeterminateSystems/magic-nix-cache-action@v8 # Upstream package sometimes has flags set that disable flake checking diff --git a/.github/workflows/stylua.yml b/.github/workflows/stylua.yml index 4789ca225..fe3ac9fca 100644 --- a/.github/workflows/stylua.yml +++ b/.github/workflows/stylua.yml @@ -1,6 +1,6 @@ name: StyLua -on: [ workflow_dispatch ] +on: [ push, pull_request ] jobs: @@ -9,8 +9,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - uses: JohnnyMorganz/stylua-action@v4 + - name: StyLua + uses: JohnnyMorganz/stylua-action@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - version: latest + version: 2.0.0 args: --check --respect-ignores -g '*.lua' -g '*.lua.in' -g '*.rockspec.in' .busted .luacov .luacheckrc build-aux/config.ld . diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1a6ba5bd8..c1b341294 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,7 +71,9 @@ jobs: ./bootstrap.sh ./configure \ ${{ matrix.luaVersion[1] }} \ - --enable-developer-mode LDOC=false LUACHECK=false NIX=false DELTA=cat STYLUA=false TYPOS=false \ + --enable-developer-mode \ + --without-developer-tools \ + BUSTED=$(which busted) \ --disable-font-variations \ --with${{ !startsWith(matrix.luaVersion[0], 'luajit') && 'out' || '' }}-luajit \ --without-system-luarocks \ diff --git a/CHANGELOG.md b/CHANGELOG.md index a40be4d58..844a188ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [0.15.7](https://github.com/sile-typesetter/sile/compare/v0.15.6...v0.15.7) (2024-11-26) + + +### New Features + +* **build:** Add configure flag to skip checks for all developer tools ([c01c867](https://github.com/sile-typesetter/sile/commit/c01c867174a708e75a4bdb929eb8cac25b7a1048)) +* **build:** Enable --with[out]-EXECUTABLE=PATH configure flags for tooling dependencies ([89b5836](https://github.com/sile-typesetter/sile/commit/89b583651f1741be4f7dca118c858b6e41c070b0)) +* **core:** Set Lua's interal locale so builtin functions respond to document language ([a614169](https://github.com/sile-typesetter/sile/commit/a614169522d23e50855b56116fe95227fa7ab43d)) +* **core:** Set system locale for subprocesses to match the document language ([b28cafd](https://github.com/sile-typesetter/sile/commit/b28cafd9bcfcffdcc1ea8546e83d4c0d18423150)) +* **math:** Add pre-defined TeX-like operator functions (cos, sin, etc.) ([8d83821](https://github.com/sile-typesetter/sile/commit/8d83821adda90020ed6c7861b36010829a934e30)) +* **math:** Support TeX-like apostrophe and multiplication sign as primes and asterisk ([b8f35ff](https://github.com/sile-typesetter/sile/commit/b8f35ff81257125f408009a053a929d35f28b88d)) +* **math:** Support TeX-like left..right delimiter syntax ([960dc3f](https://github.com/sile-typesetter/sile/commit/960dc3f49dba5a78c44cde391923cbde8e2ed986)) +* **math:** Support the MathML operator dictionary and many TeX-like aliases ([3dd25e9](https://github.com/sile-typesetter/sile/commit/3dd25e95331e19c7b6e1a5af67106098ffaf3aad)) +* **packages:** Add lightweight CSL engine ([8d3961c](https://github.com/sile-typesetter/sile/commit/8d3961c9f4f78614278d4f761f690801e9b9a63b)) +* **packages:** Keep track of cited bibliography entries ([57b3b7c](https://github.com/sile-typesetter/sile/commit/57b3b7ce607db422b6030d115653a4ffdd1e1c5f)) +* **packages:** Use experimental CSL renderer for BibTeX ([808c6bb](https://github.com/sile-typesetter/sile/commit/808c6bbd8165b8b5d4fff11489fb2a5341d6b3dd)) +* **utilities:** Add function to set environment variables ([0f0ed02](https://github.com/sile-typesetter/sile/commit/0f0ed023893212b7d6d9a2a26afb3cb17e369ead)) + + +### Bug Fixes + +* **build:** Support cross-compilation of Rust binaries ([#2178](https://github.com/sile-typesetter/sile/issues/2178)) ([19c7c1d](https://github.com/sile-typesetter/sile/commit/19c7c1d4cf07ac7baa7c4c6b24d0d82f2dcea0af)) +* **math:** A period must be allowed in TeX-like math syntax for numbers ([56edc14](https://github.com/sile-typesetter/sile/commit/56edc14c9b69806ef288ce870abad1d0f6add34f)) +* **math:** Add math.font.script.feature setting, defaulting to ssty ([2adc912](https://github.com/sile-typesetter/sile/commit/2adc912bf8ebfda5d30a7404f41d5390d986bf17)) +* **math:** Improve spacing rules on limit-like operators ([781f62a](https://github.com/sile-typesetter/sile/commit/781f62ad0527a6275b4ce8645c3231bae98a1121)) +* **math:** Spacing rules must distinguish binary and unary operators ([81a1be5](https://github.com/sile-typesetter/sile/commit/81a1be52604efbb162c18bf45bcc1473e42c1f9a)) +* **math:** Suppress invisible operators in MathML ([#2177](https://github.com/sile-typesetter/sile/issues/2177)) ([72faad5](https://github.com/sile-typesetter/sile/commit/72faad564eb28ec21460d4e68730d2224fe954ab)) +* **math:** The (escaped) percent is an ordinary atom in TeX-like syntax ([4170719](https://github.com/sile-typesetter/sile/commit/41707190c58035741de60a8b3b638ec1d5d3826c)) +* **packages:** Fix bogus command in pandoc modules's definition lists leaking bold ([8c9348b](https://github.com/sile-typesetter/sile/commit/8c9348bcd0cdf471568a71c190b1b6585f22eddb)) + ## [0.15.6](https://github.com/sile-typesetter/sile/compare/v0.15.5...v0.15.6) (2024-11-14) diff --git a/Cargo.lock b/Cargo.lock index 04309d132..374394d4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,9 +125,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", "regex-automata", @@ -163,14 +163,14 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "cc" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aeb932158bd710538c73702db6945cb68a8fb08c519e6e12706b94263b36db8" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "shlex", ] @@ -222,7 +222,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -271,9 +271,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.22.3" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -295,9 +295,9 @@ dependencies = [ [[package]] name = "core-text" -version = "19.2.0" +version = "20.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25" +checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" dependencies = [ "core-foundation", "core-graphics", @@ -307,9 +307,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -333,6 +333,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.89", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.89", +] + [[package]] name = "deranged" version = "0.3.11" @@ -342,6 +377,37 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.89", +] + [[package]] name = "digest" version = "0.10.7" @@ -360,7 +426,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -411,9 +477,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -427,18 +493,30 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" -version = "0.3.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ + "foreign-types-macros", "foreign-types-shared", ] +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "foreign-types-shared" -version = "0.1.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" @@ -449,16 +527,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "freetype" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a440748e063798e4893ceb877151e84acef9bea9a8c6800645cf3f1b3a7806e" -dependencies = [ - "freetype-sys", - "libc", -] - [[package]] name = "freetype-sys" version = "0.20.1" @@ -482,9 +550,9 @@ dependencies = [ [[package]] name = "gix" -version = "0.63.0" +version = "0.66.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "984c5018adfa7a4536ade67990b3ebc6e11ab57b3d6cd9968de0947ca99b4b06" +checksum = "9048b8d1ae2104f045cb37e5c450fc49d5d8af22609386bfc739c11ba88995eb" dependencies = [ "gix-actor", "gix-commitgraph", @@ -499,7 +567,6 @@ dependencies = [ "gix-hashtable", "gix-index", "gix-lock", - "gix-macros", "gix-object", "gix-odb", "gix-pack", @@ -519,39 +586,39 @@ dependencies = [ "parking_lot", "signal-hook", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "gix-actor" -version = "0.31.5" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0e454357e34b833cc3a00b6efbbd3dd4d18b24b9fb0c023876ec2645e8aa3f2" +checksum = "fc19e312cd45c4a66cd003f909163dc2f8e1623e30a0c0c6df3776e89b308665" dependencies = [ "bstr", "gix-date", "gix-utils", "itoa", - "thiserror", + "thiserror 1.0.69", "winnow", ] [[package]] name = "gix-bitmap" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f78312288bd02052be5dbc2ecbc342c9f4eb791986d86c0a5c06b92dc72efa" +checksum = "d48b897b4bbc881aea994b4a5bbb340a04979d7be9089791304e04a9fbc66b53" dependencies = [ - "thiserror", + "thiserror 2.0.3", ] [[package]] name = "gix-chunk" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28b58ba04f0c004722344390af9dbc85888fbb84be1981afb934da4114d4cf" +checksum = "c6ffbeb3a5c0b8b84c3fe4133a6f8c82fa962f4caefe8d0762eced025d3eb4f7" dependencies = [ - "thiserror", + "thiserror 2.0.3", ] [[package]] @@ -565,14 +632,14 @@ dependencies = [ "gix-features", "gix-hash", "memmap2", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "gix-config" -version = "0.37.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fafe42957e11d98e354a66b6bd70aeea00faf2f62dd11164188224a507c840" +checksum = "78e797487e6ca3552491de1131b4f72202f282fb33f198b1c34406d765b42bb0" dependencies = [ "bstr", "gix-config-value", @@ -584,53 +651,53 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.69", "unicode-bom", "winnow", ] [[package]] name = "gix-config-value" -version = "0.14.9" +version = "0.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3de3fdca9c75fa4b83a76583d265fa49b1de6b088ebcd210749c24ceeb74660" +checksum = "49aaeef5d98390a3bcf9dbc6440b520b793d1bf3ed99317dc407b02be995b28e" dependencies = [ "bitflags 2.6.0", "bstr", "gix-path", "libc", - "thiserror", + "thiserror 2.0.3", ] [[package]] name = "gix-date" -version = "0.8.7" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eed6931f21491ee0aeb922751bd7ec97b4b2fe8fbfedcb678e2a2dce5f3b8c0" +checksum = "691142b1a34d18e8ed6e6114bc1a2736516c5ad60ef3aa9bd1b694886e3ca92d" dependencies = [ "bstr", "itoa", - "thiserror", - "time", + "jiff", + "thiserror 2.0.3", ] [[package]] name = "gix-diff" -version = "0.44.1" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1996d5c8a305b59709467d80617c9fde48d9d75fd1f4179ea970912630886c9d" +checksum = "92c9afd80fff00f8b38b1c1928442feb4cd6d2232a6ed806b6b193151a3d336c" dependencies = [ "bstr", "gix-hash", "gix-object", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "gix-discover" -version = "0.32.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc27c699b63da66b50d50c00668bc0b7e90c3a382ef302865e891559935f3dbf" +checksum = "0577366b9567376bc26e815fd74451ebd0e6218814e242f8e5b7072c58d956d2" dependencies = [ "bstr", "dunce", @@ -639,7 +706,7 @@ dependencies = [ "gix-path", "gix-ref", "gix-sec", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -657,7 +724,7 @@ dependencies = [ "once_cell", "prodash", "sha1_smol", - "thiserror", + "thiserror 1.0.69", "walkdir", ] @@ -691,7 +758,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93d7df7366121b5018f947a04d37f034717e113dcf9ccd85c34b58e57a74d5e" dependencies = [ "faster-hex", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -707,9 +774,9 @@ dependencies = [ [[package]] name = "gix-index" -version = "0.33.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9a44eb55bd84bb48f8a44980e951968ced21e171b22d115d1cdcef82a7d73f" +checksum = "0cd4203244444017682176e65fd0180be9298e58ed90bd4a8489a357795ed22d" dependencies = [ "bitflags 2.6.0", "bstr", @@ -730,7 +797,7 @@ dependencies = [ "memmap2", "rustix", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -741,25 +808,14 @@ checksum = "e3bc7fe297f1f4614774989c00ec8b1add59571dc9b024b4c00acb7dedd4e19d" dependencies = [ "gix-tempfile", "gix-utils", - "thiserror", -] - -[[package]] -name = "gix-macros" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999ce923619f88194171a67fb3e6d613653b8d4d6078b529b15a765da0edcc17" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", + "thiserror 1.0.69", ] [[package]] name = "gix-object" -version = "0.42.3" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25da2f46b4e7c2fa7b413ce4dffb87f69eaf89c2057e386491f4c55cadbfe386" +checksum = "2f5b801834f1de7640731820c2df6ba88d95480dc4ab166a5882f8ff12b88efa" dependencies = [ "bstr", "gix-actor", @@ -770,15 +826,15 @@ dependencies = [ "gix-validate", "itoa", "smallvec", - "thiserror", + "thiserror 1.0.69", "winnow", ] [[package]] name = "gix-odb" -version = "0.61.1" +version = "0.63.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d384fe541d93d8a3bb7d5d5ef210780d6df4f50c4e684ccba32665a5e3bc9b" +checksum = "a3158068701c17df54f0ab2adda527f5a6aca38fd5fd80ceb7e3c0a2717ec747" dependencies = [ "arc-swap", "gix-date", @@ -791,14 +847,14 @@ dependencies = [ "gix-quote", "parking_lot", "tempfile", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "gix-pack" -version = "0.51.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0594491fffe55df94ba1c111a6566b7f56b3f8d2e1efc750e77d572f5f5229" +checksum = "3223aa342eee21e1e0e403cad8ae9caf9edca55ef84c347738d10681676fd954" dependencies = [ "clru", "gix-chunk", @@ -809,41 +865,40 @@ dependencies = [ "gix-path", "memmap2", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "gix-path" -version = "0.10.12" +version = "0.10.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c04e5a94fdb56b1e91eb7df2658ad16832428b8eeda24ff1a0f0288de2bce554" +checksum = "afc292ef1a51e340aeb0e720800338c805975724c1dfbd243185452efd8645b7" dependencies = [ "bstr", "gix-trace", "home", "once_cell", - "thiserror", + "thiserror 2.0.3", ] [[package]] name = "gix-quote" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89f9a1525dcfd9639e282ea939f5ab0d09d93cf2b90c1fc6104f1b9582a8e49" +checksum = "64a1e282216ec2ab2816cd57e6ed88f8009e634aec47562883c05ac8a7009a63" dependencies = [ "bstr", "gix-utils", - "thiserror", + "thiserror 2.0.3", ] [[package]] name = "gix-ref" -version = "0.44.1" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3394a2997e5bc6b22ebc1e1a87b41eeefbcfcff3dbfa7c4bd73cb0ac8f1f3e2e" +checksum = "ae0d8406ebf9aaa91f55a57f053c5a1ad1a39f60fdf0303142b7be7ea44311e5" dependencies = [ "gix-actor", - "gix-date", "gix-features", "gix-fs", "gix-hash", @@ -854,29 +909,29 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2", - "thiserror", + "thiserror 1.0.69", "winnow", ] [[package]] name = "gix-refspec" -version = "0.23.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6868f8cd2e62555d1f7c78b784bece43ace40dd2a462daf3b588d5416e603f37" +checksum = "ebb005f82341ba67615ffdd9f7742c87787544441c88090878393d0682869ca6" dependencies = [ "bstr", "gix-hash", "gix-revision", "gix-validate", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "gix-revision" -version = "0.27.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b13e43c2118c4b0537ddac7d0821ae0dfa90b7b8dbf20c711e153fb749adce" +checksum = "ba4621b219ac0cdb9256883030c3d56a6c64a6deaa829a92da73b9a576825e1e" dependencies = [ "bstr", "gix-date", @@ -885,14 +940,14 @@ dependencies = [ "gix-object", "gix-revwalk", "gix-trace", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "gix-revwalk" -version = "0.13.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b030ccaab71af141f537e0225f19b9e74f25fefdba0372246b844491cab43e0" +checksum = "b41e72544b93084ee682ef3d5b31b1ba4d8fa27a017482900e5e044d5b1b3984" dependencies = [ "gix-commitgraph", "gix-date", @@ -900,14 +955,14 @@ dependencies = [ "gix-hashtable", "gix-object", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "gix-sec" -version = "0.10.9" +version = "0.10.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2007538eda296445c07949cf04f4a767307d887184d6b3e83e2d636533ddc6e" +checksum = "a8b876ef997a955397809a2ec398d6a45b7a55b4918f2446344330f778d14fd6" dependencies = [ "bitflags 2.6.0", "gix-path", @@ -938,9 +993,9 @@ checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952" [[package]] name = "gix-traverse" -version = "0.39.2" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e499a18c511e71cf4a20413b743b9f5bcf64b3d9e81e9c3c6cd399eae55a8840" +checksum = "030da39af94e4df35472e9318228f36530989327906f38e27807df305fccb780" dependencies = [ "bitflags 2.6.0", "gix-commitgraph", @@ -950,7 +1005,7 @@ dependencies = [ "gix-object", "gix-revwalk", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -963,7 +1018,7 @@ dependencies = [ "gix-features", "gix-path", "home", - "thiserror", + "thiserror 1.0.69", "url", ] @@ -979,12 +1034,12 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.8.5" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c27dd34a49b1addf193c92070bcbf3beaf6e10f16a78544de6372e146a0acf" +checksum = "cd520d09f9f585b34b32aba1d0b36ada89ab7fefb54a8ca3fe37fc482a750937" dependencies = [ "bstr", - "thiserror", + "thiserror 2.0.3", ] [[package]] @@ -1002,16 +1057,17 @@ dependencies = [ [[package]] name = "harfbuzz-sys" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf8c27ca13930dc4ffe474880040fe9e0f03c2121600dc9c95423624cab3e467" +checksum = "eb86e2fef3ba40cebffb8fa2cba811f06aa5c5fd296a4e469473e5398d166594" dependencies = [ "cc", "core-graphics", "core-text", "foreign-types", - "freetype", + "freetype-sys", "pkg-config", + "winapi", ] [[package]] @@ -1154,9 +1210,15 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1195,15 +1257,40 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "jiff" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d9d414fc817d3e3d62b2598616733f76c4cc74fbac96069674739b881295c8" +dependencies = [ + "jiff-tzdb-platform", + "windows-sys 0.59.0", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329" +dependencies = [ + "jiff-tzdb", +] [[package]] name = "libc" -version = "0.2.162" +version = "0.2.165" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "fcb4d3d38eab6c5239a362fa8bae48c03baf980a6e7079f063942d563ef3533e" [[package]] name = "libredox" @@ -1224,9 +1311,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" @@ -1328,7 +1415,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1428,9 +1515,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1496,7 +1583,7 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" [[package]] name = "rusile" -version = "0.15.5" +version = "0.15.7" dependencies = [ "mlua", "sile", @@ -1522,7 +1609,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.87", + "syn 2.0.89", "walkdir", ] @@ -1543,11 +1630,20 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" -version = "0.38.40" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags 2.6.0", "errno", @@ -1609,14 +1705,14 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -1668,7 +1764,7 @@ dependencies = [ [[package]] name = "sile" -version = "0.15.6" +version = "0.15.7" dependencies = [ "anyhow", "clap", @@ -1678,7 +1774,7 @@ dependencies = [ "mlua", "rust-embed", "semver", - "vergen", + "vergen-gix", ] [[package]] @@ -1711,9 +1807,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -1728,7 +1824,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1760,7 +1856,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", ] [[package]] @@ -1771,7 +1876,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", ] [[package]] @@ -1846,9 +1962,9 @@ checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-normalization" @@ -1861,9 +1977,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -1890,17 +2006,44 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vergen" -version = "8.3.2" +version = "9.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566" +checksum = "349ed9e45296a581f455bc18039878f409992999bc1d5da12a6800eb18c8752f" dependencies = [ "anyhow", "cargo_metadata", - "cfg-if", - "gix", + "derive_builder", "regex", + "rustc_version", + "rustversion", + "time", + "vergen-lib", +] + +[[package]] +name = "vergen-gix" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02ef5d49e57c96e025770171c1c7ee0e30cd6f712f21a1fe501a58be6d069192" +dependencies = [ + "anyhow", + "derive_builder", + "gix", "rustversion", "time", + "vergen", + "vergen-lib", +] + +[[package]] +name = "vergen-lib" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229eaddb0050920816cf051e619affaf18caa3dd512de8de5839ccbc8e53abb0" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", ] [[package]] @@ -1931,6 +2074,22 @@ dependencies = [ "winsafe", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.9" @@ -1940,6 +2099,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.52.0" @@ -2051,9 +2216,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -2063,13 +2228,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "synstructure", ] @@ -2090,27 +2255,27 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "synstructure", ] @@ -2133,5 +2298,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] diff --git a/Cargo.toml b/Cargo.toml index 028d77f52..c4cc9f0c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,148 +1,99 @@ -[workspace] -resolver = "2" -members = [ ".", "rusile" ] - - [workspace.package] - version = "0.15.5" - edition = "2021" - rust-version = "1.71.0" - authors = [ - "Simon Cozens", - "Caleb Maclennan ", - "Olivier Nicole", - "Didier Willis" -] - homepage = "https://sile-typesetter.org" - repository = "https://github.com/sile-typesetter/sile" - license = "MIT" - -[workspace.dependencies.mlua] -version = "0.10" -features = [ "anyhow" ] - [package] name = "sile" description = "Simon’s Improved Layout Engine" readme = "README.md" build = "build-aux/build.rs" -version = "0.15.6" +version = "0.15.7" - [package.edition] - workspace = true - - [package.rust-version] - workspace = true - - [package.authors] - workspace = true +[workspace.package] +version = "0.15.7" +edition = "2021" +rust-version = "1.71.0" +authors = [ + "Simon Cozens", + "Caleb Maclennan ", + "Olivier Nicole", + "Didier Willis", +] +homepage = "https://sile-typesetter.org" +repository = "https://github.com/sile-typesetter/sile" +license = "MIT" - [package.homepage] - workspace = true +[workspace] +resolver = "2" +members = [".", "rusile"] - [package.repository] - workspace = true +[package.edition] +workspace = true - [package.license] - workspace = true +[package.rust-version] +workspace = true -[package.metadata.bacon.jobs] -cmd = [ "cargo", "build", "--color", "always" ] +[package.authors] +workspace = true -[package.metadata.typos.default] -locale = "en-us" -extend-ignore-re = [ - "(?s)(#|//|--|%)\\s*typos: ignore start.*?\\n\\s*(#|//|--|%)\\s*typos: ignore end" -] -extend-ignore-identifiers-re = [ - "[a-f0-9]{7}", - "^.{2,3}$", - "^twords?", - "[Pp]arms", - "wdth", - "0fpt", - "^ot", - "^hb_ot", - "^HB_", - "^Tyre$", - "PoDoFo", - "_Flate", - "DEPENDEES", - "EPdf", - "FileAttachement" -] +[package.homepage] +workspace = true - [package.metadata.typos.default.extend-words] - craters = "creators" - neet = "need" +[package.repository] +workspace = true -[package.metadata.typos.files] -ignore-hidden = false -extend-exclude = [ - "/.git", - "CHANGELOG.md", - "build-aux/ax*", - "languages/*/hyphens*", - "lua-libraries/*", - "lua_modules/*", - "node_modules/*", - "tests/*.expected", - "cmake/*.diff", - "libtexpdf" -] +[package.license] +workspace = true [[bin]] name = "sile" -required-features = [ "cli" ] - -[features] -default = [ - "cli", - "bash", - "elvish", - "fish", - "manpage", - "powershell", - "zsh" -] -lua54 = [ "mlua/lua54" ] -lua53 = [ "mlua/lua53" ] -lua52 = [ "mlua/lua52" ] -lua51 = [ "mlua/lua51" ] -luajit = [ "mlua/luajit" ] -vendored = [ "mlua/vendored" ] -static = [ "rust-embed" ] -variations = [ ] -completions = [ "cli", "clap_complete" ] -cli = [ "clap" ] -bash = [ "completions" ] -elvish = [ "completions" ] -fish = [ "completions" ] -manpage = [ "clap_mangen" ] -powershell = [ "completions" ] -zsh = [ "completions" ] +required-features = ["cli"] [profile.release] lto = true +[features] +default = ["cli", "bash", "elvish", "fish", "manpage", "powershell", "zsh"] +lua54 = ["mlua/lua54"] +lua53 = ["mlua/lua53"] +lua52 = ["mlua/lua52"] +lua51 = ["mlua/lua51"] +luajit = ["mlua/luajit"] +vendored = ["mlua/vendored"] +static = ["rust-embed"] +variations = [] +completions = ["cli", "clap_complete"] +cli = ["clap"] +bash = ["completions"] +elvish = ["completions"] +fish = ["completions"] +manpage = ["clap_mangen"] +powershell = ["completions"] +zsh = ["completions"] + +[workspace.dependencies.mlua] +version = "0.10" +features = ["anyhow"] + +[workspace.dependencies.sile] +path = "." +version = "0.15.7" + [dependencies.anyhow] version = "1.0" [dependencies.clap] version = "4.4" optional = true -features = [ "derive", "string", "wrap_help" ] +features = ["derive", "string", "wrap_help"] [dependencies.mlua] workspace = true -features = [ "macros" ] +features = ["macros"] [dependencies.rust-embed] version = "8.0" optional = true -features = [ "include-exclude" ] +features = ["include-exclude"] [dependencies.harfbuzz-sys] -version = "0.5" +version = "0.6" optional = true [dependencies.semver] @@ -159,9 +110,59 @@ optional = true [build-dependencies.clap] version = "4.4" optional = true -features = [ "derive" ] +features = ["derive"] -[build-dependencies.vergen] -version = "8.2" +[build-dependencies.vergen-gix] +version = "1.0" default-features = false -features = [ "build", "cargo", "git", "gitoxide" ] +features = ["build", "cargo", "rustc"] + +[package.metadata.docs.rs] +features = ["luajit", "vendored"] + +[package.metadata.typos.default] +locale = "en-us" +extend-ignore-re = [ + "(?s)(#|//|--|%)\\s*typos: ignore start.*?\\n\\s*(#|//|--|%)\\s*typos: ignore end", +] +extend-ignore-identifiers-re = [ + "[a-f0-9]{7}", + "^.{2,3}$", + "^twords?", + "[Pp]arms", + "wdth", + "0fpt", + "^ot", + "^hb_ot", + "^HB_", + "^Tyre$", + "PoDoFo", + "_Flate", + "pointint", + "DEPENDEES", + "EPdf", + "FileAttachement", +] + +[package.metadata.typos.default.extend-words] +beveled = "bevelled" # mathML uses en-gb spelling +bevelled = "bevelled" # mathML uses en-gb spelling +centred = "centred" # Unicode character description +craters = "creators" +lamda = "lamda" # Unicode character description +neet = "need" + +[package.metadata.typos.files] +ignore-hidden = false +extend-exclude = [ + "/.git", + "CHANGELOG.md", + "build-aux/ax*", + "languages/*/hyphens*", + "lua-libraries/*", + "lua_modules/*", + "node_modules/*", + "tests/*.expected", + "cmake/*.diff", + "libtexpdf", +] diff --git a/Makefile-fonts b/Makefile-fonts index e8e94fe40..7294b8946 100644 --- a/Makefile-fonts +++ b/Makefile-fonts @@ -2,12 +2,12 @@ if FONT_DOWNLOAD_TOOLS +# Target defined in Makefile.am, just adding a dependency here +.sources: fonttooling + .fonts: fonttooling [ -h .fonts ] || mkdir -p $@ -.sources: fonttooling - [ -h .sources ] || mkdir -p $@ - fonttooling: $(if $(BSDTAR),,$(error Please set BSDTAR with path or `./configure --enable-developer-mode`)) $(if $(CURL),,$(error Please set CURL with path or `./configure --enable-developer-mode`)) diff --git a/Makefile.am b/Makefile.am index 4bf08a194..bb44bdb5e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,5 +1,5 @@ ACLOCAL_AMFLAGS = -I build-aux -AM_DISTCHECK_CONFIGURE_FLAGS = --enable-developer-mode +AM_DISTCHECK_CONFIGURE_FLAGS = --enable-developer-mode --without-developer-tools PDFINFO=$(PDFINFO) .ONESHELL: .SECONDARY: @@ -76,6 +76,7 @@ dist_license_DATA = LICENSE.md EXTRA_DIST = spec tests documentation sile-dev-1.rockspec fontconfig.conf EXTRA_DIST += build-aux/action-updater.js build-aux/cargo-updater.js build-aux/config.ld build-aux/decore-automake.sh build-aux/git-version-gen EXTRA_DIST += Dockerfile build-aux/docker-bootstrap.sh build-aux/docker-fontconfig.conf hooks/build +EXTRA_DIST += build-aux/xml-entities-to-lua.xsl EXTRA_DIST += default.nix flake.nix flake.lock shell.nix build-aux/pkg.nix EXTRA_DIST += package.json # imported by both Nix and Docker EXTRA_DIST += $(FIGURES) @@ -94,6 +95,10 @@ else !SHARED EXTRA_RUNTIME_DEPS = endif +MATHML_ENTITIES = packages/math/mathml-entities.lua +EXTRA_DIST += $(MATHML_ENTITIES) +BUILT_SOURCES += $(MATHML_ENTITIES) + CLEANFILES = $(MANUAL) DISTCLEANFILES = @AMINCLUDE@ @@ -174,7 +179,7 @@ CARGO_FEATURE_ARGS += --features variations endif rusile.so: $(rusile_so_SOURCES) $(bin_PROGRAMS) - $(CARGO_ENV) $(CARGO) build $(CARGO_VERBOSE) $(RUSILE_FEATURE_ARG) $(CARGO_RELEASE_ARGS) -p rusile + $(CARGO_ENV) $(CARGO) build $(CARGO_VERBOSE) --target $(CARGO_TARGET_TRIPLE) $(RUSILE_FEATURE_ARG) $(CARGO_RELEASE_ARGS) -p rusile $(INSTALL) @builddir@/target/@RUST_TARGET_SUBDIR@/lib$@ $@ DEPDIR := .deps @@ -211,7 +216,7 @@ sile-$(VERSION).pdf: $(MANUAL) sile-%.md: CHANGELOG.md $(SED) -e '/\.\.\.v$*/,/\.\.\.v/!d' CHANGELOG.md | \ $(SED) -e '1,3d;N;$$!P;$$!D;$$d' | - $(or $(filter cat,$(TYPOS)),$(TYPOS) --write-changes -) > $@ + $(if $(TYPOS),$(TYPOS) --write-changes,cat) - > $@ check: selfcheck @@ -312,6 +317,18 @@ patterndeps = $(_FORCED) $(_TEST_DEPS) $(_DOCS_DEPS) | $(bin_PROGRAMS) $(EXTRA_R $(DOT) -Tpdf $< -o $@.gs $(GS) -q -sDEVICE=pdfwrite -dCompatibilityLevel=1.5 -o $@ $@.gs +XML_ENTITIES = .sources/unicode.xml +XML_ENTITIES_COMMIT = 77acf14428202e4e1dba54ff1e5ed43fe5ab474f + +.sources: + [ -h .sources ] || mkdir -p $@ + +$(XML_ENTITIES): + $(CURL) https://raw.githubusercontent.com/w3c/xml-entities/$(XML_ENTITIES_COMMIT)/unicode.xml -o $@ + +$(MATHML_ENTITIES): build-aux/xml-entities-to-lua.xsl + $(XSLTPROC) $< $(XML_ENTITIES) | $(or $(STYLUA),cat) - > $@ + .PHONY: force force: ; diff --git a/README.md b/README.md index 1abea4988..a7bbafa70 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ## What is SILE? SILE is a [typesetting][typesetting] system; its job is to produce beautiful printed documents. -Conceptually, SILE is similar to [TeX][tex]—from which it borrows some concepts and even syntax and algorithms—but the similarities end there. +Conceptually, SILE is similar to [TeX][tex]—from which it borrows some concepts and algorithms—but the similarities end there. Rather than being a derivative of the TeX family SILE is a new typesetting and layout engine written from the ground up using modern technologies and borrowing some ideas from graphical systems such as [InDesign][indesign]. ## Where does it run? diff --git a/action.yml b/action.yml index ff3c481e8..7afda450c 100644 --- a/action.yml +++ b/action.yml @@ -7,7 +7,7 @@ inputs: default: "" runs: using: docker - image: docker://ghcr.io/sile-typesetter/sile:v0.15.6 + image: docker://ghcr.io/sile-typesetter/sile:v0.15.7 entrypoint: sh args: - -c diff --git a/build-aux/ax_lua.m4 b/build-aux/ax_lua.m4 index 4e82e454e..ebd0182e6 100644 --- a/build-aux/ax_lua.m4 +++ b/build-aux/ax_lua.m4 @@ -57,15 +57,15 @@ # version number greater or equal to MINIMUM-VERSION and less than # TOO-BIG-VERSION will be accepted. # -# Optionally, the fifth argument ENABLE_LUAJIT can be set to control whether -# LuaJIT or PUC Lua should be considered during discovery. An empty value or +# Optionally, the fifth argument ENABLE_LUAJIT can be set to control +# whether LuaJIT or PUC Lua should be considered. An empty empty value or # 'never' means only PUC Lua installations will be considered; a value of # 'always' means PUC is not even considered and only LuaJIT is discovered; -# 'prefer' means LuaJIT should be used if found but allow PUC , and finally -# 'allow' means it should not be chosen if any PUC Lua version is found but -# it could be used as a last resort. For 'default' and 'allow' a new -# configure flag will be provided to the user --with-luajit with the default -# option being either 'yes' or 'no' respectively. +# 'prefer' means LuaJIT should be used if found but allow PUC, and finally +# 'allow' means it should not be chosen if any PUC Lua version is found +# but it could be used as a last resort. For 'default' and 'allow' a new +# configure flag will be provided to the user --with-luajit with the +# default option being either 'yes' or 'no' respectively. # # The Lua version number, LUA_VERSION, is found from the interpreter, and # substituted. LUA_PLATFORM is also found, but not currently supported (no @@ -198,7 +198,7 @@ dnl ========================================================================= dnl AX_PROG_LUA([MINIMUM-VERSION], [TOO-BIG-VERSION], -dnl [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND] +dnl [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND], dnl [ENABLE_LUAJIT]) dnl ========================================================================= AC_DEFUN([AX_PROG_LUA], @@ -210,29 +210,31 @@ AC_DEFUN([AX_PROG_LUA], dnl Make LUA a precious variable. AC_ARG_VAR([LUA], [The Lua interpreter, e.g. /usr/bin/lua5.1]) - dnl Figure out whether we should expose LuaJIT as a user facing configure flag - AS_CASE(["m4_default([$5], [never])"], - [never], [ default_luajit=no; with_luajit=no ], - [always], [ default_luajit=yes; with_luajit=yes ], + dnl Figure out whether we should expose LuaJIT as a user facing configure + dnl flag and if so whether the default option should be with or without. + m4_case(m4_default([$5], [never]), + [never], [ + with_luajit=no + ], + [always], [ + with_luajit=yes + ], [allow], [ - default_luajit=no - m4_if([$5], [allow], [ - AC_ARG_WITH([luajit], - [AS_HELP_STRING([--with-luajit], - [prefer LuaJIT over PUC Lua, even if the latter is newer])]) - ]) + AC_ARG_WITH([luajit], + [AS_HELP_STRING([--with-luajit], + [prefer LuaJIT over PUC Lua, even if the latter is newer])]) test "x$with_luajit" != 'xyes' && with_luajit=no ], [prefer], [ - default_luajit=yes - m4_if([$5], [prefer], [ - AC_ARG_WITH([luajit], - [AS_HELP_STRING([--without-luajit], - [prefer PUC Lua over LuaJIT])]) - ]) + AC_ARG_WITH([luajit], + [AS_HELP_STRING([--without-luajit], + [prefer PUC Lua over LuaJIT])]) test "x$with_luajit" != 'xno' && with_luajit=yes ], - [AC_MSG_ERROR([Unrecognized value for ENABLE_LUAJIT])]) + [ + AC_MSG_ERROR([Unrecognized value for ENABLE_LUAJIT]) + ] + ) AM_CONDITIONAL([LUAJIT], [test "x$with_luajit" == "xyes"]) dnl Find a Lua interpreter. @@ -401,7 +403,11 @@ AC_DEFUN([AX_PROG_LUA], ]) dnl AX_WITH_LUA is now the same thing as AX_PROG_LUA. -AU_DEFUN([AX_WITH_LUA], [AX_PROG_LUA], [$0 is deprecated, please use AX_PROG_LUA instead]) +AC_DEFUN([AX_WITH_LUA], +[ + AC_MSG_WARN([[$0 is deprecated, please use AX_PROG_LUA instead]]) + AX_PROG_LUA +]) dnl ========================================================================= diff --git a/build-aux/ax_lua_module.m4 b/build-aux/ax_lua_module.m4 index ef4b2e57b..3d1f74ea5 100644 --- a/build-aux/ax_lua_module.m4 +++ b/build-aux/ax_lua_module.m4 @@ -1,15 +1,53 @@ -#serial 0 - -AC_DEFUN([AX_LUA_MODULE], -[ - AX_PROG_LUA([5.1], [], [], [], [prefer]) - AC_MSG_CHECKING([whether Lua can load module $1]) - AS_IF([$LUA -e 'require("$1")' 2>/dev/null], [ - AC_MSG_RESULT([loaded]) - $3 - ], [ - AC_MSG_RESULT([unable to load]) - m4_default([$4], [AC_MSG_ERROR([cannot find Lua library $1 - install from luarocks package $2])]) - ]) +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_lua_module.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_LUA_MODULE([ROCKNAME], [MODULE]) +# +# DESCRIPTION +# +# Tests the availability of a Lua module using both available mechanisms, +# first checking if a Lua Rock manifest is available, and if not falling +# back to attempting to load a module directly. +# +# If the module name is the same as the rock name, the second argument can +# be ommitted. +# +# Example usage: +# +# AX_LUA_MODULE([ssl], [luasec]) +# +# Note: under the hood this uses AX_LUAROCKS_ROCK and AX_LUA_REQUIRE. +# +# LICENSE +# +# Copyright (c) 2024 Caleb Maclennan +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 1 + +AC_DEFUN([AX_LUA_MODULE],[ + pushdef([ROCKNAME],$1) + pushdef([MODULE],m4_default($2,$1)) + pushdef([VARIABLE],LUA_HAS_[]m4_toupper(m4_translit($1,-.,__))) + + AC_ARG_VAR(VARIABLE,Was Lua module found) + + AS_IF(test -z "$VARIABLE",[ + AX_LUAROCKS_ROCK(ROCKNAME,[VARIABLE=yes],[VARIABLE=no]) + AS_IF([test "x$VARIABLE" != xyes],[ + AX_LUA_REQUIRE(MODULE,[VARIABLE=yes]) + ]) + ]) + + popdef([ROCKNAME]) + popdef([MODULE]) ]) + diff --git a/build-aux/ax_lua_require.m4 b/build-aux/ax_lua_require.m4 new file mode 100644 index 000000000..8aebb1d1f --- /dev/null +++ b/build-aux/ax_lua_require.m4 @@ -0,0 +1,65 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_lua_require.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_LUA_REQUIRE([MODULE], [ACTION_IF_FOUND], [ACTION_IF_NOT_FOUND]) +# +# DESCRIPTION +# +# Tests whether Lua can load a module, fails if it is not loadable. +# +# Example usage: +# +# AX_LUA_REQUIRE(lpeg) +# +# Note: this is an alternative to AX_LUAROCKS_ROCK which queries the +# LuaRocks manifest for whether something is installed. Sometimes a +# proper manifest is not available, and this tests whether a given +# module name is actually loadable. +# +# It can also be useful to test for libraries that may or may not be +# built into Lua VMs. Builtin modules will return a success. +# +# LICENSE +# +# Copyright (c) 2024 Caleb Maclennan +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 1 + +AC_DEFUN([AX_LUA_REQUIRE],[ + # Make sure we have a Lua interpreter + if test -z "$LUA"; then + AX_PROG_LUA + if test -z "$LUA"; then + AC_MSG_ERROR([No Lua VM set]) + fi + fi + + AC_PREREQ([2.61]) + + pushdef([MODULE],$1) + pushdef([ACTION_IF_FOUND],$2) + pushdef([ACTION_IF_NOT_FOUND],$3) + + AC_MSG_CHECKING([whether Lua can load module MODULE]) + AS_IF([$LUA -e 'require("MODULE")' 2>/dev/null], [ + AC_MSG_RESULT([loaded]) + ACTION_IF_FOUND + ], [ + AC_MSG_RESULT([unable to load]) + m4_ifset([ACTION_IF_NOT_FOUND][ACTION_IF_NOT_FOUND], + [AC_MSG_FAILURE([cannot find Lua module MODULE])]) + ]) + + popdef([MODULE]) + popdef([ACTION_IF_FOUND]) + popdef([ACTION_IF_NOT_FOUND]) +]) + diff --git a/build-aux/ax_luarocks_rock.m4 b/build-aux/ax_luarocks_rock.m4 new file mode 100644 index 000000000..3e98700f7 --- /dev/null +++ b/build-aux/ax_luarocks_rock.m4 @@ -0,0 +1,62 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_luarocks_rock.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_LUAROCKS_ROCK([ROCKNAME], [ACTION_IF_FOUND], [ACTION_IF_NOT_FOUND]) +# +# DESCRIPTION +# +# Checks for a rock, and fails if it is not installed. +# +# Example usage: +# +# AX_LUAROCKS_ROCK(stdlib) +# +# Note: use of this macro is not normally recommended. Normally, LuaRocks +# should be used to drive the build system, and it takes care of rock +# dependencies. Use this macro only if LuaRocks cannot be used at the top +# level, for example, in a build system that uses Lua only incidentally. +# +# LICENSE +# +# Copyright (c) 2024 Caleb Maclennan +# Copyright (c) 2016 Reuben Thomas +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 4 + +AC_DEFUN([AX_LUAROCKS_ROCK],[ + # Make sure we have luarocks + if test -z "$LUAROCKS"; then + AX_WITH_PROG(LUAROCKS,luarocks) + if test -z "$LUAROCKS"; then + AC_MSG_ERROR([can't find luarocks]) + fi + fi + + AC_PREREQ([2.61]) + + pushdef([ROCKNAME],$1) + pushdef([ACTION_IF_FOUND],$2) + pushdef([ACTION_IF_NOT_FOUND],$3) + + AC_MSG_CHECKING(whether LuaRock ROCKNAME is installed) + AS_IF(["$LUAROCKS"${LUA_VERSION+ --lua-version $LUA_VERSION} show ROCKNAME > /dev/null 2>&1],[ + AC_MSG_RESULT(yes) + ACTION_IF_FOUND + ],[ + AC_MSG_RESULT(no) + m4_ifset([ACTION_IF_NOT_FOUND],[ACTION_IF_NOT_FOUND], + [AC_MSG_FAILURE([LuaRock ROCKNAME not found])]) + ]) + + popdef([ROCKNAME]) + popdef([ACTION_IF_FOUND]) + popdef([ACTION_IF_NOT_FOUND]) +]) diff --git a/build-aux/ax_with_prog.m4 b/build-aux/ax_with_prog.m4 new file mode 100644 index 000000000..b3a881c0d --- /dev/null +++ b/build-aux/ax_with_prog.m4 @@ -0,0 +1,70 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_with_prog.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_WITH_PROG([VARIABLE],[program],[VALUE-IF-NOT-FOUND],[PATH]) +# +# DESCRIPTION +# +# Locates an installed program binary, placing the result in the precious +# variable VARIABLE. Accepts a present VARIABLE, then --with-program, and +# failing that searches for program in the given path (which defaults to +# the system path). If program is found, VARIABLE is set to the full path +# of the binary; if it is not found VARIABLE is set to VALUE-IF-NOT-FOUND +# if provided, unchanged otherwise. +# +# A typical example could be the following one: +# +# AX_WITH_PROG(PERL,perl) +# +# NOTE: This macro is based upon the original AX_WITH_PYTHON macro from +# Dustin J. Mitchell . +# +# LICENSE +# +# Copyright (c) 2008 Francesco Salvestrini +# Copyright (c) 2008 Dustin J. Mitchell +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 17 + +AC_DEFUN([AX_WITH_PROG],[ + AC_PREREQ([2.61]) + + pushdef([VARIABLE],$1) + pushdef([EXECUTABLE],$2) + pushdef([VALUE_IF_NOT_FOUND],$3) + pushdef([PATH_PROG],$4) + + AC_ARG_VAR(VARIABLE,Absolute path to EXECUTABLE executable) + + AS_IF(test -z "$VARIABLE",[ + AC_MSG_CHECKING(whether EXECUTABLE executable path has been provided) + AC_ARG_WITH(EXECUTABLE,AS_HELP_STRING([--with-EXECUTABLE=[[[PATH]]]],absolute path to EXECUTABLE executable), [ + AS_IF([test "$withval" != yes && test "$withval" != no],[ + VARIABLE="$withval" + AC_MSG_RESULT($VARIABLE) + ],[ + VARIABLE="" + AC_MSG_RESULT([no]) + AS_IF([test "$withval" != no], [ + AC_PATH_PROG([]VARIABLE[],[]EXECUTABLE[],[]VALUE_IF_NOT_FOUND[],[]PATH_PROG[]) + ]) + ]) + ],[ + AC_MSG_RESULT([no]) + AC_PATH_PROG([]VARIABLE[],[]EXECUTABLE[],[]VALUE_IF_NOT_FOUND[],[]PATH_PROG[]) + ]) + ]) + + popdef([PATH_PROG]) + popdef([VALUE_IF_NOT_FOUND]) + popdef([EXECUTABLE]) + popdef([VARIABLE]) +]) diff --git a/build-aux/build.rs b/build-aux/build.rs index 2f90b107f..044eeb825 100644 --- a/build-aux/build.rs +++ b/build-aux/build.rs @@ -3,7 +3,7 @@ use clap_mangen::Man; #[cfg(any(feature = "static", feature = "completions"))] use std::path::Path; use std::{collections, env}; -use vergen::EmitBuilder; +use vergen_gix::{CargoBuilder, Emitter, GixBuilder, RustcBuilder}; #[cfg(feature = "completions")] use { clap::CommandFactory, @@ -21,14 +21,22 @@ fn main() { println!("cargo:rerun-if-changed={dependency}"); } } - let mut builder = EmitBuilder::builder(); + let mut builder = Emitter::default(); + builder + .add_instructions(&CargoBuilder::all_cargo().unwrap()) + .unwrap(); // If passed a version from automake, use that instead of vergen's formatting if let Ok(val) = env::var("VERSION_FROM_AUTOTOOLS") { - println!("cargo:rustc-env=VERGEN_GIT_DESCRIBE={val}") + println!("cargo:rustc-env=VERGEN_GIT_DESCRIBE={val}"); + builder + .add_instructions(&RustcBuilder::all_rustc().unwrap()) + .unwrap(); } else { - builder = *builder.git_describe(true, true, None); + builder + .add_instructions(&GixBuilder::all_git().unwrap()) + .unwrap(); }; - builder.emit().expect("Unable to generate the cargo keys!"); + builder.emit().unwrap(); pass_on_configure_details(); #[cfg(feature = "manpage")] generate_manpage(); diff --git a/build-aux/cargo-updater.js b/build-aux/cargo-updater.js index 1b64fda44..f91b9074e 100644 --- a/build-aux/cargo-updater.js +++ b/build-aux/cargo-updater.js @@ -1,4 +1,5 @@ const TOML = require('@iarna/toml') +const { exec } = require('node:child_process') module.exports.readVersion = function (contents) { const data = TOML.parse(contents) @@ -6,7 +7,11 @@ module.exports.readVersion = function (contents) { } module.exports.writeVersion = function (contents, version) { - const data = TOML.parse(contents) - data.package.version = version - return TOML.stringify(data) + exec('cargo-set-version set-version ' + version, (err, output) => { + if (err) { + console.error("Could not run Cargo subcommand to set version: ", err) + return + } + }) + return contents } diff --git a/build-aux/pkg.nix b/build-aux/pkg.nix index 91734bd97..c5009e063 100644 --- a/build-aux/pkg.nix +++ b/build-aux/pkg.nix @@ -18,12 +18,15 @@ rustPlatform, # buildInputs + cargo-edit, lua, harfbuzz, icu, fontconfig, libiconv, + libxslt, stylua, + taplo, typos, darwin, # FONTCONFIG_FILE @@ -43,6 +46,10 @@ let ps: with ps; [ + # used for module detection, also recommended at runtime for 3rd party module installation + luarocks + + # modules used at runtime cassowary cldr fluent @@ -113,12 +120,15 @@ stdenv.mkDerivation (finalAttrs: { buildInputs = [ + cargo-edit luaEnv harfbuzz icu fontconfig libiconv + libxslt stylua + taplo typos ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ diff --git a/build-aux/que_developer_mode.m4 b/build-aux/que_developer_mode.m4 index 18bd29fe8..f1bfeafdc 100644 --- a/build-aux/que_developer_mode.m4 +++ b/build-aux/que_developer_mode.m4 @@ -15,5 +15,12 @@ AC_DEFUN([QUE_DEVELOPER_MODE], [ [USE_DEVELOPER_MODE=]m4_if(_que_developer_def, [enable], [no], [yes])) AC_MSG_RESULT([$USE_DEVELOPER_MODE]) AM_CONDITIONAL([DEVELOPER_MODE], [test $USE_DEVELOPER_MODE = yes]) - + AM_COND_IF([DEVELOPER_MODE], [ + AC_ARG_WITH([developer-tools], + AS_HELP_STRING([--without-developer-tools], + [Avoid checks for developer workflow tooling])) + ], [ + with_developer_tools=no + ]) + AM_CONDITIONAL([DEVELOPER_TOOLS], [test "x$with_developer_tools" != "xno"]) ]) diff --git a/build-aux/que_font.m4 b/build-aux/que_font.m4 index 57db8a4fe..9599b3940 100644 --- a/build-aux/que_font.m4 +++ b/build-aux/que_font.m4 @@ -3,7 +3,7 @@ AC_DEFUN([QUE_FONT], [ if test -z "$FCMATCH"; then AC_PATH_PROG(FCMATCH, fc-match) if test -z "$FCMATCH"; then - AC_MSG_ERROR([can't find fc-match]) + AC_MSG_ERROR([can't find fc-match]) fi fi pushdef([FONT],$1) diff --git a/build-aux/que_progvar.m4 b/build-aux/que_progvar.m4 index 20d1288b4..043a93758 100644 --- a/build-aux/que_progvar.m4 +++ b/build-aux/que_progvar.m4 @@ -1,5 +1,11 @@ AC_DEFUN([QUE_PROGVAR], [ - test -n "$m4_toupper($1)" || { AC_PATH_PROG(m4_toupper($1), m4_default($2,$1)) } - test -n "$m4_toupper($1)" || AC_MSG_ERROR([m4_default($2,$1) is required]) + pushdef([VARIABLE],m4_toupper($1)) + pushdef([EXECUTABLE],m4_default($2,$1)) + AX_WITH_PROG(VARIABLE,EXECUTABLE) + AS_IF([test "x$with_$1" != xno && test -z "$VARIABLE"], [ + AC_MSG_ERROR([EXECUTABLE is required]) + ]) + popdef([EXECUTABLE]) + popdef([VARIABLE]) ]) diff --git a/build-aux/que_rust_boilerplate.am b/build-aux/que_rust_boilerplate.am index a1ba65af4..26de4ef58 100644 --- a/build-aux/que_rust_boilerplate.am +++ b/build-aux/que_rust_boilerplate.am @@ -57,8 +57,8 @@ $(COMPLETIONS_OUT_DIR)/_$(TRANSFORMED_PACKAGE_NAME): $(CARGO_BIN) | $(COMPLETION $(_RUST_OUT) $(CARGO_BIN): $(@PACKAGE_VAR@_SOURCES) $(nodist_@PACKAGE_VAR@_SOURCES) $(EXTRA_@PACKAGE_VAR@_SOURCES) set -e export AUTOTOOLS_DEPENDENCIES="$^" - $(CARGO_ENV) $(CARGO) build $(CARGO_VERBOSE) $(CARGO_FEATURE_ARGS) $(CARGO_RELEASE_ARGS) - $(CARGO_ENV) $(CARGO) build --quiet --message-format=json $(CARGO_FEATURE_ARGS) $(CARGO_RELEASE_ARGS) | \ + $(CARGO_ENV) $(CARGO) build $(CARGO_VERBOSE) --target $(CARGO_TARGET_TRIPLE) $(CARGO_FEATURE_ARGS) $(CARGO_RELEASE_ARGS) + $(CARGO_ENV) $(CARGO) build --target $(CARGO_TARGET_TRIPLE) --quiet --message-format=json $(CARGO_FEATURE_ARGS) $(CARGO_RELEASE_ARGS) | \ $(JQ) -sr 'map(select(.reason == "build-script-executed")) | last | .out_dir' > $(_RUST_OUT) RUST_DEVELOPER_TARGETS = cargo-test clippy rustfmt diff --git a/build-aux/que_rust_boilerplate.m4 b/build-aux/que_rust_boilerplate.m4 index 36fa0923d..bc3d5ffaf 100644 --- a/build-aux/que_rust_boilerplate.m4 +++ b/build-aux/que_rust_boilerplate.m4 @@ -22,13 +22,17 @@ AC_DEFUN_ONCE([QUE_RUST_BOILERPLATE], [ QUE_PROGVAR([rustfmt]) ]) + AC_ARG_VAR(CARGO_TARGET_TRIPLE, "Target triple for Rust compilations") + if test -z "$CARGO_TARGET_TRIPLE"; then + CARGO_TARGET_TRIPLE="$($RUSTC -vV | $SED -n 's/host: //p')" + fi AC_MSG_CHECKING([whether to build Rust code with debugging information]) AM_COND_IF([DEBUG_RELEASE], [ AC_MSG_RESULT(yes) - RUST_TARGET_SUBDIR=debug + RUST_TARGET_SUBDIR=$CARGO_TARGET_TRIPLE/debug ], [ AC_MSG_RESULT(no) - RUST_TARGET_SUBDIR=release + RUST_TARGET_SUBDIR=$CARGO_TARGET_TRIPLE/release ]) AC_SUBST([RUST_TARGET_SUBDIR]) diff --git a/build-aux/xml-entities-to-lua.xsl b/build-aux/xml-entities-to-lua.xsl new file mode 100644 index 000000000..499d5a2bc --- /dev/null +++ b/build-aux/xml-entities-to-lua.xsl @@ -0,0 +1,199 @@ + + + + + + + + + + + + "" + + + + + + + + + + + + + + + + + + + + + + + + + + ord + ord + bin + close + + + botaccent + accent + ord + + + ord + ord + + + + + ord + op + + + open + punct + rel + ord + ord + bin + ord + + + + + + + "" + nil + + + +--- GENERATED FILE, DO NOT EDIT MANUALLY +-- +-- Operator dictionary for unicode characters +-- +-- Extracted from https://raw.githubusercontent.com/w3c/xml-entities/gh-pages/unicode.xml +-- (https://github.com/w3c/xml-entities) +-- Copyright David Carlisle 1999-2024 +-- Use and distribution of this code are permitted under the terms of the +-- W3C Software Notice and License. +-- http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231.html +-- This file is a collection of information about how to map Unicode entities to LaTeX, +-- and various SGML/XML entity sets (ISO and MathML/HTML). +-- A Unicode character may be mapped to several entities. +-- Originally designed by Sebastian Rahtz in conjunction with Barbara Beeton for the STIX project +-- + +local atoms = require("packages.math.atoms") + +--- Transform a list of codepoints into a string +local function U (...) + local t = { ... } + local str = "" + for i = 1, #t do + str = str .. luautf8.char(t[i]) + end + return str +end + +local symbols = {} +local operatorDict = {} + +--- Register a symbol +-- @tparam string str String representation of the symbol +-- @tparam string shortatom Short atom type +-- @tparam string mathlatex TeX-like name of the symbol (from unicode-math) +-- @tparam string _ Unicode name of the symbol (informative) +-- @tparam table ops List of operator forms and their properties +local function addSymbol (str, shortatom, mathlatex, _, ops) + if mathlatex then + SU.debug("math.symbols", "Registering symbol", str, "as", mathlatex) + symbols[mathlatex] = str + end + local op = {} + op.atom = atoms.types[shortatom] + if ops then + op.forms = {} + for _, v in pairs(ops) do + if v.form then + -- NOTE: At this point the mu unit is not yet defined, so keep it as a string. + v.lspace = v.lspace and ("%smu"):format(v.lspace) or "0mu" + v.rspace = v.rspace and ("%smu"):format(v.rspace) or "0mu" + op.forms[v.form] = v + else + SU.warn("No form for operator " .. str .. " (operator dictionary is probably incomplete)") + end + end + end + operatorDict[str] = op +end + + + +return { + operatorDict = operatorDict, + symbols = symbols, +} + + + + + + + + + + + + + + + addSymbol( + + + + + + ,"", + + + + + + ,"" + + + + ,{ + + + + + } + + ,nil + + ) + + + + + { + + + + = + + + , + + }, + + + diff --git a/classes/book.lua b/classes/book.lua index 13aa4e4f9..0b7fbbf52 100644 --- a/classes/book.lua +++ b/classes/book.lua @@ -256,20 +256,15 @@ function class:registerCommands () end, "Begin a new subsection") self:registerCommand("book:chapterfont", function (_, content) - SILE.settings:temporarily(function () - SILE.call("font", { weight = 800, size = "22pt" }, content) - end) + SILE.call("font", { weight = 800, size = "22pt" }, content) end) + self:registerCommand("book:sectionfont", function (_, content) - SILE.settings:temporarily(function () - SILE.call("font", { weight = 800, size = "15pt" }, content) - end) + SILE.call("font", { weight = 800, size = "15pt" }, content) end) self:registerCommand("book:subsectionfont", function (_, content) - SILE.settings:temporarily(function () - SILE.call("font", { weight = 800, size = "12pt" }, content) - end) + SILE.call("font", { weight = 800, size = "12pt" }, content) end) end diff --git a/configure.ac b/configure.ac index 3aa73405a..098f34d58 100644 --- a/configure.ac +++ b/configure.ac @@ -21,8 +21,11 @@ AC_PROG_GREP AC_PROG_OBJC AC_PROG_SED QUE_PROGVAR([cmp]) +QUE_PROGVAR([diff]) QUE_PROGVAR([find]) +QUE_PROGVAR([head]) QUE_PROGVAR([jq]) +QUE_PROGVAR([luarocks]) QUE_PROGVAR([pdfinfo]) QUE_PROGVAR([sort]) QUE_PROGVAR([xargs]) @@ -172,25 +175,29 @@ AM_CONDITIONAL([ICU], [test "x$with_icu" = "xyes"]) # Required for downloading fonts for the manual and for tests # Since the source tarball includes a prebuilt manual we only need this for Git source builds AM_COND_IF([FONT_DOWNLOAD_TOOLS], [ - QUE_PROGVAR([curl]) QUE_PROGVAR([bsdtar]) + QUE_PROGVAR([curl]) ]) AM_COND_IF([DEVELOPER_MODE], [ + AX_WITH_PROG([DELTA], [delta], [cat]) + AX_WITH_PROG([PERL], [perl]) +]) + +AM_COND_IF([DEVELOPER_TOOLS], [ QUE_PROGVAR([busted]) + QUE_PROGVAR([cargosetversion], [cargo-set-version]) QUE_PROGVAR([curl]) - QUE_PROGVAR([delta]) - QUE_PROGVAR([diff]) - QUE_PROGVAR([head]) QUE_PROGVAR([ldoc]) QUE_PROGVAR([luacheck]) - QUE_PROGVAR([luarocks]) QUE_PROGVAR([nix]) QUE_PROGVAR([npm]) QUE_PROGVAR([perl]) QUE_PROGVAR([stylua]) + QUE_PROGVAR([taplo]) QUE_PROGVAR([tr]) QUE_PROGVAR([typos]) + QUE_PROGVAR([xsltproc]) ]) AX_PROG_LUA([5.1], [], [], [], [prefer]) @@ -199,38 +206,33 @@ AX_LUA_LIBS AM_COND_IF([SYSTEM_LUAROCKS], [ AS_IF([test "$LUA_SHORT_VERSION" -lt 52], [ - AM_COND_IF([LUAJIT], [], [ - AX_LUA_MODULE([bit32], [bit32]) - ]) - ]) - AX_LUA_MODULE([cassowary], [cassowary]) - AS_IF([test "$LUA_SHORT_VERSION" -lt 53], [ - AX_LUA_MODULE([compat53], [compat53]) + AM_COND_IF([LUAJIT], [], [AX_LUA_MODULE(bit32)]) ]) - AX_LUA_MODULE([cldr], [cldr]) - AX_LUA_MODULE([fluent], [fluent]) - AX_LUA_MODULE([linenoise], [linenoise]) - AX_LUA_MODULE([loadkit], [loadkit]) - AX_LUA_MODULE([lpeg], [lpeg]) - AX_LUA_MODULE([zlib], [lua-zlib]) - AX_LUA_MODULE([cliargs], [lua_cliargs]) - AX_LUA_MODULE([epnf], [luaepnf]) - AX_LUA_MODULE([lxp], [luaexpat]) - AX_LUA_MODULE([lfs], [luafilesystem]) - AX_LUA_MODULE([repl], [luarepl]) - AX_LUA_MODULE([ssl], [luasec]) - AX_LUA_MODULE([socket], [luasocket]) - AX_LUA_MODULE([lua-utf8], [luautf8]) - AX_LUA_MODULE([pl], [penlight]) - AX_LUA_MODULE([vstruct], [vstruct]) + AX_LUA_MODULE(cassowary) + AS_IF([test "$LUA_SHORT_VERSION" -lt 53], [AX_LUA_MODULE(compat53)]) + AX_LUA_MODULE(cldr) + AX_LUA_MODULE(fluent) + AX_LUA_MODULE(linenoise) + AX_LUA_MODULE(loadkit) + AX_LUA_MODULE(lpeg) + AX_LUA_MODULE(lua-zlib, zlib) + AX_LUA_MODULE(lua_cliargs, cliargs) + AX_LUA_MODULE(luaepnf, epnf) + AX_LUA_MODULE(luaexpat, lxp) + AX_LUA_MODULE(luafilesystem, lfs) + AX_LUA_MODULE(luafilesystem) + AX_LUA_MODULE(luarepl, repl) + AX_LUA_MODULE(luasec, ssl) + AX_LUA_MODULE(luasocket, socket) + AX_LUA_MODULE(luautf8, lua-utf8) + AX_LUA_MODULE(penlight, pl) + AX_LUA_MODULE(vstruct) ], [ - QUE_PROGVAR([luarocks]) QUE_PROGVAR([git]) # required for luarocks to install zlib rock ]) QUE_FONT(Gentium Plus) - AC_SUBST([APPKIT_TRUE]) AC_SUBST([FONTCONFIG_TRUE]) AC_SUBST([FONT_VARIATIONS_TRUE]) diff --git a/core/settings.lua b/core/settings.lua index 6693149f0..741dd3116 100644 --- a/core/settings.lua +++ b/core/settings.lua @@ -1,3 +1,5 @@ +local setenv = require("rusile").setenv + --- core settings instance --- @module SILE.settings @@ -29,6 +31,8 @@ function settings:_init () ]]):format(language, language, language, language)) end fluent:set_locale(language) + os.setlocale(language) + setenv("LANG", language) end, help = "Locale for localized language support", }) diff --git a/flake.lock b/flake.lock index f563e8704..774297951 100644 --- a/flake.lock +++ b/flake.lock @@ -72,11 +72,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1731245184, - "narHash": "sha256-vmLS8+x+gHRv1yzj3n+GTAEObwmhxmkkukB2DwtJRdU=", + "lastModified": 1732238832, + "narHash": "sha256-sQxuJm8rHY20xq6Ah+GwIUkF95tWjGRd1X8xF+Pkk38=", "owner": "nixos", "repo": "nixpkgs", - "rev": "aebe249544837ce42588aa4b2e7972222ba12e8f", + "rev": "8edf06bea5bcbee082df1b7369ff973b91618b8d", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 0a9c4ee36..5209c59b9 100644 --- a/flake.nix +++ b/flake.nix @@ -87,12 +87,9 @@ "--with-manual" ]; nativeBuildInputs = sile.nativeBuildInputs ++ [ - pkgs.luarocks # For regression test diff highlighting pkgs.delta - # For commitlint git hook - pkgs.yarn - # For npx + # For npx & commitlint git hook pkgs.nodejs # For gs, dot, and bsdtar used in building the manual pkgs.ghostscript diff --git a/package.json b/package.json index 2a0e1aa6b..587222437 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sile", - "version": "0.15.6", + "version": "0.15.7", "description": "The SILE Typesetter", "main": "sile", "scripts": { @@ -20,15 +20,15 @@ }, "homepage": "https://sile-typesetter.org", "devDependencies": { - "@commitlint/cli": "^19.3", - "@commitlint/config-conventional": "^19.2", - "@commitlint/prompt": "^19.3", + "@commitlint/cli": "^19.6", + "@commitlint/config-conventional": "^19.6", + "@commitlint/prompt": "^19.6", "@iarna/toml": "^2.2", "commitizen": "^4.3", "conventional-changelog-cli": "^5.0", - "husky": "^9.0", - "commit-and-tag-version": "^12.4", - "yaml": "^2.4" + "husky": "^9.1", + "commit-and-tag-version": "^12.5", + "yaml": "^2.6" }, "config": { "commitizen": { @@ -51,7 +51,7 @@ } ], "scripts": { - "postbump": "cargo generate-lockfile --offline", + "postbump": "taplo format Cargo.toml && cargo generate-lockfile --offline", "postcommit": "git add -u Cargo.lock && git commit --amend --no-edit" }, "infile": "CHANGELOG.md", diff --git a/packages/autodoc/init.lua b/packages/autodoc/init.lua index a48c38af7..0a2d5a6cb 100644 --- a/packages/autodoc/init.lua +++ b/packages/autodoc/init.lua @@ -371,40 +371,42 @@ function package:registerCommands () -- Homogenizing the appearance of blocks of code self:registerCommand("autodoc:codeblock", function (_, content) + -- The way we use this command in the SILE manual is both on it's own *and* nested inside + -- a \raw environment. For this purpose we don't care about and don't want to bother with + -- making sure the leading and trailing whitespace is minimized in the SIL source, so + -- we trim it off here. + content = SU.ast.trimSubContent(content) SILE.typesetter:leaveHmode() - local lskip = SILE.settings:get("document.lskip") or SILE.types.node.glue() - local rskip = SILE.settings:get("document.rskip") or SILE.types.node.glue() + local parindent = SILE.settings:get("document.parindent"):absolute() + local lskip = (SILE.settings:get("document.lskip") or SILE.types.node.glue()).width:absolute() + parindent + local rskip = (SILE.settings:get("document.rskip") or SILE.types.node.glue()).width:absolute() + parindent SILE.settings:temporarily(function () -- Note: We avoid using the verbatim environment and simplify things a bit -- (and try to better enforce novbreak points of insertion) SILE.call("verbatim:font") SILE.call("language", { main = "und" }) -- Rather than absolutizing 4 different values, just do it once and cache it - local bs = SILE.types.measurement("1bs"):absolute() - local ex = SILE.types.measurement("1ex"):absolute() - local pushline = function () + local pushline = function (offset) colorWrapper("note", function () - SILE.call("novbreak") - SILE.typesetter:pushVglue(-bs) - SILE.call("novbreak") - SILE.call("fullrule", { thickness = "0.5pt" }) - SILE.call("novbreak") - SILE.typesetter:pushVglue(-bs-ex) - SILE.call("novbreak") + SILE.call("raise", { height = offset }, function () + SILE.call("hrule", { thickness = "0.5pt", width = "100%lw" }) + end) end) end SILE.settings:set("typesetter.parseppattern", "\n") SILE.settings:set("typesetter.obeyspaces", true) - SILE.settings:set("document.lskip", SILE.types.node.glue(lskip.width.length)) - SILE.settings:set("document.rskip", SILE.types.node.glue(rskip.width.length)) + SILE.settings:set("document.lskip", SILE.types.node.glue(lskip)) + SILE.settings:set("document.rskip", SILE.types.node.glue(rskip)) SILE.settings:set("document.parindent", SILE.types.node.glue()) SILE.settings:set("document.parskip", SILE.types.node.vglue()) SILE.settings:set("document.spaceskip", SILE.types.length("1spc")) SILE.settings:set("shaper.variablespaces", false) colorWrapper("codeblock", function () - pushline() + pushline("0.2ex") + SILE.call("novbreak") SILE.process(content) - pushline() + SILE.call("novbreak") + pushline("1ex") end) SILE.typesetter:leaveHmode() end) diff --git a/packages/bibtex/csl/csl_spec.lua b/packages/bibtex/csl/csl_spec.lua new file mode 100644 index 000000000..e08b2ac7d --- /dev/null +++ b/packages/bibtex/csl/csl_spec.lua @@ -0,0 +1,74 @@ +SILE = require("core.sile") + +local CslLocale = require("packages.bibtex.csl.locale") +local CslStyle = require("packages.bibtex.csl.style") +local CslEngine = require("packages.bibtex.csl.engine") + +describe("CSL engine", function () + local locale, err1 = CslLocale.read("packages/bibtex/csl/locales/locales-en-US.xml") + local style, err2 = CslStyle.read("packages/bibtex/csl/styles/chicago-author-date.csl") + + -- The expected internal representation of the CSL entry is similar to CSL-JSON + -- but with some differences: + -- Date fields are structured tables (not an array of numbers as in CSL-JSON). + -- citation-number (mandatory) is supposed to have been added by the citation processor. + -- locator (optional, also possibly added by the citation processor) is a table with label and value fields. + local cslentrySmith2024 = { + type = "paper-conference", + ["citation-key"] = "smith2024", + ["citation-number"] = 1, + author = { + { + family = "Smith", + ["family-short"] = "S", + given = "George", + ["given-short"] = "G", + }, + }, + title = "Article title", + page = "30-50", + issued = { + year = "2024", + }, + publisher = "Publisher", + ["publisher-place"] = "Place", + volume = "10", + editor = { + { + family = "Doe", + ["family-short"] = "D", + given = "Jane", + ["given-short"] = "J", + }, + }, + locator = { + label = "page", + value = "30-35", + }, + ["collection-number"] = "3", + ["collection-title"] = "Series", + ["container-title"] = "Book Title", + } + + it("should parse locale and style", function () + assert.is.falsy(err1) + assert.is.falsy(err2) + assert.is.truthy(locale) + assert.is.truthy(style) + end) + + it("should render a citation", function () + local engine = CslEngine(style, locale) + local citation = engine:cite(cslentrySmith2024) + assert.is.equal("(Smith 2024, 30–35)", citation) + end) + + it("should render a reference", function () + local engine = CslEngine(style, locale) + local reference = engine:reference(cslentrySmith2024) + assert.is.equal( + "Smith, George. 2024. “Article title.” In Book Title, edited by Jane Doe, 10:30–50. Series 3. Place: Publisher.", + reference + ) + end) +end) diff --git a/packages/bibtex/csl/engine.lua b/packages/bibtex/csl/engine.lua new file mode 100644 index 000000000..013f2d9fb --- /dev/null +++ b/packages/bibtex/csl/engine.lua @@ -0,0 +1,1508 @@ +--- A rendering engine for CSL 1.0.2 +-- +-- @copyright License: MIT (c) 2024 Omikhleia +-- +-- Public API: +-- - (constructor) CslEngine(style, locale) -> CslEngine +-- - CslEngine:cite(entries) -> string +-- - CslEngine:reference(entries) -> string +-- +-- The expected internal representation of a CSL entry is similar to CSL-JSON +-- but with some differences: +-- Date fields are structured tables (not an array of numbers as in CSL-JSON). +-- citation-number (mandatory) is supposed to have been added by the citation processor. +-- locator (optional, also possibly added by the citation processor) is a table with label and value fields. +-- names are parsed, +-- as personal names (ex. `{ given = "George", family = "Smith" ... }`), +-- or are literal strings (ex. `{ literal = "T.C.B.S" }`). +-- +-- Important: while some consistency checks are performed, this engine is not +-- intended to handle errors in the locale, style or input data. It is assumed +-- that they are all valid. +-- +-- THINGS NOT DONE +-- - disambiguation logic (not done at all) +-- - collapse logic in citations (not done at all) +-- - other FIXME in the code on quite specific features +-- +-- luacheck: no unused args + +local CslLocale = require("packages.bibtex.csl.locale") + +local superfolding = require("packages.bibtex.csl.utils.superfolding") +local endash = luautf8.char(0x2013) +local emdash = luautf8.char(0x2014) + +local CslEngine = pl.class() + +--- (Constructor) Create a new CSL engine. +-- The optional extras table is for features not part of CSL 1.0.2. +-- Currently: +-- localizedPunctuation: boolean (default false) - use localized punctuation +-- +-- @tparam CslStyle style CSL style +-- @tparam CslLocale locale CSL locale +-- @tparam table extras Additional data to pass to the engine +-- @treturn CslEngine +function CslEngine:_init (style, locale, extras) + self.locale = locale + self.style = style + self.extras = extras + or { + localizedPunctuation = false, + italicExtension = true, + mathExtension = true, + } + + -- Shortcuts for often used style elements + self.macros = style.macros or {} + self.citation = style.citation or {} + self.locales = style.locales or {} + self.bibliography = style.bibliography or {} + self:_preprocess() + + -- Cache for some small string operations (e.g. XML escaping) + -- to avoid repeated processing. + self.cache = {} + + -- Early lookups for often used localized punctuation marks + self.punctuation = { + open_quote = self:_render_term("open-quote") or luautf8.char(0x201C), -- 0x201C curly left quote + close_quote = self:_render_term("close-quote") or luautf8.char(0x201D), -- 0x201D curly right quote + open_inner_quote = self:_render_term("open-inner-quote") or luautf8.char(0x2018), -- 0x2018 curly left single quote + close_inner_quote = self:_render_term("close-inner-quote") or luautf8.char(0x2019), -- 0x2019 curly right single quote + page_range_delimiter = self:_render_term("page-range-delimiter") or endash, + [","] = self:_render_term("comma") or ",", + [";"] = self:_render_term("semicolon") or ";", + [":"] = self:_render_term("colon") or ":", + } + + -- Small utility for page ranges, see text processing for + local sep = self.punctuation.page_range_delimiter + if sep ~= endash and sep ~= emdash and sep ~= "-" then + -- Unlikely there's a percent here, but let's be safe + sep = luautf8.gsub(sep, "%%", "%%%%") + end + local dashes = "%-" .. endash .. emdash + local textinrange = "[^" .. dashes .. "]+" + local dashinrange = "[" .. dashes .. "]+" + local page_range_capture = "(" .. textinrange .. ")%s*" .. dashinrange .. "%s*(" .. textinrange .. ")" + local page_range_replacement = "%1" .. sep .. "%2" + self.page_range_replace = function (t) + return luautf8.gsub(t, page_range_capture, page_range_replacement) + end + + -- Inheritable variables + -- There's a long list of such variables, but let's be dumb and just merge everything. + self.inheritable = { + citation = pl.tablex.union(self.style.globalOptions, self.style.citation and self.style.citation.options or {}), + bibliography = pl.tablex.union( + self.style.globalOptions, + self.style.bibliography and self.style.bibliography.options or {} + ), + } + + self.subsequentAuthorSubstitute = self.inheritable["bibliography"]["subsequent-author-substitute"] + local _, count = luautf8.gsub(self.subsequentAuthorSubstitute, "[%-_–—]", "") -- naive count + if count > 0 then + -- With many fonts, a sequence of dashes is not looking that great. + -- So replace them with a command, and let the typesetter decide for a better rendering. + -- NOTE: Avoid (quoted) attributes and dashes in tags, as some global + -- substitutions might affect quotes...So we use a simple "wrapper" command. + local trail = luautf8.gsub(self.subsequentAuthorSubstitute, "^[%-–—_]+", "") + self.subsequentAuthorSubstitute = "" .. count .. "" .. trail + end +end + +function CslEngine:_prerender () + -- Stack for processing of cs:group as conditional + self.groupQueue = {} + self.groupState = { variables = {}, count = 0 } + + -- Track first name for name-as-sort-order + self.firstName = true + + -- Track first rendered cs:names for subsequent-author-substitute + self.doAuthorSubstitute = self.mode == "bibliography" and self.subsequentAuthorSubstitute + self.hasRenderedNames = false + -- Track authors for subsequent-author-substitute + self.precAuthors = self.currentAuthors + self.currentAuthors = {} +end + +function CslEngine:_merge_locales (locale1, locale2) + -- FIXME TODO: + -- - Should we care about date formats and style options? + -- (PERHAPS, CHECK THE SPEC) + -- - Should we move this to the CslLocale class? + -- (LIKELY YES) + -- - Should we deepcopy the locale1 first, so it can be reused independently? + -- (LIKELY YES, instantiating a new CslLocale) + -- Merge terms, overriding existing ones + for term, forms in pairs(locale2.terms) do + if not locale1.terms[term] then + SU.debug("csl", "CSL local merging added:", term) + locale1.terms[term] = forms + else + for form, genderfs in pairs(forms) do + if not locale1.terms[term][form] then + SU.debug("csl", "CSL local merging added:", term, form) + locale1.terms[term][form] = genderfs + else + for genderform, value in pairs(genderfs) do + local replaced = locale1.terms[term][form][genderform] + SU.debug("csl", "CSL local merging", replaced and "replaced" or "added:", term, form, genderform) + locale1.terms[term][form][genderform] = value + end + end + end + end + end +end + +function CslEngine:_preprocess () + -- Handle locale overrides + if self.locales[self.locale.lang] then -- Direct language match + local override = CslLocale(self.locales[self.locale.lang]) + SU.debug("csl", "Locale override found for " .. self.locale.lang) + self:_merge_locales(self.locale, override) + else + for lang, locale in pairs(self.locales) do -- Fuzzy language matching + if self.locale.lang:sub(1, #lang) == lang then + local override = CslLocale(locale) + SU.debug("csl", "Locale override found for " .. self.locale.lang .. " -> " .. lang) + self:_merge_locales(self.locale, override) + end + end + end +end + +-- GROUP LOGIC (tracking variables in groups, conditional rendering) + +function CslEngine:_enterGroup () + self.groupState.count = self.groupState.count + 1 + SU.debug("csl", "Enter group", self.groupState.count, "level", #self.groupQueue) + + table.insert(self.groupQueue, self.groupState) + self.groupState = { variables = {}, count = 0 } +end + +function CslEngine:_leaveGroup (rendered) + -- Groups implicitly act as a conditional: if all variables that are called + -- are empty, the group is suppressed. + -- But the group is kept if no variable is called. + local emptyVariables = true + local hasVariables = false + for _, cond in pairs(self.groupState.variables) do + hasVariables = true + if cond then -- non-empty variable found + emptyVariables = false + break + end + end + local suppressGroup = hasVariables and emptyVariables + if suppressGroup then + rendered = nil -- Suppress group + end + self.groupState = table.remove(self.groupQueue) + -- A nested non-empty group is treated as a non-empty variable for the + -- purposes of determining suppression of the outer group. + -- So add a pseudo-variable for the inner group into the outer group, to + -- track this. + if not suppressGroup then + local groupCond = "_group_" .. self.groupState.count + self:_addGroupVariable(groupCond, true) + end + SU.debug( + "csl", + "Leave group", + self.groupState.count, + "level", + #self.groupQueue, + suppressGroup and "(suppressed)" or "(rendered)" + ) + return rendered +end + +function CslEngine:_addGroupVariable (variable, value) + SU.debug("csl", "Group variable", variable, value and "true" or "false") + self.groupState.variables[variable] = value and true or false +end + +-- INTERNAL HELPERS + +function CslEngine:_render_term (name, form, plural) + local t = self.locale:term(name, form, plural) + if t then + if self.cache[t] then + return self.cache[t] + end + t = self:_xmlEscape(t) + -- The CSL specification states, regarding terms: + -- "Superscripted Unicode characters can be used for superscripting." + -- We replace the latter with their normal form, wrapped in a command. + -- The result is cached in the term object to avoid repeated processing. + -- (Done after XML escaping as superfolding may add commands.) + t = superfolding(t) + self.cache[t] = t + end + return t +end + +function CslEngine:_render_text_specials (value) + -- Extensions for italic and math... + -- CAVEAT: the implementation is fairly naive. + local pieces = {} + for token in SU.gtoke(value, "%$([^$]+)%$") do + if token.string then + local s = token.string + if self.extras.italicExtension then + -- Typography: + -- Use pseudo-markdown italic extension (_text_) to wrap + -- the text in emphasis. + -- Skip if sorting, as it's not supposed to affect sorting. + local repl = self.sorting and "%1" or "%1" + s = luautf8.gsub(s, "_([^_]+)_", repl) + end + table.insert(pieces, s) + else + local m = token.separator + if self.extras.mathExtension then + -- Typography: + -- Use pseudo-markdown math extension ($text$) to wrap + -- the text in math mode (assumed to be in TeX-like syntax). + m = luautf8.gsub(m, "%$([^$]+)%$", "%1") + end + table.insert(pieces, m) + end + end + return table.concat(pieces) +end + +-- RENDERING ATTRIBUTES (strip-periods, affixes, formatting, text-case, display, quotes, delimiter) + +function CslEngine:_xmlEscape (t) + return t:gsub("&", "&"):gsub("<", "<"):gsub(">", ">") +end + +function CslEngine:_punctuation_extra (t) + if self.cache[t] then + return self.cache[t] + end + if self.extras.localizedPunctuation then + -- non-standard: localized punctuation + t = t:gsub("[,;:]", function (c) + return self.punctuation[c] or c + end) + end + t = self:_xmlEscape(t) + self.cache[t] = t + return t +end + +function CslEngine:_render_stripPeriods (t, options) + if t and options["strip-periods"] and t:sub(-1) == "." then + t = t:sub(1, -2) + end + return t +end + +function CslEngine:_render_affixes (t, options) + if not t then + return + end + if options.prefix then + local pref = self:_punctuation_extra(options.prefix) + t = pref .. t + end + if options.suffix then + local suff = self:_punctuation_extra(options.suffix) + t = t .. suff + end + return t +end + +function CslEngine:_render_formatting (t, options) + if not t then + return + end + if self.sorting then + -- Skip all formatting in sorting mode + return t + end + if options["font-style"] == "italic" then -- FIXME: also normal, oblique, and how nesting is supposed to work? + t = "" .. t .. "" + end + if options["font-variant"] == "small-caps" then + -- NOTE: Avoid (quoted) attributes and dashes in tags, as some global + -- substitutions might affect quotes...So we use a simple "wrapper" command. + t = "" .. t .. "" + end + if options["font-weight"] == "bold" then -- FIXME: also light, normal, and how nesting is supposed to work? + t = "" .. t .. "" + end + if options["text-decoration"] == "underline" then + t = "" .. t .. "" + end + if options["vertical-align"] == "sup" then + t = "" .. t .. "" + end + if options["vertical-align"] == "sub" then + t = "" .. t .. "" + end + return t +end + +function CslEngine:_render_textCase (t, options) + if not t then + return + end + if options["text-case"] then + t = self.locale:case(t, options["text-case"]) + end + return t +end + +function CslEngine:_render_display (t, options) + if not t then + return + end + -- FIXME NOT IMPLEMENTED: + -- If set, options.display can be "block", "left-margin", "right-inline", "indent" + -- Usual styles such as Chicago, MLA, ACS etc. do not use it. + if options.display then + SU.warn("CSL display attribute not implemented: output will likely be incorrect") + end + return t +end + +function CslEngine:_render_quotes (t, options) + if not t then + return + end + if self.sorting then + -- Skip all quotes in sorting mode + return luautf8.gsub(t, '[“”"]', "") + end + if t and options.quotes then + -- Smart transform curly quotes in the input to localized inner quotes. + t = luautf8.gsub(t, "“", self.punctuation.open_inner_quote) + t = luautf8.gsub(t, "”", self.punctuation.close_inner_quote) + -- Smart transform straight quotes in the input to localized inner quotes. + t = luautf8.gsub(t, '^"', self.punctuation.open_inner_quote) + t = luautf8.gsub(t, '"$', self.punctuation.close_inner_quote) + t = luautf8.gsub(t, '([’%s])"', "%1" .. self.punctuation.open_inner_quote) + t = luautf8.gsub(t, '"([%s%p])', self.punctuation.close_inner_quote .. "%1") + -- Wrap the result in localized outer quotes. + t = self.punctuation.open_quote .. t .. self.punctuation.close_quote + end + return t +end + +function CslEngine:_render_link (t, link) + if t and link and not self.sorting then + -- We'll let the processor implement CSL 1.0.2 link handling. + -- (appendix VI) + -- NOTE: Avoid (quoted) attributes and dashes in tags, as some global + -- substitutions might affect quotes...So we use a simple "wrapper" command. + t = "" .. t .. "" + end + return t +end + +function CslEngine:_render_delimiter (ts, delimiter) -- ts is a table of strings + local d = delimiter and self:_punctuation_extra(delimiter) + return table.concat(ts, d) +end + +-- RENDERING ELEMENTS: layout, text, date, number, names, label, group, choose + +function CslEngine:_layout (options, content, entries) + local output = {} + for _, entry in ipairs(entries) do + self:_prerender() + local elem = self:_render_children(content, entry) + -- affixes and formatting likely apply on elementary entries + -- (The CSL 1.0.2 specification is not very clear on this point.) + elem = self:_render_formatting(elem, options) + elem = self:_render_affixes(elem, options) + elem = self:_postrender(elem) + if elem then + table.insert(output, elem) + end + end + if options.delimiter then + return self:_render_delimiter(output, options.delimiter) + end + -- (Normally citations have a delimiter options, so we should only reach + -- this point for the bibliography) + local delim = self.mode == "citation" and "; " or "" + -- references all belong to a different paragraph + -- FIXME: should account for attributes on the toplevel bibliography element: + -- line-spacing + -- hanging-indent + return table.concat(output, delim) +end + +function CslEngine:_text (options, content, entry) + local t + local link + if options.macro then + if self.macros[options.macro] then + t = self:_render_children(self.macros[options.macro], entry) + else + SU.error("CSL macro " .. options.macro .. " not found") + end + elseif options.term then + t = self:_render_term(options.term, options.form, options.plural) + elseif options.variable then + local variable = options.variable + t = entry[variable] + self:_addGroupVariable(variable, t) + if variable == "locator" then + t = t and t.value + variable = entry.locator.label + end + if variable == "page" and t then + -- Replace any dash in page ranges + t = self.page_range_replace(t) + end + + -- FIXME NOT IMPLEMENTED: + -- "May be accompanied by the form attribute to select the “long” + -- (default) or “short” form of a variable (e.g. the full or short + -- title). If the “short” form is selected but unavailable, the + -- “long” form is rendered instead." + -- But CSL-JSON etc. do not seem to have standard provision for it. + + if t and (variable == "URL" or variable == "DOI" or variable == "PMID" or variable == "PMCID") then + link = variable + end + elseif options.value then + t = options.value + else + SU.error("CSL text without macro, term, variable or value") + end + t = self:_render_stripPeriods(t, options) + t = self:_render_textCase(t, options) + t = self:_render_formatting(t, options) + t = self:_render_quotes(t, options) + t = self:_render_affixes(t, options) + if link then + t = self:_render_link(t, link) + elseif t and options.variable then + t = self:_render_text_specials(t) + end + t = self:_render_display(t, options) + return t +end + +function CslEngine:_a_day (options, day, month) -- month needed to get gender for ordinal + local form = options.form + local t + if form == "numeric-leading-zeros" then + t = ("%02d"):format(day) + elseif form == "ordinal" then + local genderForm + if month then + local monthKey = ("month-%02d"):format(month) + local _, gender = self:_render_term(monthKey) + genderForm = gender or "neuter" + end + if SU.boolean(self.locale.styleOptions["limit-day-ordinals-to-day-1"], false) then + t = day == 1 and self.locale:ordinal(day, "short", genderForm) or ("%d"):format(day) + else + t = self.locale:ordinal(day, "short", genderForm) + end + else -- "numeric" by default + t = ("%d"):format(day) + end + return t +end + +function CslEngine:_a_month (options, month) + local form = options.form + local t + if form == "numeric" then + t = ("%d"):format(month) + elseif form == "numeric-leading-zeros" then + t = ("%02d"):format(month) + else -- short or long (default) + local monthKey = ("month-%02d"):format(month) + t = self:_render_term(monthKey, form or "long") + end + t = self:_render_stripPeriods(t, options) + return t +end + +function CslEngine:_a_season (options, season) + local form = options.form + local t + if form == "numeric" or form == "numeric-leading-zeros" then + -- The CSL specification does not seem to forbid it, but a numeric value + -- for the season is a weird idea, so we skip it for now. + SU.warn("CSL season formatting as a number is ignored") + else + local seasonKey = ("season-%02d"):format(season) + t = self:_render_term(seasonKey, form or "long") + end + t = self:_render_stripPeriods(t, options) + return t +end + +function CslEngine:_a_year (options, year) + local form = options.form + local t + if tonumber(year) then + if form == "numeric-leading-zeros" then + t = ("%04d"):format(year) + elseif form == "short" then + -- The spec gives as example 2005 -> 05 + t = ("%02d"):format(year % 100) + else -- "long" by default + t = ("%d"):format(year) + end + else + -- Compat with BibLaTeX (literal might not be a number) + t = year + end + return t +end + +function CslEngine:_a_date_day (options, date) + local t + if date.day then + if type(date.day) == "table" then + local t1 = self:_a_day(options, date.day[1], date.month) + local t2 = self:_a_day(options, date.day[2], date.month) + local sep = options["range-delimiter"] or endash + t = t1 .. sep .. t2 + else + t = self:_a_day(options, date.day, date.month) + end + end + return t +end + +function CslEngine:_a_date_month (options, date) + local t + if date.month then + if type(date.month) == "table" then + local t1 = self:_a_month(options, date.month[1]) + local t2 = self:_a_month(options, date.month[2]) + local sep = options["range-delimiter"] or endash + t = t1 .. sep .. t2 + else + t = self:_a_month(options, date.month) + end + elseif date.season then + if type(date.season) == "table" then + local t1 = self:_a_season(options, date.season[1]) + local t2 = self:_a_season(options, date.season[2]) + local sep = options["range-delimiter"] or endash + t = t1 .. sep .. t2 + else + t = self:_a_season(options, date.season) + end + end + return t +end + +function CslEngine:_a_date_year (options, date) + local t + if date.year then + if type(date.year) == "table" then + local t1 = self:_a_year(options, date.year[1]) + local t2 = self:_a_year(options, date.year[2]) + local sep = options["range-delimiter"] or endash + t = t1 .. sep .. t2 + else + t = self:_a_year(options, date.year) + end + end + return t +end + +function CslEngine:_date_part (options, content, date) + local name = SU.required(options, "name", "cs:date-part") + -- FIXME TODO + -- Full date range are not implemented properly + local t + local callback = "_a_date_" .. name + if self[callback] then + t = self[callback](self, options, date) + else + SU.warn("CSL date part " .. name .. " not implemented yet") + end + t = self:_render_textCase(t, options) + t = self:_render_formatting(t, options) + t = self:_render_affixes(t, options) + return t +end + +function CslEngine:_date_parts (options, content, date) + local output = {} + local cond = false + for _, part in ipairs(content) do + local t = self:_date_part(part.options, part, date) + if t then + cond = true + table.insert(output, t) + end + end + if not cond then -- not a single part rendered + self:_addGroupVariable(options.variable, false) + return + end + self:_addGroupVariable(options.variable, true) + return self:_render_delimiter(output, options.delimiter) +end + +function CslEngine:_date (options, content, entry) + local variable = SU.required(options, "variable", "CSL number") + local date = entry[variable] + if date then + if options.form then + -- Use locale date format (form is either "numeric" or "text") + content = self.locale:date(options.form) + options.delimiter = nil -- Not supposed to exist when calling a locale date + -- When calling a localized date, the date-parts attribute is used to + -- determine which parts of the date to render: year-month-day (default), + -- year-month or year. + local dp = options["date-parts"] or "year-month-day" + local hasMonthOrSeason = dp == "year-month" or dp == "year-month-day" + local hasDay = dp == "year-month-day" + date = { + year = date.year, + month = hasMonthOrSeason and date.month or nil, + season = hasMonthOrSeason and date.season or nil, + day = hasDay and date.day or nil, + } + end + local t = self:_date_parts(options, content, date) + t = self:_render_textCase(t, options) + t = self:_render_formatting(t, options) + t = self:_render_affixes(t, options) + t = self:_render_display(t, options) + return t + else + self:_addGroupVariable(variable, false) + end +end + +function CslEngine:_number (options, content, entry) + local variable = SU.required(options, "variable", "CSL number") + local value = entry[variable] + self:_addGroupVariable(variable, value) + if variable == "locator" then -- special case + value = value and value.value + end + if value then + local _, gender = self:_render_term(variable) + local genderForm = gender or "neuter" + + -- FIXME TODO: Some complex stuff about name ranges, commas, etc. in the spec. + -- Moreover: + -- "Numbers with prefixes or suffixes are never ordinalized or rendered in roman numerals" + -- Interpretation: values that are not numbers are not formatted (?) + local form = tonumber(value) and options.form or "numeric" + if form == "ordinal" then + value = self.locale:ordinal(value, "short", genderForm) + elseif form == "long-ordinal" then + value = self.locale:ordinal(value, "long", genderForm) + elseif form == "roman" then + value = SU.formatNumber(value, { system = "roman" }) + end + end + value = self:_render_textCase(value, options) + value = self:_render_formatting(value, options) + value = self:_render_affixes(value, options) + value = self:_render_display(value, options) + return value +end + +function CslEngine:_enterSubstitute (t) + SU.debug("csl", "Enter substitute") + -- Some group and variable cancellation logic applies to cs:substitute. + -- Wrap it in a pseudo-group to track referenced variables. + self:_enterGroup() + return t +end + +function CslEngine:_leaveSubstitute (t, entry) + SU.debug("csl", "Leave substitute") + local vars = self.groupState.variables + -- "Substituted variables are considered empty for the purposes of + -- determining whether to suppress an enclosing cs:group." + -- So it's as if we hadn't seen any variable in our substitute. + self.groupState.variables = {} + -- "Substituted variables are suppressed in the rest of the output + -- to prevent duplication" + -- So if the substitution was successful, we remove referenced variables + -- from the entry. + if t then + for field, cond in pairs(vars) do + if cond then + entry[field] = nil + end + end + end + -- Terminate the pseudo-group + t = self:_leaveGroup(t) + return t +end + +function CslEngine:_substitute (options, content, entry) + local t + for _, child in ipairs(content) do + self:_enterSubstitute() + if child.command == "cs:names" then + SU.required(child.options, "variable", "CSL cs:names in cs:substitute") + local opts = pl.tablex.union(options, child.options) + t = self:_names_with_resolved_opts(opts, nil, entry) + else + t = self:_render_node(child, entry) + end + t = self:_leaveSubstitute(t, entry) + if t then -- First non-empty child is returned + break + end + end + return t +end + +function CslEngine:_name_et_al (options) + local t = self:_render_term(options.term or "et-al") + t = self:_render_formatting(t, options) + return t +end + +function CslEngine:_a_name (options, content, entry) + if entry.literal then -- pass through literal names + return entry.literal + end + if not entry.family then + -- There's one element in a name we can't do without. + SU.error("Name without family: what do you expect me to do with it?") + end + local demoteNonDroppingParticle = options["demote-non-dropping-particle"] or "never" + + if self.sorting then + -- Implicitely we are in long form, name-as-sort-order all, and no formatting. + if demoteNonDroppingParticle == "never" then + -- Order is: [NDP] Family [Given] [Suffix] e.g. van Gogh Vincent III + local name = {} + if entry["non-dropping-particle"] then + table.insert(name, entry["non-dropping-particle"]) + end + table.insert(name, entry.family) + if entry.given then + table.insert(name, entry.given) + end + if entry.suffix then + table.insert(name, entry.suffix) + end + return table.concat(name, " ") + end + -- Order is: Family [Given] [DP] [Suffix] e.g. Gogh Vincent van III + local name = { entry.family } + if entry.given then + table.insert(name, entry.given) + end + if entry["dropping-particle"] then + table.insert(name, entry["dropping-particle"]) + end + if entry["non-dropping-particle"] then + table.insert(name, entry["non-dropping-particle"]) + end + if entry.suffix then + table.insert(name, entry.suffix) + end + return table.concat(name, " ") + end + + local form = options.form + local nameAsSortOrder = options["name-as-sort-order"] or "first" + + -- TODO FIXME: content can consists in name-part elements for formatting, text-case, affixes + -- Chigaco style does not seem to use them, so we keep it "simple" for now. + + if form == "short" then + -- Order is: [NDP] Family, e.g. van Gogh + if entry["non-dropping-particle"] then + return table.concat({ + entry["non-dropping-particle"], + entry.family, + }, " ") + end + return entry.family + end + + if nameAsSortOrder ~= "all" and not self.firstName then + -- Order is: [Given] [DP] [NDP] Family [Suffix] e.g. Vincent van Gogh III + local t = {} + if entry.given then + table.insert(t, entry.given) + end + if entry["dropping-particle"] then + table.insert(t, entry["dropping-particle"]) + end + if entry["non-dropping-particle"] then + table.insert(t, entry["non-dropping-particle"]) + end + table.insert(t, entry.family) + if entry.suffix then + table.insert(t, entry.suffix) + end + return table.concat(t, " ") + end + + local sep = options["sort-separator"] or (self.punctuation[","] .. " ") + if demoteNonDroppingParticle == "display-and-sort" then + -- Order is: Family, [Given] [DP] [NDP], [Suffix] e.g. Gogh, Vincent van, III + local mid = {} + if entry.given then + table.insert(mid, entry.given) + end + if entry["dropping-particle"] then + table.insert(mid, entry["dropping-particle"]) + end + if entry["non-dropping-particle"] then + table.insert(mid, entry["non-dropping-particle"]) + end + local midname = table.concat(mid, " ") + if #midname > 0 then + return table.concat({ + entry.family, + midname, + entry.suffix, -- may be nil + }, sep) + end + return table.concat({ + entry.family, + entry.suffix, -- may be nil + }, sep) + end + + -- Order is: [NDP] Family, [Given] [DP], [Suffix] e.g. van Gogh, Vincent, III + local beg = {} + if entry["non-dropping-particle"] then + table.insert(beg, entry["non-dropping-particle"]) + end + table.insert(beg, entry.family) + local begname = table.concat(beg, " ") + local mid = {} + if entry.given then + table.insert(mid, entry.given) + end + if entry["dropping-particle"] then + table.insert(mid, entry["dropping-particle"]) + end + local midname = table.concat(mid, " ") + if #midname > 0 then + return table.concat({ + begname, + midname, + entry.suffix, -- may be nil + }, sep) + end + return table.concat({ + begname, + entry.suffix, -- may be nil + }, sep) +end + +local function hasField (list, field) + -- N.B. we want a true boolean here + if string.match(" " .. list .. " ", " " .. field .. " ") then + return true + end + return false +end + +function CslEngine:_names_with_resolved_opts (options, substitute_node, entry) + local variable = options.variable + local et_al_min = options.et_al_min + local et_al_use_first = options.et_al_use_first + local and_word = options.and_word + local name_delimiter = options.name_delimiter + local is_label_first = options.is_label_first + local label_opts = options.label_opts + local et_al_opts = options.et_al_opts + local name_node = options.name_node + local names_delimiter = options.names_delimiter + local delimiter_precedes_last = options.delimiter_precedes_last + + -- Special case if both editor and translator are wanted and are the same person(s) + local editortranslator = false + if hasField(variable, "editor") and hasField(variable, "translator") then + editortranslator = entry.translator and entry.editor and pl.tablex.deepcompare(entry.translator, entry.editor) + if editortranslator then + entry.editortranslator = entry.editor + end + end + + -- Process + local vars = pl.stringx.split(variable, " ") + local output = {} + for _, var in ipairs(vars) do + self:_addGroupVariable(var, entry[var]) + + local skip = editortranslator and var == "translator" -- done via the "editor" field + if not skip and entry[var] then + local label + if label_opts and not self.sorting then + -- (labels in names are skipped in sorting mode) + local v = var == "editor" and editortranslator and "editortranslator" or var + local opts = pl.tablex.union(label_opts, { variable = v }) + label = self:_label(opts, nil, entry) + end + local needEtAl = false + local names = type(entry[var]) == "table" and entry[var] or { entry[var] } + local l = {} + + -- FIXME EXPLAIN + if not self.hasRenderedNames then + pl.tablex.insertvalues(self.currentAuthors, names) + end + if + self.doAuthorSubstitute + and not self.sorting + and not self.hasRenderedNames + and self.precAuthors + and pl.tablex.deepcompare(names, self.precAuthors) + then + -- FIXME NOT IMPLEMENTED + -- subsequent-author-substitute-rule (default "complete-all" is assumed here) + -- NOTE: Avoid (quoted) attributes and dashes in tags, as some global + -- substitutions might affect quotes... + -- So we use a simple "wrapper" command. + table.insert(l, self.subsequentAuthorSubstitute) + self.firstName = false + else + for i, name in ipairs(names) do + if #names >= et_al_min and i > et_al_use_first then + needEtAl = true + break + end + local t = self:_a_name(name_node.options, name_node, name) + self.firstName = false + table.insert(l, t) + end + end + + local joined + if needEtAl then + -- FIXME NOT IMPLEMENTED + -- They are not needed in Chicago style, so let's keep it simple for now: + -- delimiter-precedes-et-al ("contextual" by default = hard-coded) + -- et-al-use-last (default false, if true, the last is rendered as ", ... Name) instead of using et-al. + local rendered_et_all = self:_name_et_al(et_al_opts) + local sep_et_al = #l > 1 and name_delimiter or " " + joined = table.concat(l, name_delimiter) .. sep_et_al .. rendered_et_all + elseif #l == 1 then + joined = l[1] + else + -- FIXME NOT IMPLEMENTED FULLY + -- Likewise, not need in many styles, so we headed towards a shortcut: + -- Minimal support for "contextual" and "always" for Chicago style. + -- delimiter-precedes-last ("contextual" by default) + local sep_delim + if delimiter_precedes_last == "always" then + sep_delim = name_delimiter + else + sep_delim = #l > 2 and name_delimiter or " " + end + local last = table.remove(l) + joined = table.concat(l, name_delimiter) .. sep_delim .. and_word .. " " .. last + end + if label then + joined = is_label_first and (label .. joined) or (joined .. label) + end + table.insert(output, joined) + end + end + + if #output == 0 and substitute_node then + return self:_substitute(options, substitute_node, entry) + end + if #output == 0 then + return nil + end + local t = self:_render_delimiter(output, names_delimiter) + t = self:_render_formatting(t, options) + t = self:_render_affixes(t, options) + t = self:_render_display(t, options) + return t +end + +function CslEngine:_names (options, content, entry) + -- Extract needed elements and options from the content + local name_node = nil + local label_opts = nil + local et_al_opts = {} + local substitute = nil + local is_label_first = false + for _, child in ipairs(content) do + if child.command == "cs:substitute" then + substitute = child + elseif child.command == "cs:et-al" then + et_al_opts = child.options + elseif child.command == "cs:label" then + if not name_node then + is_label_first = true + end + label_opts = child.options + elseif child.command == "cs:name" then + name_node = child + end + end + if not name_node then + name_node = { command = "cs:name", options = {} } + end + -- Build inherited options + local inherited_opts = pl.tablex.union(self.inheritable[self.mode], options) + name_node.options = pl.tablex.union(inherited_opts, name_node.options) + name_node.options.form = name_node.options.form or inherited_opts["name-form"] + local et_al_min = tonumber(name_node.options["et-al-min"]) or 4 -- No default in the spec, using Chicago's + local et_al_use_first = tonumber(name_node.options["et-al-use-first"]) or 1 + local and_opt = name_node.options["and"] or "text" + local and_word = and_opt == "symbol" and "&" or self:_render_term("and") -- text by default + local name_delimiter = name_node.options.delimiter or inherited_opts["names-delimiter"] or ", " + -- local delimiter_precedes_et_al = name_node.options["delimiter-precedes-et-al"] -- FIXME NOT IMPLEMENTED + local delimiter_precedes_last = name_node.options["delimiter-precedes-last"] + or inherited_opts["delimiter-precedes-last"] + or "contextual" + + if name_delimiter and not self.cache[name_delimiter] then + name_delimiter = self:_xmlEscape(name_delimiter) + self.cache[name_delimiter] = name_delimiter + end + + local resolved = { + variable = SU.required(name_node.options, "variable", "CSL names"), + et_al_min = et_al_min, + et_al_use_first = et_al_use_first, + and_word = and_word, + name_delimiter = name_delimiter and self.cache[name_delimiter], + is_label_first = is_label_first, + label_opts = label_opts, + et_al_opts = et_al_opts, + name_node = name_node, + names_delimiter = options.delimiter or inherited_opts["names-delimiter"], + delimiter_precedes_last = delimiter_precedes_last, + } + resolved = pl.tablex.union(options, resolved) + + local rendered = self:_names_with_resolved_opts(resolved, substitute, entry) + if rendered and not self.hasRenderedNames then + self.hasRenderedNames = true + end + return rendered +end + +function CslEngine:_label (options, content, entry) + local variable = SU.required(options, "variable", "CSL label") + local value = entry[variable] + self:_addGroupVariable(variable, value) + if variable == "locator" then + variable = value and value.label + value = value and value.value + end + if value then + local plural = options.plural + if plural == "always" then + plural = true + elseif plural == "never" then + plural = false + else -- "contextual" by default + if variable == "number-of-pages" or variable == "number-of-volumes" then + local v = tonumber(value) + plural = v and v > 1 or false + else + if type(value) == "table" then + plural = #value > 1 + else + local _, count = string.gsub(tostring(value), "%d+", "") -- naive count of numbers + plural = count > 1 + end + end + end + value = self:_render_term(variable, options.form or "long", plural) + value = self:_render_stripPeriods(value, options) + value = self:_render_textCase(value, options) + value = self:_render_formatting(value, options) + value = self:_render_affixes(value, options) + return value + end + return value +end + +function CslEngine:_group (options, content, entry) + self:_enterGroup() + + local t = self:_render_children(content, entry, { delimiter = options.delimiter }) + t = self:_render_formatting(t, options) + t = self:_render_affixes(t, options) + t = self:_render_display(t, options) + + t = self:_leaveGroup(t) -- Takes care of group suppression + return t +end + +function CslEngine:_if (options, content, entry) + local match = options.match or "all" + local conds = {} + if options.variable then + local vars = pl.stringx.split(options.variable, " ") + for _, var in ipairs(vars) do + local cond = entry[var] and true or false + table.insert(conds, cond) + end + end + if options.type then + local types = pl.stringx.split(options.type, " ") + local cond = false + -- Different from other conditions: + -- For types, Zeping Lee explained the matching is always "any". + for _, typ in ipairs(types) do + if entry.type == typ then + cond = true + break + end + end + table.insert(conds, cond) + end + if options["is-numeric"] then + for _, var in ipairs(pl.stringx.split(options["is-numeric"], " ")) do + -- FIXME NOT IMPLEMENTED FULLY + -- Content is considered numeric if it solely consists of numbers. + -- Numbers may have prefixes and suffixes (“D2”, “2b”, “L2d”), and may + -- be separated by a comma, hyphen, or ampersand, with or without + -- spaces (“2, 3”, “2-4”, “2 & 4”). For example, “2nd” tests “true” whereas + -- “second” and “2nd edition” test “false”. + local cond = tonumber(entry[var]) and true or false + table.insert(conds, cond) + end + end + if options["is-uncertain-date"] then + for _, var in ipairs(pl.stringx.split(options["is-uncertain-date"], " ")) do + local d = type(entry[var]) == "table" and entry[var] + local cond = d and d.approximate and true or false + table.insert(conds, cond) + end + end + if options.locator then + for _, loc in ipairs(pl.stringx.split(options.locator, " ")) do + local cond = entry.locator and entry.locator.label == loc or false + table.insert(conds, cond) + end + end + -- FIXME NOT IMPLEMENTED other conditions: "position", "disambiguate" + for _, v in ipairs({ "position", "disambiguate" }) do + if options[v] then + SU.warn("CSL if condition '" .. v .. "' not implemented yet") + table.insert(conds, false) + end + end + -- Apply match + local matching = match ~= "any" + for _, cond in ipairs(conds) do + if match == "all" then + if not cond then + matching = false + break + end + elseif match == "any" then + if cond then + matching = true + break + end + elseif match == "none" then + if cond then + matching = false + break + end + end + end + if matching then + return self:_render_children(content, entry), true + -- FIXME: + -- The CSL specification says: "Delimiters from the nearest delimiters + -- from the nearest ancestor delimiting element are applied within the + -- output of cs:choose (i.e., the output of the matching cs:if, + -- cs:else-if, or cs:else; see delimiter)."" + -- Ugh. This is rather obscure and not implemented yet (?) + end + return nil, false +end + +function CslEngine:_choose (options, content, entry) + for _, child in ipairs(content) do + if child.command == "cs:if" or child.command == "cs:else-if" then + local t, match = self:_if(child.options, child, entry) + if match then + return t + end + elseif child.command == "cs:else" then + return self:_render_children(child, entry) + end + end +end + +local function dateToYYMMDD (date) + --- Year from BibLaTeX year field may be a literal + local y = type(date.year) == "number" and date.year or tonumber(date.year) or 0 + local m = date.month or 0 + local d = date.day or 0 + return ("%04d%02d%02d"):format(y, m, d) +end + +function CslEngine:_key (options, content, entry) + -- Attribute 'sort' is managed at a higher level + -- FIXME NOT IMPLEMENTED: + -- Attributes 'names-min', 'names-use-first', and 'names-use-last' + -- (overrides for the 'et-al-xxx' attributes) + if options.macro then + return self:_render_children(self.macros[options.macro], entry) + end + if options.variable then + local value = entry[options.variable] + if type(value) == "table" then + if value.range then + if value.startdate and value.enddate then + return dateToYYMMDD(value.startdate) .. "-" .. dateToYYMMDD(value.enddate) + end + if value.startdate then + return dateToYYMMDD(value.startdate) .. "-" + end + if value.enddate then + return dateToYYMMDD(value.enddate) + end + return dateToYYMMDD(value.from) .. "-" .. dateToYYMMDD(value.to) + end + if value.year or value.month or value.day then + return dateToYYMMDD(value) + end + -- FIXME NOT IMPLEMENTED + -- Names need a special rule here. + -- Many styles (e.g. Chicago) use a macro here (for substitutes, etc.) + -- so this case is not yet implemented. + SU.error("CSL variable not yet usable for sorting: " .. options.variable) + end + return value + end + SU.error("CSL key without variable or macro") +end + +-- FIXME: A bit ugly: When implementing SU.collatedSort, I didn't consider +-- sorting structured tables, so we need to go low level here. +-- Moreover, I made icu.compare return a boolean, so we have to pay twice +-- the comparison cost to check equality... +-- See PR #2105 +local icu = require("justenoughicu") + +function CslEngine:_sort (options, content, entries) + if not self.sorting then + -- Skipped at rendering + return + end + -- Store the sort order for each key + local ordering = {} + for _, child in ipairs(content) do + if child.command == "cs:key" then + table.insert(ordering, child.options.sort ~= "descending") -- true for ascending (default) + end + end + -- Compute the sorting keys for each entry + for _, entry in ipairs(entries) do + local keys = {} + for _, child in ipairs(content) do + if child.command == "cs:key" then + self:_prerender() + -- Deep copy the entry as cs:substitute may remove fields + -- And we may need them back in actual rendering + local ent = pl.tablex.deepcopy(entry) + local key = self:_key(child.options, child, ent) + -- No _postrender here, as we don't want to apply punctuation (?) + table.insert(keys, key or "") + end + end + entry._keys = keys + end + -- Perform the sort + -- Using the locale language (BCP47). + local lang = self.locale.lang + local collator = icu.collation_create(lang, {}) + table.sort(entries, function (a, b) + if a["citation-key"] == b["citation-key"] then + -- Lua can invoke the comparison function with the same entry. + -- Really! Due to the way it handles it pivot on partitioning. + -- Shortcut the inner keys comparison in that case. + return false + end + local ak = a._keys + local bk = b._keys + for i = 1, #ordering do + -- "Items with an empty sort key value are placed at the end of the sort, + -- both for ascending and descending sorts." + if ak[i] == "" then + return bk[i] == "" + end + if bk[i] == "" then + return true + end + + if ak[i] ~= bk[i] then -- HACK: See comment above, ugly inequality check + local cmp = icu.compare(collator, ak[i], bk[i]) + -- Hack to keep on working whenever PR #2105 lands and changes icu.compare + local islower + if type(cmp) == "number" then + islower = cmp < 0 + else + islower = cmp + end + -- Now order accordingly + if ordering[i] then + return islower + else + return not islower + end + end + end + -- If we reach this point, the keys are equal (or we had no keys) + -- Probably unlikely in real life, and not mentioned in the CSL spec + -- unless I missed it. Let's fallback to the citation order, so at + -- least cited entries are ordered predictably. + SU.warn("CSL sort keys are equal for " .. a["citation-key"] .. " and " .. b["citation-key"]) + return a["citation-number"] < b["citation-number"] + end) + icu.collation_destroy(collator) +end + +-- PROCESSING + +function CslEngine:_render_node (node, entry) + local callback = node.command:gsub("cs:", "_") + if self[callback] then + return self[callback](self, node.options, node, entry) + else + SU.warn("Unknown CSL element " .. node.command .. " (" .. callback .. ")") + end +end + +function CslEngine:_render_children (ast, entry, context) + if not ast then + return + end + local ret = {} + context = context or {} + for _, content in ipairs(ast) do + if type(content) == "table" and content.command then + local r = self:_render_node(content, entry) + if r then + table.insert(ret, r) + end + else + SU.error("CSL unexpected content") -- Should not happen + end + end + return #ret > 0 and self:_render_delimiter(ret, context.delimiter) or nil +end + +function CslEngine:_postrender (text) + local rdquote = self.punctuation.close_quote + local ldquote = self.punctuation.open_quote + local rsquote = self.punctuation.close_inner_quote + local piquote = SU.boolean(self.locale.styleOptions["punctuation-in-quote"], false) + + -- Typography: Ensure there are no double straight quotes left from the input. + text = luautf8.gsub(text, '^"', ldquote) + text = luautf8.gsub(text, '"$', rdquote) + text = luautf8.gsub(text, '([%s%p])"', "%1" .. ldquote) + text = luautf8.gsub(text, '"([%s%p])', rdquote .. "%1") + -- HACK: punctuation-in-quote is applied globally, not just to generated quotes. + -- Not so sure it's the intended behavior from the specification? + if piquote then + -- move commas and periods before closing quotes + text = luautf8.gsub(text, "([" .. rdquote .. rsquote .. "]+)%s*([.,])", "%2%1") + end + -- HACK: fix some double punctuation issues. + -- Maybe some more robust way to handle affixes and delimiters would be better? + text = luautf8.gsub(text, "%.%.", ".") + -- Typography: Prefer to have commas and periods inside italics. + -- (Better looking if italic automated corrections are applied.) + text = luautf8.gsub(text, "()([%.,])", "%2%1") + -- HACK: remove extraneous periods after exclamation and question marks. + -- (Follows the preceding rule to also account for moved periods.) + text = luautf8.gsub(text, "([…!?])%.", "%1") + if not piquote then + -- HACK: remove extraneous periods after quotes. + -- Opinionated, e.g. for French at least, some typographers wouldn't + -- frown upon a period after a quote ending with an exclamation mark + -- or a question mark. But it's ugly. + text = luautf8.gsub(text, "([…!?%.]" .. rdquote .. ")%.", "%1") + end + return text +end + +function CslEngine:_process (entries, mode) + if mode ~= "citation" and mode ~= "bibliography" then + SU.error("CSL processing mode must be 'citation' or 'bibliography'") + end + self.mode = mode + -- Deep copy the entries as cs:substitute may remove fields + entries = pl.tablex.deepcopy(entries) + + local ast = self[mode] + if not ast then + SU.error("CSL style has no " .. mode .. " definition") + end + local sort = SU.ast.findInTree(ast, "cs:sort") + if sort then + self.sorting = true + self:_sort(sort.options, sort, entries) + self.sorting = false + else + -- The CSL specification says: + -- "In the absence of cs:sort, cites and bibliographic entries appear in + -- the order in which they are cited." + -- We tracked the first citation number in 'citation-number', so for + -- the bibliography, using it makes sense. + -- For citations, we use the exact order of the input. Consider a cite + -- (work1, work2) and a subsequent cite (work2, work1). The order of + -- the bibliography should be (work1, work2), but the order of the cites + -- should be (work1, work2) and (work2, work1) respectively. + -- It seeems to be the case: Some styles (ex. American Chemical Society) + -- have an explicit sort by 'citation-number' in the citations section, + -- which would be useless if that order was impplied. + if mode == "bibliography" then + table.sort(entries, function (e1, e2) + if not e1["citation-number"] or not e2["citation-number"] then + return false -- Safeguard? + end + return e1["citation-number"] < e2["citation-number"] + end) + end + end + + return self:_render_children(ast, entries) +end + +--- Generate a citation string. +-- @tparam table entry List of CSL entries +-- @treturn string The XML citation string +function CslEngine:cite (entries) + entries = type(entries) == "table" and not entries.type and entries or { entries } + return self:_process(entries, "citation") +end + +--- Generate a reference string. +-- @tparam table entry List of CSL entries +-- @treturn string The XML reference string +function CslEngine:reference (entries) + entries = type(entries) == "table" and not entries.type and entries or { entries } + return self:_process(entries, "bibliography") +end + +return CslEngine diff --git a/packages/bibtex/csl/locale.lua b/packages/bibtex/csl/locale.lua new file mode 100644 index 000000000..98944b434 --- /dev/null +++ b/packages/bibtex/csl/locale.lua @@ -0,0 +1,244 @@ +--- Reader for CSL 1.0.2 locale files +-- +-- @copyright License: MIT (c) 2024 Omikhleia +-- +-- Public API: +-- - (static method) CslLocale.parse(doc) -> CslLocale +-- - (static method) CslLocale.read(filename) -> CslLocale +-- - CslLocale:date(form) -> table +-- - CslLocale:term(name, form?, plural?) -> string, gender +-- - CslLocale:ordinal(number, form?, gender-form?, plural?) -> string +-- - CslLocale:case(text, textCase) -> string +-- + +local casing = require("packages.bibtex.csl.utils.casing") +local xmlparser = require("packages.bibtex.csl.utils.xmlparser") + +local parse = xmlparser.parse +local rules = { + prefix = "cs:", + skipEmptyStrings = true, + preserveEmptyStrings = {}, + stripSpaces = true, + preserveSpaces = { text = true, title = true, id = true, term = true }, +} + +local CslLocale = pl.class() + +function CslLocale:_init (tree) + self.terms = {} + self.dates = {} + self.styleOptions = {} + self:_preprocess(tree) +end + +-- Store items from the syntax tree in more convenient structures and maps +function CslLocale:_preprocess (tree) + self.lang = tree.options["xml:lang"] + + for _, content in ipairs(tree) do + if content.command == "cs:terms" then + for _, term in ipairs(content) do + if term.command == "cs:term" then + local name = term.options.name + if not name then + SU.error("CSL locale term without name") + end + local form = term.options.form or "long" + -- gender-form is only used for ordinal terms, but it's simpler + -- to just store it for all terms and have a consistent internal + -- representation + local genderf = term.options["gender-form"] or "neuter" + + self.terms[name] = self.terms[name] or {} + self.terms[name][form] = self.terms[name][form] or {} + -- Whole term (not sub-content) for its attributes + self.terms[name][form][genderf] = term + end + end + elseif content.command == "cs:style-options" then + self.styleOptions = content.options + elseif content.command == "cs:date" then + local form = content.options.form + if not form then + SU.error("CSL locale date without form") + end + -- extract the cs:date-part sub-content + self.dates[form] = SU.ast.subContent(content) + end + end +end + +function CslLocale:_termvalue (term) -- luacheck: no unused args + return term[1] +end + +function CslLocale:_lookupTerm (name, form, genderf) + local t = self.terms[name] + if not t then + return nil + end + form = form or "long" + local f = t[form] + if not f then + -- If not found, check for form fallbacks + if form == "long" then + return nil -- (No fallback) + end + if form == "verb-short" then + form = "verb" + elseif form == "symbol" then + form = "short" + elseif form == "verb" or form == "short" then + form = "long" + end + return self:_lookupTerm(name, form, genderf) + end + genderf = genderf or "neuter" + local g = f[genderf] + if not g then + if genderf == "neuter" then + return nil -- (No fallback) + end + return self:_lookupTerm(name, form, "neuter") + end + SU.debug("csl", "Lookup term", name, form, genderf) + return g +end + +function CslLocale:_lookupShortOrdinal (number, genderf) + SU.debug("csl", "Lookup short-ordinal", number, genderf) + number = tonumber(number) + if not number then + SU.error("CSL ordinal term requires a number") + end + + -- Case 0-9 + if number < 10 then + local name = ("ordinal-%02d"):format(number) + local term = self:_lookupTerm(name, "long", genderf) + if term then -- direct match on 0-9 + return term + end + return self:_lookupTerm("ordinal", "long", genderf) + end + -- Case 10-99 + if number < 100 then + local name = ("ordinal-%02d"):format(number) + local term = self:_lookupTerm(name, "long", genderf) + if term then + return term + end + -- No direct match, try to match the last digit + local lastDigit = number % 10 + local nameLastDigit = ("ordinal-%02d"):format(lastDigit) + local termLastDigit = self:_lookupTerm(nameLastDigit, "long", genderf) + if termLastDigit and termLastDigit.match ~= "whole-number" then + return termLastDigit + end + return self:_lookupTerm("ordinal", "long", genderf) + end + -- TODO FIXME: CSL specs do define rules for larger numbers, but is this really useful? + -- Not bothering for now! + SU.error("CSL ordinal beyond currently supported range") +end + +-- PUBLIC METHODS + +--- Lookup a date format in the locale. +-- @tparam string form The form of the date ('numeric' or 'text') +-- @treturn table The date format as a table of cs:date-parts +function CslLocale:date (form) + local d = self.dates[form] + if not d then + SU.error("CSL locale date format not found: " .. tostring(form)) + end + return d +end + +--- Lookup a term in the locale. +-- Reserved for non-ordinal terms. +-- @tparam string name The name of the term +-- @tparam string form The form of the term (default: "long") +-- @tparam boolean plural Whether to return the plural form (default: false) +-- @treturn string,string The term (or empty string), and the gender or the term (or nil) +function CslLocale:term (name, form, plural) + local term = self:_lookupTerm(name, form) + if not term then + return nil + end + if type(term[1]) == "string" then + return self:_termvalue(term), term.options.gender + end + local sgpl = SU.ast.findInTree(term, plural and "cs:multiple" or "cs:single") + if not sgpl then + pl.pretty.dump(term) + return SU.error("CSL term error for singular/multiple: " .. name) + end + return self:_termvalue(sgpl), term.options.gender +end + +--- Lookup an ordinal term in the locale. +-- Reserved for ordinal terms. +-- @tparam number number The numeric value to be formatted +-- @tparam string name The name of the term +-- @tparam string form The form of the term (default: "short") +-- @tparam string genderf The gender-form of the term (default: "neuter") +-- @tparam boolean plural Whether to return the plural form (default: false) +function CslLocale:ordinal (number, form, genderf, plural) + if form == "long" then + -- TODO FIXME: Not sure this is widely used, not bothering for now + SU.warn("CSL long-ordinal term not implemented, fallback to short ordinals") + end + local term = self:_lookupShortOrdinal(number, genderf) + if not term then + SU.error("CSL ordinal term not found for ordinal: " .. tostring(number)) + end + if type(term[1]) == "string" then + return number .. self:_termvalue(term) + end + local sgpl = SU.ast.findInTree(term, plural and "cs:plural" or "cs:single") + if not sgpl then + SU.error("CSL ordinal term not found for ordinal: " .. tostring(number)) + end + return number .. self:_termvalue(sgpl) +end + +--- Apply a text case transformation. +-- @tparam string text Text to transform +-- @tparam string textCase CSL case transformation +-- @treturn string The transformed text +function CslLocale:case (text, textCase) + local lang = self.lang + if not casing[textCase] then + SU.warn("CSL locale case not found: " .. textCase) + return text + end + return casing[textCase](text, lang) +end + +--- Parse a CSL locale file (static method). +-- @tparam string doc The CSL locale file content +-- @treturn CslLocale The locale object (or nil, error message on failure) +function CslLocale.parse (doc) + local tree, err = parse(doc, rules) + if not tree then + return nil, err + end + return CslLocale(tree) +end + +--- Read a CSL locale file (static method). +-- @tparam string filename The resolved filename of the locale file +-- @treturn CslLocale The locale object (or nil, error message on failure) +function CslLocale.read (filename) + local file, err = io.open(filename) + if not file then + return nil, err + end + local doc = file:read("*a") + file:close() + return CslLocale.parse(doc) +end + +return CslLocale diff --git a/packages/bibtex/csl/locales/README.md b/packages/bibtex/csl/locales/README.md new file mode 100644 index 000000000..0de8a5735 --- /dev/null +++ b/packages/bibtex/csl/locales/README.md @@ -0,0 +1,7 @@ +The files in this directory are the locale files for CSL styles, from the Citation Style Language project (https://github.com/citation-style-language/locales) + +They are distributed under the Creative Commons Attribution-ShareAlike 3.0 Unported License (http://creativecommons.org/licenses/by-sa/3.0/). + +We are providing these files here for convenience, so that SILE has a default set of locales for testing its implementation of CSL. + +Please note that the CSL project may have newer versions of these files. diff --git a/packages/bibtex/csl/locales/locales-en-US.xml b/packages/bibtex/csl/locales/locales-en-US.xml new file mode 100644 index 000000000..3f9ebadd0 --- /dev/null +++ b/packages/bibtex/csl/locales/locales-en-US.xml @@ -0,0 +1,774 @@ + + + + + Andrew Dunning + + + Sebastian Karcher + + + Rintze M. Zelle + + + Denis Meier + + + Brenton M. Wiernik + + This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License + 2024-03-12T13:41:31+00:00 + + + + + + + + + + + + + + + accessed + advance online publication + album + and + and others + anonymous + anon. + at + audio recording + available at + by + circa + c. + cited + et al. + film + forthcoming + from + henceforth + ibid. + in + in press + internet + letter + loc. cit. + no date + n.d. + no place + n.p. + no publisher + n.p. + on + online + op. cit. + original work published + personal communication + podcast + podcast episode + preprint + presented at the + radio broadcast + radio series + radio series episode + + reference + references + + + ref. + refs. + + retrieved + review of + rev. of + scale + special issue + special section + television broadcast + television series + television series episode + video + working paper + + + preprint + journal article + magazine article + newspaper article + bill + + broadcast + + classic + collection + dataset + document + entry + dictionary entry + encyclopedia entry + event + + graphic + hearing + interview + legal case + legislation + manuscript + map + video recording + musical score + pamphlet + conference paper + patent + performance + periodical + personal communication + post + blog post + regulation + report + review + book review + software + audio recording + presentation + standard + thesis + treaty + webpage + + + journal art. + mag. art. + newspaper art. + + + doc. + + graph. + interv. + MS + video rec. + rep. + rev. + bk. rev. + audio rec. + + + + testimony of + review of + review of the book + + + AD + BC + BCE + CE + + + + + + + + : + , + ; + + + th + st + nd + rd + th + th + th + + + first + second + third + fourth + fifth + sixth + seventh + eighth + ninth + tenth + + + + act + acts + + + appendix + appendices + + + article + articles + + + book + books + + + canon + canons + + + chapter + chapters + + + column + columns + + + location + locations + + + equation + equations + + + figure + figures + + + folio + folios + + + number + numbers + + + line + lines + + + note + notes + + + opus + opera + + + page + pages + + + paragraph + paragraphs + + + part + parts + + + rule + rules + + + scene + scenes + + + section + sections + + + sub verbo + sub verbis + + + supplement + supplements + + + table + tables + + + + + + + title + titles + + + verse + verses + + + version + versions + + + volume + volumes + + + + + + app. + apps. + + + art. + arts. + + + bk. + bks. + + + c. + cc. + + + chap. + chaps. + + + col. + cols. + + + loc. + locs. + + + eq. + eqs. + + + fig. + figs. + + + fol. + fols. + + + no. + nos. + + + l. + ll. + + + n. + nn. + + + op. + opp. + + + p. + pp. + + + para. + paras. + + + pt. + pts. + + + r. + rr. + + + sc. + scs. + + + sec. + secs. + + + s.v. + s.vv. + + + supp. + supps. + + + tbl. + tbls. + + + tit. + tits. + + + v. + vv. + + + v. + v. + + + vol. + vols. + + + + + + ¶¶ + + + § + §§ + + + + + chapter + chapters + + + citation + citations + + + number + numbers + + + edition + editions + + + reference + references + + + number + numbers + + + page + pages + + + volume + volumes + + + page + pages + + + printing + printings + + + + + chap. + chaps. + + + cit. + cits. + + + no. + nos. + + + ed. + eds. + + + ref. + refs. + + + no. + nos. + + + p. + pp. + + + vol. + vols. + + + p. + pp. + + + print. + prints. + + + + + + + chair + chairs + + + editor + editors + + + compiler + compilers + + + contributor + contributors + + + curator + curators + + + director + directors + + + editor + editors + + + editor & translator + editors & translators + + + editor + editors + + + executive producer + executive producers + + + guest + guests + + + host + hosts + + + illustrator + illustrators + + + narrator + narrators + + + organizer + organizers + + + performer + performers + + + producer + producers + + + writer + writers + + + series creator + series creators + + + translator + translators + + + + + + ed. + eds. + + + comp. + comps. + + + contrib. + contribs. + + + cur. + curs. + + + dir. + dirs. + + + ed. + eds. + + + ed. & tran. + eds. & trans. + + + ed. + eds. + + + exec. prod. + exec. prods. + + + ill. + ills. + + + narr. + narrs. + + + org. + orgs. + + + perf. + perfs. + + + prod. + prods. + + + writ. + writs. + + + cre. + cres. + + + tran. + trans. + + + + by + chaired by + edited by + compiled by + composed by + by + with + curated by + directed by + edited by + edited & translated by + edited by + executive produced by + with guest + hosted by + illustrated by + interview by + narrated by + organized by + by + performed by + produced by + to + by + written by + created by + translated by + + + + ed. by + comp. by + comp. by + w. + cur. by + dir. by + ed. by + ed. & trans. by + ed. by + exec. prod. by + w. guest + illus. by + narr. by + org. by + perf. by + prod. by + writ. by + cre. by + trans. by + + + January + February + March + April + May + June + July + August + September + October + November + December + + + Jan. + Feb. + Mar. + Apr. + May + Jun. + Jul. + Aug. + Sep. + Oct. + Nov. + Dec. + + + Spring + Summer + Autumn + Winter + + diff --git a/packages/bibtex/csl/locales/locales-fr-FR.xml b/packages/bibtex/csl/locales/locales-fr-FR.xml new file mode 100644 index 000000000..23b6d80fa --- /dev/null +++ b/packages/bibtex/csl/locales/locales-fr-FR.xml @@ -0,0 +1,701 @@ + + + + + Grégoire Colly + + + Collectif Zotero francophone + + This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License + 2012-07-04T23:31:02+00:00 + + + + + + + + + + + + + + publication en ligne anticipée + album + enregistrement audio + film + désormais + loc. cit. + sans lieu + s. l. + sans nom + s. n. + sur + op. cit. + édition originale + communication personnelle + podcast + épisode de podcast + prépublication + émission de radio + série radiophonique + épisode de série radiophonique + numéro spécial + section spéciale + émission de télévision + série télévisée + épisode de série télévisée + vidéo + document de travail + consulté le + et + et autres + anonyme + anon. + sur + disponible sur + par + vers + v. + cité + + reference + references + + + number + numbers + + + édition + éditions + + + ref. + refs. + + + no. + nos. + + éd. + et al. + à paraître + à l'adresse + ibid. + in + sous presse + Internet + lettre + sans date + s. d. + en ligne + présenté à + + référence + références + + + réf. + réf. + + review of + rev. of + consulté + échelle + version + + + article + article de revue + article de magazine + article de presse + projet de loi + + émission + + classique + collection + jeu de données + document + entrée + entrée de dictionnaire + entrée d'encyclopédie + événement + + image + audience + entretien + affaire + acte juridique + manuscrit + carte + enregistrement vidéo + partition + pamphlet + article de colloque + brevet + interprétation + périodique + communication personnelle + billet + billet de blog + règlement + rapport + recension + recension de livre + logiciel + chanson + présentation + norme + thèse + traité + page web + + + art. de revue + art. de mag. + art. de presse + + + doc. + + graph. + interv. + ms + enr. vidéo + rap. + recens. + recens. de liv. + enr. audio + + + + testimony of + recension de + recension du livre + + + apr. J.-C. + av. J.-C. + av. n. è. + n. è. + + + «  +  » + + + +  : + , +  ; + + + + ʳᵉ + ᵉʳ + + + premier + deuxième + troisième + quatrième + cinquième + sixième + septième + huitième + neuvième + dixième + + + + acte + actes + + + appendice + appendices + + + article + articles + + + canon + canons + + + emplacement + emplacements + + + équation + équations + + + règle + règles + + + scène + scènes + + + tableau + tableaux + + + + + + + titre + titres + + + livre + livres + + + chapitre + chapitres + + + colonne + colonnes + + + figure + figures + + + folio + folios + + + numéro + numéros + + + ligne + lignes + + + note + notes + + + opus + opus + + + page + pages + + + volume + volumes + + + page + pages + + + printing + printings + + + + chap. + chaps. + + + cit. + cits. + + + nᵒ + nᵒˢ + + + page + pages + + + paragraphe + paragraphes + + + partie + parties + + + section + sections + + + supplement + supplements + + + sub verbo + sub verbis + + + verset + versets + + + volume + volumes + + + + + append. + append. + + + art. + art. + + + emplact + emplact + + + eq. + eq. + + + règle + règles + + + sc. + sc. + + + tab. + tab. + + + + + + + tit. + tit. + + liv. + chap. + col. + fig. + + fᵒ + fᵒˢ + + + nᵒ + nᵒˢ + + l. + n. + op. + + p. + p. + + + vol. + vols. + + + p. + pp. + + + print. + prints. + + + + + p. + p. + + paragr. + part. + sect. + + supp. + supps. + + + s. v. + s. vv. + + + v. + v. + + + vol. + vol. + + + + + § + § + + + chapter + chapters + + + citation + citations + + + numéro + numéros + + + § + § + + + + + ed. + eds. + + + président + présidents + + + compilateur + compilateurs + + + contributeur + contributeurs + + + commissaire + commissaires + + + producteur exécutif + producteurs exécutifs + + + invité + invités + + + hôte + hôtes + + + narrateur + narrateurs + + + organisateur + organisateurs + + + interprète + interprètes + + + producteur + producteurs + + + scénariste + scénaristes + + + créateur de série + créateurs de série + + + réalisateur + réalisateurs + + + éditeur + éditeurs + + + directeur + directeurs + + + illustrateur + illustrateurs + + + traducteur + traducteurs + + + éditeur et traducteur + éditeurs et traducteurs + + + + + compil. + compil. + + + contrib. + contrib. + + + commiss. + commiss. + + + prod. exé. + prod. exé. + + + narr. + narr. + + + org. + org. + + + interpr. + interpr. + + + prod. + prod. + + + scénar. + scénar. + + + créat. + créat. + + + réal. + réal. + + + éd. + éd. + + + dir. + dir. + + + ill. + ill. + + + trad. + trad. + + + éd. et trad. + éd. et trad. + + + + edited by + présidé par + compilé par + avec + organisé par + production exécutive par + avec pour invité + animé par + lu par + organisé par + interprété par + produit par + scénario de + créé par + par + réalisé par + édité par + sous la direction de + illustré par + entretien réalisé par + à + par + ed. by + traduit par + édité et traduit par + + + compil. par + ac + org. par + prod. exé. par + ac pr inv. + anim. par + lu par + org. par + interpr. par + prod. par + scénar. de + créé par + réal. par + éd. par + ss la dir. de + ill. par + trad. par + éd. et trad. par + + + janvier + février + mars + avril + mai + juin + juillet + août + septembre + octobre + novembre + décembre + + + janv. + févr. + mars + avr. + mai + juin + juill. + août + sept. + oct. + nov. + déc. + + + printemps + été + automne + hiver + + diff --git a/packages/bibtex/csl/style.lua b/packages/bibtex/csl/style.lua new file mode 100644 index 000000000..4ccbed5b6 --- /dev/null +++ b/packages/bibtex/csl/style.lua @@ -0,0 +1,99 @@ +--- Reader for CSL 1.0.2 locale files +-- +-- @copyright License: MIT (c) 2024 Omikhleia +-- +-- Public API: +-- - (static method) CslStyle.parse(doc) -> CslStyle +-- - (static method) CslStyle.read(filename) -> CslStyle +-- + +local xmlparser = require("packages.bibtex.csl.utils.xmlparser") + +local parse = xmlparser.parse +local rules = { + prefix = "cs:", + skipEmptyStrings = true, + preserveEmptyStrings = {}, + stripSpaces = true, + preserveSpaces = { text = true, title = true, id = true, term = true }, +} + +local CslStyle = pl.class() + +function CslStyle:_init (tree) + self.macros = {} + self.locales = {} + self.bibliography = nil + self.citation = nil + self.globalOptions = {} + self:_preprocess(tree) +end + +-- Store items from the syntax tree in more convenient structures and maps +function CslStyle:_preprocess (tree) + -- Global options and inheritable name options + self.globalOptions = tree.options + + -- Extract macros, locale overrides, citation and bibliography + for _, content in ipairs(tree) do + if content.command == "cs:macro" then + local name = content.options and content.options.name + if not name then + SU.error("CSL macro without name") + end + if self.macros[name] then + SU.warn("CSL macro " .. name .. " has multiple definitions, using the last one") + end + self.macros[name] = SU.ast.subContent(content) + elseif content.command == "cs:locale" then + local lang = content.options and content.options["xml:lang"] + if not lang then + SU.error("CSL locale without xml:lang") + end + if self.locales[lang] then + SU.warn("CSL locale " .. lang .. " has multiple definitions, using the last one") + end + -- Don't subcontent, so we have full locales here (overrides) + self.locales[lang] = content + elseif content.command == "cs:citation" then + if self.citation then + SU.warn("CSL has multiple citation definitions, using the last one") + end + -- Don't subContent, we want to keep the whole citation options (attributes) + self.citation = content + elseif content.command == "cs:bibliography" then + if self.bibliography then + SU.warn("CSL has multiple bibliography definitions, using the last one") + end + -- Don't subContent, we want to keep the whole bibliography options (attributes) + self.bibliography = content + end + -- We can ignore cs:info and don't expect other top-level elements + end +end + +--- Parse a CSL style document (static method). +-- @tparam string doc The CSL style document +-- @treturn Csl The parsed CSL style object (or nil, error message on failure) +function CslStyle.parse (doc) + local tree, err = parse(doc, rules) + if not tree then + return nil, err + end + return CslStyle(tree) +end + +--- Read a CSL style file (static method). +-- @tparam string filename The resolved filename of the CSL style file +-- @treturn Csl The parsed CSL style object (or nil, error message on failure) +function CslStyle.read (filename) + local file, err = io.open(filename) + if not file then + return nil, err + end + local doc = file:read("*a") + file:close() + return CslStyle.parse(doc) +end + +return CslStyle diff --git a/packages/bibtex/csl/styles/README.md b/packages/bibtex/csl/styles/README.md new file mode 100644 index 000000000..ef701c6a9 --- /dev/null +++ b/packages/bibtex/csl/styles/README.md @@ -0,0 +1,7 @@ +The files in this directory are the style files for CSL, from the Citation Style Language project (https://github.com/citation-style-language/styles) + +They are distributed under the Creative Commons Attribution-ShareAlike 3.0 Unported License (http://creativecommons.org/licenses/by-sa/3.0/). + +We are providing these files here for convenience, so that SILE has a default set of styles for testing its implementation of CSL. + +Please note that the CSL project may have newer versions of these files. diff --git a/packages/bibtex/csl/styles/chicago-author-date-fr.csl b/packages/bibtex/csl/styles/chicago-author-date-fr.csl new file mode 100644 index 000000000..3ec4b2be4 --- /dev/null +++ b/packages/bibtex/csl/styles/chicago-author-date-fr.csl @@ -0,0 +1,766 @@ + + diff --git a/packages/bibtex/csl/styles/chicago-author-date.csl b/packages/bibtex/csl/styles/chicago-author-date.csl new file mode 100644 index 000000000..cb34afd4f --- /dev/null +++ b/packages/bibtex/csl/styles/chicago-author-date.csl @@ -0,0 +1,704 @@ + + diff --git a/packages/bibtex/csl/utils/casing.lua b/packages/bibtex/csl/utils/casing.lua new file mode 100644 index 000000000..501b80e93 --- /dev/null +++ b/packages/bibtex/csl/utils/casing.lua @@ -0,0 +1,45 @@ +--- Casing functions for CSL locales. +-- +-- @copyright License: MIT (c) 2024 Omikhleia +-- +-- Objectives: provide functions to handle text casing in CSL locales. +-- + +local icu = require("justenoughicu") +-- N.B. We don't use the textcase package here: +-- The language is a BCP47 identifier from the CSL locale. + +local capitalizeFirst = function (text, lang) + local first = luautf8.sub(text, 1, 1) + local rest = luautf8.sub(text, 2) + return icu.case(first, lang, "upper") .. rest +end + +--- Text casing methods for CSL. +-- @table casing methods for lower, upper, capitalize-first, capitalize-all, title, sentence +local casing = { + -- Straightforward + ["lowercase"] = function (text, lang) + return icu.case(text, lang, "lower") + end, + ["uppercase"] = function (text, lang) + return icu.case(text, lang, "upper") + end, + ["capitalize-first"] = capitalizeFirst, + + -- Opinionated: even ICU does not really handle this well. + -- It does not have good support for exceptions (small words, prepositions, + -- articles), etc. in most languages + -- So fallback to capitalize-first. + ["capitalize-all"] = capitalizeFirst, + ["title"] = capitalizeFirst, + + -- Deprecated. + -- Let's not bother with it. + ["sentence"] = function (text, _) + SU.warn("Sentence case is deprecated in CSL 1.0.x (ignored)") + return text + end, +} + +return casing diff --git a/packages/bibtex/csl/utils/superfolding.lua b/packages/bibtex/csl/utils/superfolding.lua new file mode 100644 index 000000000..6a33062f4 --- /dev/null +++ b/packages/bibtex/csl/utils/superfolding.lua @@ -0,0 +1,147 @@ +--- Superscript folding for CSL locales. +-- +-- @copyright License: MIT (c) 2024 Omikhleia +-- +-- Objectives: replace Unicode superscripted characters with their normal +-- counterparts. +-- +-- Based on Datafile for Unicode Techical Report #30 +-- http://unicode.org/reports/tr30/datafiles/SuperscriptFolding.txt +-- Copyright (c) 1991-2004 Unicode, Inc. +-- For terms of use, and documentation see http://www.unicode.org/reports/tr30/ +-- +-- Note that TR30 is not normative (and is currently suspended) +-- Maybe we should use other sources, see: +-- https://en.wikipedia.org/wiki/Unicode_subscripts_and_superscripts +-- + +local supersyms = { + -- "characters with compatibility decomposition in UnicodeData.txt" + ["ª"] = "a", + ["²"] = "2", + ["³"] = "3", + ["¹"] = "1", + ["º"] = "o", + ["ʰ"] = "h", + ["ʱ"] = "ɦ", + ["ʲ"] = "j", + ["ʳ"] = "r", + ["ʴ"] = "ɹ", + ["ʵ"] = "ɻ", + ["ʶ"] = "ʁ", + ["ʷ"] = "w", + ["ʸ"] = "y", + ["ˠ"] = "ɣ", + ["ˡ"] = "l", + ["ˢ"] = "s", + ["ˣ"] = "x", + ["ˤ"] = "ʕ", + ["ᴬ"] = "A", + ["ᴭ"] = "Æ", + ["ᴮ"] = "B", + ["ᴰ"] = "D", + ["ᴱ"] = "E", + ["ᴲ"] = "Ǝ", + ["ᴳ"] = "G", + ["ᴴ"] = "H", + ["ᴵ"] = "I", + ["ᴶ"] = "J", + ["ᴷ"] = "K", + ["ᴸ"] = "L", + ["ᴹ"] = "M", + ["ᴺ"] = "N", + ["ᴼ"] = "O", + ["ᴽ"] = "Ȣ", + ["ᴾ"] = "P", + ["ᴿ"] = "R", + ["ᵀ"] = "T", + ["ᵁ"] = "U", + ["ᵂ"] = "W", + ["ᵃ"] = "a", + ["ᵄ"] = "ɐ", + ["ᵅ"] = "ɑ", + ["ᵆ"] = "ᴂ", + ["ᵇ"] = "b", + ["ᵈ"] = "d", + ["ᵉ"] = "e", + ["ᵊ"] = "ə", + ["ᵋ"] = "ɛ", + ["ᵌ"] = "ɜ", + ["ᵍ"] = "g", + ["ᵏ"] = "k", + ["ᵐ"] = "m", + ["ᵑ"] = "ŋ", + ["ᵒ"] = "o", + ["ᵓ"] = "ɔ", + ["ᵔ"] = "ᴖ", + ["ᵕ"] = "ᴗ", + ["ᵖ"] = "p", + ["ᵗ"] = "t", + ["ᵘ"] = "u", + ["ᵙ"] = "ᴝ", + ["ᵚ"] = "ɯ", + ["ᵛ"] = "v", + ["ᵜ"] = "ᴥ", + ["ᵝ"] = "β", + ["ᵞ"] = "γ", + ["ᵟ"] = "δ", + ["ᵠ"] = "φ", + ["ᵡ"] = "χ", + ["⁰"] = "0", + ["ⁱ"] = "i", + ["⁴"] = "4", + ["⁵"] = "5", + ["⁶"] = "6", + ["⁷"] = "7", + ["⁸"] = "8", + ["⁹"] = "9", + ["⁺"] = "+", + ["⁻"] = "−", + ["⁼"] = "=", + ["⁽"] = "(", + ["⁾"] = ")", + ["ⁿ"] = "n", + -- ['℠'] = 'SM', -- Keep symbol + -- ['™'] = 'TM', -- Keep symbol + -- ['㆒'] = '一', -- Keep ideographic characters (?) + -- ['㆓'] = '二', + -- ['㆔'] = '三', + -- ['㆕'] = '四', + -- ['㆖'] = '上', + -- ['㆗'] = '中', + -- ['㆘'] = '下', + -- ['㆙'] = '甲', + -- ['㆚'] = '乙', + -- ['㆛'] = '丙', + -- ['㆜'] = '丁', + -- ['㆝'] = '天', + -- ['㆞'] = '地', + -- ['㆟'] = '人', + + -- "other characters that are superscripted forms" + ["ˀ"] = "ʔ", + ["ˁ"] = "ʕ", + -- ['ۥ'] = 'و', -- Keep Arabic characters (combining?) + -- ['ۦ'] = 'ي', +} + +-- pattern for groups of superscripted characters +local vals = {} +for k in pairs(supersyms) do + table.insert(vals, k) +end +local pat = "[" .. table.concat(vals) .. "]+" + +--- Replace Unicode superscripted characters with their normal counterparts. +-- @tparam string str The string to process. +-- @treturn string The string with superscripted characters replaced. +local function superfolding (str) + return luautf8.gsub(str, pat, function (group) + local replaced = luautf8.gsub(group, ".", function (char) + return supersyms[char] + end) + return "" .. replaced .. "" + end) +end + +return superfolding diff --git a/packages/bibtex/csl/utils/xmlparser.lua b/packages/bibtex/csl/utils/xmlparser.lua new file mode 100644 index 000000000..4ba510522 --- /dev/null +++ b/packages/bibtex/csl/utils/xmlparser.lua @@ -0,0 +1,128 @@ +--- Modified XML parser +-- +-- MOSTLY ADAPTED FROM SILE's XML INPUTTER +-- BUT WITH EXTRA FEATURES FOR NAMESPACING AND SPACES CLEANING. +-- +-- It simplifies the processing a lot later... +-- TODO FIXME: This could raise an interesting discussion about the supposedly +-- generic XML support in SILE... + +local lxp = require("lxp") + +local defaultRules = { + -- NAMESPACING: + -- If defined, prefix is prepended to the tag name to create the SILE + -- command name. + -- This is a way to avoid conflicts between different XML formats and + -- SILE commands. + prefix = nil, + -- SPACES CLEANING: + -- Depending on the XML schema, some spaces may be irrelevant. + -- Some XML nodes are containers for other nodes. They may have spaces + -- in their content, due to the XML formatting and indentation. + -- Some XML nodes contain text that should be stripped of trailing and + -- leading spaces. + -- It is cumbersome to have to strip spaces in the SILE content later, + -- so we can define here the nodes for which we want to strip spaces. + -- skipEmptyStrings is eitheir a boolean or a table with tags to skip + -- text strings composed only of spaces in elements. + -- When set to true, all elements are considered by default. In that + -- case, preserveEmptyStrings is used to keep empty strings in some + -- elements. + -- stripSpaces is either a boolean or a table with tags to strip the + -- leading and trailing spaces in text elements. + -- When set to true, all elements are considered by default. In that + -- case, preserveSpaces is used to keep spaces in some tags. + stripSpaces = false, + preserveSpaces = {}, + skipEmptyStrings = false, + preserveEmptyStrings = {}, +} + +local function isStripSpaces (tag, rules) + if type(rules.stripSpaces) == "table" then + return rules.stripSpaces[tag] and not rules.preserveSpaces[tag] + end + return rules.stripSpaces and not rules.preserveSpaces[tag] +end + +local function isSkipEmptyStrings (tag, rules) + if type(rules.skipEmptyStrings) == "table" then + return rules.skipEmptyStrings[tag] and not rules.preserveEmptyStrings[tag] + end + return rules.skipEmptyStrings and not rules.preserveEmptyStrings[tag] +end + +local function startcommand (parser, command, options) + local callback = parser:getcallbacks() + local stack = callback.stack + local lno, col, pos = parser:pos() + local position = { lno = lno, col = col, pos = pos } + -- create an empty command which content will be filled on closing tag + local element = SU.ast.createCommand(command, options, nil, position) + table.insert(stack, element) +end + +local function endcommand (parser, command) + local callback = parser:getcallbacks() + local stack, rules = callback.stack, callback.rules + local element = table.remove(stack) + assert(element.command == command) + element.command = rules.prefix and (rules.prefix .. command) or command + + local level = #stack + table.insert(stack[level], element) +end + +local function text (parser, msg) + local callback = parser:getcallbacks() + local stack, rules = callback.stack, callback.rules + local element = stack[#stack] + + local stripSpaces = isStripSpaces(element.command, rules) + local skipEmptyStrings = isSkipEmptyStrings(element.command, rules) + + local txt = (stripSpaces or skipEmptyStrings) and msg:gsub("^%s+", ""):gsub("%s+$", "") or msg + if skipEmptyStrings and txt == "" then + return + end + msg = stripSpaces and txt or msg + + local n = #element + if type(element[n]) == "string" then + element[n] = element[n] .. msg + else + table.insert(element, msg) + end +end + +local function parse (doc, rules) + local content = { + StartElement = startcommand, + EndElement = endcommand, + CharacterData = text, + _nonstrict = true, + stack = { {} }, + rules = rules or defaultRules, + } + local parser = lxp.new(content) + local status, err + if type(doc) == "string" then + status, err = parser:parse(doc) + if not status then + return nil, err + end + else + return nil, "Only string input should be supported" + end + status, err = parser:parse() + if not status then + return nil, err + end + parser:close() + return content.stack[1][1] +end + +return { + parse = parse, +} diff --git a/packages/bibtex/init.lua b/packages/bibtex/init.lua index c27c79a8f..070cecc49 100644 --- a/packages/bibtex/init.lua +++ b/packages/bibtex/init.lua @@ -1,5 +1,40 @@ local base = require("packages.base") +local loadkit = require("loadkit") +local cslStyleLoader = loadkit.make_loader("csl") +local cslLocaleLoader = loadkit.make_loader("xml") + +local CslLocale = require("packages.bibtex.csl.locale") +local CslStyle = require("packages.bibtex.csl.style") +local CslEngine = require("packages.bibtex.csl.engine") + +local function loadCslLocale (name) + local filename = SILE.resolveFile("packages/bibtex/csl/locales/locales-" .. name .. ".xml") + or cslLocaleLoader("packages.bibtex.csl.locales.locales-" .. name) + if not filename then + SU.error("Could not find CSL locale '" .. name .. "'") + end + local locale, err = CslLocale.read(filename) + if not locale then + SU.error("Could not open CSL locale '" .. name .. "'': " .. err) + return + end + return locale +end +local function loadCslStyle (name) + local filename = SILE.resolveFile("packages/bibtex/csl/styles/" .. name .. ".csl") + or cslStyleLoader("packages.bibtex.csl.styles." .. name) + if not filename then + SU.error("Could not find CSL style '" .. name .. "'") + end + local style, err = CslStyle.read(filename) + if not style then + SU.error("Could not open CSL style '" .. name .. "'': " .. err) + return + end + return style +end + local package = pl.class(base) package._name = "bibtex" @@ -7,6 +42,8 @@ local epnf = require("epnf") local nbibtex = require("packages.bibtex.support.nbibtex") local namesplit, parse_name = nbibtex.namesplit, nbibtex.parse_name local isodatetime = require("packages.bibtex.support.isodatetime") +local bib2csl = require("packages.bibtex.support.bib2csl") +local locators = require("packages.bibtex.support.locators") local Bibliography @@ -160,7 +197,7 @@ local function parseBibtex (fn, biblio) for i = 1, #t do if t[i].id == "entry" then local ent = t[i][1] - local entry = { type = ent.type, attributes = ent[1] } + local entry = { type = ent.type, label = ent.label, attributes = ent[1] } if biblio[ent.label] then SU.warn("Duplicate entry key '" .. ent.label .. "', picking the last one") end @@ -241,10 +278,50 @@ local function crossrefAndXDataResolve (bib, entry) end end +local function resolveEntry (bib, key) + local entry = bib[key] + if not entry then + SU.warn("Unknown citation key " .. key) + return + end + if entry.type == "xdata" then + SU.warn("Skipped citation of @xdata entry " .. key) + return + end + crossrefAndXDataResolve(bib, entry) + return entry +end + +function package:loadOptPackage (pack) + local ok, _ = pcall(function () + self:loadPackage(pack) + return true + end) + SU.debug("bibtex", "Optional package " .. pack .. (ok and " loaded" or " not loaded")) + return ok +end + function package:_init () base._init(self) - SILE.scratch.bibtex = { bib = {} } + SILE.scratch.bibtex = { bib = {}, cited = { keys = {}, citnums = {} } } Bibliography = require("packages.bibtex.bibliography") + -- For DOI, PMID, PMCID and URL support. + self:loadPackage("url") + -- For underline styling support + self:loadPackage("rules") + -- For TeX-like math support (extension) + self:loadPackage("math") + -- For superscripting support in number formatting + -- Play fair: try to load 3rd-party optional textsubsuper package. + -- If not available, fallback to raiselower to implement textsuperscript + if not self:loadOptPackage("textsubsuper") then + self:loadPackage("raiselower") + self:registerCommand("textsuperscript", function (_, content) + SILE.call("raise", { height = "0.7ex" }, function () + SILE.call("font", { size = "1.5ex" }, content) + end) + end) + end end function package.declareSettings (_) @@ -262,45 +339,65 @@ function package:registerCommands () parseBibtex(file, SILE.scratch.bibtex.bib) end) + -- LEGACY COMMANDS + self:registerCommand("bibstyle", function (_, _) SU.deprecated("\\bibstyle", "\\set[parameter=bibtex.style]", "0.13.2", "0.14.0") end) self:registerCommand("cite", function (options, content) + local style = SILE.settings:get("bibtex.style") + if style == "csl" then + SILE.call("csl:cite", options, content) + return -- done via CSL + end + if not self._deprecated_legacy_warning then + self._deprecated_legacy_warning = true + SU.warn("Legacy bibtex.style is deprecated, consider enabling the CSL implementation.") + end if not options.key then options.key = SU.ast.contentToString(content) end - local entry = SILE.scratch.bibtex.bib[options.key] + local entry = resolveEntry(SILE.scratch.bibtex.bib, options.key) if not entry then - SU.warn("Unknown reference in citation " .. options.key) - return - end - if entry.type == "xdata" then - SU.warn("Skipped citation of @xdata entry " .. options.key) return end - crossrefAndXDataResolve(SILE.scratch.bibtex.bib, entry) - local style = SILE.settings:get("bibtex.style") + -- Keep track of cited entries + table.insert(SILE.scratch.bibtex.cited.keys, options.key) + local citnum = #SILE.scratch.bibtex.cited.keys + SILE.scratch.bibtex.cited.citnums[options.key] = citnum + local bibstyle = require("packages.bibtex.styles." .. style) local cite = Bibliography.produceCitation(options, SILE.scratch.bibtex.bib, bibstyle) SILE.processString(("%s"):format(cite), "xml") end) self:registerCommand("reference", function (options, content) + local style = SILE.settings:get("bibtex.style") + if style == "csl" then + SILE.call("csl:reference", options, content) + return -- done via CSL + end + if not self._deprecated_legacy_warning then + self._deprecated_legacy_warning = true + SU.warn("Legacy bibtex.style is deprecated, consider enabling the CSL implementation.") + end if not options.key then options.key = SU.ast.contentToString(content) end - local entry = SILE.scratch.bibtex.bib[options.key] + local entry = resolveEntry(SILE.scratch.bibtex.bib, options.key) if not entry then - SU.warn("Unknown reference in citation " .. options.key) return end - if entry.type == "xdata" then - SU.warn("Skipped citation of @xdata entry " .. options.key) - return + + local citnum = SILE.scratch.bibtex.cited.citnums[options.key] + if not citnum then + SU.warn("Reference to a non-cited entry " .. options.key) + -- Make it cited + table.insert(SILE.scratch.bibtex.cited.keys, options.key) + citnum = #SILE.scratch.bibtex.cited.keys + SILE.scratch.bibtex.cited.citnums[options.key] = citnum end - crossrefAndXDataResolve(SILE.scratch.bibtex.bib, entry) - local style = SILE.settings:get("bibtex.style") local bibstyle = require("packages.bibtex.styles." .. style) local cite, err = Bibliography.produceReference(options, SILE.scratch.bibtex.bib, bibstyle) if cite == Bibliography.Errors.UNKNOWN_TYPE then @@ -309,25 +406,277 @@ function package:registerCommands () end SILE.processString(("%s"):format(cite), "xml") end) + + -- NEW CSL IMPLEMENTATION + + -- Hooks for CSL processing + + self:registerCommand("bibSmallCaps", function (_, content) + -- To avoid attributes in the CSL-processed content + SILE.call("font", { features = "+smcp" }, content) + end) + + -- CSL 1.0.2 appendix VI + -- "If the bibliography entry for an item renders any of the following + -- identifiers, the identifier should be anchored as a link, with the + -- target of the link as follows: + -- url: output as is + -- doi: prepend with “https://doi.org/” + -- pmid: prepend with “https://www.ncbi.nlm.nih.gov/pubmed/” + -- pmcid: prepend with “https://www.ncbi.nlm.nih.gov/pmc/articles/” + -- NOT IMPLEMENTED: + -- "Citation processors should include an option flag for calling + -- applications to disable bibliography linking behavior." + -- (But users can redefine these commands to their liking...) + self:registerCommand("bibLink", function (options, content) + SILE.call("href", { src = options.src }, { + SU.ast.createCommand("url", {}, { content[1] }), + }) + end) + self:registerCommand("bibURL", function (_, content) + local link = content[1] + if not link:match("^https?://") then + -- Play safe + link = "https://" .. link + end + SILE.call("bibLink", { src = link }, content) + end) + self:registerCommand("bibDOI", function (_, content) + local link = content[1] + if not link:match("^https?://") then + link = "https://doi.org/" .. link + end + SILE.call("bibLink", { src = link }, content) + end) + self:registerCommand("bibPMID", function (_, content) + local link = content[1] + if not link:match("^https?://") then + link = "https://www.ncbi.nlm.nih.gov/pubmed/" .. link + end + SILE.call("bibLink", { src = link }, content) + end) + self:registerCommand("bibPMCID", function (_, content) + local link = content[1] + if not link:match("^https?://") then + link = "https://www.ncbi.nlm.nih.gov/pmc/articles/" .. link + end + SILE.call("bibLink", { src = link }, content) + end) + + self:registerCommand("bibRule", function (_, content) + local n = content[1] and tonumber(content[1]) or 3 + local width = n .. "em" + SILE.call("raise", { height = "0.4ex" }, function () + SILE.call("hrule", { height = "0.4pt", width = width }) + end) + end) + + -- Style and locale loading + + self:registerCommand("bibliographystyle", function (options, _) + local sty = SU.required(options, "style", "bibliographystyle") + local style = loadCslStyle(sty) + -- FIXME: lang is mandatory until we can map document.lang to a resolved + -- BCP47 with region always present, as this is what CSL locales require. + if not options.lang then + -- Pick the default locale from the style, if any + options.lang = style.globalOptions["default-locale"] + end + local lang = SU.required(options, "lang", "bibliographystyle") + local locale = loadCslLocale(lang) + SILE.scratch.bibtex.engine = CslEngine(style, locale, { + localizedPunctuation = SU.boolean(options.localizedPunctuation, false), + italicExtension = SU.boolean(options.italicExtension, true), + mathExtension = SU.boolean(options.mathExtension, true), + }) + end) + + self:registerCommand("csl:cite", function (options, content) + -- TODO: + -- - multiple citation keys (but how to handle locators then?) + local locator + for k, v in pairs(options) do + if k ~= "key" then + if not locators[k] then + SU.warn("Unknown option '" .. k .. "' in \\csl:cite") + else + if not locator then + local label = locators[k] + locator = { label = label, value = v } + else + SU.warn("Multiple locators in \\csl:cite, using the first one") + end + end + end + end + if not SILE.scratch.bibtex.engine then + SILE.call("bibliographystyle", { lang = "en-US", style = "chicago-author-date" }) + end + local engine = SILE.scratch.bibtex.engine + if not options.key then + options.key = SU.ast.contentToString(content) + end + local entry = resolveEntry(SILE.scratch.bibtex.bib, options.key) + if not entry then + return + end + + -- Keep track of cited entries + table.insert(SILE.scratch.bibtex.cited.keys, options.key) + local citnum = #SILE.scratch.bibtex.cited.keys + SILE.scratch.bibtex.cited.citnums[options.key] = citnum + + local csljson = bib2csl(entry, citnum) + csljson.locator = locator + local cite = engine:cite(csljson) + + SILE.processString(("%s"):format(cite), "xml") + end) + + self:registerCommand("csl:reference", function (options, content) + if not SILE.scratch.bibtex.engine then + SILE.call("bibliographystyle", { lang = "en-US", style = "chicago-author-date" }) + end + local engine = SILE.scratch.bibtex.engine + if not options.key then + options.key = SU.ast.contentToString(content) + end + local entry = resolveEntry(SILE.scratch.bibtex.bib, options.key) + if not entry then + return + end + + local citnum = SILE.scratch.bibtex.cited.citnums[options.key] + if not citnum then + SU.warn("Reference to a non-cited entry " .. options.key) + -- Make it cited + table.insert(SILE.scratch.bibtex.cited.keys, options.key) + citnum = #SILE.scratch.bibtex.cited.keys + SILE.scratch.bibtex.cited.citnums[options.key] = citnum + end + local cslentry = bib2csl(entry, citnum) + local cite = engine:reference(cslentry) + + SILE.processString(("%s"):format(cite), "xml") + end) + + self:registerCommand("printbibliography", function (options, _) + if not SILE.scratch.bibtex.engine then + SILE.call("bibliographystyle", { lang = "en-US", style = "chicago-author-date" }) + end + local engine = SILE.scratch.bibtex.engine + + local bib + if SU.boolean(options.cited, true) then + bib = {} + for _, key in ipairs(SILE.scratch.bibtex.cited.keys) do + bib[key] = SILE.scratch.bibtex.bib[key] + end + else + bib = SILE.scratch.bibtex.bib + end + + local entries = {} + local ncites = #SILE.scratch.bibtex.cited.keys + for key, entry in pairs(bib) do + if entry.type ~= "xdata" then + crossrefAndXDataResolve(bib, entry) + if entry then + local citnum = SILE.scratch.bibtex.cited.citnums[key] + if not citnum then + -- This is just to make happy CSL styles that require a citation number + -- However, table order is not guaranteed in Lua so the output may be + -- inconsistent across runs with styles that use this number for sorting. + -- This may only happen for non-cited entries in the bibliography, and it + -- would be a bad practive to use such a style to print the full bibliography, + -- so I don't see a strong need to fix this at the expense of performance. + -- (and we can't really, some styles might have several sorting criteria + -- leading to impredictable order anyway). + ncites = ncites + 1 + citnum = ncites + end + local cslentry = bib2csl(entry, citnum) + table.insert(entries, cslentry) + end + end + end + print("") + local cite = engine:reference(entries) + SILE.processString(("%s"):format(cite), "xml") + + SILE.scratch.bibtex.cited = { keys = {}, citnums = {} } + end) end package.documentation = [[ \begin{document} BibTeX is a citation management system. It was originally designed for TeX but has since been integrated into a variety of situations. +This experimental package allows SILE to read and process Bib(La)TeX \code{.bib} files and output citations and full text references. -This experimental package allows SILE to read and process BibTeX \code{.bib} files and output citations and full text references. -(It doesn’t currently produce full bibliography listings.) +\smallskip +\noindent +\em{Loading a bibliography} +\novbreak + +\indent +To load a BibTeX file, issue the command \autodoc:command{\loadbibliography[file=]}. +You can load multiple files, and the entries will be merged into a single bibliography database. + +\smallskip +\noindent +\em{Producing citations and references (legacy commands)} +\novbreak -To load a BibTeX file, issue the command \autodoc:command{\loadbibliography[file=]} +\indent +The “legacy” implementation is based on a custom rendering system. +The plan is to eventually deprecate it in favor of the CSL implementation. To produce an inline citation, call \autodoc:command{\cite{}}, which will typeset something like “Jones 1982”. If you want to cite a particular page number, use \autodoc:command{\cite[page=22]{}}. -To produce a full reference, use \autodoc:command{\reference{}}. +To produce a bibliographic reference, use \autodoc:command{\reference{}}. + +The \autodoc:setting[check=false]{bibtex.style} setting controls the style of the bibliography. +It currently defaults to \code{chicago}, the only style supported out of the box. +It can however be set to \code{csl} to enforce the use of the CSL implementation on the above commands. + +This implementation doesn’t currently produce full bibliography listings. +(Actually, you can use the \autodoc:command{\printbibliography} introduced below, but then it always uses the CSL implementation for rendering the bibliography, differing from the output of the \autodoc:command{\reference} command.) + +\smallskip +\noindent +\em{Producing citations and references (CSL implementation)} +\novbreak + +\indent +While an experimental work-in-progress, the CSL (Citation Style Language) implementation is more powerful and flexible than the legacy commands. + +You should first invoke \autodoc:command{\bibliographystyle[style=