Skip to content

Commit f392bc9

Browse files
author
Sunrisepeak
committed
compat.sheenbidi 3.0.0: SheenBidi, the Unicode bidirectional algorithm (Apache-2.0)
SheenBidi 3.0.0 implements UAX #9 for Unicode 17.0.0 in C with no dependencies, and passes every case of that version's BidiTest.txt and BidiCharacterTest.txt (upstream's AlgorithmTests, run against this build). It is the permissively licensed alternative to compat.fribidi (LGPL-2.1-or-later), so a program can link it statically. Built from the tag archive through upstream's unity source (Source/SheenBidi.c under SB_CONFIG_UNITY), a static library exporting only the 51 SB* entry points of the public headers. Consumers see Headers/ only; Source/, where the internal headers live, is a private include directory of each platform block. The experimental text API stays off, as upstream's default builds have it. GLOBAL is the GitHub tag archive, CN the gitcode mcpp-res mirror, byte-identical; autoupdate follows the v${version} tags. The member tests/examples/sheenbidi runs on all three platforms: the version, bidi types, a Unicode 17.0 letter (Sidetic), general category, mirroring, scripts, paragraph base levels, a line's visual runs and levels, the mirror locator, and UTF-8 / UTF-16 / UTF-32 input.
1 parent d9b6bbb commit f392bc9

5 files changed

Lines changed: 346 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99

1010
### Added
1111

12+
- **compat.sheenbidi 3.0.0(新包)。** SheenBidi:Unicode 双向算法(UAX #9)的 C 实现,Apache-2.0,无依赖;
13+
3.0.0 实现 Unicode 17.0.0,通过该版本的 BidiTest.txt 与 BidiCharacterTest.txt 全部用例。以上游的单一编译单元
14+
`Source/SheenBidi.c`(`SB_CONFIG_UNITY`)从源码构建为静态库,只导出公共头文件的 51 个 `SB*` 入口;使用者的包含路径
15+
只有 `Headers`(`#include <SheenBidi/SheenBidi.h>`),内部头文件所在的 `Source` 是私有包含目录。未定义
16+
`SB_CONFIG_EXPERIMENTAL_TEXT_API`(上游默认,实验性的 SBText 接口不编译)。是 compat.fribidi(LGPL-2.1-or-later)
17+
的宽松许可替代,可静态链接。GLOBAL 为 GitHub 标签归档,CN 为 GitCode `mcpp-res` 镜像,sha256 两处一致;
18+
声明 `extensions.autoupdate`(`v${version}` 标签)。新增测试成员 `tests/examples/sheenbidi`(三平台)。
1219
- **openlifa.openmui 0.3.0。** OpenLifa 标准 openmui 的规范:registry、C ABI(`include/openmui.h`,
1320
在使用者的包含路径上)和声明其值类型的 C++ 模块 `openmui.spec`(语义树节点及其一致性检查、actor、
1421
信封、Core 组件契约)。actor、效果、授权与信封采用 openas 0.2。只依赖标准库(头文件为 C99);代码

‎mcpp.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ members = [
131131
"tests/examples/safeint-c-api",
132132
"tests/examples/sdl2",
133133
"tests/examples/sdl3",
134+
"tests/examples/sheenbidi",
134135
"tests/examples/spdlog",
135136
"tests/examples/fontconfig",
136137
"tests/examples/freetype",

‎pkgs/c/compat.sheenbidi.lua‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
-- compat.sheenbidi — the Unicode bidirectional algorithm (UAX #9), Apache-2.0.
2+
--
3+
-- SheenBidi is a C implementation of UAX #9 with no dependencies: paragraphs,
4+
-- lines and their visual runs, mirroring, bracket pairs, and the script and
5+
-- general category lookups a shaper needs beside them. 3.0.0 implements
6+
-- Unicode 17.0.0 and passes all of Unicode's BidiTest.txt and
7+
-- BidiCharacterTest.txt for that version (upstream's AlgorithmTests).
8+
--
9+
-- It is the permissively licensed alternative to compat.fribidi
10+
-- (LGPL-2.1-or-later): a program can link it statically without the LGPL's
11+
-- relinking terms.
12+
--
13+
-- ─────────────────────────────────────────────────────────────────────────
14+
-- NOTHING TO GENERATE, NOTHING TO CONFIGURE
15+
--
16+
-- The Unicode tables (Source/Data/*Lookup.c) are generated upstream and
17+
-- committed, and SBConfig.h is a header of commented-out switches whose
18+
-- defaults are this build. So the package is the release tarball and one
19+
-- source file:
20+
--
21+
-- Source/SheenBidi.c upstream's unity build (meson's default,
22+
-- `unity_mode=enabled`): under SB_CONFIG_UNITY it
23+
-- #includes every other .c and makes the internal
24+
-- functions static, so the library exports only the
25+
-- SB* entry points of the public headers.
26+
--
27+
-- SB_CONFIG_EXPERIMENTAL_TEXT_API is not defined, as upstream's default
28+
-- builds do not define it: the SBText / attribute / run-iterator API is
29+
-- declared experimental, and its headers compile to nothing without it. The
30+
-- algorithm API (SBAlgorithm, SBParagraph, SBLine, SBMirrorLocator,
31+
-- SBScriptLocator, SBCodepoint, SBAllocator) is all there.
32+
--
33+
-- ─────────────────────────────────────────────────────────────────────────
34+
-- THE INTERNAL HEADERS ARE PRIVATE
35+
--
36+
-- The sources include their internal headers by Source-relative names
37+
-- (<API/SBBase.h>, <Core/List.h>, <UBA/BidiChain.h>), so `*/Source` has to be
38+
-- on this package's include path. It is in each PLATFORM block as both
39+
-- `include_dirs` and `private_include_dirs` — the shape compat.eui-neo and
40+
-- gnome.glib measured: the -I reaches this package's own translation units
41+
-- and not its consumers', whose include path gets only `*/Headers` (the
42+
-- public <SheenBidi/…> headers). A base-level `private_include_dirs` is
43+
-- inert, which is why it is not written once.
44+
--
45+
-- kind = "lib": a static library. On Windows SB_PUBLIC is empty unless
46+
-- SB_CONFIG_DLL_EXPORT / SB_CONFIG_DLL_IMPORT is defined, and neither is, so
47+
-- the consumer's declarations match without any define of its own.
48+
package = {
49+
spec = "1",
50+
namespace = "compat",
51+
name = "sheenbidi",
52+
description = "SheenBidi 3.0.0 — the Unicode bidirectional algorithm (UAX #9, Unicode 17.0.0) in C, Apache-2.0, no dependencies",
53+
licenses = {"Apache-2.0"},
54+
repo = "https://github.com/Tehreer/SheenBidi",
55+
type = "package",
56+
57+
extensions = {
58+
autoupdate = {
59+
tag = "v${version}",
60+
url = "https://github.com/Tehreer/SheenBidi/archive/refs/tags/${tag}.tar.gz",
61+
},
62+
},
63+
64+
-- One source tarball for every platform: portable C11 whose platform
65+
-- choices (atomics, thread-local storage) are made by the compiler's own
66+
-- predefined macros.
67+
xpm = {
68+
linux = {
69+
["3.0.0"] = {
70+
url = {
71+
GLOBAL = "https://github.com/Tehreer/SheenBidi/archive/refs/tags/v3.0.0.tar.gz",
72+
CN = "https://gitcode.com/mcpp-res/sheenbidi/releases/download/3.0.0/sheenbidi-3.0.0.tar.gz",
73+
},
74+
sha256 = "86c56014034739ba39a24c23eb00323b0bf6f737354f665786015fca842af786",
75+
},
76+
},
77+
macosx = {
78+
["3.0.0"] = {
79+
url = {
80+
GLOBAL = "https://github.com/Tehreer/SheenBidi/archive/refs/tags/v3.0.0.tar.gz",
81+
CN = "https://gitcode.com/mcpp-res/sheenbidi/releases/download/3.0.0/sheenbidi-3.0.0.tar.gz",
82+
},
83+
sha256 = "86c56014034739ba39a24c23eb00323b0bf6f737354f665786015fca842af786",
84+
},
85+
},
86+
windows = {
87+
["3.0.0"] = {
88+
url = {
89+
GLOBAL = "https://github.com/Tehreer/SheenBidi/archive/refs/tags/v3.0.0.tar.gz",
90+
CN = "https://gitcode.com/mcpp-res/sheenbidi/releases/download/3.0.0/sheenbidi-3.0.0.tar.gz",
91+
},
92+
sha256 = "86c56014034739ba39a24c23eb00323b0bf6f737354f665786015fca842af786",
93+
},
94+
},
95+
},
96+
97+
mcpp = {
98+
language = "c++23",
99+
import_std = false,
100+
-- C11: upstream's atomics and thread-local storage take their C11
101+
-- paths (<stdatomic.h>, _Thread_local) on every compiler here.
102+
c_standard = "c11",
103+
104+
-- The public headers, included as <SheenBidi/SheenBidi.h>. They carry
105+
-- their own extern "C" (SB_EXTERN_C_BEGIN), so a C++ consumer includes
106+
-- them as they are.
107+
include_dirs = { "*/Headers" },
108+
109+
sources = { "*/Source/SheenBidi.c" },
110+
cflags = { "-DSB_CONFIG_UNITY" },
111+
112+
linux = {
113+
include_dirs = { "*/Source" },
114+
private_include_dirs = { "*/Source" },
115+
-- Linked into shared objects by its consumers: position independent.
116+
cflags = { "-fPIC" },
117+
},
118+
macosx = {
119+
include_dirs = { "*/Source" },
120+
private_include_dirs = { "*/Source" },
121+
cflags = { "-fPIC" },
122+
},
123+
-- No -fPIC: PE/COFF has no such distinction, and clang rejects it for
124+
-- the MSVC target.
125+
windows = {
126+
include_dirs = { "*/Source" },
127+
private_include_dirs = { "*/Source" },
128+
},
129+
130+
targets = { sheenbidi = { kind = "lib" } },
131+
deps = {},
132+
},
133+
}

‎tests/examples/sheenbidi/mcpp.toml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# compat.sheenbidi — the Unicode bidirectional algorithm (Apache-2.0), on text
2+
# that needs it, on every platform the descriptor declares.
3+
[package]
4+
name = "sheenbidi-tests"
5+
version = "0.1.0"
6+
standard = "c++23"
7+
8+
[dependencies]
9+
compat.sheenbidi = "3.0.0"
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// compat.sheenbidi — the Unicode bidirectional algorithm, on text that needs it.
2+
//
3+
// WHAT THIS ASSERTS AND WHY
4+
//
5+
// The Unicode tables are most of the package: Source/Data/*Lookup.c encode the
6+
// Unicode Character Database, and a build that compiled none of them would not
7+
// link — but one that compiled stale ones would, and would quietly get new
8+
// scripts wrong. So every check asks the library a question whose answer is in
9+
// a table or in the algorithm over it:
10+
//
11+
// the version SBVersionGetString, the linked library's own string
12+
// bidi types BidiTypeLookup.c — L, R, AL, EN, AN
13+
// Unicode 17.0.0 a Sidetic letter (new in 17.0) is R and Sidetic
14+
// general category GeneralCategoryLookup.c
15+
// mirroring, brackets PairingLookup.c — "(" mirrors to ")"
16+
// scripts ScriptLookup.c, the script locator over mixed text
17+
// paragraphs the base level from the first strong character
18+
// lines and runs UAX #9 itself: runs in visual order, with levels
19+
// the mirror locator which characters of a line are drawn mirrored
20+
// UTF-8 and UTF-16 the same answer whatever the code units
21+
//
22+
// A pass means the tarball's committed tables were compiled, through the unity
23+
// source — the claim that lets this be a descriptor with nothing generated.
24+
25+
#include <SheenBidi/SheenBidi.h>
26+
27+
#include <cstdint>
28+
#include <cstdio>
29+
#include <cstring>
30+
#include <vector>
31+
32+
namespace {
33+
34+
int failures = 0;
35+
36+
void check(bool ok, const char *what)
37+
{
38+
std::printf("%-62s %s\n", what, ok ? "ok" : "FAILED");
39+
if (!ok) {
40+
++failures;
41+
}
42+
}
43+
44+
SBCodepointSequence utf32(const std::vector<SBUInt32> &text)
45+
{
46+
return SBCodepointSequence{SBStringEncodingUTF32, text.data(), text.size()};
47+
}
48+
49+
} // namespace
50+
51+
int main()
52+
{
53+
// ── 1. the version ────────────────────────────────────────────────────
54+
std::printf(" SheenBidi %s\n", SBVersionGetString());
55+
check(std::strcmp(SBVersionGetString(), "3.0.0") == 0,
56+
"SBVersionGetString is 3.0.0, as the descriptor is");
57+
check(std::strcmp(SBVersionGetString(), SHEENBIDI_VERSION_STRING) == 0,
58+
"...and agrees with the header it was compiled against");
59+
60+
// ── 2. bidi types — BidiTypeLookup.c ──────────────────────────────────
61+
check(SBCodepointGetBidiType('a') == SBBidiTypeL, "'a' is L");
62+
check(SBCodepointGetBidiType(0x05D0) == SBBidiTypeR, "U+05D0 HEBREW ALEF is R");
63+
check(SBCodepointGetBidiType(0x0627) == SBBidiTypeAL, "U+0627 ARABIC ALEF is AL");
64+
check(SBCodepointGetBidiType('1') == SBBidiTypeEN, "'1' is EN");
65+
check(SBCodepointGetBidiType(0x0661) == SBBidiTypeAN, "U+0661 ARABIC-INDIC ONE is AN");
66+
check(SBBidiTypeIsStrong(SBCodepointGetBidiType(0x05D0)), "SBBidiTypeIsStrong(R)");
67+
68+
// ── 3. Unicode 17.0.0 — the tables are this release's ─────────────────
69+
// U+10940 SIDETIC LETTER N01 was added in Unicode 17.0: right-to-left,
70+
// script Sidetic. Tables older than 17.0 answer L (unassigned) and Zzzz.
71+
check(SBCodepointGetBidiType(0x10940) == SBBidiTypeR,
72+
"U+10940 SIDETIC LETTER N01 (Unicode 17.0) is R");
73+
check(SBCodepointGetScript(0x10940) == SBScriptSIDT, "...and its script is Sidetic");
74+
75+
// ── 4. general category, mirroring, scripts ───────────────────────────
76+
check(SBCodepointGetGeneralCategory('A') == SBGeneralCategoryLU, "'A' is Lu");
77+
check(SBCodepointGetGeneralCategory(0x0627) == SBGeneralCategoryLO, "U+0627 is Lo");
78+
check(SBCodepointGetMirror('(') == ')', "'(' mirrors to ')'");
79+
check(SBCodepointGetMirror('a') == 0, "'a' has no mirror (0, not itself)");
80+
check(SBCodepointGetScript(0x0627) == SBScriptARAB, "U+0627 is Arabic script");
81+
check(SBScriptGetUnicodeTag(SBScriptHEBR) == 0x48656272u /* 'Hebr' */,
82+
"the ISO 15924 tag of Hebrew is 'Hebr'");
83+
84+
// ── 5. paragraphs: the base level from the first strong character ─────
85+
{
86+
const std::vector<SBUInt32> hebrew_first = {0x05D0, ' ', 'A'};
87+
const std::vector<SBUInt32> latin_first = {'A', ' ', 0x05D0};
88+
for (int i = 0; i < 2; ++i) {
89+
const auto seq = utf32(i == 0 ? hebrew_first : latin_first);
90+
SBAlgorithmRef algorithm = SBAlgorithmCreate(&seq);
91+
SBParagraphRef paragraph =
92+
SBAlgorithmCreateParagraph(algorithm, 0, seq.stringLength, SBLevelDefaultLTR);
93+
const SBLevel base = SBParagraphGetBaseLevel(paragraph);
94+
check(i == 0 ? base == 1 : base == 0,
95+
i == 0 ? "a Hebrew-initial paragraph has base level 1"
96+
: "...and a Latin-initial one base level 0");
97+
SBParagraphRelease(paragraph);
98+
SBAlgorithmRelease(algorithm);
99+
}
100+
}
101+
102+
// ── 6. lines and runs: UAX #9 itself ──────────────────────────────────
103+
// "abc אבג" in a right-to-left paragraph. The Latin run is at level 2, the
104+
// space and the Hebrew at level 1. A line's runs come in VISUAL order, left
105+
// to right: the space and Hebrew (offset 3, length 4, level 1) first, then
106+
// "abc" (offset 0, length 3, level 2).
107+
{
108+
const std::vector<SBUInt32> text = {'a', 'b', 'c', ' ', 0x05D0, 0x05D1, 0x05D2};
109+
const auto seq = utf32(text);
110+
SBAlgorithmRef algorithm = SBAlgorithmCreate(&seq);
111+
SBParagraphRef paragraph = SBAlgorithmCreateParagraph(algorithm, 0, text.size(), 1);
112+
check(SBParagraphGetLength(paragraph) == text.size(), "the paragraph covers the text");
113+
114+
const SBLevel *levels = SBParagraphGetLevelsPtr(paragraph);
115+
check(levels[0] == 2 && levels[2] == 2 && levels[3] == 1 && levels[4] == 1,
116+
"levels: Latin 2, space 1, Hebrew 1");
117+
118+
SBLineRef line = SBParagraphCreateLine(paragraph, 0, text.size());
119+
const SBUInteger count = SBLineGetRunCount(line);
120+
const SBRun *runs = SBLineGetRunsPtr(line);
121+
std::printf(" runs:");
122+
for (SBUInteger r = 0; r < count; ++r) {
123+
std::printf(" {%zu,%zu,L%u}", static_cast<size_t>(runs[r].offset),
124+
static_cast<size_t>(runs[r].length), static_cast<unsigned>(runs[r].level));
125+
}
126+
std::printf("\n");
127+
check(count == 2, "the line has two runs");
128+
check(count == 2 && runs[0].offset == 3 && runs[0].length == 4 && runs[0].level == 1,
129+
"the first visual run is the space and the Hebrew, at level 1");
130+
check(count == 2 && runs[1].offset == 0 && runs[1].length == 3 && runs[1].level == 2,
131+
"...and the second is \"abc\", at level 2");
132+
133+
SBLineRelease(line);
134+
SBParagraphRelease(paragraph);
135+
SBAlgorithmRelease(algorithm);
136+
}
137+
138+
// ── 7. the mirror locator — what a line draws mirrored ────────────────
139+
// "אב(ג)" right to left: both parentheses resolve to level 1, so both are
140+
// drawn as their mirrors.
141+
{
142+
const std::vector<SBUInt32> text = {0x05D0, 0x05D1, '(', 0x05D2, ')'};
143+
const auto seq = utf32(text);
144+
SBAlgorithmRef algorithm = SBAlgorithmCreate(&seq);
145+
SBParagraphRef paragraph =
146+
SBAlgorithmCreateParagraph(algorithm, 0, text.size(), SBLevelDefaultLTR);
147+
SBLineRef line = SBParagraphCreateLine(paragraph, 0, text.size());
148+
SBMirrorLocatorRef locator = SBMirrorLocatorCreate();
149+
SBMirrorLocatorLoadLine(locator, line, text.data());
150+
std::vector<SBMirrorAgent> agents;
151+
while (SBMirrorLocatorMoveNext(locator)) {
152+
agents.push_back(*SBMirrorLocatorGetAgent(locator));
153+
}
154+
check(agents.size() == 2, "two characters are drawn mirrored");
155+
check(agents.size() == 2 && agents[0].index == 2 && agents[0].mirror == ')' &&
156+
agents[1].index == 4 && agents[1].mirror == '(',
157+
"...'(' at 2 as ')' and ')' at 4 as '('");
158+
SBMirrorLocatorRelease(locator);
159+
SBLineRelease(line);
160+
SBParagraphRelease(paragraph);
161+
SBAlgorithmRelease(algorithm);
162+
}
163+
164+
// ── 8. scripts over mixed text, from UTF-8 ────────────────────────────
165+
// "abc عربي": Latin, then (the space joins the run before it) Arabic.
166+
{
167+
const char *utf8 = "abc \xD8\xB9\xD8\xB1\xD8\xA8\xD9\x8A";
168+
const SBCodepointSequence seq{SBStringEncodingUTF8, utf8, std::strlen(utf8)};
169+
SBScriptLocatorRef locator = SBScriptLocatorCreate();
170+
SBScriptLocatorLoadCodepoints(locator, &seq);
171+
std::vector<SBScriptAgent> runs;
172+
while (SBScriptLocatorMoveNext(locator)) {
173+
runs.push_back(*SBScriptLocatorGetAgent(locator));
174+
}
175+
check(runs.size() == 2 && runs[0].script == SBScriptLATN && runs[0].offset == 0 &&
176+
runs[1].script == SBScriptARAB && runs[1].offset + runs[1].length == seq.stringLength,
177+
"script runs over UTF-8: Latin, then Arabic to the end");
178+
SBScriptLocatorRelease(locator);
179+
}
180+
181+
// ── 9. the same paragraph from UTF-16 code units ──────────────────────
182+
{
183+
const SBUInt16 utf16[] = {'a', ' ', 0x05D0, 0x05D1};
184+
const SBCodepointSequence seq{SBStringEncodingUTF16, utf16, 4};
185+
SBAlgorithmRef algorithm = SBAlgorithmCreate(&seq);
186+
SBParagraphRef paragraph = SBAlgorithmCreateParagraph(algorithm, 0, 4, SBLevelDefaultRTL);
187+
const SBLevel *levels = SBParagraphGetLevelsPtr(paragraph);
188+
check(SBParagraphGetBaseLevel(paragraph) == 0 && levels[0] == 0 && levels[2] == 1,
189+
"UTF-16: base 0 (first strong is 'a'), Hebrew at level 1");
190+
SBParagraphRelease(paragraph);
191+
SBAlgorithmRelease(algorithm);
192+
}
193+
194+
std::printf("\n%d check(s) failed\n", failures);
195+
return failures == 0 ? 0 : 1;
196+
}

0 commit comments

Comments
 (0)