modm-pinout 1.0.0__tar.gz

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 (30) hide show
  1. modm_pinout-1.0.0/.github/workflows/pypi.yaml +40 -0
  2. modm_pinout-1.0.0/.gitignore +4 -0
  3. modm_pinout-1.0.0/LICENSE +27 -0
  4. modm_pinout-1.0.0/PKG-INFO +88 -0
  5. modm_pinout-1.0.0/README.md +63 -0
  6. modm_pinout-1.0.0/pyproject.toml +54 -0
  7. modm_pinout-1.0.0/setup.cfg +4 -0
  8. modm_pinout-1.0.0/src/modm_pinout/__init__.py +5 -0
  9. modm_pinout-1.0.0/src/modm_pinout/__main__.py +11 -0
  10. modm_pinout-1.0.0/src/modm_pinout/assets/scripts/pinout-app.js +22 -0
  11. modm_pinout-1.0.0/src/modm_pinout/assets/scripts/pinout-core.js +328 -0
  12. modm_pinout-1.0.0/src/modm_pinout/assets/scripts/pinout-io.js +238 -0
  13. modm_pinout-1.0.0/src/modm_pinout/assets/scripts/pinout-loader.js +66 -0
  14. modm_pinout-1.0.0/src/modm_pinout/assets/scripts/pinout-package-renderers.js +118 -0
  15. modm_pinout-1.0.0/src/modm_pinout/assets/scripts/pinout-package.js +1459 -0
  16. modm_pinout-1.0.0/src/modm_pinout/assets/scripts/pinout-persistence.js +560 -0
  17. modm_pinout-1.0.0/src/modm_pinout/assets/scripts/pinout-regex.js +690 -0
  18. modm_pinout-1.0.0/src/modm_pinout/assets/scripts/pinout-table.js +458 -0
  19. modm_pinout-1.0.0/src/modm_pinout/assets/styles/pinout.css +819 -0
  20. modm_pinout-1.0.0/src/modm_pinout/catalog.py +52 -0
  21. modm_pinout-1.0.0/src/modm_pinout/device_loader.py +306 -0
  22. modm_pinout-1.0.0/src/modm_pinout/resources.py +30 -0
  23. modm_pinout-1.0.0/src/modm_pinout/templates/pin_index.html.j2 +113 -0
  24. modm_pinout-1.0.0/src/modm_pinout/templates/pin_matrix.html.j2 +61 -0
  25. modm_pinout-1.0.0/src/modm_pinout.egg-info/PKG-INFO +88 -0
  26. modm_pinout-1.0.0/src/modm_pinout.egg-info/SOURCES.txt +28 -0
  27. modm_pinout-1.0.0/src/modm_pinout.egg-info/dependency_links.txt +1 -0
  28. modm_pinout-1.0.0/src/modm_pinout.egg-info/entry_points.txt +2 -0
  29. modm_pinout-1.0.0/src/modm_pinout.egg-info/requires.txt +7 -0
  30. modm_pinout-1.0.0/src/modm_pinout.egg-info/top_level.txt +1 -0
@@ -0,0 +1,40 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ pypi-publish:
10
+ name: Upload release to PyPI
11
+ runs-on: ubuntu-latest
12
+ environment:
13
+ name: pypi
14
+ url: https://pypi.org/p/modm-pinout
15
+ permissions:
16
+ contents: read
17
+ id-token: write
18
+ steps:
19
+ - name: Clone repository
20
+ uses: actions/checkout@v4
21
+ with:
22
+ fetch-depth: 0
23
+
24
+ - name: Set up Python 3.12
25
+ uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.12"
28
+
29
+ - name: Install release tools
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ python -m pip install build twine setuptools-scm[toml]
33
+
34
+ - name: Package library
35
+ run: |
36
+ python -m build
37
+ twine check dist/*
38
+
39
+ - name: Upload to PyPI
40
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,4 @@
1
+ build*
2
+ .venv/
3
+ dist/
4
+ *.egg-info/
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2026, Niklas Hauser
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of [project] nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: modm-pinout
3
+ Version: 1.0.0
4
+ Summary: Static pin mapping site generator for modm device definitions
5
+ Keywords: embedded,modm,pinout,generator,static-site
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Environment :: Console
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: Software Development :: Code Generators
14
+ Classifier: Topic :: Software Development :: Embedded Systems
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: jinja2>=3.1
19
+ Requires-Dist: modm-devices>=0.13.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: build>=1.2; extra == "dev"
22
+ Requires-Dist: setuptools-scm[toml]>=8; extra == "dev"
23
+ Requires-Dist: twine>=5.0; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # modm-pinout
27
+
28
+ `modm-pinout` builds a static pin-mapping site from device definitions provided by `modm-devices`.
29
+
30
+ The generated output contains:
31
+
32
+ - one HTML page per device
33
+ - shared CSS and JavaScript assets
34
+ - shared JSON datasets grouped by source XML
35
+ - an index page for device lookup
36
+ - a manifest describing generated pages and datasets
37
+
38
+ The package installs a CLI named `modm_pinout`.
39
+
40
+ > Please note that this code was vibe-coded, since we're not good at web development.
41
+ > `modm-devices` is still written by humans though and it shows lol.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ python -m pip install modm-pinout
47
+ ```
48
+
49
+ For local development:
50
+
51
+ ```bash
52
+ python -m venv .venv
53
+ source .venv/bin/activate
54
+ python -m pip install -e .
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ Generate one device page:
60
+
61
+ ```bash
62
+ modm_pinout stm32c011f4p6 -o build
63
+ ```
64
+
65
+ Generate all supported devices:
66
+
67
+ ```bash
68
+ modm_pinout --all -o build
69
+ ```
70
+
71
+ ## Output Layout
72
+
73
+ The generated site uses this structure:
74
+
75
+ ```text
76
+ build/
77
+ index.html
78
+ manifest.json
79
+ assets/
80
+ data/
81
+ devices/
82
+ ```
83
+
84
+ ## Notes
85
+
86
+ - Device metadata and XML inputs are loaded from the installed `modm-devices` package.
87
+ - Templates and frontend assets are bundled as package data inside `modm_pinout`.
88
+ - The package and CLI naming are generic so additional device families can be integrated later.
@@ -0,0 +1,63 @@
1
+ # modm-pinout
2
+
3
+ `modm-pinout` builds a static pin-mapping site from device definitions provided by `modm-devices`.
4
+
5
+ The generated output contains:
6
+
7
+ - one HTML page per device
8
+ - shared CSS and JavaScript assets
9
+ - shared JSON datasets grouped by source XML
10
+ - an index page for device lookup
11
+ - a manifest describing generated pages and datasets
12
+
13
+ The package installs a CLI named `modm_pinout`.
14
+
15
+ > Please note that this code was vibe-coded, since we're not good at web development.
16
+ > `modm-devices` is still written by humans though and it shows lol.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ python -m pip install modm-pinout
22
+ ```
23
+
24
+ For local development:
25
+
26
+ ```bash
27
+ python -m venv .venv
28
+ source .venv/bin/activate
29
+ python -m pip install -e .
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Generate one device page:
35
+
36
+ ```bash
37
+ modm_pinout stm32c011f4p6 -o build
38
+ ```
39
+
40
+ Generate all supported devices:
41
+
42
+ ```bash
43
+ modm_pinout --all -o build
44
+ ```
45
+
46
+ ## Output Layout
47
+
48
+ The generated site uses this structure:
49
+
50
+ ```text
51
+ build/
52
+ index.html
53
+ manifest.json
54
+ assets/
55
+ data/
56
+ devices/
57
+ ```
58
+
59
+ ## Notes
60
+
61
+ - Device metadata and XML inputs are loaded from the installed `modm-devices` package.
62
+ - Templates and frontend assets are bundled as package data inside `modm_pinout`.
63
+ - The package and CLI naming are generic so additional device families can be integrated later.
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "setuptools-scm[toml]>=8", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "modm-pinout"
7
+ dynamic = ["version"]
8
+ description = "Static pin mapping site generator for modm device definitions"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ keywords = ["embedded", "modm", "pinout", "generator", "static-site"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Topic :: Software Development :: Code Generators",
21
+ "Topic :: Software Development :: Embedded Systems",
22
+ ]
23
+ dependencies = [
24
+ "jinja2>=3.1",
25
+ "modm-devices>=0.13.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "build>=1.2",
31
+ "setuptools-scm[toml]>=8",
32
+ "twine>=5.0",
33
+ ]
34
+
35
+ [project.scripts]
36
+ modm_pinout = "modm_pinout:main"
37
+
38
+ [tool.setuptools]
39
+ package-dir = {"" = "src"}
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.setuptools.package-data]
45
+ modm_pinout = [
46
+ "assets/scripts/*.js",
47
+ "assets/styles/*.css",
48
+ "templates/*.j2",
49
+ ]
50
+
51
+ [tool.setuptools_scm]
52
+ version_scheme = "no-guess-dev"
53
+ local_scheme = "no-local-version"
54
+ tag_regex = "^(?:v)?(?P<version>.+)$"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """modm pinout static site builder."""
2
+
3
+ from .builder import build_all_devices, build_single_device, main
4
+
5
+ __all__ = ["build_all_devices", "build_single_device", "main"]
@@ -0,0 +1,11 @@
1
+ """Module entrypoint for the modm pinout static site builder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .builder import main
8
+
9
+
10
+ if __name__ == "__main__":
11
+ sys.exit(main())
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ window.initializeModmPinoutApp = function initializeModmPinoutApp(config) {
4
+ const api = window.ModmPinout || {};
5
+ if (typeof api.createContext !== "function") {
6
+ throw new Error("Pin matrix core module did not load.");
7
+ }
8
+
9
+ const ctx = api.createContext(config);
10
+ api.attachCore(ctx);
11
+ api.attachPersistence(ctx);
12
+ api.attachRegex(ctx);
13
+ api.attachImportExport(ctx);
14
+ api.attachTable(ctx);
15
+ api.attachPackage(ctx);
16
+
17
+ ctx.bindPageEventHandlers();
18
+ ctx.renderPackageDiagram();
19
+ ctx.initializeWithRecovery().catch((error) => {
20
+ ctx.setStatus(`Failed to initialize table: ${String(error)}`, true);
21
+ });
22
+ };
@@ -0,0 +1,328 @@
1
+ "use strict";
2
+
3
+ (function registerModmPinoutCore(global) {
4
+ const api = global.ModmPinout = global.ModmPinout || {};
5
+
6
+ api.createContext = function createContext(config) {
7
+ const deviceData = config && config.deviceData ? config.deviceData : null;
8
+ const cookieKey = config && typeof config.cookieKey === "string" ? config.cookieKey : "";
9
+
10
+ if (!deviceData || typeof deviceData !== "object") {
11
+ throw new Error("Missing device data.");
12
+ }
13
+ if (!cookieKey) {
14
+ throw new Error("Missing cookie key.");
15
+ }
16
+
17
+ return {
18
+ DEVICE_DATA: deviceData,
19
+ COOKIE_KEY: cookieKey,
20
+ SAVE_DAYS: 365,
21
+ COOKIE_MAX_VALUE_BYTES: 3800,
22
+ SHARE_URL_PARAM: "share",
23
+ SHARE_URL_FORMAT: "modm-pinout-share",
24
+ SHARE_URL_VERSION: 1,
25
+ ADD_REGEX_FIELD: "__add_regex__",
26
+ BASE_REGEX_FILTER_FIELDS: ["position", "short_name", "selected_function", "internal_name"],
27
+ table: null,
28
+ saveTimer: null,
29
+ htmlDecodeNode: null,
30
+ textMeasureCtx: null,
31
+ state: {
32
+ selectedByRowId: {},
33
+ namesByRowId: {},
34
+ baseFilterPatterns: {},
35
+ reviewByRowId: {},
36
+ unmappedRows: [],
37
+ regexColumns: [],
38
+ nextRegexId: 1,
39
+ },
40
+ };
41
+ };
42
+
43
+ api.attachCore = function attachCore(ctx) {
44
+ const state = ctx.state;
45
+
46
+ ctx.escapeHtml = function escapeHtml(value) {
47
+ return String(value)
48
+ .replaceAll("&", "&amp;")
49
+ .replaceAll("<", "&lt;")
50
+ .replaceAll(">", "&gt;")
51
+ .replaceAll('"', "&quot;")
52
+ .replaceAll("'", "&#39;");
53
+ };
54
+
55
+ ctx.setStatus = function setStatus(message, isError = false) {
56
+ const node = document.getElementById("save-status");
57
+ node.textContent = message;
58
+ node.classList.toggle("error", isError);
59
+ };
60
+
61
+ ctx.debounce = function debounce(fn, waitMs) {
62
+ return function debounced(...args) {
63
+ if (ctx.saveTimer !== null) {
64
+ window.clearTimeout(ctx.saveTimer);
65
+ }
66
+ ctx.saveTimer = window.setTimeout(() => fn(...args), waitMs);
67
+ };
68
+ };
69
+
70
+ ctx.normalizeRegexColumn = function normalizeRegexColumn(meta) {
71
+ if (!meta || typeof meta !== "object") {
72
+ return null;
73
+ }
74
+ const id = Number(meta.id);
75
+ if (!Number.isInteger(id) || id <= 0) {
76
+ return null;
77
+ }
78
+ const title = typeof meta.title === "string" && meta.title.trim()
79
+ ? meta.title.trim()
80
+ : `Regex ${id}`;
81
+ const pattern = typeof meta.pattern === "string" ? meta.pattern : "";
82
+ return {
83
+ id,
84
+ field: `regex_${id}`,
85
+ title,
86
+ pattern,
87
+ };
88
+ };
89
+
90
+ ctx.normalizeSelectedFunctionList = function normalizeSelectedFunctionList(value, availableFunctions = null) {
91
+ const allowedSet = Array.isArray(availableFunctions) ? new Set(availableFunctions) : null;
92
+ const rawValues = Array.isArray(value)
93
+ ? value
94
+ : (typeof value === "string" || typeof value === "number" ? [value] : []);
95
+
96
+ const normalized = [];
97
+ const seen = new Set();
98
+ for (const entry of rawValues) {
99
+ const item = String(entry ?? "").trim();
100
+ if (!item || seen.has(item)) {
101
+ continue;
102
+ }
103
+ if (allowedSet && !allowedSet.has(item)) {
104
+ continue;
105
+ }
106
+
107
+ seen.add(item);
108
+ normalized.push(item);
109
+ }
110
+
111
+ return normalized;
112
+ };
113
+
114
+ ctx.renderSelectedFunctionsHtml = function renderSelectedFunctionsHtml(value) {
115
+ const selected = ctx.normalizeSelectedFunctionList(value);
116
+ return selected.map((fn) => ctx.escapeHtml(fn)).join("<br>");
117
+ };
118
+
119
+ ctx.decodeHtmlText = function decodeHtmlText(value) {
120
+ const text = String(value || "");
121
+ if (!text || text.indexOf("&") < 0) {
122
+ return text;
123
+ }
124
+
125
+ if (!ctx.htmlDecodeNode) {
126
+ ctx.htmlDecodeNode = document.createElement("textarea");
127
+ }
128
+ ctx.htmlDecodeNode.innerHTML = text;
129
+ return ctx.htmlDecodeNode.value;
130
+ };
131
+
132
+ ctx.valueToSortableText = function valueToSortableText(value) {
133
+ if (Array.isArray(value)) {
134
+ return ctx.normalizeSelectedFunctionList(value).join("\n");
135
+ }
136
+
137
+ if (value == null) {
138
+ return "";
139
+ }
140
+
141
+ const withBreaks = String(value).replace(/<br\s*\/?>/gi, "\n");
142
+ const withoutTags = withBreaks.replace(/<[^>]*>/g, "");
143
+ return ctx.decodeHtmlText(withoutTags).trim();
144
+ };
145
+
146
+ ctx.compareNaturalText = function compareNaturalText(left, right) {
147
+ return String(left || "").localeCompare(String(right || ""), undefined, {
148
+ numeric: true,
149
+ sensitivity: "base",
150
+ });
151
+ };
152
+
153
+ ctx.displayedTextSorter = function displayedTextSorter(a, b) {
154
+ return ctx.compareNaturalText(ctx.valueToSortableText(a), ctx.valueToSortableText(b));
155
+ };
156
+
157
+ ctx.pinSorter = function pinSorter(a, b) {
158
+ const left = String(a || "").trim();
159
+ const right = String(b || "").trim();
160
+ const leftIsInt = /^\d+$/.test(left);
161
+ const rightIsInt = /^\d+$/.test(right);
162
+
163
+ if (leftIsInt && rightIsInt) {
164
+ return Number(left) - Number(right);
165
+ }
166
+ if (leftIsInt) {
167
+ return -1;
168
+ }
169
+ if (rightIsInt) {
170
+ return 1;
171
+ }
172
+
173
+ return ctx.compareNaturalText(left, right);
174
+ };
175
+
176
+ ctx.compareRowIdValues = function compareRowIdValues(left, right) {
177
+ const leftRaw = String(left ?? "");
178
+ const rightRaw = String(right ?? "");
179
+ const leftNum = Number(leftRaw);
180
+ const rightNum = Number(rightRaw);
181
+ const leftIsNumeric = Number.isFinite(leftNum) && leftRaw.trim() !== "";
182
+ const rightIsNumeric = Number.isFinite(rightNum) && rightRaw.trim() !== "";
183
+
184
+ if (leftIsNumeric && rightIsNumeric) {
185
+ return leftNum - rightNum;
186
+ }
187
+ if (leftIsNumeric) {
188
+ return -1;
189
+ }
190
+ if (rightIsNumeric) {
191
+ return 1;
192
+ }
193
+
194
+ return ctx.compareNaturalText(leftRaw, rightRaw);
195
+ };
196
+
197
+ ctx.normalizePinNameKey = function normalizePinNameKey(value) {
198
+ return String(value ?? "").trim().toUpperCase();
199
+ };
200
+
201
+ ctx.normalizeStoredUnmappedRow = function normalizeStoredUnmappedRow(node, index) {
202
+ if (!node || typeof node !== "object") {
203
+ return null;
204
+ }
205
+
206
+ const shortName = String(node.short_name ?? node.name ?? "").trim();
207
+ if (!shortName) {
208
+ return null;
209
+ }
210
+
211
+ const idCandidate = String(node.row_id ?? "").trim();
212
+ const rowId = idCandidate.startsWith("unmapped_") ? idCandidate : `unmapped_${index + 1}`;
213
+ return {
214
+ row_id: rowId,
215
+ short_name: shortName,
216
+ internal_name: String(node.internal_name ?? node.refName ?? node.ref_name ?? "").trim(),
217
+ };
218
+ };
219
+
220
+ ctx.buildRows = function buildRows() {
221
+ const rows = ctx.DEVICE_DATA.rows.map((row) => {
222
+ const rowId = String(row.row_id);
223
+ const functions = Array.isArray(row.functions) ? row.functions.slice() : [];
224
+ const selected = state.selectedByRowId[rowId];
225
+ const internalName = state.namesByRowId[rowId];
226
+ const selectedFunction = ctx.normalizeSelectedFunctionList(selected, functions);
227
+ const resolvedInternalName = typeof internalName === "string" ? internalName : "";
228
+
229
+ return {
230
+ row_id: row.row_id,
231
+ position: row.position,
232
+ short_name: row.short_name,
233
+ pin_label: row.pin_label,
234
+ functions,
235
+ selected_function: selectedFunction,
236
+ internal_name: resolvedInternalName,
237
+ needs_review: Boolean(state.reviewByRowId[rowId]),
238
+ is_unmapped: false,
239
+ };
240
+ });
241
+
242
+ const existingRowIds = new Set(rows.map((row) => String(row.row_id)));
243
+ const normalizedUnmappedRows = state.unmappedRows
244
+ .map((node, index) => ctx.normalizeStoredUnmappedRow(node, index))
245
+ .filter((node) => node !== null);
246
+
247
+ for (const [index, row] of normalizedUnmappedRows.entries()) {
248
+ let rowId = String(row.row_id || `unmapped_${index + 1}`);
249
+ if (!rowId.startsWith("unmapped_")) {
250
+ rowId = `unmapped_${index + 1}`;
251
+ }
252
+
253
+ if (existingRowIds.has(rowId)) {
254
+ let suffix = 1;
255
+ while (existingRowIds.has(`${rowId}_${suffix}`)) {
256
+ suffix += 1;
257
+ }
258
+ rowId = `${rowId}_${suffix}`;
259
+ }
260
+
261
+ existingRowIds.add(rowId);
262
+ const internalName = state.namesByRowId[rowId];
263
+ rows.push({
264
+ row_id: rowId,
265
+ position: "",
266
+ short_name: row.short_name,
267
+ pin_label: row.short_name,
268
+ functions: [],
269
+ selected_function: [],
270
+ internal_name: typeof internalName === "string" ? internalName : row.internal_name,
271
+ needs_review: Boolean(state.reviewByRowId[rowId]),
272
+ is_unmapped: true,
273
+ });
274
+ }
275
+
276
+ return rows;
277
+ };
278
+
279
+ ctx.getRowDataById = function getRowDataById(rowId) {
280
+ const rowKey = String(rowId);
281
+ if (ctx.table) {
282
+ const tableRow = ctx.table.getRow(rowId);
283
+ if (tableRow) {
284
+ return tableRow.getData();
285
+ }
286
+ }
287
+
288
+ return ctx.buildRows().find((row) => String(row.row_id) === rowKey) || null;
289
+ };
290
+
291
+ ctx.updateSelectedFunctionsForRow = function updateSelectedFunctionsForRow(rowId, value) {
292
+ const rowKey = String(rowId);
293
+ const rowData = ctx.getRowDataById(rowKey);
294
+ if (!rowData) {
295
+ return [];
296
+ }
297
+
298
+ const availableFunctions = Array.isArray(rowData.functions) ? rowData.functions : [];
299
+ const normalized = ctx.normalizeSelectedFunctionList(value, availableFunctions);
300
+
301
+ if (normalized.length > 0) {
302
+ state.selectedByRowId[rowKey] = normalized;
303
+ } else {
304
+ delete state.selectedByRowId[rowKey];
305
+ }
306
+
307
+ if (ctx.table) {
308
+ const tableRow = ctx.table.getRow(rowId);
309
+ if (tableRow) {
310
+ Promise.resolve(tableRow.update({ selected_function: normalized })).then(() => {
311
+ if (typeof tableRow.reformat === "function") {
312
+ tableRow.reformat();
313
+ }
314
+ });
315
+ }
316
+ }
317
+
318
+ ctx.saveStateDebounced();
319
+ return normalized;
320
+ };
321
+
322
+ ctx.applyReviewRowClass = function applyReviewRowClass(row) {
323
+ const data = row.getData();
324
+ const rowElement = row.getElement();
325
+ rowElement.classList.toggle("needs-review-row", Boolean(data && data.needs_review));
326
+ };
327
+ };
328
+ }(window));