cotomy 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -149,6 +149,9 @@ Scoped CSS sharing/re-hydrationとインスタンス単位のイベント管理
149
149
 
150
150
  ```bash
151
151
  npm test -- --run tests/view.spec.ts -t "constructs from multiple sources and applies scoped css"
152
+ npm test -- --run tests/view.spec.ts -t "auto-prefixes [root] when no scope placeholder is present"
153
+ npm test -- --run tests/view.spec.ts -t "auto-prefixes every selector without a scope placeholder"
154
+ npm test -- --run tests/view.spec.ts -t "scopes nested rules while preserving non-style at-rules"
152
155
  npm test -- --run tests/view.spec.ts -t "preserves scope ids when cloning, including descendants"
153
156
  npm test -- --run tests/view.spec.ts -t "regenerates instance ids and lifecycle hooks when cloning"
154
157
  npm test -- --run tests/view.spec.ts -t "keeps event handlers isolated by instance even when sharing scope"
@@ -271,7 +274,7 @@ The Form layer builds on `CotomyElement` for common form flows.
271
274
  - `utc` — Treats the value as UTC (or appends `Z` when missing) and formats with `data-cotomy-format` (default `YYYY/MM/DD HH:mm`). By default it renders in local time; set `data-cotomy-timezone` (element or ancestor) to render in a specific IANA timezone.
272
275
  - `date` — Renders local dates with `data-cotomy-format` (default `YYYY/MM/DD`) when the input is a valid `Date` value. By default it renders in local time; set `data-cotomy-timezone` (element or ancestor) to render in a specific IANA timezone.
273
276
 
274
- `data-cotomy-bindtype` values are matched case-insensitively and surrounding whitespace is ignored. Custom renderers registered through `renderer(type, callback)` are stored with lowercase keys. If you override the protected `renderers` map directly, return lowercase keys such as `date` or `customtype`.
277
+ `data-cotomy-bindtype` values are matched case-insensitively and surrounding whitespace is ignored. Custom renderers registered through `renderer(type, callback)` are stored with lowercase keys. If you override the protected `renderers` map directly, keys are also matched case-insensitively.
275
278
 
276
279
  ### UTC Renderer
277
280
 
@@ -1690,9 +1690,7 @@ class CotomyElement {
1690
1690
  const cssid = this.scopedCssElementId;
1691
1691
  CotomyElement.find(`#${cssid}`).forEach(e => e.remove());
1692
1692
  const element = document.createElement("style");
1693
- const hasRoot = /\[root\]/.test(css);
1694
- const normalizedCss = hasRoot ? css : `[root] ${css}`;
1695
- const writeCss = normalizedCss.replace(/\[root\]/g, `[data-cotomy-scopeid="${this.scopeId}"]`);
1693
+ const writeCss = this.createScopedCss(css);
1696
1694
  const node = document.createTextNode(writeCss);
1697
1695
  element.appendChild(node);
1698
1696
  element.id = cssid;
@@ -1708,6 +1706,219 @@ class CotomyElement {
1708
1706
  }
1709
1707
  return this;
1710
1708
  }
1709
+ createScopedCss(css) {
1710
+ return this.scopeCssRules(css);
1711
+ }
1712
+ scopeCssRules(css) {
1713
+ let scoped = "";
1714
+ let cursor = 0;
1715
+ while (cursor < css.length) {
1716
+ const open = this.findNextCssBlockStart(css, cursor);
1717
+ if (open < 0) {
1718
+ scoped += css.slice(cursor);
1719
+ break;
1720
+ }
1721
+ const prelude = css.slice(cursor, open);
1722
+ const statementEnd = this.findCssStatementEnd(prelude);
1723
+ if (prelude.trimStart().startsWith("@") && statementEnd >= 0) {
1724
+ scoped += css.slice(cursor, cursor + statementEnd + 1);
1725
+ cursor += statementEnd + 1;
1726
+ continue;
1727
+ }
1728
+ const close = this.findCssBlockEnd(css, open);
1729
+ if (close < 0) {
1730
+ scoped += css.slice(cursor);
1731
+ break;
1732
+ }
1733
+ const body = css.slice(open + 1, close);
1734
+ scoped += this.scopeCssPrelude(prelude) + "{";
1735
+ scoped += this.shouldScopeNestedCss(prelude) ? this.scopeCssRules(body) : body;
1736
+ scoped += "}";
1737
+ cursor = close + 1;
1738
+ }
1739
+ return scoped;
1740
+ }
1741
+ scopeCssPrelude(prelude) {
1742
+ const trimmed = prelude.trimStart();
1743
+ if (trimmed.startsWith("@"))
1744
+ return prelude;
1745
+ return this.scopeSelectorList(prelude);
1746
+ }
1747
+ shouldScopeNestedCss(prelude) {
1748
+ const atRule = prelude.trimStart().match(/^@([a-z-]+)/i)?.[1].toLowerCase();
1749
+ return atRule === "media"
1750
+ || atRule === "supports"
1751
+ || atRule === "container"
1752
+ || atRule === "layer"
1753
+ || atRule === "document";
1754
+ }
1755
+ scopeSelectorList(selectors) {
1756
+ return this.splitCssSelectorList(selectors).map(selector => {
1757
+ if (!selector.trim())
1758
+ return selector;
1759
+ const leading = selector.match(/^\s*/)?.[0] ?? "";
1760
+ const trailing = selector.match(/\s*$/)?.[0] ?? "";
1761
+ const body = selector.slice(leading.length, selector.length - trailing.length);
1762
+ if (/\[root\]/.test(body)) {
1763
+ return leading + body.replace(/\[root\]/g, this.scopedRootSelector) + trailing;
1764
+ }
1765
+ return `${leading}${this.scopedRootSelector} ${body}${trailing}`;
1766
+ }).join(",");
1767
+ }
1768
+ splitCssSelectorList(selectors) {
1769
+ const result = [];
1770
+ let start = 0;
1771
+ let stringQuote = null;
1772
+ let bracketDepth = 0;
1773
+ let parenthesisDepth = 0;
1774
+ for (let i = 0; i < selectors.length; i++) {
1775
+ const char = selectors[i];
1776
+ const next = selectors[i + 1];
1777
+ if (stringQuote) {
1778
+ if (char === "\\") {
1779
+ i++;
1780
+ }
1781
+ else if (char === stringQuote) {
1782
+ stringQuote = null;
1783
+ }
1784
+ continue;
1785
+ }
1786
+ if (char === "/" && next === "*") {
1787
+ i = selectors.indexOf("*/", i + 2);
1788
+ if (i < 0)
1789
+ break;
1790
+ i++;
1791
+ continue;
1792
+ }
1793
+ if (char === "\"" || char === "'") {
1794
+ stringQuote = char;
1795
+ }
1796
+ else if (char === "[") {
1797
+ bracketDepth++;
1798
+ }
1799
+ else if (char === "]") {
1800
+ bracketDepth = Math.max(0, bracketDepth - 1);
1801
+ }
1802
+ else if (char === "(") {
1803
+ parenthesisDepth++;
1804
+ }
1805
+ else if (char === ")") {
1806
+ parenthesisDepth = Math.max(0, parenthesisDepth - 1);
1807
+ }
1808
+ else if (char === "," && bracketDepth === 0 && parenthesisDepth === 0) {
1809
+ result.push(selectors.slice(start, i));
1810
+ start = i + 1;
1811
+ }
1812
+ }
1813
+ result.push(selectors.slice(start));
1814
+ return result;
1815
+ }
1816
+ findCssStatementEnd(css) {
1817
+ let stringQuote = null;
1818
+ let parenthesisDepth = 0;
1819
+ for (let i = 0; i < css.length; i++) {
1820
+ const char = css[i];
1821
+ const next = css[i + 1];
1822
+ if (stringQuote) {
1823
+ if (char === "\\") {
1824
+ i++;
1825
+ }
1826
+ else if (char === stringQuote) {
1827
+ stringQuote = null;
1828
+ }
1829
+ continue;
1830
+ }
1831
+ if (char === "/" && next === "*") {
1832
+ i = css.indexOf("*/", i + 2);
1833
+ if (i < 0)
1834
+ return -1;
1835
+ i++;
1836
+ continue;
1837
+ }
1838
+ if (char === "\"" || char === "'") {
1839
+ stringQuote = char;
1840
+ }
1841
+ else if (char === "(") {
1842
+ parenthesisDepth++;
1843
+ }
1844
+ else if (char === ")") {
1845
+ parenthesisDepth = Math.max(0, parenthesisDepth - 1);
1846
+ }
1847
+ else if (char === ";" && parenthesisDepth === 0) {
1848
+ return i;
1849
+ }
1850
+ }
1851
+ return -1;
1852
+ }
1853
+ findNextCssBlockStart(css, start) {
1854
+ let stringQuote = null;
1855
+ for (let i = start; i < css.length; i++) {
1856
+ const char = css[i];
1857
+ const next = css[i + 1];
1858
+ if (stringQuote) {
1859
+ if (char === "\\") {
1860
+ i++;
1861
+ }
1862
+ else if (char === stringQuote) {
1863
+ stringQuote = null;
1864
+ }
1865
+ continue;
1866
+ }
1867
+ if (char === "/" && next === "*") {
1868
+ i = css.indexOf("*/", i + 2);
1869
+ if (i < 0)
1870
+ return -1;
1871
+ i++;
1872
+ continue;
1873
+ }
1874
+ if (char === "\"" || char === "'") {
1875
+ stringQuote = char;
1876
+ }
1877
+ else if (char === "{") {
1878
+ return i;
1879
+ }
1880
+ }
1881
+ return -1;
1882
+ }
1883
+ findCssBlockEnd(css, open) {
1884
+ let depth = 0;
1885
+ let stringQuote = null;
1886
+ for (let i = open; i < css.length; i++) {
1887
+ const char = css[i];
1888
+ const next = css[i + 1];
1889
+ if (stringQuote) {
1890
+ if (char === "\\") {
1891
+ i++;
1892
+ }
1893
+ else if (char === stringQuote) {
1894
+ stringQuote = null;
1895
+ }
1896
+ continue;
1897
+ }
1898
+ if (char === "/" && next === "*") {
1899
+ i = css.indexOf("*/", i + 2);
1900
+ if (i < 0)
1901
+ return -1;
1902
+ i++;
1903
+ continue;
1904
+ }
1905
+ if (char === "\"" || char === "'") {
1906
+ stringQuote = char;
1907
+ }
1908
+ else if (char === "{") {
1909
+ depth++;
1910
+ }
1911
+ else if (char === "}") {
1912
+ depth--;
1913
+ if (depth === 0)
1914
+ return i;
1915
+ }
1916
+ }
1917
+ return -1;
1918
+ }
1919
+ get scopedRootSelector() {
1920
+ return `[data-cotomy-scopeid="${this.scopeId}"]`;
1921
+ }
1711
1922
  ensureScopedCss() {
1712
1923
  if (!this._scopedCss || !this.stylable)
1713
1924
  return;
@@ -3235,17 +3446,20 @@ class CotomyViewRenderer {
3235
3446
  return this;
3236
3447
  }
3237
3448
  bindPrimitiveValue(propertyName, value) {
3449
+ let renderers;
3238
3450
  this.element.find(`[data-cotomy-bind="${propertyName}" i]`).forEach(element => {
3239
3451
  if (CotomyDebugSettings.isEnabled(CotomyDebugFeature.Bind)) {
3240
3452
  console.debug(`Binding data to element [data-cotomy-bind="${propertyName}"]:`, value);
3241
3453
  }
3242
3454
  const type = element.attribute("data-cotomy-bindtype")?.trim().toLowerCase();
3243
- if (type && this.renderers[type]) {
3244
- this.renderers[type](element, value);
3245
- }
3246
- else {
3247
- element.text = String(value ?? "");
3455
+ if (type) {
3456
+ renderers ?? (renderers = Object.fromEntries(Object.entries(this.renderers).map(([key, renderer]) => [key.toLowerCase(), renderer])));
3457
+ if (renderers[type]) {
3458
+ renderers[type](element, value);
3459
+ return;
3460
+ }
3248
3461
  }
3462
+ element.text = String(value ?? "");
3249
3463
  });
3250
3464
  }
3251
3465
  async applyArrayAsync(values, propertyName) {