poe-svelte-ui-lib 1.73.0 → 1.74.1

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.
@@ -8,5 +8,6 @@ export { crc16Kermit, crc8Rohc } from "./crc.js";
8
8
  export { BROADCAST_BUS_ID, makeCmd, splitCmd, EXTERNAL_BUS_ID, packetObjectCrc, encodePacket, decodePacket, objectsFor, createPacketStreamParser, type PacketObject, type DecodedPacketObject, type PacketParse, } from "./packet.js";
9
9
  export { createPlotGroups, debPlotValues, DEB_PLOT_ARGUMENT, type PlotPoint } from "./plotGroups.js";
10
10
  export { encodeXPoeDebugFrame, createXPoeDebugParser, xPoeDebugCmd, xPoeDebugValueFromText, type XPoeDebugItem } from "./xPoeDebug.js";
11
+ export { formatXPoeDebugValue, formatXPoeDebugValueForTable, formatXPoeDebugModal, bindXPoeDebugCopyButtons } from "./xPoeDebugView.js";
11
12
  export { CAN_FINAL_BIT, CAN_PACKET_ID_MASK, canFragments, encodeSlcanFrame, encodeCanPacket, createCanParser, slcanOpenCommands, type CanFrame, type CanMessage, } from "./can.js";
12
13
  export { HEADER_CODES, DEFAULT_ARGUMENT_CODES, parseArgumentDictionary, nameByCode } from "./dictionary.js";
@@ -16,6 +16,8 @@ export { BROADCAST_BUS_ID, makeCmd, splitCmd, EXTERNAL_BUS_ID, packetObjectCrc,
16
16
  export { createPlotGroups, debPlotValues, DEB_PLOT_ARGUMENT } from "./plotGroups.js";
17
17
  /* System Console over UART: CBOR [SrcID, HdrArg, Value, FHS, CRC16], plus stream parsing */
18
18
  export { encodeXPoeDebugFrame, createXPoeDebugParser, xPoeDebugCmd, xPoeDebugValueFromText } from "./xPoeDebug.js";
19
+ /* Отображение Value xPOE-Debug в таблице/модалке и копирование Value/HEX */
20
+ export { formatXPoeDebugValue, formatXPoeDebugValueForTable, formatXPoeDebugModal, bindXPoeDebugCopyButtons } from "./xPoeDebugView.js";
19
21
  /* CAN/CAN FD через slcan-адаптер: нарезка пакета на кадры как в can_service прошивки, кадры slcan, потоковый разбор со сборкой фрагментов */
20
22
  export { CAN_FINAL_BIT, CAN_PACKET_ID_MASK, canFragments, encodeSlcanFrame, encodeCanPacket, createCanParser, slcanOpenCommands, } from "./can.js";
21
23
  /* Словарь заголовков и аргументов Cmd — по умолчанию из мастер-словаря dictionary.yaml библиотеки, разбор файла словаря */
@@ -19,7 +19,7 @@ const encodeUInt = (input) => {
19
19
  if (value <= 0xffffn)
20
20
  return [0x19, Number(value >> 8n), Number(value & 0xffn)];
21
21
  if (value <= 0xffffffffn)
22
- return [0x1a, Number(value >> 24n), Number(value >> 16n & 0xffn), Number(value >> 8n & 0xffn), Number(value & 0xffn)];
22
+ return [0x1a, Number(value >> 24n), Number((value >> 16n) & 0xffn), Number((value >> 8n) & 0xffn), Number(value & 0xffn)];
23
23
  throw new Error("xPOE-Debug integer is out of range");
24
24
  };
25
25
  export const xPoeDebugCmd = (header, argument) => {
@@ -41,8 +41,13 @@ export const encodeXPoeDebugFrame = (header, argument, value, srcId = 0, freeHea
41
41
  const crc = crc16Kermit(0, prefix);
42
42
  return new Uint8Array([...prefix, ...encodeUInt(crc)]);
43
43
  };
44
- const textItems = (bytes) => decoder.decode(bytes).split("\n").map((line) => line.trim()).filter((line) => line && ![...line].some((char) => char.charCodeAt(0) < 0x20 && char !== "\t")).map((text) => ({ type: "raw", text }));
45
- const readUInt = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffff ? value : null;
44
+ const textItems = (bytes) => decoder
45
+ .decode(bytes)
46
+ .split("\n")
47
+ .map((line) => line.trim())
48
+ .filter((line) => line && ![...line].some((char) => char.charCodeAt(0) < 0x20 && char !== "\t"))
49
+ .map((text) => ({ type: "raw", text }));
50
+ const readUInt = (value) => (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffff ? value : null);
46
51
  /* Декодирует одну definite array из начала буфера; null означает неполный или невалидный кадр. */
47
52
  const frameItem = (buffer) => {
48
53
  if (buffer[0] !== ARRAY5)
@@ -63,14 +68,27 @@ const frameItem = (buffer) => {
63
68
  const freeHeapValue = fields.decode();
64
69
  const crcStart = 1 + fields.offset;
65
70
  const crc = readUInt(fields.decode());
66
- if (fields.offset !== length - 1 || crc === null || (freeHeapValue !== null && (typeof freeHeapValue !== "number" || !Number.isInteger(freeHeapValue) || freeHeapValue < 0)))
71
+ if (fields.offset !== length - 1 ||
72
+ crc === null ||
73
+ (freeHeapValue !== null && (typeof freeHeapValue !== "number" || !Number.isInteger(freeHeapValue) || freeHeapValue < 0)))
67
74
  return { status: "garbage" };
68
75
  const valueBytes = buffer.slice(valueStart, valueEnd);
69
76
  const { header, argument } = splitCmd(cmd);
70
77
  return {
71
78
  status: "complete",
72
79
  length,
73
- item: { type: "frame", srcId, cmd, header: Object.entries(HEADER_CODES).find(([, code]) => code === header)?.[0] ?? String(header), argument: nameByCode(DEFAULT_ARGUMENT_CODES, argument), value: Array.from(valueBytes, (byte) => byte.toString(16).padStart(2, "0").toUpperCase()).join(""), valueBytes, crc: crc.toString(16).toUpperCase().padStart(4, "0"), crcOk: crc16Kermit(0, buffer.subarray(0, crcStart)) === crc, freeHeap: typeof freeHeapValue === "number" ? String(freeHeapValue) : "" },
80
+ item: {
81
+ type: "frame",
82
+ srcId,
83
+ cmd,
84
+ header: Object.entries(HEADER_CODES).find(([, code]) => code === header)?.[0] ?? String(header),
85
+ argument: nameByCode(DEFAULT_ARGUMENT_CODES, argument),
86
+ value: Array.from(valueBytes, (byte) => byte.toString(16).padStart(2, "0").toUpperCase()).join(""),
87
+ valueBytes,
88
+ crc: crc.toString(16).toUpperCase().padStart(4, "0"),
89
+ crcOk: crc16Kermit(0, buffer.subarray(0, crcStart)) === crc,
90
+ freeHeap: typeof freeHeapValue === "number" ? String(freeHeapValue) : "",
91
+ },
74
92
  };
75
93
  }
76
94
  catch (error) {
@@ -124,7 +142,9 @@ export const createXPoeDebugParser = () => {
124
142
  }
125
143
  return items;
126
144
  },
127
- reset() { buffer = new Uint8Array(); },
145
+ reset() {
146
+ buffer = new Uint8Array();
147
+ },
128
148
  };
129
149
  };
130
150
  /* Value form input: ready CBOR hex passes through; JSON is converted to CBOR. */
@@ -0,0 +1,4 @@
1
+ export declare function formatXPoeDebugValue(rawValue: string): string;
2
+ export declare function formatXPoeDebugValueForTable(rawValue: string, isCbor?: boolean): string;
3
+ export declare function bindXPoeDebugCopyButtons(): () => void;
4
+ export declare function formatXPoeDebugModal(text: string): string;
@@ -0,0 +1,125 @@
1
+ import { cborToValue } from "./cbor/value.js";
2
+ const LOG_LEVELS = { 1: "🔴", 2: "🟡", 3: "🟢", 4: "🔵" };
3
+ const HTML_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
4
+ const escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => HTML_ESCAPES[char]);
5
+ const bytesToHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0").toUpperCase()).join(" ");
6
+ const normalizeCborValue = (value) => {
7
+ if (value instanceof Uint8Array)
8
+ return `h'${bytesToHex(value)}'`;
9
+ if (Array.isArray(value))
10
+ return value.map(normalizeCborValue);
11
+ if (value && typeof value === "object")
12
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalizeCborValue(item)]));
13
+ return value;
14
+ };
15
+ const compactCborValue = (value) => {
16
+ if (typeof value === "string")
17
+ return escapeHtml(value);
18
+ if (value instanceof Uint8Array)
19
+ return `bytes(${value.length}): ${escapeHtml(bytesToHex(value))}`;
20
+ return escapeHtml(JSON.stringify(normalizeCborValue(value)) ?? String(value));
21
+ };
22
+ /* Декодирует CBOR Value для таблицы; системный лог имеет вид [уровень, тег, текст]. */
23
+ export function formatXPoeDebugValue(rawValue) {
24
+ const hex = rawValue.trim().replace(/\s/g, "");
25
+ if (!hex || hex.length % 2 !== 0 || !/^[0-9A-Fa-f]+$/.test(hex))
26
+ return escapeHtml(rawValue);
27
+ try {
28
+ const value = cborToValue(Uint8Array.from(hex.match(/.{2}/g), (byte) => parseInt(byte, 16)));
29
+ if (Array.isArray(value) &&
30
+ value.length === 3 &&
31
+ typeof value[0] === "number" &&
32
+ LOG_LEVELS[value[0]] &&
33
+ typeof value[1] === "string" &&
34
+ (typeof value[2] === "string" || value[2] instanceof Uint8Array)) {
35
+ const level = LOG_LEVELS[value[0]];
36
+ const detail = compactCborValue(value[2]);
37
+ return `<span class="font-semibold">${level}</span> · ${escapeHtml(value[1])}${detail ? ` · ${detail}` : ""}`;
38
+ }
39
+ return compactCborValue(value);
40
+ }
41
+ catch {
42
+ return escapeHtml(rawValue);
43
+ }
44
+ }
45
+ /* Сохраняет исходный HEX скрытым до раскрытия строки в общей таблице. */
46
+ export function formatXPoeDebugValueForTable(rawValue, isCbor = true) {
47
+ if (!isCbor)
48
+ return escapeHtml(rawValue);
49
+ const formatted = formatXPoeDebugValue(rawValue);
50
+ return formatted === escapeHtml(rawValue) ? formatted : `${formatted}<span class="hidden">\nHEX\n${escapeHtml(rawValue)}</span>`;
51
+ }
52
+ const COPY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 11c0-2.828 0-4.243.879-5.121C7.757 5 9.172 5 12 5h3c2.828 0 4.243 0 5.121.879C21 6.757 21 8.172 21 11v5c0 2.828 0 4.243-.879 5.121C19.243 22 17.828 22 15 22h-3c-2.828 0-4.243 0-5.121-.879C6 20.243 6 18.828 6 16z"/><path d="M6 19a3 3 0 0 1-3-3v-6c0-3.771 0-5.657 1.172-6.828S7.229 2 11 2h4a3 3 0 0 1 3 3"/></g></svg>`;
53
+ const COPIED_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4L19 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
54
+ let copyBindings = 0;
55
+ let copyObserver = null;
56
+ const removeXPoeDebugModalFooters = () => {
57
+ document.querySelectorAll("[data-modal]").forEach((modal) => {
58
+ if (!modal.querySelector("[data-xpoe-debug-modal]"))
59
+ return;
60
+ const footer = modal.lastElementChild;
61
+ if (!footer || modal.children.length < 3)
62
+ return;
63
+ footer.remove();
64
+ });
65
+ };
66
+ const handleCopyClick = (event) => {
67
+ if (!(event.target instanceof Element))
68
+ return;
69
+ const button = event.target.closest("button[data-xpoe-copy]");
70
+ if (!button)
71
+ return;
72
+ event.preventDefault();
73
+ event.stopPropagation();
74
+ const value = button.closest("section")?.querySelector("pre")?.textContent ?? "";
75
+ const label = button.getAttribute("aria-label") ?? "Копировать";
76
+ navigator.clipboard
77
+ .writeText(value)
78
+ .then(() => {
79
+ button.innerHTML = COPIED_ICON;
80
+ button.setAttribute("aria-label", "Скопировано");
81
+ window.setTimeout(() => {
82
+ button.innerHTML = COPY_ICON;
83
+ button.setAttribute("aria-label", label);
84
+ }, 1300);
85
+ })
86
+ .catch(() => {
87
+ button.setAttribute("aria-label", "Не удалось скопировать");
88
+ window.setTimeout(() => button.setAttribute("aria-label", label), 1300);
89
+ });
90
+ };
91
+ /* Делегированный обработчик кнопок модального окна; возвращённую функцию вызывают при уничтожении страницы. */
92
+ export function bindXPoeDebugCopyButtons() {
93
+ if (copyBindings++ === 0) {
94
+ document.addEventListener("click", handleCopyClick);
95
+ removeXPoeDebugModalFooters();
96
+ copyObserver = new MutationObserver(removeXPoeDebugModalFooters);
97
+ copyObserver.observe(document.documentElement, { childList: true, subtree: true });
98
+ }
99
+ let isBound = true;
100
+ return () => {
101
+ if (!isBound)
102
+ return;
103
+ isBound = false;
104
+ if (--copyBindings === 0) {
105
+ document.removeEventListener("click", handleCopyClick);
106
+ copyObserver?.disconnect();
107
+ copyObserver = null;
108
+ }
109
+ };
110
+ }
111
+ /* Две прокручиваемые секции модалки; HEX-раздел появляется, если форматтер таблицы добавил исходные байты. */
112
+ export function formatXPoeDebugModal(text) {
113
+ const separator = "\nHEX\n";
114
+ const hexIndex = text.lastIndexOf(separator);
115
+ const decoded = hexIndex < 0 ? text : text.slice(0, hexIndex);
116
+ const hex = hexIndex < 0 ? "" : text.slice(hexIndex + separator.length);
117
+ const modalStyle = "display:flex;flex-direction:column;gap:.75rem;max-height:80vh;min-width:0;overflow-y:auto;overflow-x:hidden";
118
+ const sectionStyle = "min-width:0;padding:.25rem 0 .75rem";
119
+ const headingStyle = "display:flex;align-items:center;justify-content:space-between;gap:.75rem;margin:0 0 .5rem;font-weight:600";
120
+ const buttonStyle = "display:flex;flex:0 0 28px;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:.5rem;background:transparent;color:var(--faint-color);cursor:pointer";
121
+ const buttonClass = "flex size-7 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-(--faint-color) transition-colors duration-150 hover:bg-(--container-color) hover:text-(--font-color)";
122
+ const textStyle = "display:block;min-width:0;max-width:100%;margin:0;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font:inherit";
123
+ const pane = (title, target, value) => `<section style="${sectionStyle}"><div style="${headingStyle}"><span>${title}</span><button type="button" class="${buttonClass}" data-xpoe-copy="${target}" aria-label="Копировать ${title}" style="${buttonStyle}">${COPY_ICON}</button></div><pre style="${textStyle}">${value}</pre></section>`;
124
+ return `<div data-xpoe-debug-modal style="${modalStyle}">${pane("Value", "value", decoded)}${hexIndex < 0 ? "" : pane("HEX", "hex", hex)}</div>`;
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.73.0",
3
+ "version": "1.74.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Oleg-Perevyshin/ElementsUI.git"