@markdy/core 0.8.6 → 0.8.8

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.
Files changed (2) hide show
  1. package/dist/index.js +189 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -351,7 +351,7 @@ var EDGE_OPERATORS = {
351
351
  };
352
352
  var RESERVED_SELECTORS = /* @__PURE__ */ new Set(["$title", "$nodes", "$edges"]);
353
353
  var BEAT_CUE_KEYWORDS = /* @__PURE__ */ new Set(["show", "hide", "glow", "focus", "frame", "use"]);
354
- var SCENE_KEYS = /* @__PURE__ */ new Set(["width", "height", "fps", "theme", "duration", "direction"]);
354
+ var SCENE_KEYS = /* @__PURE__ */ new Set(["width", "height", "fps", "theme", "duration", "direction", "layout"]);
355
355
  function nodeRole(kind) {
356
356
  return TECHNICAL_NODE_KINDS[kind] ?? "compute";
357
357
  }
@@ -758,24 +758,79 @@ var ParseError = class extends Error {
758
758
  }
759
759
  };
760
760
  var FLOW_OP_RE = /(->|<-|~>|--)/;
761
- var PROP_RE = /(\w[\w.-]*)=(\S+)/g;
762
761
  function stripComment(line) {
763
- const idx = line.indexOf("//");
764
- return idx >= 0 ? line.slice(0, idx) : line;
762
+ let inString = false;
763
+ let escaped = false;
764
+ for (let i = 0; i < line.length; i++) {
765
+ const ch = line[i];
766
+ if (escaped) {
767
+ escaped = false;
768
+ continue;
769
+ }
770
+ if (inString && ch === "\\") {
771
+ escaped = true;
772
+ continue;
773
+ }
774
+ if (ch === '"') {
775
+ inString = !inString;
776
+ continue;
777
+ }
778
+ if (inString) continue;
779
+ if (ch === "/" && line[i + 1] === "/") return line.slice(0, i);
780
+ if (ch === "#" && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i);
781
+ }
782
+ return line;
783
+ }
784
+ function parsePropValue(raw) {
785
+ let val = raw;
786
+ if (raw.startsWith('"')) {
787
+ const parsed = parseStringToken(raw);
788
+ if (parsed && parsed.rest === "") val = parsed.value;
789
+ }
790
+ if (typeof val === "string") {
791
+ if (/^\d+(\.\d+)?$/.test(val)) val = Number(val);
792
+ else if (val === "true") val = true;
793
+ else if (val === "false") val = false;
794
+ else if (/^\d+ms$/.test(val)) val = Number(val.slice(0, -2)) / 1e3;
795
+ else if (/^\d+(\.\d+)?s$/.test(val)) val = Number(val.slice(0, -1));
796
+ }
797
+ return val;
765
798
  }
766
799
  function parseProps(raw) {
767
800
  const props = {};
768
- for (const match of raw.matchAll(PROP_RE)) {
769
- const key = match[1];
770
- let val = match[2];
771
- if (typeof val === "string") {
772
- if (/^\d+(\.\d+)?$/.test(val)) val = Number(val);
773
- else if (val === "true") val = true;
774
- else if (val === "false") val = false;
775
- else if (/^\d+ms$/.test(val)) val = Number(val.slice(0, -2)) / 1e3;
776
- else if (/^\d+(\.\d+)?s$/.test(val)) val = Number(val.slice(0, -1));
801
+ let i = 0;
802
+ while (i < raw.length) {
803
+ const ch = raw[i];
804
+ if (ch === '"') {
805
+ const parsed = parseStringToken(raw.slice(i));
806
+ if (!parsed) break;
807
+ i = raw.length - parsed.rest.length;
808
+ continue;
809
+ }
810
+ const keyMatch = raw.slice(i).match(/^(\w[\w.-]*)=/);
811
+ if (!keyMatch) {
812
+ i++;
813
+ continue;
777
814
  }
778
- props[key] = val;
815
+ const key = keyMatch[1];
816
+ i += key.length + 1;
817
+ let value = "";
818
+ if (raw[i] === '"') {
819
+ const start = i;
820
+ const parsed = parseStringToken(raw.slice(i));
821
+ if (!parsed) {
822
+ value = raw.slice(i);
823
+ i = raw.length;
824
+ } else {
825
+ i = raw.length - parsed.rest.length;
826
+ value = raw.slice(start, i).trim();
827
+ }
828
+ } else {
829
+ const start = i;
830
+ while (i < raw.length && !/\s/.test(raw[i])) i++;
831
+ value = raw.slice(start, i);
832
+ }
833
+ props[key] = parsePropValue(value);
779
834
  }
780
835
  return props;
781
836
  }
@@ -800,6 +855,56 @@ function parseStringToken(raw) {
800
855
  function splitTargets(raw) {
801
856
  return raw.split(/[\s,]+/).map((t) => t.trim()).filter(Boolean);
802
857
  }
858
+ function splitOutsideQuotes(raw, separator) {
859
+ const parts = [];
860
+ let start = 0;
861
+ let inString = false;
862
+ let escaped = false;
863
+ for (let i = 0; i < raw.length; i++) {
864
+ const ch = raw[i];
865
+ if (escaped) {
866
+ escaped = false;
867
+ continue;
868
+ }
869
+ if (inString && ch === "\\") {
870
+ escaped = true;
871
+ continue;
872
+ }
873
+ if (ch === '"') {
874
+ inString = !inString;
875
+ continue;
876
+ }
877
+ if (!inString && ch === separator && /\s/.test(raw[i - 1] ?? "") && /\s/.test(raw[i + 1] ?? "")) {
878
+ parts.push(raw.slice(start, i).trim());
879
+ start = i + 1;
880
+ }
881
+ }
882
+ parts.push(raw.slice(start).trim());
883
+ return parts.filter(Boolean);
884
+ }
885
+ function stripCueProps(raw, keys) {
886
+ let inString = false;
887
+ let escaped = false;
888
+ for (let i = 0; i < raw.length; i++) {
889
+ const ch = raw[i];
890
+ if (escaped) {
891
+ escaped = false;
892
+ continue;
893
+ }
894
+ if (inString && ch === "\\") {
895
+ escaped = true;
896
+ continue;
897
+ }
898
+ if (ch === '"') {
899
+ inString = !inString;
900
+ continue;
901
+ }
902
+ if (inString || !/\s/.test(ch)) continue;
903
+ const rest = raw.slice(i + 1);
904
+ if (keys.some((key) => rest.startsWith(`${key}=`))) return raw.slice(0, i).trim();
905
+ }
906
+ return raw.trim();
907
+ }
803
908
  function parseFlowChain(line, lineNo) {
804
909
  const segments = [];
805
910
  const parts = line.split(FLOW_OP_RE).map((p) => p.trim()).filter(Boolean);
@@ -837,16 +942,22 @@ function splitTargetLabel(token, lineNo) {
837
942
  function parseCueLine(line, lineNo) {
838
943
  const trimmed = line.trim();
839
944
  const props = parseProps(trimmed);
840
- if (trimmed.includes(" & ")) {
841
- const parts = trimmed.split(/\s+&\s+/);
945
+ if (/^@\+?\d/.test(trimmed) || /^\w[\w.-]*\.\w+\(/.test(trimmed) || /^camera\./.test(trimmed)) {
946
+ throw new ParseError(
947
+ "unsupported timeline command; use beat cues like show, frame, focus, glow, and flow lines",
948
+ lineNo
949
+ );
950
+ }
951
+ const parallelParts = splitOutsideQuotes(trimmed, "&");
952
+ if (parallelParts.length > 1) {
842
953
  return {
843
954
  kind: "parallel",
844
- cues: parts.map((p, idx) => parseCueLine(p, lineNo + idx * 1e-3)),
955
+ cues: parallelParts.map((p, idx) => parseCueLine(p, lineNo + idx * 1e-3)),
845
956
  line: lineNo
846
957
  };
847
958
  }
848
959
  if (FLOW_OP_RE.test(trimmed)) {
849
- const chainPart = trimmed.split(/\s+(?:dur|stagger|color|strength|zoom|after)=/)[0].trim();
960
+ const chainPart = stripCueProps(trimmed, ["dur", "stagger", "color", "strength", "zoom", "after"]);
850
961
  return {
851
962
  kind: "flow",
852
963
  segments: parseFlowChain(chainPart, lineNo),
@@ -857,7 +968,7 @@ function parseCueLine(line, lineNo) {
857
968
  const [head, ...rest] = trimmed.split(/\s+/);
858
969
  const keyword = head.toLowerCase();
859
970
  if (keyword === "show" || keyword === "hide") {
860
- const targetRaw = rest.join(" ").split(/\s+(?:dur|stagger)=/)[0];
971
+ const targetRaw = stripCueProps(rest.join(" "), ["dur", "stagger"]);
861
972
  return {
862
973
  kind: keyword,
863
974
  targets: splitTargets(targetRaw),
@@ -867,7 +978,7 @@ function parseCueLine(line, lineNo) {
867
978
  };
868
979
  }
869
980
  if (keyword === "glow") {
870
- const targetRaw = rest.join(" ").split(/\s+(?:color|strength|dur)=/)[0];
981
+ const targetRaw = stripCueProps(rest.join(" "), ["color", "strength", "dur"]);
871
982
  return {
872
983
  kind: "glow",
873
984
  targets: splitTargets(targetRaw),
@@ -878,7 +989,7 @@ function parseCueLine(line, lineNo) {
878
989
  };
879
990
  }
880
991
  if (keyword === "focus") {
881
- const targetRaw = rest.join(" ").split(/\s+(?:zoom|dur)=/)[0];
992
+ const targetRaw = stripCueProps(rest.join(" "), ["zoom", "dur"]);
882
993
  return {
883
994
  kind: "focus",
884
995
  targets: splitTargets(targetRaw),
@@ -888,7 +999,7 @@ function parseCueLine(line, lineNo) {
888
999
  };
889
1000
  }
890
1001
  if (keyword === "frame") {
891
- const targetRaw = rest.join(" ").split(/\s+(?:zoom|dur)=/)[0];
1002
+ const targetRaw = stripCueProps(rest.join(" "), ["zoom", "dur"]);
892
1003
  const targets = splitTargets(targetRaw);
893
1004
  if (targets.length === 0) throw new ParseError(`expected frame target`, lineNo);
894
1005
  return {
@@ -992,6 +1103,45 @@ function readIndentedBody(blocks, startIdx, parentIndent) {
992
1103
  }
993
1104
  return { body, nextIdx: i };
994
1105
  }
1106
+ function readBody(blocks, startIdx, parentIndent, braceDelimited) {
1107
+ if (!braceDelimited) return readIndentedBody(blocks, startIdx, parentIndent);
1108
+ const body = [];
1109
+ let i = startIdx;
1110
+ while (i < blocks.length) {
1111
+ if (blocks[i].text === "}") return { body, nextIdx: i + 1 };
1112
+ body.push(blocks[i]);
1113
+ i++;
1114
+ }
1115
+ return { body, nextIdx: i };
1116
+ }
1117
+ function normalizeCueBlocks(blocks) {
1118
+ const normalized = [];
1119
+ for (const block of blocks) {
1120
+ if (block.text.startsWith("& ")) {
1121
+ const prev = normalized[normalized.length - 1];
1122
+ if (!prev) throw new ParseError(`parallel continuation must follow a cue`, block.line);
1123
+ prev.text = `${prev.text} ${block.text}`;
1124
+ continue;
1125
+ }
1126
+ normalized.push({ ...block });
1127
+ }
1128
+ return normalized;
1129
+ }
1130
+ function unsupportedSyntaxMessage(line) {
1131
+ if (/^var\s+/.test(line)) {
1132
+ return "unsupported variable declaration; use scene properties and style declarations instead";
1133
+ }
1134
+ if (/^actor\s+/.test(line) || /\bfigure\s*\(/.test(line) || /\bbox\s*\(/.test(line) || /\bat\s*\(/.test(line)) {
1135
+ return "unsupported manual drawing syntax; declare architecture nodes like service API, cache Redis, and database DB";
1136
+ }
1137
+ if (/^@\+?\d/.test(line)) {
1138
+ return "unsupported timeline command; put flow and cue lines inside beat blocks";
1139
+ }
1140
+ if (/^camera\./.test(line)) {
1141
+ return "unsupported camera command; use frame NodeOrGroup zoom=... inside a beat";
1142
+ }
1143
+ return null;
1144
+ }
995
1145
  function pushWarning(diagnostics, seen, line, message) {
996
1146
  const key = `${line}:${message}`;
997
1147
  if (seen.has(key)) return;
@@ -1077,14 +1227,24 @@ function parse(source, opts = {}) {
1077
1227
  const block = blocks[i];
1078
1228
  const line = block.text;
1079
1229
  const lineNo = block.line;
1230
+ const unsupportedMessage = unsupportedSyntaxMessage(line);
1231
+ if (unsupportedMessage) throw new ParseError(unsupportedMessage, lineNo);
1080
1232
  if (line.startsWith("scene")) {
1081
1233
  const rest = line.slice(5).trim();
1234
+ if (line.endsWith("{")) {
1235
+ throw new ParseError(`nested scene blocks are not supported; use one scene with multiple beat blocks`, lineNo);
1236
+ }
1082
1237
  let remainder = rest;
1083
1238
  const str = parseStringToken(rest);
1084
1239
  if (str) {
1085
1240
  title = str.value;
1086
1241
  remainder = str.rest;
1087
1242
  }
1243
+ const inlineLayout = remainder.match(/\blayout\s+(LR|RL|TB|BT)\b/i);
1244
+ if (inlineLayout) {
1245
+ meta.direction = inlineLayout[1].toUpperCase();
1246
+ remainder = remainder.replace(/\blayout\s+(LR|RL|TB|BT)\b/i, " ");
1247
+ }
1088
1248
  const props = parseProps(remainder);
1089
1249
  for (const [k, v] of Object.entries(props)) {
1090
1250
  if (!SCENE_KEYS.has(k)) {
@@ -1094,7 +1254,7 @@ function parse(source, opts = {}) {
1094
1254
  if (k === "width" || k === "height" || k === "fps") meta[k] = Number(v);
1095
1255
  else if (k === "duration") meta.duration = Number(v);
1096
1256
  else if (k === "theme") meta.theme = String(v);
1097
- else if (k === "direction") meta.direction = String(v).toUpperCase();
1257
+ else if (k === "direction" || k === "layout") meta.direction = String(v).toUpperCase();
1098
1258
  }
1099
1259
  i++;
1100
1260
  continue;
@@ -1112,11 +1272,11 @@ function parse(source, opts = {}) {
1112
1272
  continue;
1113
1273
  }
1114
1274
  if (line.startsWith("pattern ")) {
1115
- const m = line.match(/^pattern\s+(\w+)\s*\(([^)]*)\)\s*:\s*$/);
1275
+ const m = line.match(/^pattern\s+(\w+)\s*\(([^)]*)\)\s*(?::|\{)\s*$/);
1116
1276
  if (!m) throw new ParseError(`expected pattern name(params):`, lineNo);
1117
1277
  const params = m[2].trim() ? m[2].split(",").map((p) => p.trim()) : [];
1118
- const { body, nextIdx } = readIndentedBody(blocks, i + 1, block.indent);
1119
- const cues = body.map((b) => parseCueLine(b.text, b.line));
1278
+ const { body, nextIdx } = readBody(blocks, i + 1, block.indent, line.endsWith("{"));
1279
+ const cues = normalizeCueBlocks(body).map((b) => parseCueLine(b.text, b.line));
1120
1280
  patterns[m[1]] = { name: m[1], params, body: cues, line: lineNo };
1121
1281
  i = nextIdx;
1122
1282
  continue;
@@ -1153,10 +1313,10 @@ function parse(source, opts = {}) {
1153
1313
  continue;
1154
1314
  }
1155
1315
  if (line.startsWith("beat ")) {
1156
- const m = line.match(/^beat\s+(\w+)(?:\s+"([^"]*)")?\s*:\s*$/);
1316
+ const m = line.match(/^beat\s+(\w+)(?:\s+"([^"]*)")?\s*(?::|\{)\s*$/);
1157
1317
  if (!m) throw new ParseError(`expected beat name:`, lineNo);
1158
- const { body, nextIdx } = readIndentedBody(blocks, i + 1, block.indent);
1159
- let cues = body.map((b) => parseCueLine(b.text, b.line));
1318
+ const { body, nextIdx } = readBody(blocks, i + 1, block.indent, line.endsWith("{"));
1319
+ let cues = normalizeCueBlocks(body).map((b) => parseCueLine(b.text, b.line));
1160
1320
  cues = expandPatternCues(cues, patterns, lineNo);
1161
1321
  beats.push({ name: m[1], label: m[2], cues, line: lineNo });
1162
1322
  i = nextIdx;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/core",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "MarkdyScript parser and diagram compiler (auto-layout, edge routing, cue scheduling) — zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",