Skip to content

Generate the cursive joining tables from Unicode, so a letter beside a recently added character joins (#251) - #261

Open
jakejackson1 wants to merge 3 commits into
gravitypdffrom
fix/251-generated-joining-tables
Open

jakejackson1 wants to merge 3 commits into
gravitypdffrom
fix/251-generated-joining-tables

Conversation

@jakejackson1

@jakejackson1 jakejackson1 commented Sep 18, 2026

Copy link
Copy Markdown
Member

Shaper\Arabic::$leftJoining and ::$rightJoining stopped at the blocks Unicode had when they were
written, so a character added since was in neither. A character in neither table joins nothing and
unjoins its neighbour
: the letter before it is drawn with no form where HarfBuzz gives it init or
medi.

Both tables are now generated from Unicode's ArabicShaping.txt, the way Ucdn::$ot_languages is
generated from HarfBuzz's table. Hand-extending was the alternative; the lists had already lagged twice,
and generating them also turned up a data error nobody was looking for (U+0847, below).

Scope: five blocks, not the four the issue lists

77 characters change, every one of them in a script resolveJoining() is actually called for —
arab, syrc, nko and mand. $leftJoining 218 → 262, $rightJoining 300 → 375.

block characters
Mandaic U+0840–085F 5
Syriac Supplement U+0860–086F 9
Arabic Extended-B U+0870–089F 30
Arabic Extended-A U+08A0–08FF 28
Arabic Extended-C U+10EC0–10EFF 5 — not in the issue's table

Arabic Extended-C belongs by the same test as the others: Ucdn::get_script(0x10EC2) is
Ucdn::SCRIPT_ARABIC, so those characters reach the Arabic shaper exactly as U+0870 does. Measured,
beh + U+10EC2 drew a formless beh before and draws init after, identically to beh + U+0870.

Nothing from Mongolian, Phags-pa, Adlam, Manichaean, Hanifi Rohingya, Sogdian, Old Uyghur, Psalter
Pahlavi or Chorasmian. resolveJoining() is never called for those, and whether mPDF claims them is a
separate question.

One correction rather than an addition

U+0847 MANDAIC IT is Joining_Type=R, but it was in both tables, so it drew the following letter
as though that letter joined backwards over it. It is right-joining only now. No Mandaic font is in the
corpus, so the oracle reads it with an Arabic letter — the joining type is the character's own:

$ hb-shape --font-file=packages/Middle-East-Scripts-Bundle/fonts/LateefRegOT.ttf \
    --no-clusters --no-positions --unicodes=0847,0628
[uni0628|.notdef]
$ ... --unicodes=0645,0628
[uni0628.fina|uni0645.init]

Measured, before and after

                    before             after
beh+meem         U+E16E U+E0C5      U+E16E U+E0C5     the reference: init, fina
beh+0870         U+0628 U+0870      U+E16E U+0870     Arabic Extended-B
beh+08B3         U+0628 U+08B3      U+E16E U+08B3     Arabic Extended-A
beh+10EC2        U+0628 U+10EC2     U+E16E U+10EC2    Arabic Extended-C
beth+alaph+074F  U+E008 U+E004 …    U+E008 U+E004 …   the reference: med2
beth+alaph+0860  U+E008 U+E005 …    U+E008 U+E004 …   the #244 consequence
$ hb-shape --font-file=packages/Middle-East-Scripts-Bundle/fonts/LateefRegOT.ttf \
    --no-clusters --no-positions --unicodes=0628,0870
[.notdef|uni0628.init]
$ ... --unicodes=0628,08B3
[.notdef|uni0628.init]
$ ... --unicodes=0628,10EC2
[.notdef|uni0628.init]
$ hb-shape --font-file=packages/Middle-East-Scripts-Bundle/fonts/Estrangelo-Edessa.otf \
    --no-clusters --no-positions --unicodes=0712,0710,0860
[.notdef|U0710Medi2|U0712Init]
$ ... --unicodes=0712,0710,074F
[.notdef|U0710Medi2|U0712Init]

The last pair is why this composes with #244. That change made the Alaph read its form from joining
types instead of a hard-coded Syriac block range; this makes the types complete. E005 is Estrangelo
Edessa's fina Alaph and E004 its med2, so beth + alaph + U+0860 is only correct with both
changes in. U+0860 is MALAYALAM NGA; D, dual-joining, so it must join back over the Alaph exactly as
the Sogdian U+074F does.

Mandaic is reached at the Arabic::resolveJoining() / shape() seam with script tag mand, not
through a rendered page — no font in the corpus draws it.

The generator

tests/Mpdf/ArabicJoining.php + utils/arabicjoining_update.php + composer arabicjoining:update,
following UcdnTables and OtLanguageTags in every respect: Mpdf\ under autoload-dev,
GeneratedTable, a DEFAULT_VERSION, a rewritten // UNIDATA_VERSION marker, a utils/data/ucd/<version>
cache, and a round-trip test asserting the checked-in tables are what the generator writes.

Unicode 17.0.0, as const DEFAULT_VERSION = UcdnTables::DEFAULT_VERSION, so the repo names its
Unicode release in one place. Unicode 18 would add 22 more Arabic Extended-C characters; which Unicode
mPDF claims is a decision to make alongside ucdn:update, not one to slip in here.
composer arabicjoining:update 18.0.0 is all it takes later.

Source data is fetched, not checked in, as both existing generators do. One consequence worth
knowing: the round-trip test skips where the UCD tree is not unpacked, so CI reports 4 skipped where
it reported 3
(measured against PR #262, which adds no generator test and reports 3). It does not skip
locally, where its one cached file is present, so the local count stays at 2.

The arrays are one ascending sequence now rather than hand-grouped by script — the generator has no
block names, and sorted order is what keeps a future diff readable. Formatting stays at 8 entries a line.

ArabicJoining::scripts() restated Otl::selectShaper()'s decision about which scripts reach this
shaper, so it is held against it by a reflection test that goes red if a fifth script joins that branch.
The // X_VERSION rewrite was a third copy of one preg_replace, so it moved to
GeneratedTable::replaceVersion() and gained the missing-marker check replaceArray() already had;
that touches tests/Mpdf/UcdnTables.php and tests/Mpdf/OtLanguageTags.php by one line each, and both
round-trip tests still pass.

Verified

tests/Mpdf/Shaper/JoiningTableCoverageTest.php, 15 tests, 14 fail against the previous
src/Shaper/Arabic.php. The one that passes is [ATT, 084F], because U+084F was already in
$rightJoining so that direction already worked; its companion [084F, ATT] fails.

1) …testALetterBesideARecentlyAddedCharacterTakesTheFormItJoinsIn
   with data set "Arabic Extended-B after a Beh" ('arab', array('00628', '00870'), 0, array('B_INIT', 2))
-    0 => 'B_INIT'        -    1 => 2
+    0 => 'B_ISOL'        +    1 => 0
10) …testALetterAfterARightJoiningMandaicLetterStandsAlone
-    0 => 'A_ISOL'        -    1 => 0
+    0 => 'A_FINA'        +    1 => 1
FAILURES!  Tests: 15, Assertions: 15, Failures: 14.

No master or fixture moved. tests/Snapshots/JoiningTablesSnapshotTest.php exercises U+0712,
U+0723, U+074F, U+0628, U+0627 and U+08AD, and all six come out identically — U+08AD is
Joining_Type=U, absent from both tables before and after.

composer test                                        2532 tests, 6703 assertions, 2 skipped, OK
composer test -- --group=snapshot                    79 tests, 109 assertions, OK
composer cs                                          clean
phpstan --no-progress --memory-limit=2G              33 errors, all pre-existing

The third table in the same file, $transparent, has the identical gap and is not fixed here:
#259. It needs DerivedJoiningType.txt rather than ArabicShaping.txt, and a decision about the
five U+FC5E–FC62 presentation-form ligatures that table also carries.

Closes #251

🤖 Generated with Claude Code

jakejackson1 and others added 3 commits September 18, 2026 12:20
…a recently added character joins (#251)

Shaper\Arabic's two joining tables stopped at the blocks Unicode had when they
were last extended by hand. A character in neither table joins nothing and
unjoins its neighbour, so the letter before a recently added one was drawn with
no form where HarfBuzz gives it init or medi.

Both tables are now written by composer arabicjoining:update from
ArabicShaping.txt, beside a test that the checked-in tables are what the
generator writes - the pattern Ucdn::$ot_languages already uses. What is in
scope is read from Ucdn's script for each character, because the script is what
routes a run to this shaper, which is why the generator reads the same Unicode
version Ucdn's scripts were built from.

77 characters change: the 72 the issue measured over Mandaic, the Syriac
Supplement and Arabic Extended-A and -B, and five in Arabic Extended-C, which is
script Arabic and so resolved here for the same reason. U+0847 MANDAIC IT was the
one filed in the wrong table and is now right-joining only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eads them (#251)

The scope test read Ucdn's script and admitted Common and Inherited, on the
grounds that a character Ucdn has no script for reaches no shaper. It does:
Otl::analyseCharacters() will not start a run on Unknown any more than on those
two, so the character stays in the run before it and the Arabic shaper is handed
it. Measured through Lateef, a Beh after U+10EE8 CROWN FEH - Arabic and
left-joining as of Unicode 18, Unknown to Ucdn's Unicode 17 - is drawn with no
form until the table has it, which is this defect with a shorter fuse. Unknown is
now in scope with the other two, and the tables are no longer tied to Ucdn's
release to be correct.

The four scripts are Otl::selectShaper()'s decision rather than the generator's,
so a test holds the two lists together; a fifth script in that branch would
otherwise bring the defect back for it with nothing failing.

Also: the version line each generator rewrites was a third copy of one
preg_replace, so it moves to GeneratedTable and gains the missing-marker check
replaceArray() already had; the partition reads the three joining types it wants
rather than the two it does not; and U+0883 to U+0885 and U+07FA are Join_Causing
with scripts of their own, so the comment claiming Join_Causing means "no script"
is gone.

No table content moves: no character of Unicode 17 is Unknown to Ucdn 17.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working create-upstream-pr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shaper\Arabic's joining tables miss the 72 characters Unicode has added since they were written, so a letter beside one of them loses its form

1 participant