@gemboss/gem-framework 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ gem-framework của GemTheme đóng thành package, cho các repo **ngoài** mon
6
6
  | Cổng vào | Là gì | Nguồn |
7
7
  |---|---|---|
8
8
  | `@gemboss/gem-framework` | token, `createGemThemeCss`, runtime, manifest, bộ kiểm framework. Từ 0.3.4 manifest khai họ `gem-cart-line-item` (khối, `__media`, `__details`, `__title`, `__variant`, `__plan`, `__meta`, `__remove`, `__quantity`, `__price`): framework vốn style và adapter Shopify vốn render nó, nên section dùng markup dòng giỏ của framework không còn bị `unknown_gem_class` | `packages/gem-framework/src` |
9
- | `…/sections` | 4 hàm `headless.tsx` được import (`elementOverrides`, `imageSrcSet`, `subscriptionPlans`, `defaultPlan`) | `packages/gem-sections/src` |
9
+ | `…/sections` | Các hàm `headless.tsx` được import: `elementOverrides`, `imageSrcSet`, `subscriptionPlans`, `defaultPlan`; từ 0.15.0 thêm bộ đọc nhấn trong câu `inlineMarkRuns`, `phraseMarkRuns`, `markClassName`, `inlineMarkText`, `safeMarkHref`, `MARK_KINDS` | `packages/gem-sections/src` |
10
10
  | `…/section-gate` | `validateSectionArtifacts`: ĐÚNG bộ kiểm section gemtheme dùng khi duyệt. Từ 0.3.0 thêm ba cổng của thư viện cho MỘT section: `layoutKnobCheck` (+ `LAYOUT_ATTRS`), `slotContractDrift` (+ `slotsReadBy`, `slotSourceFromElementTree`), `cssPairingOrphans` (+ `classesStyledBy`, truyền CSS từ `createGemThemeCss`). Từ 0.3.2 thêm hai cổng pháp lý: `fabricatedProofInSource` / `fabricatedProofInSchema` (điểm, số lượt, người, lời hứa bịa) và `inventedTestimonyIn` (hằng DEMO_*_QUOTE/REVIEW… làm giá trị khi thiếu dữ liệu). Từ 0.3.3 hai cổng này đọc được tiếng Việt ("4,8/5", "đã bán", "miễn phí vận chuyển", "199k", cả chữ NFD), tra giá trị của `DEMO.<key>` ở mọi chỗ đọc nó (`??`, `\|\|`, toán tử ba ngôi, dòng bị Prettier ngắt), và bỏ qua số lượng hiển thị (`reviews_to_show`…). Nâng từ 0.3.2 lên có thể làm đỏ section trước đó xanh: đó là lời khẳng định nằm trong DEMO. Từ 0.3.5 bắt thêm chữ ký người Việt (tên kèm nơi chốn như "Chị Lan, Hà Nội", hoặc họ + Thị/Văn như "Nguyễn Thị Hoa"; tiêu đề viết hoa từng chữ thì không), điểm sao ("4,9 ★"), `rating: 4.9` / `reviewCount: "1.284"` trong mảng sản phẩm mẫu, và hằng demo ở nhánh else của toán tử ba ngôi (`bound ? [] : DEMO_REVIEWS`). Mỗi thứ bịa chỉ báo MỘT lần: tại dòng DEMO nếu dòng đó tự lộ, không thì tại chỗ đọc. "đồng hồ" không còn bị đọc là giá. Từ 0.3.6 bắt lời hứa có thời hạn đứng sau từ khoá ("Dùng thử 30 ngày", "Bảo hành 12 tháng", "Nhận trong 2–4 ngày", "Giao 2 giờ") và "hoàn tiền" không kèm số; "Chính sách hoàn tiền" vẫn là nhãn. Từ 0.3.7 câu hỏi (kết thúc bằng "?") không bao giờ là lời hứa, "không hợp lệ"/số bước/nhịp giao không bị báo, và bắt thêm "trong vòng N ngày", "Giao hàng 2–4 ngày", "Bảo hành 1 đổi 1…", "Try it free for 30 days", "Ships in 24h". Từ 0.3.8 xuất thêm `claimIn` (câu hỏi "một chuỗi này có phải lời khẳng định, người bịa, hay lời hứa?"). Từ 0.3.9 bắt "cancel anytime", "guaranteed delivery", "huỷ bất cứ lúc nào" (chính sách huỷ gói và giao hàng). Từ 0.3.10 lời từ chối ("cannot be canceled", "Không thể hủy") không bị báo, "giao dịch/giao diện" không phải giao hàng, "Delivery not guaranteed" là lời miễn trừ, một chuỗi chỉ gồm con số viết tắt ("199k", "12k+") mang nhãn trung tính "hardcoded figure" (không phân biệt được giá với số đếm), và `stringLiteralsIn(line)` thay cho hằng `STRING_LITERAL` (đã gỡ: một regex `/g` dùng chung rò `lastIndex`; 0 chỗ dùng ở gemboss/gemfactory/gemsite). Từ 0.3.11 "Huỷ bỏ lọc…" và "Hủy đơn, gọi chúng tôi bất cứ lúc nào" không bị đọc là chính sách huỷ; "huỷ đăng ký" là bỏ nhận tin (như "Unsubscribe anytime"); `SOURCE.json` ghi `gemtheme_dirty`, và `npm pack` từ chối khi ĐẦU VÀO của gói (`packages/`, gói này trừ `package.json`, `scripts/lib/`) có thay đổi chưa commit hoặc git không trả lời được. Từ 0.3.12 bắt lời hứa hoàn tiền không có chữ "hoàn tiền" liền nhau ("nhận lại toàn bộ tiền", "Hoàn lại 100% tiền", "Trả lại tiền nếu không hài lòng"); lời từ chối ("Không được hoàn lại tiền") và tiền thừa/thối khi thanh toán không bị bắt Từ 0.4.0: `demoOutsidePreview(tsx)` (chữ mẫu DEMO chỉ hiện trong bản xem trước, tức khi `section.demo === true`; plan xuất bản không bao giờ mang cờ đó) và schema `SectionSpec` khai trường `demo` | `packages/gem-sections/src/section-artifacts.ts`, `section-contract.ts` |
11
11
  | `…/settings-schema` | `validateSettingsSchema` | `scripts/lib/settings-schema-contract.mjs` |
12
12
  | `…/gem-framework.css`, `…/gem-runtime.js` | CSS token mặc định + runtime mọi plugin, dựng sẵn. `gem-runtime.js` tự chạy khi tải: đăng ký và lên lịch đủ 23 plugin | build |
@@ -93,6 +93,29 @@ Từ 0.8.2 mỗi module có `.d.ts` cạnh nó (`mount(root?: ParentNode | null,
93
93
 
94
94
  ## Nhật ký thay đổi
95
95
 
96
+ - **0.15.0** (11/10/2026): CHỈ THÊM, store đang chạy không đổi pixel nào (chưa trang nào mang các lớp mới). Gap #39: **nhấn
97
+ trong câu**. Chữ advertorial sống nhờ cách nhấn (kho GemFactory `research/advertorial-blocks-2026-10-10`: souleora 84 cụm đậm,
98
+ 5 nghiêng, 4 đổi màu, 1 gạch chân trong 152 đoạn; 28pilates chốt thẻ "đừng mua" bằng câu đỏ, tô màu nhấn cụm kết quả trong
99
+ lời khách; 29% store advertorial dẫn đi mua bằng link trong đoạn), mà element `text` / `heading` chỉ đậm được, và ba section
100
+ của kho mỗi cái chép một bộ đọc dấu riêng.
101
+ - `…/sections` (và `@gemtheme/gem-sections`): `inlineMarkRuns(chuỗi)` đọc `**đậm**`, `*nghiêng*`, `***đậm nghiêng***`,
102
+ `__gạch chân__`, `==tô sáng==`, `^^đậm màu nhấn^^`, `!!màu cảnh báo!!`, `[link](url)`, lồng được hai chiều (đậm trong
103
+ nghiêng, nghiêng trong đậm). Dấu chỉ mở / đóng sát chữ (không sát khoảng trắng), nên dấu câu thường giữ nguyên ("Hurry!!
104
+ Only 3 left!!", "^^", "a == b"); `__` không mở giữa từ (URL `a__b__c`); `\` giữ ký tự dấu nguyên văn; đọc từng dòng (dấu không
105
+ vắt qua xuống dòng), một dòng dài hơn 20.000 ký tự vẽ trơn; tuyến tính trên chuỗi xấu (dấu `[` lặp, đậm không đóng). `phraseMarkRuns(chữ,
106
+ { bold, italic, underline, highlight, accent, alert, links })` là cách của element: chữ vẫn trơn, mỗi dấu là danh sách
107
+ cụm từ (mỗi dòng một cụm, đặt ở chỗ xuất hiện ĐẦU TIÊN không chồng cụm cùng dấu, đúng luật `bold` đang dùng nên
108
+ `{ bold }` vẽ y như trước), link là dòng `cụm | url`. Cả hai trả về DỮ LIỆU (đoạn chữ + dấu + href), người gọi tự vẽ
109
+ `strong` / `em` / `span.gem-mark--…` / `a.gem-mark-link`: gói không kéo React, không chèn HTML; link chỉ giữ http(s),
110
+ đường dẫn trong site và #neo. `markClassName(marks)`, `inlineMarkText(chuỗi)` (chữ bỏ dấu), `safeMarkHref`.
111
+ - CSS (`GEM_INLINE_MARK_CSS`, trong `createPrimitiveCss`): `.gem-mark--underline`, `--highlight` (bút dạ: nền
112
+ `--gem-mark-highlight` #fff1a6, mực `--gem-mark-highlight-ink` #1f1d18, đọc được cả trên băng tối), `--accent`, `--alert`
113
+ (`--gem-color-danger`) trong `:where()` (độ ưu tiên 0: section muốn dáng riêng tự đè); `.gem-mark-link` (màu nhấn, gạch
114
+ chân) là một lớp thường, vì `a { color: inherit }` của framework và gemsite thắng mọi thứ trong `:where()`; link nằm
115
+ trong / chứa tô sáng giữ mực của bút dạ. Sáu lớp khai
116
+ trong manifest (nhóm content), nên section vẽ chúng không bị `unknown_gem_class`.
117
+ - Người dùng: gemboss phải thêm các tên mới vào requireShim của bản xem trước section; gemsite chép `inline-marks.ts`
118
+ byte-identical (cặp kiểm của `check-elements-copied-from-gemtheme`) rồi vẽ dấu cho element `text` / `heading`.
96
119
  - **0.14.0** (11/10/2026): CHỈ THÊM, store đang chạy không đổi pixel nào (không section nào của gemtheme, gemsite, gemboss
97
120
  đọc các tên dưới đây; 30 mục kho GemFactory tự khai thang dùng tên riêng theo mục). Hai gap của bộ interaction "Gọn"
98
121
  (Lily 08/10/2026) mà kho đang chép tay vào từng mục:
package/dist/SOURCE.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "package": "@gemboss/gem-framework",
3
- "version": "0.14.0",
4
- "gemtheme_sha": "75954020da59dcca83cdae5676c0a9746e40ba4b",
3
+ "version": "0.15.0",
4
+ "gemtheme_sha": "c69c7c6e8d2c2dcf1543170990f6a644805c4dd8",
5
5
  "gemtheme_dirty": false,
6
- "framework_version": "0.14.0"
6
+ "framework_version": "0.15.0"
7
7
  }
@@ -1123,6 +1123,14 @@ a {
1123
1123
  :where(img[data-gem-bound-img-in][data-gem-img-in-state='loading']) { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; }
1124
1124
  @media (prefers-reduced-motion: no-preference) { :where(img[data-gem-bound-img-in][data-gem-img-in-state='in']) { animation: gem-img-in var(--gem-motion-slow, 260ms) var(--gem-ease-enter, cubic-bezier(.33, 1, .68, 1)); } }
1125
1125
  @keyframes gem-img-in { from { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; } }
1126
+ :where(.gem-mark--underline) { text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }
1127
+ :where(.gem-mark--highlight) { background: var(--gem-mark-highlight, #fff1a6); color: var(--gem-mark-highlight-ink, #1f1d18); padding-inline: 0.12em; border-radius: 0.15em; -webkit-box-decoration-break: clone; box-decoration-break: clone; }
1128
+ :where(.gem-mark--accent) { color: var(--gem-color-accent); }
1129
+ :where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }
1130
+ :where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }
1131
+ .gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }
1132
+ .gem-mark-link:hover { text-decoration-thickness: 2px; }
1133
+ .gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }
1126
1134
  @keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }
1127
1135
  @keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
1128
1136
  @keyframes gem-marquee-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
package/dist/index.js CHANGED
@@ -280,7 +280,7 @@ function gemButtonGradientEndCss(accent = "var(--gem-color-accent)", accentText
280
280
  }
281
281
 
282
282
  // ../../packages/gem-framework/src/tokens.ts
283
- var gemFrameworkVersion = "0.14.0";
283
+ var gemFrameworkVersion = "0.15.0";
284
284
  var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
285
285
  var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
286
286
  var GEM_FOCUS_OFFSET_CSS_VAR = "--gem-focus-offset: 3px;";
@@ -941,7 +941,15 @@ var gemFrameworkPrimitiveGroups = [
941
941
  "gem-pull-quote",
942
942
  "gem-byline",
943
943
  "gem-disclaimer",
944
- "gem-subhead"
944
+ "gem-subhead",
945
+ // Nhấn trong câu (gap #39): lớp @gemtheme/gem-sections inline-marks.ts bảo người gọi vẽ, GEM_INLINE_MARK_CSS tô.
946
+ // Không khai thì section vẽ đúng dấu của framework vẫn bị gem_framework_unknown_gem_class (review #740).
947
+ "gem-mark",
948
+ "gem-mark--underline",
949
+ "gem-mark--highlight",
950
+ "gem-mark--accent",
951
+ "gem-mark--alert",
952
+ "gem-mark-link"
945
953
  ]
946
954
  },
947
955
  {
@@ -1583,6 +1591,19 @@ var GEM_IMG_IN_CSS_LINES = [
1583
1591
  "@keyframes gem-img-in { from { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; } }"
1584
1592
  ];
1585
1593
  var GEM_IMG_IN_CSS = GEM_IMG_IN_CSS_LINES.join("\n");
1594
+ var GEM_INLINE_MARK_CSS_LINES = [
1595
+ ":where(.gem-mark--underline) { text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }",
1596
+ ":where(.gem-mark--highlight) { background: var(--gem-mark-highlight, #fff1a6); color: var(--gem-mark-highlight-ink, #1f1d18); padding-inline: 0.12em; border-radius: 0.15em; -webkit-box-decoration-break: clone; box-decoration-break: clone; }",
1597
+ ":where(.gem-mark--accent) { color: var(--gem-color-accent); }",
1598
+ ":where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }",
1599
+ ":where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }",
1600
+ ".gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }",
1601
+ ".gem-mark-link:hover { text-decoration-thickness: 2px; }",
1602
+ // Bút dạ thắng màu link: chữ trên nền vàng giữ mực đậm, dù span nằm trong link hay link nằm trong span (review #740 đo màu nhấn
1603
+ // trên vàng 4,32:1 ở nền trắng, 2,47:1 trên băng tối, 1,05:1 trên băng màu nhấn).
1604
+ ".gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }"
1605
+ ];
1606
+ var GEM_INLINE_MARK_CSS = GEM_INLINE_MARK_CSS_LINES.join("\n");
1586
1607
  var GEM_AMBIENT_CSS_LINES = [
1587
1608
  "@keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }",
1588
1609
  "@keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }",
@@ -2755,6 +2776,7 @@ function createPrimitiveCss() {
2755
2776
  "}",
2756
2777
  ...GEM_REVEAL_CSS_LINES,
2757
2778
  ...GEM_IMG_IN_CSS_LINES,
2779
+ ...GEM_INLINE_MARK_CSS_LINES,
2758
2780
  ...GEM_AMBIENT_CSS_LINES,
2759
2781
  ...GEM_DIVIDER_CSS_LINES,
2760
2782
  ...GEM_CARD_CSS_LINES,
@@ -7412,6 +7434,7 @@ export {
7412
7434
  GEM_GRID_COLUMNS_ROW_KNOB_CSS,
7413
7435
  GEM_HEADING_SIZE_CSS_VARS,
7414
7436
  GEM_IMG_IN_CSS,
7437
+ GEM_INLINE_MARK_CSS,
7415
7438
  GEM_LINE_HEIGHT_TOKEN_KEYS,
7416
7439
  GEM_MOTION_REDUCED,
7417
7440
  GEM_MOTION_SCALE_DEFAULTS,
@@ -1032,6 +1032,14 @@ a {
1032
1032
  :where(img[data-gem-bound-img-in][data-gem-img-in-state='loading']) { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; }
1033
1033
  @media (prefers-reduced-motion: no-preference) { :where(img[data-gem-bound-img-in][data-gem-img-in-state='in']) { animation: gem-img-in var(--gem-motion-slow, 260ms) var(--gem-ease-enter, cubic-bezier(.33, 1, .68, 1)); } }
1034
1034
  @keyframes gem-img-in { from { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; } }
1035
+ :where(.gem-mark--underline) { text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }
1036
+ :where(.gem-mark--highlight) { background: var(--gem-mark-highlight, #fff1a6); color: var(--gem-mark-highlight-ink, #1f1d18); padding-inline: 0.12em; border-radius: 0.15em; -webkit-box-decoration-break: clone; box-decoration-break: clone; }
1037
+ :where(.gem-mark--accent) { color: var(--gem-color-accent); }
1038
+ :where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }
1039
+ :where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }
1040
+ .gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }
1041
+ .gem-mark-link:hover { text-decoration-thickness: 2px; }
1042
+ .gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }
1035
1043
  @keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }
1036
1044
  @keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
1037
1045
  @keyframes gem-marquee-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@@ -746,7 +746,15 @@ var gemFrameworkPrimitiveGroups = [
746
746
  "gem-pull-quote",
747
747
  "gem-byline",
748
748
  "gem-disclaimer",
749
- "gem-subhead"
749
+ "gem-subhead",
750
+ // Nhấn trong câu (gap #39): lớp @gemtheme/gem-sections inline-marks.ts bảo người gọi vẽ, GEM_INLINE_MARK_CSS tô.
751
+ // Không khai thì section vẽ đúng dấu của framework vẫn bị gem_framework_unknown_gem_class (review #740).
752
+ "gem-mark",
753
+ "gem-mark--underline",
754
+ "gem-mark--highlight",
755
+ "gem-mark--accent",
756
+ "gem-mark--alert",
757
+ "gem-mark-link"
750
758
  ]
751
759
  },
752
760
  {
@@ -1244,6 +1252,19 @@ var GEM_IMG_IN_CSS_LINES = [
1244
1252
  "@keyframes gem-img-in { from { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; } }"
1245
1253
  ];
1246
1254
  var GEM_IMG_IN_CSS = GEM_IMG_IN_CSS_LINES.join("\n");
1255
+ var GEM_INLINE_MARK_CSS_LINES = [
1256
+ ":where(.gem-mark--underline) { text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }",
1257
+ ":where(.gem-mark--highlight) { background: var(--gem-mark-highlight, #fff1a6); color: var(--gem-mark-highlight-ink, #1f1d18); padding-inline: 0.12em; border-radius: 0.15em; -webkit-box-decoration-break: clone; box-decoration-break: clone; }",
1258
+ ":where(.gem-mark--accent) { color: var(--gem-color-accent); }",
1259
+ ":where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }",
1260
+ ":where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }",
1261
+ ".gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }",
1262
+ ".gem-mark-link:hover { text-decoration-thickness: 2px; }",
1263
+ // Bút dạ thắng màu link: chữ trên nền vàng giữ mực đậm, dù span nằm trong link hay link nằm trong span (review #740 đo màu nhấn
1264
+ // trên vàng 4,32:1 ở nền trắng, 2,47:1 trên băng tối, 1,05:1 trên băng màu nhấn).
1265
+ ".gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }"
1266
+ ];
1267
+ var GEM_INLINE_MARK_CSS = GEM_INLINE_MARK_CSS_LINES.join("\n");
1247
1268
  var GEM_AMBIENT_CSS_LINES = [
1248
1269
  "@keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }",
1249
1270
  "@keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }",
@@ -2409,6 +2430,7 @@ function createPrimitiveCss() {
2409
2430
  "}",
2410
2431
  ...GEM_REVEAL_CSS_LINES,
2411
2432
  ...GEM_IMG_IN_CSS_LINES,
2433
+ ...GEM_INLINE_MARK_CSS_LINES,
2412
2434
  ...GEM_AMBIENT_CSS_LINES,
2413
2435
  ...GEM_DIVIDER_CSS_LINES,
2414
2436
  ...GEM_CARD_CSS_LINES,
package/dist/sections.js CHANGED
@@ -104,9 +104,164 @@ function defaultPlan(plans) {
104
104
  if (!plans.length) return null;
105
105
  return plans.reduce((best, p) => p.amount < best.amount ? p : best, plans[0]);
106
106
  }
107
+
108
+ // ../../packages/gem-sections/src/inline-marks.ts
109
+ var MARK_KINDS = ["bold", "italic", "underline", "highlight", "accent", "alert"];
110
+ var MAX_PHRASES = 12;
111
+ var MAX_PHRASE = 160;
112
+ var MAX_DEPTH = 3;
113
+ var MAX_INLINE = 2e4;
114
+ function safeMarkHref(href) {
115
+ if (typeof href !== "string") return void 0;
116
+ const h = href.trim();
117
+ return /^(https?:\/\/[^\s\\"<>]+|\/(?![/\\])[^\s\\"<>]*|#[^\s\\"<>]*)$/i.test(h) ? h : void 0;
118
+ }
119
+ function markPhrases(value) {
120
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split("\n") : [];
121
+ return raw.filter((p) => typeof p === "string").map((p) => p.trim()).filter((p) => p && p.length <= MAX_PHRASE).slice(0, MAX_PHRASES);
122
+ }
123
+ function linkPhrases(value) {
124
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split("\n") : [];
125
+ const out = [];
126
+ for (const item of raw) {
127
+ let phrase, href;
128
+ if (typeof item === "string") {
129
+ const at = item.lastIndexOf("|");
130
+ if (at < 0) continue;
131
+ phrase = item.slice(0, at);
132
+ href = item.slice(at + 1);
133
+ } else if (item && typeof item === "object") {
134
+ phrase = item.text;
135
+ href = item.href;
136
+ }
137
+ const p = typeof phrase === "string" ? phrase.trim() : "";
138
+ const h = safeMarkHref(href);
139
+ if (p && p.length <= MAX_PHRASE && h) out.push({ phrase: p, href: h });
140
+ if (out.length >= MAX_PHRASES) break;
141
+ }
142
+ return out;
143
+ }
144
+ var sameMarks = (a, b) => a.href === b.href && a.marks.length === b.marks.length && a.marks.every((m, i) => m === b.marks[i]);
145
+ var ordered = (marks) => MARK_KINDS.filter((k) => new Set(marks).has(k));
146
+ function merge(runs) {
147
+ const out = [];
148
+ for (const r of runs) {
149
+ if (!r.text) continue;
150
+ const last = out[out.length - 1];
151
+ if (last && sameMarks(last, r)) last.text += r.text;
152
+ else out.push({ text: r.text, marks: [...r.marks], ...r.href ? { href: r.href } : {} });
153
+ }
154
+ return out.length ? out : [{ text: "", marks: [] }];
155
+ }
156
+ function place(text, phrases) {
157
+ const taken = [];
158
+ phrases.forEach((phrase, index) => {
159
+ for (let at = text.indexOf(phrase); at >= 0; at = text.indexOf(phrase, at + 1)) {
160
+ const end = at + phrase.length;
161
+ if (!taken.some(([s, e]) => at < e && s < end)) {
162
+ taken.push([at, end, index]);
163
+ break;
164
+ }
165
+ }
166
+ });
167
+ return taken;
168
+ }
169
+ function phraseMarkRuns(text, lists = {}) {
170
+ const copy = String(text ?? "");
171
+ const n = copy.length;
172
+ const marks = Array.from({ length: n });
173
+ const hrefs = Array.from({ length: n });
174
+ let any = false;
175
+ for (const kind of MARK_KINDS) {
176
+ for (const [s, e] of place(copy, markPhrases(lists[kind]))) {
177
+ for (let i = s; i < e; i++) (marks[i] ??= /* @__PURE__ */ new Set()).add(kind);
178
+ any = true;
179
+ }
180
+ }
181
+ const links = linkPhrases(lists.links);
182
+ for (const [s, e, index] of place(copy, links.map((l) => l.phrase))) {
183
+ for (let i = s; i < e; i++) hrefs[i] = links[index].href;
184
+ any = true;
185
+ }
186
+ if (!any) return [{ text: copy, marks: [] }];
187
+ const runs = [];
188
+ for (let i = 0; i < n; i++) runs.push({ text: copy[i], marks: ordered(marks[i] ?? []), ...hrefs[i] ? { href: hrefs[i] } : {} });
189
+ return merge(runs);
190
+ }
191
+ var NS = "(?=[^\\s*])";
192
+ var NE = "(?<=[^\\s*])";
193
+ var INLINE = new RegExp(
194
+ [
195
+ "\\[[^\\[\\]\\n]+\\]\\((?:[^()\\s]|\\([^()\\s]*\\))+\\)",
196
+ `\\*\\*\\*${NS}[^*\\n]+?${NE}\\*\\*\\*(?!\\*)`,
197
+ `\\*\\*${NS}(?:[^*\\n]|\\*\\*\\*[^*\\n]+?\\*\\*\\*|\\*(?!\\*))+?${NE}\\*\\*(?!\\*)|\\*\\*${NS}[^*\\n]*?\\*${NS}[^*\\n]+?${NE}\\*\\*\\*(?!\\*)`,
198
+ `\\*${NS}(?:[^*\\n]|\\*\\*\\*[^*\\n]+?\\*\\*\\*|\\*\\*[^*\\n]+?\\*\\*)+?${NE}\\*(?!\\*)`,
199
+ "==(?=[^\\s=])[^=\\n]+?(?<=[^\\s=])==",
200
+ "(?<![\\p{L}\\p{N}_])__(?=[^\\s_])[^_\\n]+?(?<=[^\\s_])__(?![\\p{L}\\p{N}_])",
201
+ "!!(?=[^\\s!])[^!\\n]+?(?<=[^\\s!])!!",
202
+ "\\^\\^(?=[^\\s^])[^\\^\\n]+?(?<=[^\\s^])\\^\\^"
203
+ ].join("|"),
204
+ "gu"
205
+ );
206
+ var OPEN = [["***", ["bold", "italic"]], ["**", ["bold"]], ["==", ["highlight"]], ["__", ["underline"]], ["!!", ["alert"]], ["^^", ["bold", "accent"]], ["*", ["italic"]]];
207
+ var ESCAPABLE = "*_=!^[]()\\";
208
+ var STAND_IN = 57344;
209
+ var escapeMarkers = (src) => src.replace(/\\([*_=!^[\]()\\])/g, (_, c) => String.fromCharCode(STAND_IN + ESCAPABLE.indexOf(c)));
210
+ var unescapeMarkers = (text) => text.replace(/[-]/g, (c) => ESCAPABLE[c.charCodeAt(0) - STAND_IN]);
211
+ function read(src, inherited, href, depth) {
212
+ const out = [];
213
+ const at = (text, marks, h = href) => ({ text, marks: ordered(marks), ...h ? { href: h } : {} });
214
+ let last = 0;
215
+ for (const m of src.matchAll(INLINE)) {
216
+ const i = m.index ?? 0;
217
+ if (i > last) out.push(at(src.slice(last, i), inherited));
218
+ const tok = m[0];
219
+ if (tok.startsWith("[")) {
220
+ const lm = /^\[([^\]]+)\]\((.+)\)$/.exec(tok);
221
+ const safe = lm && !href ? safeMarkHref(unescapeMarkers(lm[2])) : void 0;
222
+ if (lm && safe && depth < MAX_DEPTH) out.push(...read(lm[1], inherited, safe, depth + 1));
223
+ else out.push(at(lm ? lm[1] : tok, inherited));
224
+ } else {
225
+ const [open, add] = OPEN.find(([o]) => tok.startsWith(o) && tok.endsWith(o) && tok.length > 2 * o.length) ?? OPEN.find(([o]) => tok.startsWith(o) && tok.length > 2 * o.length) ?? ["", []];
226
+ const close = tok.endsWith(open) ? open.length : 0;
227
+ const inner = open ? tok.slice(open.length, tok.length - close) : tok;
228
+ const marks = [...inherited, ...add];
229
+ if (open && depth < MAX_DEPTH) out.push(...read(inner, marks, href, depth + 1));
230
+ else out.push(at(open ? inner : tok, marks));
231
+ }
232
+ last = i + tok.length;
233
+ }
234
+ if (last < src.length) out.push(at(src.slice(last), inherited));
235
+ return out;
236
+ }
237
+ function inlineMarkRuns(src) {
238
+ const text = String(src ?? "");
239
+ const escapes = !/[\ue000-\ue009]/.test(text);
240
+ const runs = [];
241
+ text.split("\n").forEach((line, i) => {
242
+ if (i) runs.push({ text: "\n", marks: [] });
243
+ if (line.length > MAX_INLINE) return void runs.push({ text: line, marks: [] });
244
+ const read1 = read(escapes ? escapeMarkers(line) : line, [], void 0, 0);
245
+ runs.push(...escapes ? read1.map((r) => ({ ...r, text: unescapeMarkers(r.text) })) : read1);
246
+ });
247
+ return merge(runs);
248
+ }
249
+ function markClassName(marks) {
250
+ const span = marks.filter((m) => m !== "bold" && m !== "italic");
251
+ return span.length ? ["gem-mark", ...span.map((m) => `gem-mark--${m}`)].join(" ") : "";
252
+ }
253
+ function inlineMarkText(src) {
254
+ return inlineMarkRuns(src).map((r) => r.text).join("");
255
+ }
107
256
  export {
257
+ MARK_KINDS,
108
258
  defaultPlan,
109
259
  elementOverrides,
110
260
  imageSrcSet,
261
+ inlineMarkRuns,
262
+ inlineMarkText,
263
+ markClassName,
264
+ phraseMarkRuns,
265
+ safeMarkHref,
111
266
  subscriptionPlans
112
267
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemboss/gem-framework",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "gem-framework của GemTheme (CSS, runtime, token, bộ kiểm) cộng đúng bộ kiểm section gemtheme dùng khi duyệt. Build từ gemtheme, không sửa ở đây.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",