@tinacms/mdx 2.3.0 → 2.4.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.
@@ -21210,10 +21210,108 @@ function isObject(value) {
21210
21210
  return typeof value === "object" && value !== null && !Array.isArray(value);
21211
21211
  }
21212
21212
 
21213
- // src/stringify/marks.ts
21214
- var matches = (a, b) => {
21215
- return a.some((v) => b.includes(v));
21213
+ // src/sanitize-css-color.ts
21214
+ var COLOR_FUNCTIONS = /* @__PURE__ */ new Set([
21215
+ "rgb",
21216
+ "rgba",
21217
+ "hsl",
21218
+ "hsla",
21219
+ "hwb",
21220
+ "lab",
21221
+ "lch",
21222
+ "oklab",
21223
+ "oklch",
21224
+ "color",
21225
+ "color-mix",
21226
+ "light-dark",
21227
+ "calc",
21228
+ "var"
21229
+ ]);
21230
+ var isSafeColorFunction = (value) => {
21231
+ if (!/^[a-z-]+\([a-z0-9_.,%/#() +-]*\)$/i.test(value)) {
21232
+ return false;
21233
+ }
21234
+ let depth = 0;
21235
+ for (const [token, name2 = ""] of value.matchAll(/([a-z-]*)\(|\)/gi)) {
21236
+ if (token === ")") {
21237
+ depth--;
21238
+ } else if (COLOR_FUNCTIONS.has(name2.toLowerCase())) {
21239
+ depth++;
21240
+ } else {
21241
+ return false;
21242
+ }
21243
+ if (depth < 0) {
21244
+ return false;
21245
+ }
21246
+ }
21247
+ return depth === 0;
21248
+ };
21249
+ var isSafeCssColor = (value) => {
21250
+ const trimmed = value.trim();
21251
+ const color3 = trimmed.toLowerCase().endsWith("!important") ? trimmed.slice(0, -"!important".length).trimEnd() : trimmed;
21252
+ return /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(color3) || /^[a-z]+$/i.test(color3) || isSafeColorFunction(color3);
21253
+ };
21254
+
21255
+ // src/stringify/text-color.ts
21256
+ var safeTextColor = ({ textColor }) => textColor && isSafeCssColor(textColor) ? textColor : void 0;
21257
+ var wrapColor = (content3, node2, wrap2) => {
21258
+ const color3 = safeTextColor(content3);
21259
+ if (!color3) {
21260
+ return [node2];
21261
+ }
21262
+ if (node2.type !== "text") {
21263
+ return wrap2(color3, node2);
21264
+ }
21265
+ const { value } = node2;
21266
+ const core2 = value.trim();
21267
+ const lead = value.slice(0, value.length - value.trimStart().length);
21268
+ const trail2 = value.slice(value.trimEnd().length);
21269
+ if (!core2) {
21270
+ return [node2];
21271
+ }
21272
+ return [
21273
+ ...lead ? [{ type: "text", value: lead }] : [],
21274
+ ...wrap2(color3, { type: "text", value: core2 }),
21275
+ ...trail2 ? [{ type: "text", value: trail2 }] : []
21276
+ ];
21277
+ };
21278
+ var textColorElement = (content3, node2) => wrapColor(content3, node2, (color3, child) => [
21279
+ {
21280
+ type: "mdxJsxTextElement",
21281
+ name: "span",
21282
+ attributes: [
21283
+ {
21284
+ type: "mdxJsxAttribute",
21285
+ name: "style",
21286
+ value: {
21287
+ type: "mdxJsxAttributeValueExpression",
21288
+ value: `{ color: ${JSON.stringify(color3)} }`
21289
+ }
21290
+ }
21291
+ ],
21292
+ children: [child]
21293
+ }
21294
+ ]);
21295
+ var textColorHtml = (content3, node2) => wrapColor(content3, node2, (color3, child) => [
21296
+ { type: "html", value: `<span style="color: ${color3}">` },
21297
+ child,
21298
+ { type: "html", value: "</span>" }
21299
+ ]);
21300
+ var longestMarkRun = (content3, marks) => {
21301
+ let markToProcess;
21302
+ let runLength = 0;
21303
+ for (const mark of marks) {
21304
+ const end = content3.findIndex((node2) => !getMarks(node2).includes(mark));
21305
+ const length = end === -1 ? content3.length : end;
21306
+ if (length > runLength) {
21307
+ markToProcess = mark;
21308
+ runLength = length;
21309
+ }
21310
+ }
21311
+ return { markToProcess, runLength };
21216
21312
  };
21313
+
21314
+ // src/stringify/marks.ts
21217
21315
  var replaceLinksWithTextNodes = (content3) => {
21218
21316
  const newItems = [];
21219
21317
  content3?.forEach((item) => {
@@ -21338,108 +21436,61 @@ var eat2 = (c, field, imageCallback) => {
21338
21436
  ...eat2(content3.slice(1), field, imageCallback)
21339
21437
  ];
21340
21438
  }
21341
- const marks = getMarks(first);
21342
- if (marks.length === 0) {
21343
- if (first.linkifyTextNode) {
21344
- const f = first;
21345
- return [
21346
- first.linkifyTextNode(text5({ text: f.text })),
21347
- ...eat2(content3.slice(1), field, imageCallback)
21348
- ];
21349
- } else {
21350
- const f = first;
21351
- return [
21352
- text5({ text: f.text }),
21353
- ...eat2(content3.slice(1), field, imageCallback)
21354
- ];
21355
- }
21356
- }
21357
- let nonMatchingSiblingIndex = 0;
21358
- if (content3.slice(1).every((content4, index2) => {
21359
- if (matches(marks, getMarks(content4))) {
21360
- return true;
21361
- } else {
21362
- nonMatchingSiblingIndex = index2;
21363
- return false;
21364
- }
21365
- })) {
21366
- nonMatchingSiblingIndex = content3.length - 1;
21367
- }
21368
- const matchingSiblings = content3.slice(1, nonMatchingSiblingIndex + 1);
21369
- const markCounts = {};
21370
- marks.forEach((mark) => {
21371
- let count2 = 1;
21372
- matchingSiblings.every((sibling2, index2) => {
21373
- if (getMarks(sibling2).includes(mark)) {
21374
- count2 = index2 + 1;
21375
- return true;
21376
- }
21377
- });
21378
- markCounts[mark] = count2;
21379
- });
21380
- let count = 0;
21381
- let markToProcess = null;
21382
- Object.entries(markCounts).forEach(([mark, markCount]) => {
21383
- const m = mark;
21384
- if (markCount > count) {
21385
- count = markCount;
21386
- markToProcess = m;
21387
- }
21388
- });
21389
- if (!markToProcess) {
21390
- const f = first;
21439
+ const textNode = first;
21440
+ const { markToProcess, runLength } = longestMarkRun(
21441
+ content3,
21442
+ getMarks(textNode).filter(
21443
+ (mark) => mark !== "highlight"
21444
+ )
21445
+ );
21446
+ if (!markToProcess && !textNode.highlight) {
21391
21447
  return [
21392
- text5({ text: f.text }),
21448
+ ...leaf(textNode, text5(textNode)),
21393
21449
  ...eat2(content3.slice(1), field, imageCallback)
21394
21450
  ];
21395
21451
  }
21396
- if (markToProcess === "highlight") {
21397
- const f = first;
21398
- const innerText = text5({ text: f.text });
21399
- const child = first.linkifyTextNode ? first.linkifyTextNode(innerText) : innerText;
21452
+ if (!markToProcess) {
21400
21453
  return [
21401
21454
  {
21402
21455
  type: "mdxJsxTextElement",
21403
21456
  name: "mark",
21404
- attributes: markAttributes(f),
21405
- children: [child]
21457
+ attributes: markAttributes(textNode),
21458
+ children: leaf(textNode, text5(textNode))
21406
21459
  },
21407
21460
  ...eat2(content3.slice(1), field, imageCallback)
21408
21461
  ];
21409
21462
  }
21410
21463
  if (markToProcess === "inlineCode") {
21411
- if (nonMatchingSiblingIndex) {
21464
+ if (runLength > 1) {
21412
21465
  throw new Error(
21413
21466
  "Inline code can't have other formatting on it. Remove the formatting from the code text."
21414
21467
  );
21415
21468
  }
21416
- const f = first;
21417
- const node2 = {
21418
- type: markToProcess,
21419
- value: f.text
21420
- };
21421
21469
  return [
21422
- // @ts-ignore
21423
- first.linkifyTextNode?.(node2) ?? node2,
21424
- ...eat2(content3.slice(nonMatchingSiblingIndex + 1), field, imageCallback)
21470
+ ...leaf(textNode, { type: "inlineCode", value: textNode.text }),
21471
+ ...eat2(content3.slice(1), field, imageCallback)
21425
21472
  ];
21426
21473
  }
21427
21474
  return [
21428
21475
  {
21429
21476
  type: markToProcess,
21430
21477
  children: eat2(
21431
- [
21432
- ...[first, ...matchingSiblings].map(
21433
- (sibling2) => cleanNode(sibling2, markToProcess)
21434
- )
21435
- ],
21478
+ content3.slice(0, runLength).map((sibling2) => cleanNode(sibling2, markToProcess)),
21436
21479
  field,
21437
21480
  imageCallback
21438
21481
  )
21439
21482
  },
21440
- ...eat2(content3.slice(nonMatchingSiblingIndex + 1), field, imageCallback)
21483
+ ...eat2(content3.slice(runLength), field, imageCallback)
21441
21484
  ];
21442
21485
  };
21486
+ var leaf = (first, node2) => {
21487
+ return first.linkifyTextNode ? [
21488
+ {
21489
+ ...first.linkifyTextNode({ type: "text", value: "" }),
21490
+ children: textColorElement(first, node2)
21491
+ }
21492
+ ] : textColorElement(first, node2);
21493
+ };
21443
21494
  var cleanNode = (node2, mark) => {
21444
21495
  if (!mark) {
21445
21496
  return node2;
@@ -21817,9 +21868,6 @@ var getMarks = (content3) => {
21817
21868
  };
21818
21869
 
21819
21870
  // src/next/stringify/marks.ts
21820
- var matches2 = (a, b) => {
21821
- return a.some((v) => b.includes(v));
21822
- };
21823
21871
  var replaceLinksWithTextNodes2 = (content3) => {
21824
21872
  const newItems = [];
21825
21873
  content3?.forEach((item) => {
@@ -21951,112 +21999,65 @@ var eat3 = (c, field, imageCallback) => {
21951
21999
  ...eat3(content3.slice(1), field, imageCallback)
21952
22000
  ];
21953
22001
  }
21954
- const marks = getMarks(first);
21955
- if (marks.length === 0) {
21956
- const f = first;
21957
- if (first.linkifyTextNode) {
21958
- return [
21959
- first.linkifyTextNode(text6({ text: f.text })),
21960
- ...eat3(content3.slice(1), field, imageCallback)
21961
- ];
21962
- } else {
21963
- return [
21964
- text6({ text: f.text }),
21965
- ...eat3(content3.slice(1), field, imageCallback)
21966
- ];
21967
- }
21968
- }
21969
- let nonMatchingSiblingIndex = 0;
21970
- if (content3.slice(1).every((content4, index2) => {
21971
- if (matches2(marks, getMarks(content4))) {
21972
- return true;
21973
- } else {
21974
- nonMatchingSiblingIndex = index2;
21975
- return false;
21976
- }
21977
- })) {
21978
- nonMatchingSiblingIndex = content3.length - 1;
21979
- }
21980
- const matchingSiblings = content3.slice(1, nonMatchingSiblingIndex + 1);
21981
- const markCounts = {};
21982
- marks.forEach((mark) => {
21983
- let count2 = 1;
21984
- matchingSiblings.every((sibling2, index2) => {
21985
- if (getMarks(sibling2).includes(mark)) {
21986
- count2 = index2 + 1;
21987
- return true;
21988
- }
21989
- });
21990
- markCounts[mark] = count2;
21991
- });
21992
- let count = 0;
21993
- let markToProcess = null;
21994
- Object.entries(markCounts).forEach(([mark, markCount]) => {
21995
- const m = mark;
21996
- if (markCount > count) {
21997
- count = markCount;
21998
- markToProcess = m;
21999
- }
22000
- });
22002
+ const textNode = first;
22003
+ const { markToProcess, runLength } = longestMarkRun(
22004
+ content3,
22005
+ getMarks(textNode)
22006
+ );
22001
22007
  if (!markToProcess) {
22002
- const f = first;
22003
22008
  return [
22004
- text6({ text: f.text }),
22009
+ ...leaf2(textNode, text6(textNode)),
22005
22010
  ...eat3(content3.slice(1), field, imageCallback)
22006
22011
  ];
22007
22012
  }
22008
22013
  if (markToProcess === "highlight") {
22009
- if (nonMatchingSiblingIndex) {
22014
+ if (runLength > 1) {
22010
22015
  throw new Error(
22011
22016
  "Highlighted text can't have other formatting on it. Remove the formatting from the highlighted text."
22012
22017
  );
22013
22018
  }
22014
- const f = first;
22015
- const innerText = text6({ text: f.text });
22016
- const child = first.linkifyTextNode ? first.linkifyTextNode(innerText) : innerText;
22017
22019
  return [
22018
22020
  {
22019
22021
  type: "mdxJsxTextElement",
22020
22022
  name: "mark",
22021
- attributes: markAttributes2(f),
22022
- children: [child]
22023
+ attributes: markAttributes2(textNode),
22024
+ children: leaf2(textNode, text6(textNode))
22023
22025
  },
22024
- ...eat3(content3.slice(nonMatchingSiblingIndex + 1), field, imageCallback)
22026
+ ...eat3(content3.slice(1), field, imageCallback)
22025
22027
  ];
22026
22028
  }
22027
22029
  if (markToProcess === "inlineCode") {
22028
- const f = first;
22029
- if (nonMatchingSiblingIndex) {
22030
+ if (runLength > 1) {
22030
22031
  throw new Error(
22031
22032
  "Inline code can't have other formatting on it. Remove the formatting from the code text."
22032
22033
  );
22033
22034
  }
22034
- const node2 = {
22035
- type: markToProcess,
22036
- value: f.text
22037
- };
22038
22035
  return [
22039
- // @ts-ignore
22040
- first.linkifyTextNode?.(node2) ?? node2,
22041
- ...eat3(content3.slice(nonMatchingSiblingIndex + 1), field, imageCallback)
22036
+ ...leaf2(textNode, { type: "inlineCode", value: textNode.text }),
22037
+ ...eat3(content3.slice(1), field, imageCallback)
22042
22038
  ];
22043
22039
  }
22044
22040
  return [
22045
22041
  {
22046
22042
  type: markToProcess,
22047
22043
  children: eat3(
22048
- [
22049
- ...[first, ...matchingSiblings].map(
22050
- (sibling2) => cleanNode2(sibling2, markToProcess)
22051
- )
22052
- ],
22044
+ content3.slice(0, runLength).map((sibling2) => cleanNode2(sibling2, markToProcess)),
22053
22045
  field,
22054
22046
  imageCallback
22055
22047
  )
22056
22048
  },
22057
- ...eat3(content3.slice(nonMatchingSiblingIndex + 1), field, imageCallback)
22049
+ ...eat3(content3.slice(runLength), field, imageCallback)
22058
22050
  ];
22059
22051
  };
22052
+ var leaf2 = (first, node2) => {
22053
+ const children = textColorHtml(first, node2);
22054
+ return first.linkifyTextNode ? [
22055
+ {
22056
+ ...first.linkifyTextNode({ type: "text", value: "" }),
22057
+ children
22058
+ }
22059
+ ] : children;
22060
+ };
22060
22061
  var cleanNode2 = (node2, mark) => {
22061
22062
  if (!mark) {
22062
22063
  return node2;
@@ -23595,6 +23596,49 @@ var sanitizeUrl = (url) => {
23595
23596
  };
23596
23597
 
23597
23598
  // src/parse/mark.ts
23599
+ var readDeclarations = (declarations) => {
23600
+ const colors = {};
23601
+ let extra = false;
23602
+ for (const [key, value] of declarations) {
23603
+ if (key === "color" && value !== null && isSafeCssColor(value)) {
23604
+ colors.color = value;
23605
+ } else {
23606
+ extra = true;
23607
+ }
23608
+ }
23609
+ return { colors, extra };
23610
+ };
23611
+ var readStringStyle = (value) => readDeclarations(
23612
+ value.split(";").filter((part) => part.trim()).map((declaration) => {
23613
+ const [key = "", ...rest] = declaration.split(":");
23614
+ return [key.trim().toLowerCase(), rest.join(":").trim()];
23615
+ })
23616
+ );
23617
+ var readExpressionStyle = (value) => {
23618
+ const [statement, ...others] = value.data?.estree?.body ?? [];
23619
+ if (others.length || statement?.type !== "ExpressionStatement" || statement.expression.type !== "ObjectExpression") {
23620
+ return { colors: {}, extra: true };
23621
+ }
23622
+ return readDeclarations(
23623
+ statement.expression.properties.map((property) => {
23624
+ if (property.type !== "Property" || property.computed || property.kind !== "init" || property.value.type !== "Literal" || typeof property.value.value !== "string") {
23625
+ return ["", null];
23626
+ }
23627
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : "";
23628
+ return [key, property.value.value];
23629
+ })
23630
+ );
23631
+ };
23632
+ var readColorStyle = (attributes2 = []) => {
23633
+ const styleAttribute = attributes2.find(
23634
+ (attribute) => attribute.type === "mdxJsxAttribute" && attribute.name === "style"
23635
+ );
23636
+ const value = styleAttribute?.value;
23637
+ if (value === null || value === void 0) {
23638
+ return { colors: {}, extra: false };
23639
+ }
23640
+ return typeof value === "string" ? readStringStyle(value) : readExpressionStyle(value);
23641
+ };
23598
23642
  var getHighlightColorFromAttributes = (attributes2 = []) => {
23599
23643
  const styleAttribute = attributes2.find(
23600
23644
  (attribute) => attribute.type === "mdxJsxAttribute" && attribute.name === "style"
@@ -23621,16 +23665,23 @@ var getHighlightColorFromAttributes = (attributes2 = []) => {
23621
23665
  }
23622
23666
  return void 0;
23623
23667
  };
23668
+ var getColorMarks = (content3) => {
23669
+ if (content3.name === "mark") {
23670
+ const highlightColor = getHighlightColorFromAttributes(content3.attributes);
23671
+ return { highlight: true, ...highlightColor ? { highlightColor } : {} };
23672
+ }
23673
+ const { colors, extra } = readColorStyle(content3.attributes);
23674
+ if (content3.name === "span" && !extra && (content3.attributes ?? []).length === 1 && colors.color) {
23675
+ return { textColor: colors.color };
23676
+ }
23677
+ return null;
23678
+ };
23624
23679
  var parseMarkMdxText = (content3, extraMarks = {}, parseChild) => {
23625
- if (content3.name !== "mark") {
23680
+ const colorMarks = getColorMarks(content3);
23681
+ if (!colorMarks) {
23626
23682
  return null;
23627
23683
  }
23628
- const highlightColor = getHighlightColorFromAttributes(content3.attributes);
23629
- const markProps = {
23630
- highlight: true,
23631
- ...highlightColor ? { highlightColor } : {},
23632
- ...extraMarks
23633
- };
23684
+ const markProps = { ...colorMarks, ...extraMarks };
23634
23685
  return (content3.children || []).flatMap((child) => {
23635
23686
  if (parseChild) {
23636
23687
  return applyMarksToInlineElements(parseChild(child), markProps);
@@ -23647,13 +23698,15 @@ var parseMarkMdxText = (content3, extraMarks = {}, parseChild) => {
23647
23698
  return [];
23648
23699
  });
23649
23700
  };
23701
+ var innerColor = ({ textColor }) => textColor === void 0 ? {} : { textColor };
23650
23702
  var applyMarksToInlineElements = (elements, marks) => {
23651
23703
  const items = Array.isArray(elements) ? elements : [elements];
23652
23704
  return items.map((item) => {
23653
23705
  if (item.type === "text") {
23654
23706
  return {
23655
23707
  ...item,
23656
- ...marks
23708
+ ...marks,
23709
+ ...innerColor(item)
23657
23710
  };
23658
23711
  }
23659
23712
  if (item.type === "a") {
@@ -24530,6 +24583,84 @@ var RichTextParseError = class _RichTextParseError extends Error {
24530
24583
  }
24531
24584
  };
24532
24585
 
24586
+ // src/next/parse/color-marks.ts
24587
+ var OPEN_TAG = /^<(span)(?:\s+style=(?:"([^"]*)"|'([^']*)'))?\s*>$/i;
24588
+ var isHtml = (node2) => node2?.type === "html";
24589
+ var isParent2 = (node2) => Array.isArray(node2.children);
24590
+ var openColorMark = (html2) => {
24591
+ const match = OPEN_TAG.exec(html2.value.trim());
24592
+ if (!match) {
24593
+ return null;
24594
+ }
24595
+ const [, name2 = "", double, single] = match;
24596
+ const style = double ?? single;
24597
+ const element2 = {
24598
+ type: "mdxJsxTextElement",
24599
+ name: name2.toLowerCase(),
24600
+ attributes: style === void 0 ? [] : [{ type: "mdxJsxAttribute", name: "style", value: style }],
24601
+ children: []
24602
+ };
24603
+ return getColorMarks(element2) ? element2 : null;
24604
+ };
24605
+ var findClosingTag = (siblings, start3, name2) => {
24606
+ let depth = 0;
24607
+ for (let index2 = start3; index2 < siblings.length; index2++) {
24608
+ const sibling2 = siblings[index2];
24609
+ if (!isHtml(sibling2)) {
24610
+ continue;
24611
+ }
24612
+ const value = sibling2.value.trim().toLowerCase();
24613
+ if (new RegExp(`^<${name2}[\\s>]`).test(value)) {
24614
+ depth++;
24615
+ } else if (value === `</${name2}>`) {
24616
+ depth--;
24617
+ if (depth === 0) {
24618
+ return index2;
24619
+ }
24620
+ }
24621
+ }
24622
+ return -1;
24623
+ };
24624
+ var PHRASING_PARENTS = /* @__PURE__ */ new Set([
24625
+ "paragraph",
24626
+ "heading",
24627
+ "emphasis",
24628
+ "strong",
24629
+ "delete",
24630
+ "link",
24631
+ "tableCell"
24632
+ ]);
24633
+ var foldSiblings = (siblings) => {
24634
+ const folded = [];
24635
+ for (let index2 = 0; index2 < siblings.length; index2++) {
24636
+ const sibling2 = siblings[index2];
24637
+ if (!sibling2) {
24638
+ continue;
24639
+ }
24640
+ const element2 = isHtml(sibling2) ? openColorMark(sibling2) : null;
24641
+ const close = element2 ? findClosingTag(siblings, index2, element2.name ?? "") : -1;
24642
+ if (element2 && close !== -1) {
24643
+ element2.children = foldSiblings(
24644
+ siblings.slice(index2 + 1, close)
24645
+ );
24646
+ folded.push(element2);
24647
+ index2 = close;
24648
+ continue;
24649
+ }
24650
+ folded.push(sibling2);
24651
+ }
24652
+ return folded;
24653
+ };
24654
+ var foldColorMarkHtml = (node2) => {
24655
+ if (isParent2(node2)) {
24656
+ if (PHRASING_PARENTS.has(node2.type)) {
24657
+ node2.children = foldSiblings(node2.children);
24658
+ }
24659
+ node2.children.forEach(foldColorMarkHtml);
24660
+ }
24661
+ return node2;
24662
+ };
24663
+
24533
24664
  // src/next/parse/post-processing.ts
24534
24665
  var postProcessor = (tree, field, imageCallback) => {
24535
24666
  const addPropsToMdxFlow = (node2) => {
@@ -24567,7 +24698,7 @@ var postProcessor = (tree, field, imageCallback) => {
24567
24698
  };
24568
24699
  visit(tree, "mdxJsxFlowElement", addPropsToMdxFlow);
24569
24700
  visit(tree, "mdxJsxTextElement", addPropsToMdxFlow);
24570
- return remarkToSlate(tree, field, imageCallback, "", true);
24701
+ return remarkToSlate(foldColorMarkHtml(tree), field, imageCallback, "", true);
24571
24702
  };
24572
24703
 
24573
24704
  // src/next/parse/index.ts
package/dist/index.js CHANGED
@@ -23076,10 +23076,108 @@ function isObject(value) {
23076
23076
  return typeof value === "object" && value !== null && !Array.isArray(value);
23077
23077
  }
23078
23078
 
23079
- // src/stringify/marks.ts
23080
- var matches = (a, b) => {
23081
- return a.some((v) => b.includes(v));
23079
+ // src/sanitize-css-color.ts
23080
+ var COLOR_FUNCTIONS = /* @__PURE__ */ new Set([
23081
+ "rgb",
23082
+ "rgba",
23083
+ "hsl",
23084
+ "hsla",
23085
+ "hwb",
23086
+ "lab",
23087
+ "lch",
23088
+ "oklab",
23089
+ "oklch",
23090
+ "color",
23091
+ "color-mix",
23092
+ "light-dark",
23093
+ "calc",
23094
+ "var"
23095
+ ]);
23096
+ var isSafeColorFunction = (value) => {
23097
+ if (!/^[a-z-]+\([a-z0-9_.,%/#() +-]*\)$/i.test(value)) {
23098
+ return false;
23099
+ }
23100
+ let depth = 0;
23101
+ for (const [token, name2 = ""] of value.matchAll(/([a-z-]*)\(|\)/gi)) {
23102
+ if (token === ")") {
23103
+ depth--;
23104
+ } else if (COLOR_FUNCTIONS.has(name2.toLowerCase())) {
23105
+ depth++;
23106
+ } else {
23107
+ return false;
23108
+ }
23109
+ if (depth < 0) {
23110
+ return false;
23111
+ }
23112
+ }
23113
+ return depth === 0;
23114
+ };
23115
+ var isSafeCssColor = (value) => {
23116
+ const trimmed = value.trim();
23117
+ const color3 = trimmed.toLowerCase().endsWith("!important") ? trimmed.slice(0, -"!important".length).trimEnd() : trimmed;
23118
+ return /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(color3) || /^[a-z]+$/i.test(color3) || isSafeColorFunction(color3);
23119
+ };
23120
+
23121
+ // src/stringify/text-color.ts
23122
+ var safeTextColor = ({ textColor }) => textColor && isSafeCssColor(textColor) ? textColor : void 0;
23123
+ var wrapColor = (content3, node2, wrap2) => {
23124
+ const color3 = safeTextColor(content3);
23125
+ if (!color3) {
23126
+ return [node2];
23127
+ }
23128
+ if (node2.type !== "text") {
23129
+ return wrap2(color3, node2);
23130
+ }
23131
+ const { value } = node2;
23132
+ const core2 = value.trim();
23133
+ const lead = value.slice(0, value.length - value.trimStart().length);
23134
+ const trail2 = value.slice(value.trimEnd().length);
23135
+ if (!core2) {
23136
+ return [node2];
23137
+ }
23138
+ return [
23139
+ ...lead ? [{ type: "text", value: lead }] : [],
23140
+ ...wrap2(color3, { type: "text", value: core2 }),
23141
+ ...trail2 ? [{ type: "text", value: trail2 }] : []
23142
+ ];
23143
+ };
23144
+ var textColorElement = (content3, node2) => wrapColor(content3, node2, (color3, child) => [
23145
+ {
23146
+ type: "mdxJsxTextElement",
23147
+ name: "span",
23148
+ attributes: [
23149
+ {
23150
+ type: "mdxJsxAttribute",
23151
+ name: "style",
23152
+ value: {
23153
+ type: "mdxJsxAttributeValueExpression",
23154
+ value: `{ color: ${JSON.stringify(color3)} }`
23155
+ }
23156
+ }
23157
+ ],
23158
+ children: [child]
23159
+ }
23160
+ ]);
23161
+ var textColorHtml = (content3, node2) => wrapColor(content3, node2, (color3, child) => [
23162
+ { type: "html", value: `<span style="color: ${color3}">` },
23163
+ child,
23164
+ { type: "html", value: "</span>" }
23165
+ ]);
23166
+ var longestMarkRun = (content3, marks) => {
23167
+ let markToProcess;
23168
+ let runLength = 0;
23169
+ for (const mark of marks) {
23170
+ const end = content3.findIndex((node2) => !getMarks(node2).includes(mark));
23171
+ const length = end === -1 ? content3.length : end;
23172
+ if (length > runLength) {
23173
+ markToProcess = mark;
23174
+ runLength = length;
23175
+ }
23176
+ }
23177
+ return { markToProcess, runLength };
23082
23178
  };
23179
+
23180
+ // src/stringify/marks.ts
23083
23181
  var replaceLinksWithTextNodes = (content3) => {
23084
23182
  const newItems = [];
23085
23183
  content3?.forEach((item) => {
@@ -23204,108 +23302,61 @@ var eat2 = (c, field, imageCallback) => {
23204
23302
  ...eat2(content3.slice(1), field, imageCallback)
23205
23303
  ];
23206
23304
  }
23207
- const marks = getMarks(first);
23208
- if (marks.length === 0) {
23209
- if (first.linkifyTextNode) {
23210
- const f = first;
23211
- return [
23212
- first.linkifyTextNode(text5({ text: f.text })),
23213
- ...eat2(content3.slice(1), field, imageCallback)
23214
- ];
23215
- } else {
23216
- const f = first;
23217
- return [
23218
- text5({ text: f.text }),
23219
- ...eat2(content3.slice(1), field, imageCallback)
23220
- ];
23221
- }
23222
- }
23223
- let nonMatchingSiblingIndex = 0;
23224
- if (content3.slice(1).every((content4, index2) => {
23225
- if (matches(marks, getMarks(content4))) {
23226
- return true;
23227
- } else {
23228
- nonMatchingSiblingIndex = index2;
23229
- return false;
23230
- }
23231
- })) {
23232
- nonMatchingSiblingIndex = content3.length - 1;
23233
- }
23234
- const matchingSiblings = content3.slice(1, nonMatchingSiblingIndex + 1);
23235
- const markCounts = {};
23236
- marks.forEach((mark) => {
23237
- let count2 = 1;
23238
- matchingSiblings.every((sibling2, index2) => {
23239
- if (getMarks(sibling2).includes(mark)) {
23240
- count2 = index2 + 1;
23241
- return true;
23242
- }
23243
- });
23244
- markCounts[mark] = count2;
23245
- });
23246
- let count = 0;
23247
- let markToProcess = null;
23248
- Object.entries(markCounts).forEach(([mark, markCount]) => {
23249
- const m = mark;
23250
- if (markCount > count) {
23251
- count = markCount;
23252
- markToProcess = m;
23253
- }
23254
- });
23255
- if (!markToProcess) {
23256
- const f = first;
23305
+ const textNode = first;
23306
+ const { markToProcess, runLength } = longestMarkRun(
23307
+ content3,
23308
+ getMarks(textNode).filter(
23309
+ (mark) => mark !== "highlight"
23310
+ )
23311
+ );
23312
+ if (!markToProcess && !textNode.highlight) {
23257
23313
  return [
23258
- text5({ text: f.text }),
23314
+ ...leaf(textNode, text5(textNode)),
23259
23315
  ...eat2(content3.slice(1), field, imageCallback)
23260
23316
  ];
23261
23317
  }
23262
- if (markToProcess === "highlight") {
23263
- const f = first;
23264
- const innerText = text5({ text: f.text });
23265
- const child = first.linkifyTextNode ? first.linkifyTextNode(innerText) : innerText;
23318
+ if (!markToProcess) {
23266
23319
  return [
23267
23320
  {
23268
23321
  type: "mdxJsxTextElement",
23269
23322
  name: "mark",
23270
- attributes: markAttributes(f),
23271
- children: [child]
23323
+ attributes: markAttributes(textNode),
23324
+ children: leaf(textNode, text5(textNode))
23272
23325
  },
23273
23326
  ...eat2(content3.slice(1), field, imageCallback)
23274
23327
  ];
23275
23328
  }
23276
23329
  if (markToProcess === "inlineCode") {
23277
- if (nonMatchingSiblingIndex) {
23330
+ if (runLength > 1) {
23278
23331
  throw new Error(
23279
23332
  "Inline code can't have other formatting on it. Remove the formatting from the code text."
23280
23333
  );
23281
23334
  }
23282
- const f = first;
23283
- const node2 = {
23284
- type: markToProcess,
23285
- value: f.text
23286
- };
23287
23335
  return [
23288
- // @ts-ignore
23289
- first.linkifyTextNode?.(node2) ?? node2,
23290
- ...eat2(content3.slice(nonMatchingSiblingIndex + 1), field, imageCallback)
23336
+ ...leaf(textNode, { type: "inlineCode", value: textNode.text }),
23337
+ ...eat2(content3.slice(1), field, imageCallback)
23291
23338
  ];
23292
23339
  }
23293
23340
  return [
23294
23341
  {
23295
23342
  type: markToProcess,
23296
23343
  children: eat2(
23297
- [
23298
- ...[first, ...matchingSiblings].map(
23299
- (sibling2) => cleanNode(sibling2, markToProcess)
23300
- )
23301
- ],
23344
+ content3.slice(0, runLength).map((sibling2) => cleanNode(sibling2, markToProcess)),
23302
23345
  field,
23303
23346
  imageCallback
23304
23347
  )
23305
23348
  },
23306
- ...eat2(content3.slice(nonMatchingSiblingIndex + 1), field, imageCallback)
23349
+ ...eat2(content3.slice(runLength), field, imageCallback)
23307
23350
  ];
23308
23351
  };
23352
+ var leaf = (first, node2) => {
23353
+ return first.linkifyTextNode ? [
23354
+ {
23355
+ ...first.linkifyTextNode({ type: "text", value: "" }),
23356
+ children: textColorElement(first, node2)
23357
+ }
23358
+ ] : textColorElement(first, node2);
23359
+ };
23309
23360
  var cleanNode = (node2, mark) => {
23310
23361
  if (!mark) {
23311
23362
  return node2;
@@ -23683,9 +23734,6 @@ var getMarks = (content3) => {
23683
23734
  };
23684
23735
 
23685
23736
  // src/next/stringify/marks.ts
23686
- var matches2 = (a, b) => {
23687
- return a.some((v) => b.includes(v));
23688
- };
23689
23737
  var replaceLinksWithTextNodes2 = (content3) => {
23690
23738
  const newItems = [];
23691
23739
  content3?.forEach((item) => {
@@ -23817,112 +23865,65 @@ var eat3 = (c, field, imageCallback) => {
23817
23865
  ...eat3(content3.slice(1), field, imageCallback)
23818
23866
  ];
23819
23867
  }
23820
- const marks = getMarks(first);
23821
- if (marks.length === 0) {
23822
- const f = first;
23823
- if (first.linkifyTextNode) {
23824
- return [
23825
- first.linkifyTextNode(text6({ text: f.text })),
23826
- ...eat3(content3.slice(1), field, imageCallback)
23827
- ];
23828
- } else {
23829
- return [
23830
- text6({ text: f.text }),
23831
- ...eat3(content3.slice(1), field, imageCallback)
23832
- ];
23833
- }
23834
- }
23835
- let nonMatchingSiblingIndex = 0;
23836
- if (content3.slice(1).every((content4, index2) => {
23837
- if (matches2(marks, getMarks(content4))) {
23838
- return true;
23839
- } else {
23840
- nonMatchingSiblingIndex = index2;
23841
- return false;
23842
- }
23843
- })) {
23844
- nonMatchingSiblingIndex = content3.length - 1;
23845
- }
23846
- const matchingSiblings = content3.slice(1, nonMatchingSiblingIndex + 1);
23847
- const markCounts = {};
23848
- marks.forEach((mark) => {
23849
- let count2 = 1;
23850
- matchingSiblings.every((sibling2, index2) => {
23851
- if (getMarks(sibling2).includes(mark)) {
23852
- count2 = index2 + 1;
23853
- return true;
23854
- }
23855
- });
23856
- markCounts[mark] = count2;
23857
- });
23858
- let count = 0;
23859
- let markToProcess = null;
23860
- Object.entries(markCounts).forEach(([mark, markCount]) => {
23861
- const m = mark;
23862
- if (markCount > count) {
23863
- count = markCount;
23864
- markToProcess = m;
23865
- }
23866
- });
23868
+ const textNode = first;
23869
+ const { markToProcess, runLength } = longestMarkRun(
23870
+ content3,
23871
+ getMarks(textNode)
23872
+ );
23867
23873
  if (!markToProcess) {
23868
- const f = first;
23869
23874
  return [
23870
- text6({ text: f.text }),
23875
+ ...leaf2(textNode, text6(textNode)),
23871
23876
  ...eat3(content3.slice(1), field, imageCallback)
23872
23877
  ];
23873
23878
  }
23874
23879
  if (markToProcess === "highlight") {
23875
- if (nonMatchingSiblingIndex) {
23880
+ if (runLength > 1) {
23876
23881
  throw new Error(
23877
23882
  "Highlighted text can't have other formatting on it. Remove the formatting from the highlighted text."
23878
23883
  );
23879
23884
  }
23880
- const f = first;
23881
- const innerText = text6({ text: f.text });
23882
- const child = first.linkifyTextNode ? first.linkifyTextNode(innerText) : innerText;
23883
23885
  return [
23884
23886
  {
23885
23887
  type: "mdxJsxTextElement",
23886
23888
  name: "mark",
23887
- attributes: markAttributes2(f),
23888
- children: [child]
23889
+ attributes: markAttributes2(textNode),
23890
+ children: leaf2(textNode, text6(textNode))
23889
23891
  },
23890
- ...eat3(content3.slice(nonMatchingSiblingIndex + 1), field, imageCallback)
23892
+ ...eat3(content3.slice(1), field, imageCallback)
23891
23893
  ];
23892
23894
  }
23893
23895
  if (markToProcess === "inlineCode") {
23894
- const f = first;
23895
- if (nonMatchingSiblingIndex) {
23896
+ if (runLength > 1) {
23896
23897
  throw new Error(
23897
23898
  "Inline code can't have other formatting on it. Remove the formatting from the code text."
23898
23899
  );
23899
23900
  }
23900
- const node2 = {
23901
- type: markToProcess,
23902
- value: f.text
23903
- };
23904
23901
  return [
23905
- // @ts-ignore
23906
- first.linkifyTextNode?.(node2) ?? node2,
23907
- ...eat3(content3.slice(nonMatchingSiblingIndex + 1), field, imageCallback)
23902
+ ...leaf2(textNode, { type: "inlineCode", value: textNode.text }),
23903
+ ...eat3(content3.slice(1), field, imageCallback)
23908
23904
  ];
23909
23905
  }
23910
23906
  return [
23911
23907
  {
23912
23908
  type: markToProcess,
23913
23909
  children: eat3(
23914
- [
23915
- ...[first, ...matchingSiblings].map(
23916
- (sibling2) => cleanNode2(sibling2, markToProcess)
23917
- )
23918
- ],
23910
+ content3.slice(0, runLength).map((sibling2) => cleanNode2(sibling2, markToProcess)),
23919
23911
  field,
23920
23912
  imageCallback
23921
23913
  )
23922
23914
  },
23923
- ...eat3(content3.slice(nonMatchingSiblingIndex + 1), field, imageCallback)
23915
+ ...eat3(content3.slice(runLength), field, imageCallback)
23924
23916
  ];
23925
23917
  };
23918
+ var leaf2 = (first, node2) => {
23919
+ const children = textColorHtml(first, node2);
23920
+ return first.linkifyTextNode ? [
23921
+ {
23922
+ ...first.linkifyTextNode({ type: "text", value: "" }),
23923
+ children
23924
+ }
23925
+ ] : children;
23926
+ };
23926
23927
  var cleanNode2 = (node2, mark) => {
23927
23928
  if (!mark) {
23928
23929
  return node2;
@@ -25461,6 +25462,49 @@ var sanitizeUrl = (url) => {
25461
25462
  };
25462
25463
 
25463
25464
  // src/parse/mark.ts
25465
+ var readDeclarations = (declarations) => {
25466
+ const colors = {};
25467
+ let extra = false;
25468
+ for (const [key, value] of declarations) {
25469
+ if (key === "color" && value !== null && isSafeCssColor(value)) {
25470
+ colors.color = value;
25471
+ } else {
25472
+ extra = true;
25473
+ }
25474
+ }
25475
+ return { colors, extra };
25476
+ };
25477
+ var readStringStyle = (value) => readDeclarations(
25478
+ value.split(";").filter((part) => part.trim()).map((declaration) => {
25479
+ const [key = "", ...rest] = declaration.split(":");
25480
+ return [key.trim().toLowerCase(), rest.join(":").trim()];
25481
+ })
25482
+ );
25483
+ var readExpressionStyle = (value) => {
25484
+ const [statement, ...others] = value.data?.estree?.body ?? [];
25485
+ if (others.length || statement?.type !== "ExpressionStatement" || statement.expression.type !== "ObjectExpression") {
25486
+ return { colors: {}, extra: true };
25487
+ }
25488
+ return readDeclarations(
25489
+ statement.expression.properties.map((property) => {
25490
+ if (property.type !== "Property" || property.computed || property.kind !== "init" || property.value.type !== "Literal" || typeof property.value.value !== "string") {
25491
+ return ["", null];
25492
+ }
25493
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : "";
25494
+ return [key, property.value.value];
25495
+ })
25496
+ );
25497
+ };
25498
+ var readColorStyle = (attributes2 = []) => {
25499
+ const styleAttribute = attributes2.find(
25500
+ (attribute) => attribute.type === "mdxJsxAttribute" && attribute.name === "style"
25501
+ );
25502
+ const value = styleAttribute?.value;
25503
+ if (value === null || value === void 0) {
25504
+ return { colors: {}, extra: false };
25505
+ }
25506
+ return typeof value === "string" ? readStringStyle(value) : readExpressionStyle(value);
25507
+ };
25464
25508
  var getHighlightColorFromAttributes = (attributes2 = []) => {
25465
25509
  const styleAttribute = attributes2.find(
25466
25510
  (attribute) => attribute.type === "mdxJsxAttribute" && attribute.name === "style"
@@ -25487,16 +25531,23 @@ var getHighlightColorFromAttributes = (attributes2 = []) => {
25487
25531
  }
25488
25532
  return void 0;
25489
25533
  };
25534
+ var getColorMarks = (content3) => {
25535
+ if (content3.name === "mark") {
25536
+ const highlightColor = getHighlightColorFromAttributes(content3.attributes);
25537
+ return { highlight: true, ...highlightColor ? { highlightColor } : {} };
25538
+ }
25539
+ const { colors, extra } = readColorStyle(content3.attributes);
25540
+ if (content3.name === "span" && !extra && (content3.attributes ?? []).length === 1 && colors.color) {
25541
+ return { textColor: colors.color };
25542
+ }
25543
+ return null;
25544
+ };
25490
25545
  var parseMarkMdxText = (content3, extraMarks = {}, parseChild) => {
25491
- if (content3.name !== "mark") {
25546
+ const colorMarks = getColorMarks(content3);
25547
+ if (!colorMarks) {
25492
25548
  return null;
25493
25549
  }
25494
- const highlightColor = getHighlightColorFromAttributes(content3.attributes);
25495
- const markProps = {
25496
- highlight: true,
25497
- ...highlightColor ? { highlightColor } : {},
25498
- ...extraMarks
25499
- };
25550
+ const markProps = { ...colorMarks, ...extraMarks };
25500
25551
  return (content3.children || []).flatMap((child) => {
25501
25552
  if (parseChild) {
25502
25553
  return applyMarksToInlineElements(parseChild(child), markProps);
@@ -25513,13 +25564,15 @@ var parseMarkMdxText = (content3, extraMarks = {}, parseChild) => {
25513
25564
  return [];
25514
25565
  });
25515
25566
  };
25567
+ var innerColor = ({ textColor }) => textColor === void 0 ? {} : { textColor };
25516
25568
  var applyMarksToInlineElements = (elements, marks) => {
25517
25569
  const items = Array.isArray(elements) ? elements : [elements];
25518
25570
  return items.map((item) => {
25519
25571
  if (item.type === "text") {
25520
25572
  return {
25521
25573
  ...item,
25522
- ...marks
25574
+ ...marks,
25575
+ ...innerColor(item)
25523
25576
  };
25524
25577
  }
25525
25578
  if (item.type === "a") {
@@ -26396,6 +26449,84 @@ var RichTextParseError = class _RichTextParseError extends Error {
26396
26449
  }
26397
26450
  };
26398
26451
 
26452
+ // src/next/parse/color-marks.ts
26453
+ var OPEN_TAG = /^<(span)(?:\s+style=(?:"([^"]*)"|'([^']*)'))?\s*>$/i;
26454
+ var isHtml = (node2) => node2?.type === "html";
26455
+ var isParent2 = (node2) => Array.isArray(node2.children);
26456
+ var openColorMark = (html2) => {
26457
+ const match = OPEN_TAG.exec(html2.value.trim());
26458
+ if (!match) {
26459
+ return null;
26460
+ }
26461
+ const [, name2 = "", double, single] = match;
26462
+ const style = double ?? single;
26463
+ const element = {
26464
+ type: "mdxJsxTextElement",
26465
+ name: name2.toLowerCase(),
26466
+ attributes: style === void 0 ? [] : [{ type: "mdxJsxAttribute", name: "style", value: style }],
26467
+ children: []
26468
+ };
26469
+ return getColorMarks(element) ? element : null;
26470
+ };
26471
+ var findClosingTag = (siblings, start3, name2) => {
26472
+ let depth = 0;
26473
+ for (let index2 = start3; index2 < siblings.length; index2++) {
26474
+ const sibling2 = siblings[index2];
26475
+ if (!isHtml(sibling2)) {
26476
+ continue;
26477
+ }
26478
+ const value = sibling2.value.trim().toLowerCase();
26479
+ if (new RegExp(`^<${name2}[\\s>]`).test(value)) {
26480
+ depth++;
26481
+ } else if (value === `</${name2}>`) {
26482
+ depth--;
26483
+ if (depth === 0) {
26484
+ return index2;
26485
+ }
26486
+ }
26487
+ }
26488
+ return -1;
26489
+ };
26490
+ var PHRASING_PARENTS = /* @__PURE__ */ new Set([
26491
+ "paragraph",
26492
+ "heading",
26493
+ "emphasis",
26494
+ "strong",
26495
+ "delete",
26496
+ "link",
26497
+ "tableCell"
26498
+ ]);
26499
+ var foldSiblings = (siblings) => {
26500
+ const folded = [];
26501
+ for (let index2 = 0; index2 < siblings.length; index2++) {
26502
+ const sibling2 = siblings[index2];
26503
+ if (!sibling2) {
26504
+ continue;
26505
+ }
26506
+ const element = isHtml(sibling2) ? openColorMark(sibling2) : null;
26507
+ const close = element ? findClosingTag(siblings, index2, element.name ?? "") : -1;
26508
+ if (element && close !== -1) {
26509
+ element.children = foldSiblings(
26510
+ siblings.slice(index2 + 1, close)
26511
+ );
26512
+ folded.push(element);
26513
+ index2 = close;
26514
+ continue;
26515
+ }
26516
+ folded.push(sibling2);
26517
+ }
26518
+ return folded;
26519
+ };
26520
+ var foldColorMarkHtml = (node2) => {
26521
+ if (isParent2(node2)) {
26522
+ if (PHRASING_PARENTS.has(node2.type)) {
26523
+ node2.children = foldSiblings(node2.children);
26524
+ }
26525
+ node2.children.forEach(foldColorMarkHtml);
26526
+ }
26527
+ return node2;
26528
+ };
26529
+
26399
26530
  // src/next/parse/post-processing.ts
26400
26531
  var postProcessor = (tree, field, imageCallback) => {
26401
26532
  const addPropsToMdxFlow = (node2) => {
@@ -26433,7 +26564,7 @@ var postProcessor = (tree, field, imageCallback) => {
26433
26564
  };
26434
26565
  visit(tree, "mdxJsxFlowElement", addPropsToMdxFlow);
26435
26566
  visit(tree, "mdxJsxTextElement", addPropsToMdxFlow);
26436
- return remarkToSlate(tree, field, imageCallback, "", true);
26567
+ return remarkToSlate(foldColorMarkHtml(tree), field, imageCallback, "", true);
26437
26568
  };
26438
26569
 
26439
26570
  // src/next/parse/index.ts
@@ -0,0 +1,10 @@
1
+ import type { Node } from 'unist';
2
+ /**
3
+ * Markdown has no JSX, so text colour is written as HTML
4
+ * (`<span style="color: …">…</span>`) and arrives here as separate
5
+ * opening/closing `html` nodes. Folds each matched
6
+ * pair (siblings inside the same inline parent) into a JSX element so it
7
+ * parses into marks just like the MDX form. Tags at block level, and tags we
8
+ * wouldn't claim, are left as raw HTML.
9
+ */
10
+ export declare const foldColorMarkHtml: <T extends Node>(node: T) => T;
@@ -0,0 +1,2 @@
1
+ import type { RichTextField } from '@tinacms/schema-tools';
2
+ export declare const field: RichTextField;
@@ -0,0 +1,2 @@
1
+ import type { RichTextField } from '@tinacms/schema-tools';
2
+ export declare const field: RichTextField;
@@ -1,7 +1,21 @@
1
1
  import type { PhrasingContent } from 'mdast';
2
2
  import type { MdxJsxAttribute, MdxJsxExpressionAttribute, MdxJsxTextElement } from 'mdast-util-mdx-jsx';
3
3
  import type * as Plate from './plate';
4
+ type ColorMarks = Pick<Plate.TextElement, 'highlight' | 'highlightColor' | 'textColor'>;
4
5
  export declare const getHighlightColorFromAttributes: (attributes?: (MdxJsxAttribute | MdxJsxExpressionAttribute)[]) => string | undefined;
6
+ /**
7
+ * The Plate marks a `<mark>` or text colour `<span>` stands for, or null when
8
+ * the element isn't one of ours. A `<span>` is only claimed when its sole
9
+ * attribute is a style holding just a safe `color`, so spans carrying
10
+ * anything else keep round-tripping as raw HTML.
11
+ */
12
+ export declare const getColorMarks: (content: MdxJsxTextElement) => ColorMarks | null;
13
+ /**
14
+ * Converts `<mark>` (highlight) and `<span style={{ color }}>` (text colour)
15
+ * into marked text nodes. Children are parsed with `parseChild`, so nested
16
+ * bold/italic/etc. keep their marks. Returns null for any other element.
17
+ */
5
18
  export declare const parseMarkMdxText: <TChild extends PhrasingContent = PhrasingContent>(content: MdxJsxTextElement & {
6
19
  children?: TChild[];
7
- }, extraMarks?: Record<string, boolean | string>, parseChild?: (child: TChild) => Plate.InlineElement | Plate.InlineElement[]) => Plate.InlineElement[] | null;
20
+ }, extraMarks?: Record<string, boolean>, parseChild?: (child: TChild) => Plate.InlineElement | Plate.InlineElement[]) => Plate.InlineElement[] | null;
21
+ export {};
@@ -182,6 +182,8 @@ export type TextElement = {
182
182
  strikethrough?: boolean;
183
183
  highlight?: boolean;
184
184
  highlightColor?: string;
185
+ /** CSS colour for the text itself, eg. `#CC4141` */
186
+ textColor?: string;
185
187
  };
186
188
  /**
187
189
  * @remarks
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Validates CSS colours from rich-text colour marks.
3
+ *
4
+ * This file is intentionally dependency-free — it's built as its own entry point
5
+ * (`@tinacms/mdx/sanitize-css-color`) so renderers and editors that only need
6
+ * colour validation (eg. `tinacms`'s rich-text renderer) don't pull in the
7
+ * markdown-parsing toolchain the rest of `@tinacms/mdx` bundles.
8
+ */
9
+ /**
10
+ * Whether `value` is a plain CSS colour that's safe to drop into a `style`:
11
+ * hex, a named colour, or colour functions (`rgb()`, `hsl()`, `lab()`,
12
+ * `oklch()`, `color-mix()`, `var(--name, fallback)` and the like) nested only
13
+ * in each other. Anything else (`;`, braces, quotes, comments, `url(`) is
14
+ * rejected so a colour can't smuggle in extra CSS. Used when parsing and
15
+ * rendering rich-text text colour and highlight colour.
16
+ */
17
+ export declare const isSafeCssColor: (value: string) => boolean;
@@ -0,0 +1,43 @@
1
+ const COLOR_FUNCTIONS = /* @__PURE__ */ new Set([
2
+ "rgb",
3
+ "rgba",
4
+ "hsl",
5
+ "hsla",
6
+ "hwb",
7
+ "lab",
8
+ "lch",
9
+ "oklab",
10
+ "oklch",
11
+ "color",
12
+ "color-mix",
13
+ "light-dark",
14
+ "calc",
15
+ "var"
16
+ ]);
17
+ const isSafeColorFunction = (value) => {
18
+ if (!/^[a-z-]+\([a-z0-9_.,%/#() +-]*\)$/i.test(value)) {
19
+ return false;
20
+ }
21
+ let depth = 0;
22
+ for (const [token, name = ""] of value.matchAll(/([a-z-]*)\(|\)/gi)) {
23
+ if (token === ")") {
24
+ depth--;
25
+ } else if (COLOR_FUNCTIONS.has(name.toLowerCase())) {
26
+ depth++;
27
+ } else {
28
+ return false;
29
+ }
30
+ if (depth < 0) {
31
+ return false;
32
+ }
33
+ }
34
+ return depth === 0;
35
+ };
36
+ const isSafeCssColor = (value) => {
37
+ const trimmed = value.trim();
38
+ const color = trimmed.toLowerCase().endsWith("!important") ? trimmed.slice(0, -"!important".length).trimEnd() : trimmed;
39
+ return /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(color) || /^[a-z]+$/i.test(color) || isSafeColorFunction(color);
40
+ };
41
+ export {
42
+ isSafeCssColor
43
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import type * as Md from 'mdast';
2
+ import type * as Plate from '../parse/plate';
3
+ import { type Marks } from './index';
4
+ /**
5
+ * MDX form of a text colour, always the innermost element:
6
+ * `<span style={{ color: "#CC4141" }}>text</span>`.
7
+ */
8
+ export declare const textColorElement: (content: Plate.TextElement, node: Md.PhrasingContent) => Md.PhrasingContent[];
9
+ /** Markdown form, as raw HTML: `<span style="color: #CC4141">text</span>`. */
10
+ export declare const textColorHtml: (content: Plate.TextElement, node: Md.PhrasingContent) => Md.PhrasingContent[];
11
+ /**
12
+ * Picks the mark to write outermost: the one shared by the longest run of
13
+ * leading nodes, ties going to the earliest in `getMarks` order. Only that
14
+ * run is wrapped, so neighbours without the mark never gain it.
15
+ */
16
+ export declare const longestMarkRun: <M extends Marks>(content: Plate.InlineElement[], marks: M[]) => {
17
+ markToProcess: M | undefined;
18
+ runLength: number;
19
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinacms/mdx",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,6 +18,10 @@
18
18
  "./sanitize-url": {
19
19
  "types": "./dist/sanitize-url.d.ts",
20
20
  "default": "./dist/sanitize-url.js"
21
+ },
22
+ "./sanitize-css-color": {
23
+ "types": "./dist/sanitize-css-color.d.ts",
24
+ "default": "./dist/sanitize-css-color.js"
21
25
  }
22
26
  },
23
27
  "license": "Apache-2.0",
@@ -28,7 +32,8 @@
28
32
  "target": "node",
29
33
  "bundleDeps": true
30
34
  },
31
- "src/sanitize-url.ts"
35
+ "src/sanitize-url.ts",
36
+ "src/sanitize-css-color.ts"
32
37
  ]
33
38
  },
34
39
  "dependencies": {
@@ -59,7 +64,7 @@
59
64
  "unist-util-stringify-position": "3.0.3",
60
65
  "unist-util-visit": "4.1.2",
61
66
  "vfile-message": "3.1.4",
62
- "@tinacms/schema-tools": "^2.10.1"
67
+ "@tinacms/schema-tools": "^2.11.0"
63
68
  },
64
69
  "publishConfig": {
65
70
  "registry": "https://registry.npmjs.org"