@burger-editor/local 4.0.0-alpha.3 → 4.0.0-alpha.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/dist/client.js CHANGED
@@ -3131,14 +3131,14 @@ function next (prev, current, isPre) {
3131
3131
  * Set up window for Node.js
3132
3132
  */
3133
3133
 
3134
- var root$5 = (typeof window !== 'undefined' ? window : {});
3134
+ var root$6 = (typeof window !== 'undefined' ? window : {});
3135
3135
 
3136
3136
  /*
3137
3137
  * Parsing HTML strings
3138
3138
  */
3139
3139
 
3140
3140
  function canParseHTMLNatively$1 () {
3141
- var Parser = root$5.DOMParser;
3141
+ var Parser = root$6.DOMParser;
3142
3142
  var canParse = false;
3143
3143
 
3144
3144
  // Adapted from https://gist.github.com/1129031
@@ -3184,12 +3184,12 @@ function shouldUseActiveX$1 () {
3184
3184
  try {
3185
3185
  document.implementation.createHTMLDocument('').open();
3186
3186
  } catch (e) {
3187
- if (root$5.ActiveXObject) useActiveX = true;
3187
+ if (root$6.ActiveXObject) useActiveX = true;
3188
3188
  }
3189
3189
  return useActiveX
3190
3190
  }
3191
3191
 
3192
- var HTMLParser = canParseHTMLNatively$1() ? root$5.DOMParser : createHTMLParser$1();
3192
+ var HTMLParser = canParseHTMLNatively$1() ? root$6.DOMParser : createHTMLParser$1();
3193
3193
 
3194
3194
  function RootNode (input, options) {
3195
3195
  var root;
@@ -6323,6 +6323,20 @@ let NoHTMLElementError$1 = class NoHTMLElementError extends Error {
6323
6323
  }
6324
6324
  };
6325
6325
 
6326
+ /**
6327
+ * List of dangerous HTML elements to block for XSS protection
6328
+ */
6329
+ const DANGEROUS_ELEMENTS$1 = [
6330
+ 'script',
6331
+ 'style',
6332
+ 'template',
6333
+ 'object',
6334
+ 'embed',
6335
+ 'iframe',
6336
+ 'frame',
6337
+ 'frameset',
6338
+ 'applet',
6339
+ ];
6326
6340
  /**
6327
6341
  *
6328
6342
  * @param kvs
@@ -6346,31 +6360,302 @@ function arrayToHash$1(kvs) {
6346
6360
  }
6347
6361
  return result;
6348
6362
  }
6363
+ const kebabCaseCache$1 = new Map();
6349
6364
  /**
6350
6365
  *
6351
6366
  * @param str
6352
6367
  */
6353
6368
  function kebabCase$1(str) {
6354
- return str.replaceAll(/([A-Z])/g, '-$1').toLowerCase();
6369
+ if (kebabCaseCache$1.has(str)) {
6370
+ return kebabCaseCache$1.get(str);
6371
+ }
6372
+ const result = str.replaceAll(/([A-Z])/g, '-$1').toLowerCase();
6373
+ kebabCaseCache$1.set(str, result);
6374
+ return result;
6355
6375
  }
6376
+ const camelCaseCache$1 = new Map();
6356
6377
  /**
6357
6378
  *
6358
6379
  * @param str
6359
6380
  */
6360
6381
  function camelCase$1(str) {
6361
- return str.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
6382
+ if (camelCaseCache$1.has(str)) {
6383
+ return camelCaseCache$1.get(str);
6384
+ }
6385
+ const result = str.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
6386
+ camelCaseCache$1.set(str, result);
6387
+ return result;
6388
+ }
6389
+ /**
6390
+ *
6391
+ * @param el
6392
+ * @param nodeName
6393
+ * @param attr
6394
+ * @param xssSanitize
6395
+ */
6396
+ function replaceNode$1(el, nodeName, attr, xssSanitize = true) {
6397
+ nodeName = nodeName.toLowerCase();
6398
+ const currentName = el.localName ?? el.nodeName.toLowerCase();
6399
+ if (currentName === nodeName) {
6400
+ return el;
6401
+ }
6402
+ // Check element name if XSS protection is enabled
6403
+ if (xssSanitize && // Disallow dangerous elements
6404
+ DANGEROUS_ELEMENTS$1.includes(nodeName)) {
6405
+ return el;
6406
+ }
6407
+ const node = (el.ownerDocument ?? document).createElement(nodeName);
6408
+ for (const child of el.childNodes) {
6409
+ node.append(child.cloneNode(true));
6410
+ }
6411
+ const fields = getFields$1(el, attr);
6412
+ const attrs = new Set(fields.map((field) => field.propName));
6413
+ for (const { name, value } of el.attributes) {
6414
+ if (attrs.has(name)) {
6415
+ continue;
6416
+ }
6417
+ // Check attribute value if XSS protection is enabled
6418
+ if (xssSanitize) {
6419
+ const sanitizedValue = sanitizeAttributeValue$1(name, value);
6420
+ if (sanitizedValue !== null) {
6421
+ node.setAttribute(name, sanitizedValue);
6422
+ }
6423
+ continue;
6424
+ }
6425
+ node.setAttribute(name, value);
6426
+ }
6427
+ el.replaceWith(node);
6428
+ return node;
6429
+ }
6430
+ /**
6431
+ * Removes dangerous script elements from HTML string
6432
+ * @param html HTML string
6433
+ * @returns Sanitized HTML
6434
+ */
6435
+ function sanitizeHtml$1(html) {
6436
+ if (typeof html !== 'string') {
6437
+ return '';
6438
+ }
6439
+ // Simple approach to handle script tags content
6440
+ // First remove script tags but keep their content
6441
+ let sanitized = html;
6442
+ // For each dangerous element, replace tag but keep content using regex
6443
+ // Example: <script>alert("XSS")</script> -> alert("XSS")
6444
+ for (const tagName of DANGEROUS_ELEMENTS$1) {
6445
+ const regex = new RegExp(`<${tagName}[^>]*>(.*?)<\\/${tagName}>`, 'gis');
6446
+ sanitized = sanitized.replace(regex, '$1');
6447
+ }
6448
+ // Parse HTML safely using DOM parser for remaining sanitization
6449
+ const doc = new DOMParser().parseFromString(sanitized, 'text/html');
6450
+ const body = doc.body;
6451
+ // Remove event handler attributes from all elements
6452
+ sanitizeElementAndChildren$1(body);
6453
+ return body.innerHTML;
6454
+ }
6455
+ /**
6456
+ * Sanitizes an element and all its child elements
6457
+ * @param element Target element
6458
+ */
6459
+ function sanitizeElementAndChildren$1(element) {
6460
+ // Remove dangerous attributes from current element
6461
+ sanitizeAttributes$1(element);
6462
+ // Process child elements recursively
6463
+ const children = element.children;
6464
+ for (const child of children) {
6465
+ sanitizeElementAndChildren$1(child);
6466
+ }
6467
+ }
6468
+ /**
6469
+ * Sanitizes attribute value
6470
+ * @param name Attribute name
6471
+ * @param value Attribute value
6472
+ * @returns Safe attribute value, or null if dangerous
6473
+ */
6474
+ function sanitizeAttributeValue$1(name, value) {
6475
+ if (typeof value !== 'string') {
6476
+ return value;
6477
+ }
6478
+ name = name.toLowerCase();
6479
+ // Disallow event handler attributes
6480
+ if (name.startsWith('on')) {
6481
+ return null;
6482
+ }
6483
+ // Check for dangerous protocol in link attributes
6484
+ if (name === 'href' ||
6485
+ name === 'src' ||
6486
+ name === 'action' ||
6487
+ name === 'formaction' ||
6488
+ name === 'xlink:href') {
6489
+ const trimmedValue = value.trim().toLowerCase();
6490
+ if (trimmedValue.startsWith('javascript:') ||
6491
+ trimmedValue.startsWith('data:') ||
6492
+ trimmedValue.startsWith('vbscript:')) {
6493
+ return null;
6494
+ }
6495
+ }
6496
+ // Other dangerous attributes
6497
+ if (['manifest'].includes(name)) {
6498
+ return null;
6499
+ }
6500
+ return value;
6501
+ }
6502
+ /**
6503
+ * Removes dangerous attributes from an element
6504
+ * @param element Target element
6505
+ */
6506
+ function sanitizeAttributes$1(element) {
6507
+ // Collect list of dangerous attributes
6508
+ const dangerousAttrs = [...element.attributes]
6509
+ .filter((attr) => sanitizeAttributeValue$1(attr.name, attr.value) === null)
6510
+ .map((attr) => attr.name);
6511
+ // Remove dangerous attributes
6512
+ for (const attrName of dangerousAttrs) {
6513
+ element.removeAttribute(attrName);
6514
+ }
6515
+ }
6516
+ /**
6517
+ *
6518
+ * @param data
6519
+ * @param definedFields
6520
+ */
6521
+ function maxLengthOf$1(data, definedFields) {
6522
+ let maxLength = 0;
6523
+ for (const key in data) {
6524
+ if (definedFields.includes(key) && Array.isArray(data[key])) {
6525
+ maxLength = Math.max(maxLength, data[key].length);
6526
+ }
6527
+ }
6528
+ return maxLength;
6529
+ }
6530
+ /**
6531
+ *
6532
+ * @param el
6533
+ * @param attr
6534
+ */
6535
+ function definedFields$1(el, attr) {
6536
+ const fieldDefinitions = getFields$1(el, attr);
6537
+ return fieldDefinitions.map((field) => field.fieldName);
6538
+ }
6539
+ /**
6540
+ *
6541
+ * @param el
6542
+ * @param attr
6543
+ */
6544
+ function getFields$1(el, attr) {
6545
+ const fields = el.getAttribute(`data-${attr}`);
6546
+ if (!fields) {
6547
+ return [];
6548
+ }
6549
+ return parseFields$1(fields);
6550
+ }
6551
+ /**
6552
+ *
6553
+ * @param el
6554
+ * @param attr
6555
+ * @param dataKeyName
6556
+ */
6557
+ function hasField$1(el, attr, dataKeyName) {
6558
+ const fieldDefinitions = getFields$1(el, attr);
6559
+ return fieldDefinitions.some((field) => field.fieldName === dataKeyName);
6560
+ }
6561
+ /**
6562
+ *
6563
+ * @param fields
6564
+ * @param oldPropName
6565
+ * @param newPropName
6566
+ */
6567
+ function replaceProp$1(fields, oldPropName, newPropName) {
6568
+ return fields.map((field) => {
6569
+ if (field.propName === oldPropName) {
6570
+ return {
6571
+ ...field,
6572
+ propName: newPropName,
6573
+ };
6574
+ }
6575
+ return field;
6576
+ });
6577
+ }
6578
+ /**
6579
+ *
6580
+ * @param fields
6581
+ * @param propName
6582
+ */
6583
+ function removeProp$1(fields, propName) {
6584
+ return fields.filter((field) => field.propName !== propName);
6585
+ }
6586
+ /**
6587
+ * - 値が配列の場合は、index に対応する値を返す。
6588
+ * - 値が配列でない場合は、そのまま返す。
6589
+ * - ⚠️ 値が配列で、index に対応する値が undefined の場合は、配列の最初の値を返す。
6590
+ * @param data
6591
+ * @param index
6592
+ */
6593
+ function flattenData$1(data, index) {
6594
+ const result = {};
6595
+ for (const key in data) {
6596
+ result[key] = Array.isArray(data[key])
6597
+ ? data[key][index] === undefined
6598
+ ? data[key][0]
6599
+ : data[key][index]
6600
+ : data[key];
6601
+ }
6602
+ return result;
6603
+ }
6604
+ /**
6605
+ * 特定の要素で、`propName in element`で判定できない属性名
6606
+ */
6607
+ const specificProp$1 = {
6608
+ /*
6609
+ * JSDOMの未実装対策
6610
+ * @see https://github.com/jsdom/jsdom/blob/main/lib/jsdom/living/nodes/HTMLSourceElement.webidl
6611
+ * Living Standardでは実装済み
6612
+ * @see https://html.spec.whatwg.org/multipage/embedded-content.html#htmlsourceelement
6613
+ */
6614
+ source: ['width', 'height'],
6615
+ };
6616
+ /**
6617
+ *
6618
+ * @param el
6619
+ * @param name
6620
+ */
6621
+ function propInElement$1(el, name) {
6622
+ const has = name in el;
6623
+ if (has) {
6624
+ return true;
6625
+ }
6626
+ return specificProp$1[el.localName]?.includes(name) ?? false;
6362
6627
  }
6363
6628
 
6629
+ const cache$1 = new Map();
6630
+ /**
6631
+ *
6632
+ * @param query
6633
+ */
6634
+ function parseFields$1(query) {
6635
+ const cached = cache$1.get(query);
6636
+ if (cached) {
6637
+ return cached;
6638
+ }
6639
+ const fields = query.split(',');
6640
+ const result = fields.map(parseField$1);
6641
+ cache$1.set(query, result);
6642
+ return result;
6643
+ }
6364
6644
  /**
6365
6645
  *
6366
6646
  * @param field
6367
6647
  */
6368
- function fieldNameParser$1(field) {
6648
+ function parseField$1(field) {
6369
6649
  field = field.trim().toLowerCase();
6370
6650
  if (!field) {
6371
6651
  throw new Error('Field name is empty.');
6372
6652
  }
6373
- let [fieldName, propName] = field.split(':');
6653
+ let [fieldName, propName,
6654
+ // eslint-disable-next-line prefer-const
6655
+ ...rest] = field.split(':');
6656
+ if (rest.length > 0) {
6657
+ throw new Error('Invalid field format.');
6658
+ }
6374
6659
  propName = propName?.trim() || '';
6375
6660
  fieldName = fieldName?.trim() || propName || field;
6376
6661
  return {
@@ -6390,58 +6675,67 @@ function fieldNameParser$1(field) {
6390
6675
  * @param datum A datum of value
6391
6676
  * @param attr Data attribute name for specifying the node that FrozenPatty treats as a field
6392
6677
  * @param filter
6678
+ * @param xssSanitize Enable XSS protection
6393
6679
  */
6394
- function setValue$1(el, name, datum, attr = 'field', filter) {
6680
+ function setValue$1(el, name, datum, attr = 'field', filter, xssSanitize = true) {
6395
6681
  const rawValue = el.getAttribute(`data-${attr}`);
6396
- const fieldList = rawValue?.split(/\s*,\s*/) ?? [];
6397
- for (const field of fieldList) {
6398
- const { fieldName, propName } = fieldNameParser$1(field);
6399
- if (name !== fieldName) {
6400
- continue;
6401
- }
6402
- if (filter) {
6403
- datum = filter(datum);
6404
- }
6405
- // console.log({ name, field, fieldName, propName, datum, el: el.innerHTML });
6406
- if (propName) {
6407
- if (/^style\([a-z-]+\)$/i.test(propName)) {
6408
- const cssPropertyName = propName.replace(/^style\(([a-z-]+)\)$/i, '$1');
6409
- let cssValue;
6410
- switch (cssPropertyName) {
6411
- case 'background-image': {
6412
- //
6413
- // NGパターン
6414
- // $changeDom.css(cssPropertyName, 'url("' + value + '")');
6415
- //
6416
- // cssメソッドを経由すると styleAPIを使用するので URLがホストを含めた絶対パスになる
6417
- // デモサーバーから本番サーバーへの移行ができなくなってしまうので避ける
6418
- // 単純な文字列を流し込む(setAttributeを利用)
6419
- // urlはマルチバイト文字や空白記号を含まないはずであるがエスケープする
6420
- const url = encodeURI(`${datum}`);
6421
- cssValue = `url(${url})`;
6422
- break;
6423
- }
6424
- //
6425
- // TODO: 他にもvalueに単位が必要なケースなどに対応したい
6426
- //
6427
- default: {
6428
- cssValue = `${datum}`;
6429
- }
6430
- }
6431
- el.setAttribute('style', `${cssPropertyName}: ${cssValue}`);
6432
- }
6433
- else if (el instanceof HTMLElement) {
6434
- // HTMLElement
6435
- set$4(el, attr, propName, datum);
6682
+ const fields = parseFields$1(rawValue ?? '');
6683
+ const field = fields.find((field) => field.fieldName === name);
6684
+ if (!field) {
6685
+ return;
6686
+ }
6687
+ const { propName } = field;
6688
+ if (filter) {
6689
+ datum = filter(datum);
6690
+ }
6691
+ // console.log({ name, field, fieldName, propName, datum, el: el.innerHTML });
6692
+ if (!propName) {
6693
+ setContent$1(el, datum, undefined, xssSanitize);
6694
+ return;
6695
+ }
6696
+ if (propName === 'style' && el instanceof HTMLElement) {
6697
+ el.style.cssText = `${datum}`;
6698
+ return;
6699
+ }
6700
+ if (/^style\([a-z-]+\)$/i.test(propName)) {
6701
+ const cssPropertyName = propName.replace(/^style\(([a-z-]+)\)$/i, '$1');
6702
+ let cssValue;
6703
+ switch (cssPropertyName) {
6704
+ case 'background-image': {
6705
+ // Using CSS method would create absolute URLs with host
6706
+ // This would prevent migration from demo to production server
6707
+ // Using simple string insertion (setAttribute) instead
6708
+ // URL may not contain multibyte or whitespace characters, but escape anyway
6709
+ const url = encodeURI(`${datum}`);
6710
+ cssValue = `url(${url})`;
6711
+ break;
6436
6712
  }
6437
- else {
6438
- // SVGElement or more
6439
- el.setAttribute(propName, `${datum}`);
6713
+ //
6714
+ // TODO: Handle other cases where values need units
6715
+ //
6716
+ default: {
6717
+ cssValue = `${datum}`;
6440
6718
  }
6441
- return;
6442
6719
  }
6443
- setContent$1(el, datum);
6720
+ el.setAttribute('style', `${cssPropertyName}: ${cssValue}`);
6721
+ return;
6444
6722
  }
6723
+ if (el instanceof HTMLElement) {
6724
+ // HTMLElement
6725
+ set$2(el, attr, propName, datum, xssSanitize);
6726
+ return;
6727
+ }
6728
+ // SVGElement or more
6729
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
6730
+ // Check attribute value if XSS protection is enabled
6731
+ if (xssSanitize) {
6732
+ const safeValue = sanitizeAttributeValue$1(propName, `${datum}`);
6733
+ if (safeValue !== null) {
6734
+ el.setAttribute(propName, safeValue);
6735
+ }
6736
+ return;
6737
+ }
6738
+ el.setAttribute(propName, `${datum}`);
6445
6739
  }
6446
6740
  /**
6447
6741
  *
@@ -6449,41 +6743,54 @@ function setValue$1(el, name, datum, attr = 'field', filter) {
6449
6743
  * @param prefix
6450
6744
  * @param name
6451
6745
  * @param datum
6746
+ * @param xssSanitize
6452
6747
  */
6453
- function set$4(el, prefix, name, datum) {
6748
+ function set$2(el, prefix, name, datum, xssSanitize = true) {
6454
6749
  if (datum == null) {
6455
6750
  el.removeAttribute(name);
6456
6751
  return;
6457
6752
  }
6458
6753
  switch (name) {
6459
6754
  case 'text': {
6460
- setContent$1(el, datum, false);
6755
+ setContent$1(el, datum, false, xssSanitize);
6461
6756
  return;
6462
6757
  }
6463
6758
  case 'html': {
6464
- setContent$1(el, datum, true);
6759
+ // Sanitize HTML if XSS protection is enabled
6760
+ if (xssSanitize && typeof datum === 'string') {
6761
+ setContent$1(el, sanitizeHtml$1(datum), true, xssSanitize);
6762
+ }
6763
+ else {
6764
+ setContent$1(el, datum, true, xssSanitize);
6765
+ }
6465
6766
  return;
6466
6767
  }
6467
6768
  case 'node': {
6468
6769
  if (typeof datum !== 'string') {
6469
6770
  return;
6470
6771
  }
6471
- const nodeName = el.localName ?? el.nodeName.toLowerCase();
6472
- if (nodeName === datum.toLowerCase()) {
6473
- return;
6474
- }
6475
- const node = (el.ownerDocument ?? document).createElement(`${datum}`);
6476
- for (const child of el.childNodes) {
6477
- node.append(child.cloneNode(true));
6478
- }
6479
- for (const { name, value } of el.attributes) {
6480
- node.setAttribute(name, value);
6481
- }
6482
- el.replaceWith(node);
6772
+ replaceNode$1(el, datum, prefix, xssSanitize);
6773
+ return;
6774
+ }
6775
+ }
6776
+ // If XSS protection is enabled
6777
+ if (xssSanitize) {
6778
+ // Don't set event handler attributes (starting with 'on')
6779
+ if (name.toLowerCase().startsWith('on')) {
6780
+ return;
6781
+ }
6782
+ const beforeSanitize = datum;
6783
+ datum = sanitizeAttributeValue$1(name, datum);
6784
+ // If sanitization results in null value (except when original value was null), don't change the value
6785
+ if (datum === null && beforeSanitize !== null) {
6483
6786
  return;
6484
6787
  }
6485
6788
  }
6486
- if (!name.startsWith('data-') && !(name in el)) {
6789
+ if (datum == null) {
6790
+ el.removeAttribute(name);
6791
+ return;
6792
+ }
6793
+ if (!name.startsWith('data-') && !propInElement$1(el, name)) {
6487
6794
  const dataAttr = `data-${prefix}-${kebabCase$1(name)}`;
6488
6795
  if (el.hasAttribute(dataAttr)) {
6489
6796
  el.setAttribute(dataAttr, `${datum}`);
@@ -7055,8 +7362,9 @@ function set$4(el, prefix, name, datum) {
7055
7362
  * @param el
7056
7363
  * @param datum
7057
7364
  * @param asHtml
7365
+ * @param xssSanitize
7058
7366
  */
7059
- function setContent$1(el, datum, asHtml = true) {
7367
+ function setContent$1(el, datum, asHtml = true, xssSanitize = true) {
7060
7368
  if (el instanceof HTMLInputElement ||
7061
7369
  el instanceof HTMLSelectElement ||
7062
7370
  el instanceof HTMLTextAreaElement ||
@@ -7074,9 +7382,18 @@ function setContent$1(el, datum, asHtml = true) {
7074
7382
  return;
7075
7383
  }
7076
7384
  if (asHtml) {
7077
- el.innerHTML = datum == null ? '' : `${datum}`;
7385
+ // Sanitize HTML if XSS protection is enabled
7386
+ const htmlStr = datum == null ? '' : `${datum}`;
7387
+ if (xssSanitize && typeof htmlStr === 'string') {
7388
+ // sanitizeHtml will now replace dangerous elements instead of removing them
7389
+ el.innerHTML = sanitizeHtml$1(htmlStr);
7390
+ }
7391
+ else {
7392
+ el.innerHTML = htmlStr;
7393
+ }
7078
7394
  return;
7079
7395
  }
7396
+ // No need to sanitize textContent (automatically escaped)
7080
7397
  el.textContent = datum == null ? '' : `${datum}`;
7081
7398
  }
7082
7399
  /**
@@ -7113,16 +7430,16 @@ function toInt$1(datum) {
7113
7430
  */
7114
7431
  function getValues$1(el, convertType = false, attr = 'field', filter) {
7115
7432
  const result = [];
7116
- const rawValue = el.getAttribute(`data-${attr}`);
7117
- const listRoot = el.closest(`[data-${attr}-list]`);
7118
- const forceArray = !!listRoot;
7119
- if (rawValue == null) {
7433
+ const query = el.getAttribute(`data-${attr}`);
7434
+ if (query == null) {
7120
7435
  throw new Error(`data-${attr} attriblute is empty.`);
7121
7436
  }
7122
- const fieldList = rawValue.split(/\s*,\s*/);
7123
- for (const field of fieldList) {
7437
+ const listRoot = el.closest(`[data-${attr}-list]`);
7438
+ const forceArray = !!listRoot;
7439
+ const fields = parseFields$1(query);
7440
+ for (const field of fields) {
7124
7441
  let value;
7125
- const { fieldName, propName } = fieldNameParser$1(field);
7442
+ const { fieldName, propName } = field;
7126
7443
  if (propName) {
7127
7444
  switch (propName) {
7128
7445
  case 'node': {
@@ -7137,6 +7454,12 @@ function getValues$1(el, convertType = false, attr = 'field', filter) {
7137
7454
  value = el.innerHTML.trim();
7138
7455
  break;
7139
7456
  }
7457
+ case 'style': {
7458
+ if (el instanceof HTMLElement) {
7459
+ value = el.style.cssText;
7460
+ }
7461
+ break;
7462
+ }
7140
7463
  default: {
7141
7464
  if (/^style\([a-z-]+\)$/i.test(propName)) {
7142
7465
  const css = propName.replace(/^style\(([a-z-]+)\)$/i, '$1');
@@ -7153,7 +7476,7 @@ function getValues$1(el, convertType = false, attr = 'field', filter) {
7153
7476
  }
7154
7477
  break;
7155
7478
  }
7156
- value = getAttribute$1(el, attr, propName, convertType);
7479
+ value = getAttribute$1(el, propName, attr, convertType);
7157
7480
  }
7158
7481
  }
7159
7482
  }
@@ -7181,11 +7504,11 @@ function getValues$1(el, convertType = false, attr = 'field', filter) {
7181
7504
  /**
7182
7505
  *
7183
7506
  * @param el
7184
- * @param attr
7185
7507
  * @param keyAttr
7508
+ * @param attr
7186
7509
  * @param typeConvert
7187
7510
  */
7188
- function getAttribute$1(el, attr, keyAttr, typeConvert) {
7511
+ function getAttribute$1(el, keyAttr, attr, typeConvert) {
7189
7512
  switch (keyAttr) {
7190
7513
  case 'contenteditable': {
7191
7514
  if (el instanceof HTMLElement) {
@@ -7207,24 +7530,35 @@ function getAttribute$1(el, attr, keyAttr, typeConvert) {
7207
7530
  return el.hasAttribute('download') ? el.getAttribute('download') : null;
7208
7531
  }
7209
7532
  case 'href': {
7210
- // return (el as HTMLAnchorElement).href;
7211
- return el.getAttribute(keyAttr) ?? ''; // return plain string
7533
+ // Example: (el as HTMLAnchorElement).href;
7534
+ // Expected: return defined value of plain string
7535
+ return el.getAttribute(keyAttr) ?? '';
7212
7536
  }
7213
- default: {
7214
- let value;
7215
- const dataAttr = ['data', attr, kebabCase$1(keyAttr)].join('-');
7216
- if (el.hasAttribute(dataAttr)) {
7217
- value = el.getAttribute(dataAttr) || '';
7218
- }
7219
- else {
7220
- value = el.getAttribute(keyAttr) || '';
7221
- }
7222
- if (typeConvert) {
7223
- value = convert$1(value);
7224
- }
7225
- return value;
7537
+ }
7538
+ let value =
7539
+ // @ts-ignore
7540
+ el[keyAttr];
7541
+ if (value !== undefined) {
7542
+ if (typeConvert) {
7543
+ value = convert$1(value);
7226
7544
  }
7545
+ return value;
7227
7546
  }
7547
+ // For shorthand notation, get value from data-field-* attribute
7548
+ const dataAttr = ['data', attr, kebabCase$1(keyAttr)].join('-');
7549
+ if (el.hasAttribute(dataAttr)) {
7550
+ return el.getAttribute(dataAttr) ?? '';
7551
+ }
7552
+ value = el.getAttribute(keyAttr) ?? null;
7553
+ if ((value === '' || value == null) &&
7554
+ typeof keyAttr === 'string' &&
7555
+ keyAttr.startsWith('data-')) {
7556
+ return '';
7557
+ }
7558
+ if (typeConvert) {
7559
+ value = convert$1(value);
7560
+ }
7561
+ return value;
7228
7562
  }
7229
7563
  /**
7230
7564
  *
@@ -7232,8 +7566,9 @@ function getAttribute$1(el, attr, keyAttr, typeConvert) {
7232
7566
  */
7233
7567
  function convert$1(value) {
7234
7568
  value = parse$1(value);
7235
- if (URL.canParse(value)) {
7236
- const url = new URL(value, location.href);
7569
+ const str = `${value}`;
7570
+ if (URL.canParse(str)) {
7571
+ const url = new URL(str, location.href);
7237
7572
  if (url.origin === location.origin) {
7238
7573
  return url.pathname;
7239
7574
  }
@@ -7246,7 +7581,7 @@ function convert$1(value) {
7246
7581
  */
7247
7582
  function parse$1(value) {
7248
7583
  try {
7249
- return JSON.parse(value);
7584
+ return JSON.parse(`${value}`);
7250
7585
  }
7251
7586
  catch {
7252
7587
  return value;
@@ -7261,6 +7596,7 @@ function getBackgroundImagePath$1(value) {
7261
7596
  return decodeURI(value.replace(/^url\(["']?([^"']+)["']?\)$/i, '$1').replace(origin, ''));
7262
7597
  }
7263
7598
 
7599
+ const reverseListElementSelectors$1 = ['picture'];
7264
7600
  /**
7265
7601
  *
7266
7602
  * @param el
@@ -7268,9 +7604,18 @@ function getBackgroundImagePath$1(value) {
7268
7604
  * @param typeConvert
7269
7605
  * @param filter
7270
7606
  */
7271
- function get$2 (el, attr, typeConvert, filter) {
7272
- // eslint-disable-next-line unicorn/prefer-spread
7273
- const filedElements = Array.from(el.querySelectorAll(`[data-${attr}]`));
7607
+ function getComponent$1(el, attr, typeConvert, filter) {
7608
+ el = el.cloneNode(true);
7609
+ const reverseListElements = el.querySelectorAll(`:is(${reverseListElementSelectors$1.join(',')})[data-${attr}-list]`);
7610
+ for (const reverseListElement of reverseListElements) {
7611
+ const children = [];
7612
+ while (reverseListElement.lastElementChild) {
7613
+ children.push(reverseListElement.lastElementChild);
7614
+ reverseListElement.lastElementChild.remove();
7615
+ }
7616
+ reverseListElement.append(...children);
7617
+ }
7618
+ const filedElements = el.querySelectorAll(`[data-${attr}]`);
7274
7619
  let values = [];
7275
7620
  for (const _el of filedElements) {
7276
7621
  values = [...values, ...getValues$1(_el, typeConvert, attr, filter)];
@@ -7279,59 +7624,105 @@ function get$2 (el, attr, typeConvert, filter) {
7279
7624
  return result;
7280
7625
  }
7281
7626
 
7627
+ /**
7628
+ * Combines multiple field definitions with comma separation
7629
+ * @param fields Array of field definitions
7630
+ * @returns Comma-separated field definition string
7631
+ */
7632
+ function stringifyFields$1(fields) {
7633
+ return fields
7634
+ .map((field) => stringifyField$1(field.fieldName, field.propName))
7635
+ .join(', ');
7636
+ }
7637
+ /**
7638
+ * Generates a field definition string from field name and attribute name
7639
+ * @param fieldName Field name
7640
+ * @param propName Attribute name (optional)
7641
+ * @returns Field definition string
7642
+ */
7643
+ function stringifyField$1(fieldName, propName) {
7644
+ if (!propName) {
7645
+ return fieldName;
7646
+ }
7647
+ // Use shorthand notation when fieldName and propName are the same
7648
+ if (fieldName === propName) {
7649
+ return `:${propName}`;
7650
+ }
7651
+ return `${fieldName}:${propName}`;
7652
+ }
7653
+
7282
7654
  /**
7283
7655
  *
7284
7656
  * @param el
7285
7657
  * @param data
7286
7658
  * @param attr
7287
7659
  * @param filter
7660
+ * @param xssSanitize Enable XSS protection
7288
7661
  */
7289
- function set$3 (el, data, attr, filter) {
7662
+ function setComponent$1(el, data, attr, filter, xssSanitize = true) {
7290
7663
  el = el.cloneNode(true);
7291
7664
  for (const dataKeyName in data) {
7292
- if (dataKeyName in data) {
7293
- const datum = data[dataKeyName];
7294
- const selector = `[data-${attr}*="${kebabCase$1(dataKeyName)}"]`;
7295
- // eslint-disable-next-line unicorn/prefer-spread
7296
- const targetList = Array.from(el.querySelectorAll(selector));
7297
- if (Array.isArray(datum)) {
7298
- const targetEl = targetList[0];
7299
- if (!targetEl) {
7300
- continue;
7301
- }
7302
- const listRoot = targetEl.closest(`[data-${attr}-list]`);
7303
- if (!listRoot || (listRoot && listRoot.children.length === 0)) {
7304
- continue;
7305
- }
7306
- const listItem = listRoot.children[0]?.cloneNode(true);
7307
- while (listItem && datum.length > listRoot.children.length) {
7308
- listRoot.append(listItem.cloneNode(true));
7309
- }
7310
- // eslint-disable-next-line unicorn/prefer-spread
7311
- const newChildren = Array.from(listRoot.querySelectorAll(selector));
7312
- // eslint-disable-next-line unicorn/prefer-spread
7313
- const oldChildList = Array.from(listRoot.children);
7314
- let deleteNodeList = [];
7315
- for (const [i, child] of [...newChildren].entries()) {
7316
- if (datum[i] == null) {
7317
- setValue$1(child, dataKeyName, '', attr, filter);
7318
- const oldChild = oldChildList[i];
7319
- if (oldChild) {
7320
- deleteNodeList.push(oldChild);
7321
- }
7665
+ const datum = data[dataKeyName];
7666
+ if (Array.isArray(datum)) {
7667
+ continue;
7668
+ }
7669
+ const targetList = [
7670
+ // Include self
7671
+ el,
7672
+ // Perform rough filtering for performance
7673
+ ...el.querySelectorAll(`[data-${attr}*="${kebabCase$1(dataKeyName)}"]`),
7674
+ ]
7675
+ // Perform more accurate filtering
7676
+ .filter((el) => hasField$1(el, attr, dataKeyName));
7677
+ for (const targetEl of targetList) {
7678
+ setValue$1(targetEl, dataKeyName, datum, attr, filter, xssSanitize);
7679
+ }
7680
+ }
7681
+ for (const listRoot of el.querySelectorAll(`[data-${attr}-list]`)) {
7682
+ const decendants = listRoot.querySelectorAll('*');
7683
+ const definedFieldsOfDecendants = new Set([...decendants].flatMap((el) => definedFields$1(el, attr)));
7684
+ const maxLength = maxLengthOf$1(data, [...definedFieldsOfDecendants]);
7685
+ const listItem = listRoot.children[0]?.cloneNode(true);
7686
+ if (!listItem) {
7687
+ continue;
7688
+ }
7689
+ while (listRoot.firstChild) {
7690
+ listRoot.firstChild.remove();
7691
+ }
7692
+ for (let i = 0; i < maxLength; i++) {
7693
+ let item = listItem.cloneNode(true);
7694
+ const itemData = flattenData$1(data, i);
7695
+ let reverse = false;
7696
+ switch (listRoot.localName) {
7697
+ case 'picture': {
7698
+ reverse = true;
7699
+ let fields = getFields$1(item, attr);
7700
+ if (i === 0) {
7701
+ // Convert first item to img element
7702
+ item = replaceNode$1(item, 'img', attr, xssSanitize);
7703
+ fields = replaceProp$1(fields, 'srcset', 'src');
7704
+ fields = removeProp$1(fields, 'sizes');
7705
+ const fieldQuery = stringifyFields$1(fields);
7706
+ item.setAttribute(`data-${attr}`, fieldQuery);
7322
7707
  }
7323
7708
  else {
7324
- setValue$1(child, dataKeyName, datum[i], attr, filter);
7325
- deleteNodeList = [];
7709
+ // Convert subsequent items to source elements
7710
+ item = replaceNode$1(item, 'source', attr, xssSanitize);
7711
+ fields = replaceProp$1(fields, 'src', 'srcset');
7712
+ fields = removeProp$1(fields, 'alt');
7713
+ fields = removeProp$1(fields, 'loading');
7714
+ const fieldQuery = stringifyFields$1(fields);
7715
+ item.setAttribute(`data-${attr}`, fieldQuery);
7326
7716
  }
7717
+ break;
7327
7718
  }
7328
- for (const node of deleteNodeList)
7329
- node.remove();
7719
+ }
7720
+ const newEl = setComponent$1(item, itemData, attr, filter, xssSanitize);
7721
+ if (reverse) {
7722
+ listRoot.prepend(newEl);
7330
7723
  }
7331
7724
  else {
7332
- for (const [, targetEl] of [...targetList].entries()) {
7333
- setValue$1(targetEl, dataKeyName, datum, attr, filter);
7334
- }
7725
+ listRoot.append(newEl);
7335
7726
  }
7336
7727
  }
7337
7728
  }
@@ -7346,6 +7737,10 @@ let FrozenPatty$1 = class FrozenPatty {
7346
7737
  */
7347
7738
  #filter;
7348
7739
  #typeConvert = false;
7740
+ /**
7741
+ * Enable XSS protection
7742
+ */
7743
+ #xssSanitize = true;
7349
7744
  /**
7350
7745
  *
7351
7746
  * @param html Original HTML
@@ -7353,7 +7748,14 @@ let FrozenPatty$1 = class FrozenPatty {
7353
7748
  */
7354
7749
  constructor(html, options) {
7355
7750
  this.#dom = document.createElement('fp-placeholer');
7356
- this.#dom.innerHTML = html;
7751
+ // Sanitize initial HTML if XSS protection is enabled
7752
+ if (options?.xssSanitize === false) {
7753
+ this.#dom.innerHTML = html;
7754
+ this.#xssSanitize = false;
7755
+ }
7756
+ else {
7757
+ this.#dom.innerHTML = sanitizeHtml$1(html);
7758
+ }
7357
7759
  if (options) {
7358
7760
  if (options.attr) {
7359
7761
  this.#attr = options.attr;
@@ -7365,7 +7767,8 @@ let FrozenPatty$1 = class FrozenPatty {
7365
7767
  merge(data) {
7366
7768
  const currentData = this.toJSON(false);
7367
7769
  const newData = Object.assign(currentData, data);
7368
- this.#dom = set$3(this.#dom, newData, this.#attr, this.#filter);
7770
+ // Pass XSS protection flag to set (default is true if not set)
7771
+ this.#dom = setComponent$1(this.#dom, newData, this.#attr, this.#filter, this.#xssSanitize);
7369
7772
  return this;
7370
7773
  }
7371
7774
  toDOM() {
@@ -7376,7 +7779,7 @@ let FrozenPatty$1 = class FrozenPatty {
7376
7779
  }
7377
7780
  toJSON(filtering = true) {
7378
7781
  const filter = filtering ? this.#filter : undefined;
7379
- return get$2(this.#dom, this.#attr, this.#typeConvert, filter);
7782
+ return getComponent$1(this.#dom, this.#attr, this.#typeConvert, filter);
7380
7783
  }
7381
7784
  };
7382
7785
 
@@ -7595,7 +7998,7 @@ var style$9 = ".bgi-btn-container {\n\ttext-align: center;\n}\n\n.bgi-btn {\n\ta
7595
7998
  var template$A = "<div class=\"bgi-btn-container\" data-bgi-button-kind=\"link\" data-bge=\"kind:data-bgi-button-kind\">\n\t<a class=\"bgi-btn\" href=\"\" data-bge=\"link:href, target:target\">\n\t\t<span class=\"bgi-btn__text\" data-bge=\"text\">ボタン</span>\n\t</a>\n</div>\n";
7596
7999
 
7597
8000
  var button = createItem({
7598
- version: "4.0.0-alpha.1",
8001
+ version: "4.0.0-alpha.3",
7599
8002
  name: "button",
7600
8003
  template: template$A,
7601
8004
  style: style$9,
@@ -7609,7 +8012,7 @@ var style$8 = ".bgi-link__size {\n\t&::before {\n\t\tcontent: '(';\n\t}\n\n\t&::
7609
8012
  var template$z = "<div class=\"bgi-download-file\">\n\t<a class=\"bgi-download-file__link\" href=\"./files/bgeditor/bg-sample.pdf\" target=\"_blank\" data-bge=\"path:href, download:download\">\n\t\t<span class=\"bgi-link__icon bgi-link__icon--before\" role=\"none\"></span>\n\t\t<span class=\"bgi-link__name\" data-bge=\"name\">サンプルダウンロードファイル</span>\n\t\t<span class=\"bgi-link__size\" data-bge=\"formated-size, size:data-size\" data-size=\"138158\">134.92kB</span>\n\t\t<span class=\"bgi-link__icon bgi-link__icon--after\" role=\"none\"></span>\n\t</a>\n</div>\n";
7610
8013
 
7611
8014
  var downloadFile = createItem({
7612
- version: "4.0.0-alpha.1",
8015
+ version: "4.0.0-alpha.3",
7613
8016
  name: "download-file",
7614
8017
  template: template$z,
7615
8018
  style: style$8,
@@ -7648,7 +8051,7 @@ var style$7 = "[data-bgi='google-maps'] {\n\tdiv {\n\t\tinline-size: 100%;\n\t\t
7648
8051
  var template$y = "<div data-lat=\"35.681382\" data-lng=\"139.766084\" data-zoom=\"16\" data-bge=\"lat:data-lat, lng:data-lng, zoom:data-zoom\">\n\t<img data-bge=\"img:src\" src=\"https://maps.google.com/maps/api/staticmap?center=35.681382,139.766084&amp;zoom=16&amp;size=640x400&amp;markers=color:red|color:red|35.681382,139.766084&amp;scale=2&amp;key=%googleMapsApiKey%\" width=\"8\" height=\"5\" alt=\"Google Maps\" />\n</div>\n<a href=\"https://maps.apple.com/?q=35.681382,139.766084\" data-bge=\"url:href\" target=\"_blank\"><span>アプリで開く</span></a>\n";
7649
8052
 
7650
8053
  var googleMaps = createItem({
7651
- version: "4.0.0-alpha.1",
8054
+ version: "4.0.0-alpha.3",
7652
8055
  name: "google-maps",
7653
8056
  template: template$y,
7654
8057
  style: style$7,
@@ -7783,7 +8186,7 @@ var style$6 = "[data-bgi='hr'] {\n\t--border-color: #000;\n\t--border-width: 1px
7783
8186
  var template$x = "<div class=\"bgi-hr-container\" data-bgi-hr-kind=\"primary\" data-bge=\"kind:data-bgi-hr-kind\">\n\t<hr class=\"bgi-hr\" />\n</div>\n";
7784
8187
 
7785
8188
  var hr$1 = createItem({
7786
- version: "4.0.0-alpha.1",
8189
+ version: "4.0.0-alpha.3",
7787
8190
  name: "hr",
7788
8191
  template: template$x,
7789
8192
  style: style$6,
@@ -7803,22 +8206,22 @@ var hr$1 = createItem({
7803
8206
  }
7804
8207
  });
7805
8208
 
7806
- var editor$5 = "<div data-bge-dialog=\"2col\">\n\t<div data-bge-dialog-ui=\"sticky\">\n\t\t<input type=\"hidden\" name=\"bge-width\" />\n\t\t<input type=\"hidden\" name=\"bge-height\" />\n\t\t<input type=\"hidden\" name=\"bge-srcset\" />\n\t\t<input type=\"hidden\" name=\"bge-style\" />\n\n\t\t<div data-bge-editor-ui=\"preview\"></div>\n\t\t<input type=\"hidden\" name=\"bge-path\" />\n\t\t<input type=\"hidden\" name=\"bge-empty\" />\n\t\t<input type=\"hidden\" name=\"bge-file-size\" />\n\n\t\t<div>\n\t\t\t<label>\n\t\t\t\t<span>画像の代替テキスト(alt)</span>\n\t\t\t\t<input type=\"text\" name=\"bge-alt\" />\n\t\t\t</label>\n\t\t\t<label>\n\t\t\t\t<span>キャプション</span>\n\t\t\t\t<input type=\"text\" name=\"bge-caption\" />\n\t\t\t</label>\n\t\t\t<fieldset>\n\t\t\t\t<legend>画像のサイズ</legend>\n\t\t\t\t<div role=\"radiogroup\" aria-labelledby=\"bgi-image__radio-group1\">\n\t\t\t\t\t<div id=\"bgi-image__radio-group1\">基準</div>\n\t\t\t\t\t<label><input type=\"radio\" name=\"bge-scale-type\" value=\"container\" /><span>基準</span></label>\n\t\t\t\t\t<label><input type=\"radio\" name=\"bge-scale-type\" value=\"original\" checked />画像基準</label>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<span>\n\t\t\t\t\t\t<label for=\"bgi-image__range\">幅</label>\n\t\t\t\t\t\t<output name=\"bge-css-width\"></output>\n\t\t\t\t\t</span>\n\t\t\t\t\t<input id=\"bgi-image__range\" type=\"range\" name=\"bge-scale\" min=\"1\" max=\"100\" step=\"1\" value=\"100\" />\n\t\t\t\t</div>\n\t\t\t\t<hr />\n\t\t\t\t<div role=\"radiogroup\" aria-labelledby=\"bgi-image__radio-group2\">\n\t\t\t\t\t<div id=\"bgi-image__radio-group2\">縦横比</div>\n\t\t\t\t\t<label><input type=\"radio\" name=\"bge-aspect-ratio\" value=\"unset\" checked />オリジナル</label>\n\t\t\t\t\t<label><input type=\"radio\" name=\"bge-aspect-ratio\" value=\"1/1\" />1 : 1</label>\n\t\t\t\t\t<label><input type=\"radio\" name=\"bge-aspect-ratio\" value=\"4/3\" />4 : 3</label>\n\t\t\t\t\t<label><input type=\"radio\" name=\"bge-aspect-ratio\" value=\"16/9\" />16 : 9</label>\n\t\t\t\t</div>\n\t\t\t</fieldset>\n\t\t\t<fieldset>\n\t\t\t\t<legend>リンク</legend>\n\t\t\t\t<label><input type=\"checkbox\" name=\"bge-popup\" />ポップアップで画像を開く</label>\n\t\t\t\t<label>\n\t\t\t\t\t<span>リンク先URL</span>\n\t\t\t\t\t<input type=\"url\" name=\"bge-href\" />\n\t\t\t\t</label>\n\t\t\t\t<label><input type=\"checkbox\" name=\"bge-target-blank\" />別タブで開く</label>\n\t\t\t</fieldset>\n\t\t\t<label><input type=\"checkbox\" name=\"bge-lazy\" checked aria-describedby=\"bge-lazy-desc\" />遅延読み込み</label>\n\t\t\t<small id=\"bge-lazy-desc\">画像がブラウザの表示エリアに現れるまでファイルを読み込みません。</small>\n\t\t</div>\n\t</div>\n\t<div>\n\t\t<div data-bge-editor-ui=\"imageUploader\"></div>\n\t\t<div data-bge-editor-ui=\"imageList\"></div>\n\t</div>\n</div>\n";
8209
+ var editor$5 = "<div data-bge-dialog=\"2col\">\n\t<div data-bge-dialog-ui=\"sticky\">\n\t\t<div>\n\t\t\t<div data-bge-editor-ui=\"tabs\" data-bge-editor-ui-for=\"bgi-image__tabs-content\"></div>\n\n\t\t\t<div id=\"bgi-image__tabs-content\" role=\"tabpanel\" aria-label=\"画像\">\n\t\t\t\t<div data-bge-editor-ui=\"preview\"></div>\n\t\t\t\t<input type=\"hidden\" name=\"bge-path[]\" />\n\t\t\t\t<input type=\"hidden\" name=\"bge-alt[]\" />\n\t\t\t\t<input type=\"hidden\" name=\"bge-width[]\" />\n\t\t\t\t<input type=\"hidden\" name=\"bge-height[]\" />\n\t\t\t\t<input type=\"hidden\" name=\"bge-media[]\" />\n\n\t\t\t\t<input type=\"hidden\" name=\"bge-file-size\" />\n\t\t\t\t<div>\n\t\t\t\t\t<label>\n\t\t\t\t\t\t<span>メディアクエリー</span>\n\t\t\t\t\t\t<input type=\"text\" name=\"bge-media-input\" />\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div>\n\t\t\t<fieldset>\n\t\t\t\t<legend>画像のサイズ</legend>\n\t\t\t\t<div role=\"radiogroup\" aria-labelledby=\"bgi-image__radio-group1\">\n\t\t\t\t\t<div id=\"bgi-image__radio-group1\">基準</div>\n\t\t\t\t\t<label><input type=\"radio\" name=\"bge-scale-type\" value=\"container\" /><span>基準</span></label>\n\t\t\t\t\t<label><input type=\"radio\" name=\"bge-scale-type\" value=\"original\" checked />画像基準</label>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<span>\n\t\t\t\t\t\t<label for=\"bgi-image__range\">幅</label>\n\t\t\t\t\t\t<output name=\"bge-css-width\"></output>\n\t\t\t\t\t</span>\n\t\t\t\t\t<input id=\"bgi-image__range\" type=\"range\" name=\"bge-scale\" min=\"1\" max=\"100\" step=\"1\" value=\"100\" />\n\t\t\t\t</div>\n\t\t\t\t<hr />\n\t\t\t\t<div role=\"radiogroup\" aria-labelledby=\"bgi-image__radio-group2\">\n\t\t\t\t\t<div id=\"bgi-image__radio-group2\">縦横比</div>\n\t\t\t\t\t<label><input type=\"radio\" name=\"bge-aspect-ratio\" value=\"unset\" checked />オリジナル</label>\n\t\t\t\t\t<label><input type=\"radio\" name=\"bge-aspect-ratio\" value=\"1/1\" />1 : 1</label>\n\t\t\t\t\t<label><input type=\"radio\" name=\"bge-aspect-ratio\" value=\"4/3\" />4 : 3</label>\n\t\t\t\t\t<label><input type=\"radio\" name=\"bge-aspect-ratio\" value=\"16/9\" />16 : 9</label>\n\t\t\t\t</div>\n\t\t\t</fieldset>\n\t\t\t<label>\n\t\t\t\t<span>画像の代替テキスト(alt)</span>\n\t\t\t\t<input type=\"text\" name=\"bge-alt\" />\n\t\t\t</label>\n\t\t\t<label>\n\t\t\t\t<span>キャプション</span>\n\t\t\t\t<input type=\"text\" name=\"bge-caption\" />\n\t\t\t</label>\n\t\t\t<fieldset>\n\t\t\t\t<legend>リンク</legend>\n\t\t\t\t<label><input type=\"checkbox\" name=\"bge-popup\" />ポップアップで画像を開く</label>\n\t\t\t\t<label>\n\t\t\t\t\t<span>リンク先URL</span>\n\t\t\t\t\t<input type=\"url\" name=\"bge-href\" />\n\t\t\t\t</label>\n\t\t\t\t<label><input type=\"checkbox\" name=\"bge-target-blank\" />別タブで開く</label>\n\t\t\t</fieldset>\n\t\t\t<label><input type=\"checkbox\" name=\"bge-lazy\" checked aria-describedby=\"bge-lazy-desc\" />遅延読み込み</label>\n\t\t\t<small id=\"bge-lazy-desc\">画像がブラウザの表示エリアに現れるまでファイルを読み込みません。</small>\n\t\t</div>\n\t</div>\n\t<div>\n\t\t<div data-bge-editor-ui=\"imageUploader\"></div>\n\t\t<div data-bge-editor-ui=\"imageList\"></div>\n\t</div>\n</div>\n";
7807
8210
 
7808
8211
  var style$5 = "[data-bgi='image'] {\n\tinline-size: 100%;\n\n\tfigure {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tinline-size: 100%;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\n\t\t> :is(div, a, button) {\n\t\t\tdisplay: block;\n\t\t\tpadding: 0;\n\t\t\tbackground: transparent;\n\t\t\tborder: none;\n\t\t}\n\t}\n\n\timg {\n\t\tdisplay: block;\n\t\tinline-size: var(--css-width, auto);\n\t\tmax-inline-size: 100%;\n\t\tblock-size: auto;\n\t\taspect-ratio: var(--aspect-ratio, unset);\n\t\tobject-fit: var(--object-fit, unset);\n\t}\n\n\tfigcaption {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tinline-size: 100%;\n\n\t\t&:empty {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n}\n";
7809
8212
 
7810
- var template$w = "<div data-bge=\":style, :scale, :scale-type, :aspect-ratio\" data-bge-scale=\"100\" data-bge-scale-type=\"container\" data-bge-aspect-ratio=\"unset\" style=\"--css-width: 100cqi; --object-fit: cover; --aspect-ratio: unset\">\n\t<figure>\n\t\t<div data-bge=\":node, :href, :target, :command\">\n\t\t\t<img src=\"%sampleImagePath%\" alt=\"サンプル画像\" data-bge=\"path:src, :srcset, :alt, :width, :height, :loading\" width=\"400\" height=\"300\" loading=\"lazy\" />\n\t\t</div>\n\t\t<figcaption data-bge=\"caption\"></figcaption>\n\t</figure>\n</div>\n";
8213
+ var template$w = "<div data-bge=\":style, :scale, :scale-type, :aspect-ratio\" data-bge-scale=\"100\" data-bge-scale-type=\"container\" data-bge-aspect-ratio=\"unset\" style=\"--css-width: 100cqi; --object-fit: cover; --aspect-ratio: unset\">\n\t<figure>\n\t\t<div data-bge=\":node, :href, :target, :command\">\n\t\t\t<picture data-bge-list>\n\t\t\t\t<img src=\"%sampleImagePath%\" alt=\"サンプル画像\" data-bge=\"path:src, :alt, :width, :height, :loading, :media\" width=\"400\" height=\"300\" loading=\"lazy\" />\n\t\t\t</picture>\n\t\t</div>\n\t\t<figcaption data-bge=\"caption\"></figcaption>\n\t</figure>\n</div>\n";
7811
8214
 
7812
8215
  const ORIGIN = "__org";
7813
8216
  var image = createItem({
7814
- version: "4.0.0-alpha.1",
8217
+ version: "4.0.0-alpha.3",
7815
8218
  name: "image",
7816
8219
  template: template$w,
7817
8220
  style: style$5,
7818
8221
  editor: editor$5,
7819
8222
  editorOptions: {
7820
8223
  beforeOpen(data) {
7821
- const path = data.path.replace(ORIGIN, "");
8224
+ const path = data.path.map((p) => p.replace(ORIGIN, ""));
7822
8225
  const lazy = data.loading === "lazy";
7823
8226
  const popup = data.node === "button" && data.command === "show-modal";
7824
8227
  const targetBlank = data.node === "a" && data.target === "_blank";
@@ -7830,48 +8233,71 @@ var image = createItem({
7830
8233
  targetBlank
7831
8234
  };
7832
8235
  },
7833
- open(data, editor2) {
7834
- editor2.engine.componentObserver.notify("file-select", {
7835
- path: data.path,
7836
- fileSize: Number.parseFloat(data.fileSize ?? "0"),
7837
- isEmpty: data.path === "",
7838
- isMounted: false
7839
- });
8236
+ open(_, editor2) {
8237
+ let currentIndex = 0;
8238
+ fileSelect();
8239
+ function fileSelect() {
8240
+ editor2.engine.componentObserver.notify("file-select", {
8241
+ path: editor2.get("$path")[currentIndex],
8242
+ fileSize: Number.parseFloat(editor2.get("$fileSize") ?? "0"),
8243
+ isEmpty: editor2.get("$path")[currentIndex] === "",
8244
+ isMounted: false
8245
+ });
8246
+ }
7840
8247
  editor2.engine.componentObserver.on("file-select", ({ path, isEmpty }) => {
7841
8248
  if (isEmpty) {
7842
8249
  return;
7843
8250
  }
7844
- const { src, origin } = originImage(path);
7845
- void Promise.all([loadImage(src), origin ? loadImage(origin) : null]).then(
7846
- ([$src, $origin]) => {
7847
- if (!$src) {
7848
- editor2.update("$path", src);
7849
- return;
7850
- }
7851
- if ($origin) {
7852
- editor2.update("$path", $origin.src);
7853
- editor2.update("$srcset", `${$src.src}, ${$origin.src} 2x`);
7854
- editor2.update("$width", $origin.width);
7855
- editor2.update("$height", $origin.height);
7856
- updateCSSWidth();
7857
- return;
7858
- }
7859
- editor2.update("$path", $src.src);
7860
- editor2.update("$width", $src.width);
7861
- editor2.update("$height", $src.height);
7862
- updateCSSWidth();
7863
- }
7864
- );
8251
+ void _updateImage(path);
8252
+ });
8253
+ async function _updateImage(path) {
8254
+ const $src = await loadImage(path);
8255
+ updateImage($src);
8256
+ }
8257
+ function updateImage($src) {
8258
+ if (!$src) {
8259
+ console.error("\u753B\u50CF\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
8260
+ return;
8261
+ }
8262
+ const path = [...editor2.get("$path")];
8263
+ path[currentIndex] = $src.src;
8264
+ editor2.update("$path", path);
8265
+ const width = [...editor2.get("$width")];
8266
+ width[currentIndex] = $src.width;
8267
+ editor2.update("$width", width);
8268
+ const height = [...editor2.get("$height")];
8269
+ height[currentIndex] = $src.height;
8270
+ editor2.update("$height", height);
8271
+ const media = [...editor2.get("$media")];
8272
+ media[currentIndex] = editor2.get("$mediaInput");
8273
+ editor2.update("$media", media);
8274
+ updateCSSWidth();
8275
+ }
8276
+ editor2.engine.componentObserver.on("select-tab-in-item-editor", ({ index }) => {
8277
+ currentIndex = index;
8278
+ fileSelect();
8279
+ void _updateImage(editor2.get("$path")[currentIndex]);
8280
+ const media = editor2.get("$media")[currentIndex];
8281
+ editor2.disable("$mediaInput", currentIndex === 0);
8282
+ editor2.update("$mediaInput", media);
7865
8283
  });
7866
8284
  editor2.onChange("$scale", updateCSSWidth);
7867
8285
  editor2.onChange("$scaleType", updateCSSWidth);
8286
+ editor2.onChange("$mediaInput", (value) => {
8287
+ const media = [...editor2.get("$media")];
8288
+ media[currentIndex] = value;
8289
+ editor2.update("$media", media);
8290
+ });
7868
8291
  function updateCSSWidth() {
7869
8292
  const scale = editor2.get("$scale");
7870
8293
  const width = editor2.get("$width");
7871
8294
  const scaleType = editor2.get("$scaleType");
7872
8295
  editor2.update(
7873
8296
  "$cssWidth",
7874
- scaleType === "container" ? `${scale}cqi` : `${Math.round(width * scale / 100)}px`
8297
+ scaleType === "container" ? `${scale}cqi` : (
8298
+ // TODO: 複数画像の場合は、最初の画像の幅を使用するか、それともすべての画像の幅を使用するか検討
8299
+ `${Math.round(width[0] * scale / 100)}px`
8300
+ )
7875
8301
  );
7876
8302
  }
7877
8303
  editor2.onChange("$popup", (disable) => {
@@ -7929,20 +8355,6 @@ async function loadImage(src) {
7929
8355
  }, 3e4);
7930
8356
  });
7931
8357
  }
7932
- function originImage(src) {
7933
- const filePath = src.match(/^(.*)(\.(?:jpe?g|gif|png|webp))$/i);
7934
- if (filePath) {
7935
- const [, name, ext] = filePath;
7936
- return {
7937
- src,
7938
- origin: `${name}${ORIGIN}${ext}`
7939
- };
7940
- }
7941
- return {
7942
- src,
7943
- origin: null
7944
- };
7945
- }
7946
8358
 
7947
8359
  var editor$4 = "<div data-bge-dialog=\"wide\">\n\t<div>\n\t\t<label>\n\t\t\t<span>表見出し</span>\n\t\t\t<input type=\"text\" name=\"bge-caption\" />\n\t\t</label>\n\t</div>\n\n\t<div data-bge-editor-ui=\"tableEditor\"></div>\n\t<input type=\"hidden\" name=\"bge\" />\n</div>\n";
7948
8360
 
@@ -7951,7 +8363,7 @@ var style$4 = ".bge-type-table {\n\tmargin: 0;\n\n\tth {\n\t\tinline-size: calc(
7951
8363
  var template$v = "<table>\n\t<caption data-bge=\"caption\">\n\t\tキャプションを入力してください\n\t</caption>\n\t<tbody data-bge-list>\n\t\t<tr>\n\t\t\t<th data-bge=\"th\">表組の見出し</th>\n\t\t\t<td data-bge=\"td\">表組の内容を入力してください</td>\n\t\t</tr>\n\t</tbody>\n</table>\n";
7952
8364
 
7953
8365
  var table = createItem({
7954
- version: "4.0.0-alpha.1",
8366
+ version: "4.0.0-alpha.3",
7955
8367
  name: "table",
7956
8368
  template: template$v,
7957
8369
  style: style$4,
@@ -7979,7 +8391,7 @@ var style$3 = ".bge-title-h2 {\n\tmargin-block-end: 0;\n}\n";
7979
8391
  var template$u = "<h2 class=\"bge-title-h2\" data-bge=\"title-h2\">見出しを入力してください</h2>\n";
7980
8392
 
7981
8393
  var titleH2 = createItem({
7982
- version: "4.0.0-alpha.1",
8394
+ version: "4.0.0-alpha.3",
7983
8395
  name: "title-h2",
7984
8396
  template: template$u,
7985
8397
  style: style$3,
@@ -7993,7 +8405,7 @@ var style$2 = ".bge-title-h3 {\n\tmargin-block-end: 0;\n}\n";
7993
8405
  var template$t = "<h2 class=\"bge-title-h3\" data-bge=\"title-h3\">見出しを入力してください</h2>\n";
7994
8406
 
7995
8407
  var titleH3 = createItem({
7996
- version: "4.0.0-alpha.1",
8408
+ version: "4.0.0-alpha.3",
7997
8409
  name: "title-h3",
7998
8410
  template: template$t,
7999
8411
  style: style$2,
@@ -8007,7 +8419,7 @@ var style$1 = "/* No Styling */\n";
8007
8419
  var template$s = "<div class=\"bge-wysiwyg\" data-bge=\"wysiwyg\"><p>本文を入力してください</p></div>\n";
8008
8420
 
8009
8421
  var wysiwyg = createItem({
8010
- version: "4.0.0-alpha.1",
8422
+ version: "4.0.0-alpha.3",
8011
8423
  name: "wysiwyg",
8012
8424
  template: template$s,
8013
8425
  style: style$1,
@@ -8058,7 +8470,7 @@ var template$r = "<div data-id=\"3KtWfp0UopM\" data-title=\"YouTube動画\" data
8058
8470
 
8059
8471
  const FALLBACK_TITLE = "YouTube\u52D5\u753B";
8060
8472
  var youtube = createItem({
8061
- version: "4.0.0-alpha.1",
8473
+ version: "4.0.0-alpha.3",
8062
8474
  name: "youtube",
8063
8475
  template: template$r,
8064
8476
  style,
@@ -8120,7 +8532,8 @@ function importItems(template) {
8120
8532
  try {
8121
8533
  const data = attr ? JSON.parse(`${attr}`) : {};
8122
8534
  html = dataToHtml$1(typedItem.template, data);
8123
- } catch {
8535
+ } catch (error) {
8536
+ console.error(error);
8124
8537
  throw new Error(`${typedItem.name}\u306E\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u30A4\u30F3\u30DD\u30FC\u30C8\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002`);
8125
8538
  }
8126
8539
  return `<div data-bgi="${typedItem.name}" data-bgi-ver="${typedItem.version}">${html}</div>`;
@@ -8211,7 +8624,7 @@ const blockTemplate$j = {
8211
8624
 
8212
8625
  var icon$i = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"17.85mm\" height=\"13.87mm\" viewBox=\"0 0 50.59 39.31\"><defs><clipPath id=\"a14548ce-d842-4fd8-917c-74fc065283e0\"><rect width=\"50.59\" height=\"39.31\" fill=\"none\"/></clipPath><clipPath id=\"a760b4d9-d8b5-4ed5-a648-35d410384b4b\"><rect width=\"50.59\" height=\"37.5\" rx=\"5.06\" fill=\"none\"/></clipPath><linearGradient id=\"f1b0e6a1-76a5-46d8-9e26-0d5144fc95c2\" x1=\"187.4\" y1=\"214.88\" x2=\"188.39\" y2=\"214.88\" gradientTransform=\"matrix(0, -72.25, -72.25, 0, 15550.6, 13581.17)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\"0\" stop-color=\"#d9e7a4\"/><stop offset=\"1\" stop-color=\"#b2d57b\"/></linearGradient></defs><title>image1</title><g id=\"f1bc02b5-2ece-4cc1-abc8-0695f8a7f32b\" data-name=\"レイヤー 2\"><g id=\"ee57bbe4-f1e8-4009-96f2-c3c0f25e8376\" data-name=\"レイヤー1\"><g clip-path=\"url(#a14548ce-d842-4fd8-917c-74fc065283e0)\"><rect y=\"1.81\" width=\"50.59\" height=\"37.5\" rx=\"5.06\" fill=\"#b7c678\"/></g><g clip-path=\"url(#a760b4d9-d8b5-4ed5-a648-35d410384b4b)\"><rect width=\"50.59\" height=\"37.5\" fill=\"url(#f1b0e6a1-76a5-46d8-9e26-0d5144fc95c2)\"/></g><g clip-path=\"url(#a14548ce-d842-4fd8-917c-74fc065283e0)\"><rect x=\"9.75\" y=\"12.22\" width=\"31.09\" height=\"14.41\" fill=\"#7aaf39\"/><polygon points=\"40.84 26.63 9.75 26.63 40.84 12.22 40.84 26.63\" fill=\"#649432\"/></g></g></g></svg>";
8213
8626
 
8214
- var template$i = "<div data-bge-container=\"grid\">\n\t<div data-bge-group>\n\t\t<div data-bge-item><!-- image --></div>\n\t</div>\n\t<div data-bge-group>\n\t\t<div data-bge-item><!-- image --></div>\n\t</div>\n</div>\n";
8627
+ var template$i = "<div data-bge-container=\"grid:1\">\n\t<div data-bge-group>\n\t\t<div data-bge-item><!-- image --></div>\n\t</div>\n</div>\n";
8215
8628
 
8216
8629
  const blockTemplate$i = {
8217
8630
  name: "image",
@@ -11012,14 +11425,14 @@ _Lexer.lex;
11012
11425
  * Set up window for Node.js
11013
11426
  */
11014
11427
 
11015
- var root$b = (typeof window !== 'undefined' ? window : {});
11428
+ var root$c = (typeof window !== 'undefined' ? window : {});
11016
11429
 
11017
11430
  /*
11018
11431
  * Parsing HTML strings
11019
11432
  */
11020
11433
 
11021
11434
  function canParseHTMLNatively () {
11022
- var Parser = root$b.DOMParser;
11435
+ var Parser = root$c.DOMParser;
11023
11436
  var canParse = false;
11024
11437
 
11025
11438
  // Adapted from https://gist.github.com/1129031
@@ -11065,12 +11478,12 @@ function shouldUseActiveX () {
11065
11478
  try {
11066
11479
  document.implementation.createHTMLDocument('').open();
11067
11480
  } catch (e) {
11068
- if (root$b.ActiveXObject) useActiveX = true;
11481
+ if (root$c.ActiveXObject) useActiveX = true;
11069
11482
  }
11070
11483
  return useActiveX
11071
11484
  }
11072
11485
 
11073
- canParseHTMLNatively() ? root$b.DOMParser : createHTMLParser();
11486
+ canParseHTMLNatively() ? root$c.DOMParser : createHTMLParser();
11074
11487
 
11075
11488
  function getDefaultExportFromCjs (x) {
11076
11489
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
@@ -14642,6 +15055,20 @@ function createBgeEvent(type, eventData) {
14642
15055
  return new CustomEvent(type, { detail: eventData });
14643
15056
  }
14644
15057
 
15058
+ /**
15059
+ * List of dangerous HTML elements to block for XSS protection
15060
+ */
15061
+ const DANGEROUS_ELEMENTS = [
15062
+ 'script',
15063
+ 'style',
15064
+ 'template',
15065
+ 'object',
15066
+ 'embed',
15067
+ 'iframe',
15068
+ 'frame',
15069
+ 'frameset',
15070
+ 'applet',
15071
+ ];
14645
15072
  /**
14646
15073
  *
14647
15074
  * @param kvs
@@ -14665,31 +15092,302 @@ function arrayToHash(kvs) {
14665
15092
  }
14666
15093
  return result;
14667
15094
  }
15095
+ const kebabCaseCache = new Map();
14668
15096
  /**
14669
15097
  *
14670
15098
  * @param str
14671
15099
  */
14672
- function kebabCase(str) {
14673
- return str.replaceAll(/([A-Z])/g, '-$1').toLowerCase();
15100
+ function kebabCase(str) {
15101
+ if (kebabCaseCache.has(str)) {
15102
+ return kebabCaseCache.get(str);
15103
+ }
15104
+ const result = str.replaceAll(/([A-Z])/g, '-$1').toLowerCase();
15105
+ kebabCaseCache.set(str, result);
15106
+ return result;
15107
+ }
15108
+ const camelCaseCache = new Map();
15109
+ /**
15110
+ *
15111
+ * @param str
15112
+ */
15113
+ function camelCase(str) {
15114
+ if (camelCaseCache.has(str)) {
15115
+ return camelCaseCache.get(str);
15116
+ }
15117
+ const result = str.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
15118
+ camelCaseCache.set(str, result);
15119
+ return result;
15120
+ }
15121
+ /**
15122
+ *
15123
+ * @param el
15124
+ * @param nodeName
15125
+ * @param attr
15126
+ * @param xssSanitize
15127
+ */
15128
+ function replaceNode(el, nodeName, attr, xssSanitize = true) {
15129
+ nodeName = nodeName.toLowerCase();
15130
+ const currentName = el.localName ?? el.nodeName.toLowerCase();
15131
+ if (currentName === nodeName) {
15132
+ return el;
15133
+ }
15134
+ // Check element name if XSS protection is enabled
15135
+ if (xssSanitize && // Disallow dangerous elements
15136
+ DANGEROUS_ELEMENTS.includes(nodeName)) {
15137
+ return el;
15138
+ }
15139
+ const node = (el.ownerDocument ?? document).createElement(nodeName);
15140
+ for (const child of el.childNodes) {
15141
+ node.append(child.cloneNode(true));
15142
+ }
15143
+ const fields = getFields(el, attr);
15144
+ const attrs = new Set(fields.map((field) => field.propName));
15145
+ for (const { name, value } of el.attributes) {
15146
+ if (attrs.has(name)) {
15147
+ continue;
15148
+ }
15149
+ // Check attribute value if XSS protection is enabled
15150
+ if (xssSanitize) {
15151
+ const sanitizedValue = sanitizeAttributeValue(name, value);
15152
+ if (sanitizedValue !== null) {
15153
+ node.setAttribute(name, sanitizedValue);
15154
+ }
15155
+ continue;
15156
+ }
15157
+ node.setAttribute(name, value);
15158
+ }
15159
+ el.replaceWith(node);
15160
+ return node;
15161
+ }
15162
+ /**
15163
+ * Removes dangerous script elements from HTML string
15164
+ * @param html HTML string
15165
+ * @returns Sanitized HTML
15166
+ */
15167
+ function sanitizeHtml(html) {
15168
+ if (typeof html !== 'string') {
15169
+ return '';
15170
+ }
15171
+ // Simple approach to handle script tags content
15172
+ // First remove script tags but keep their content
15173
+ let sanitized = html;
15174
+ // For each dangerous element, replace tag but keep content using regex
15175
+ // Example: <script>alert("XSS")</script> -> alert("XSS")
15176
+ for (const tagName of DANGEROUS_ELEMENTS) {
15177
+ const regex = new RegExp(`<${tagName}[^>]*>(.*?)<\\/${tagName}>`, 'gis');
15178
+ sanitized = sanitized.replace(regex, '$1');
15179
+ }
15180
+ // Parse HTML safely using DOM parser for remaining sanitization
15181
+ const doc = new DOMParser().parseFromString(sanitized, 'text/html');
15182
+ const body = doc.body;
15183
+ // Remove event handler attributes from all elements
15184
+ sanitizeElementAndChildren(body);
15185
+ return body.innerHTML;
15186
+ }
15187
+ /**
15188
+ * Sanitizes an element and all its child elements
15189
+ * @param element Target element
15190
+ */
15191
+ function sanitizeElementAndChildren(element) {
15192
+ // Remove dangerous attributes from current element
15193
+ sanitizeAttributes(element);
15194
+ // Process child elements recursively
15195
+ const children = element.children;
15196
+ for (const child of children) {
15197
+ sanitizeElementAndChildren(child);
15198
+ }
15199
+ }
15200
+ /**
15201
+ * Sanitizes attribute value
15202
+ * @param name Attribute name
15203
+ * @param value Attribute value
15204
+ * @returns Safe attribute value, or null if dangerous
15205
+ */
15206
+ function sanitizeAttributeValue(name, value) {
15207
+ if (typeof value !== 'string') {
15208
+ return value;
15209
+ }
15210
+ name = name.toLowerCase();
15211
+ // Disallow event handler attributes
15212
+ if (name.startsWith('on')) {
15213
+ return null;
15214
+ }
15215
+ // Check for dangerous protocol in link attributes
15216
+ if (name === 'href' ||
15217
+ name === 'src' ||
15218
+ name === 'action' ||
15219
+ name === 'formaction' ||
15220
+ name === 'xlink:href') {
15221
+ const trimmedValue = value.trim().toLowerCase();
15222
+ if (trimmedValue.startsWith('javascript:') ||
15223
+ trimmedValue.startsWith('data:') ||
15224
+ trimmedValue.startsWith('vbscript:')) {
15225
+ return null;
15226
+ }
15227
+ }
15228
+ // Other dangerous attributes
15229
+ if (['manifest'].includes(name)) {
15230
+ return null;
15231
+ }
15232
+ return value;
15233
+ }
15234
+ /**
15235
+ * Removes dangerous attributes from an element
15236
+ * @param element Target element
15237
+ */
15238
+ function sanitizeAttributes(element) {
15239
+ // Collect list of dangerous attributes
15240
+ const dangerousAttrs = [...element.attributes]
15241
+ .filter((attr) => sanitizeAttributeValue(attr.name, attr.value) === null)
15242
+ .map((attr) => attr.name);
15243
+ // Remove dangerous attributes
15244
+ for (const attrName of dangerousAttrs) {
15245
+ element.removeAttribute(attrName);
15246
+ }
15247
+ }
15248
+ /**
15249
+ *
15250
+ * @param data
15251
+ * @param definedFields
15252
+ */
15253
+ function maxLengthOf(data, definedFields) {
15254
+ let maxLength = 0;
15255
+ for (const key in data) {
15256
+ if (definedFields.includes(key) && Array.isArray(data[key])) {
15257
+ maxLength = Math.max(maxLength, data[key].length);
15258
+ }
15259
+ }
15260
+ return maxLength;
15261
+ }
15262
+ /**
15263
+ *
15264
+ * @param el
15265
+ * @param attr
15266
+ */
15267
+ function definedFields(el, attr) {
15268
+ const fieldDefinitions = getFields(el, attr);
15269
+ return fieldDefinitions.map((field) => field.fieldName);
15270
+ }
15271
+ /**
15272
+ *
15273
+ * @param el
15274
+ * @param attr
15275
+ */
15276
+ function getFields(el, attr) {
15277
+ const fields = el.getAttribute(`data-${attr}`);
15278
+ if (!fields) {
15279
+ return [];
15280
+ }
15281
+ return parseFields(fields);
15282
+ }
15283
+ /**
15284
+ *
15285
+ * @param el
15286
+ * @param attr
15287
+ * @param dataKeyName
15288
+ */
15289
+ function hasField(el, attr, dataKeyName) {
15290
+ const fieldDefinitions = getFields(el, attr);
15291
+ return fieldDefinitions.some((field) => field.fieldName === dataKeyName);
15292
+ }
15293
+ /**
15294
+ *
15295
+ * @param fields
15296
+ * @param oldPropName
15297
+ * @param newPropName
15298
+ */
15299
+ function replaceProp(fields, oldPropName, newPropName) {
15300
+ return fields.map((field) => {
15301
+ if (field.propName === oldPropName) {
15302
+ return {
15303
+ ...field,
15304
+ propName: newPropName,
15305
+ };
15306
+ }
15307
+ return field;
15308
+ });
15309
+ }
15310
+ /**
15311
+ *
15312
+ * @param fields
15313
+ * @param propName
15314
+ */
15315
+ function removeProp(fields, propName) {
15316
+ return fields.filter((field) => field.propName !== propName);
15317
+ }
15318
+ /**
15319
+ * - 値が配列の場合は、index に対応する値を返す。
15320
+ * - 値が配列でない場合は、そのまま返す。
15321
+ * - ⚠️ 値が配列で、index に対応する値が undefined の場合は、配列の最初の値を返す。
15322
+ * @param data
15323
+ * @param index
15324
+ */
15325
+ function flattenData(data, index) {
15326
+ const result = {};
15327
+ for (const key in data) {
15328
+ result[key] = Array.isArray(data[key])
15329
+ ? data[key][index] === undefined
15330
+ ? data[key][0]
15331
+ : data[key][index]
15332
+ : data[key];
15333
+ }
15334
+ return result;
15335
+ }
15336
+ /**
15337
+ * 特定の要素で、`propName in element`で判定できない属性名
15338
+ */
15339
+ const specificProp = {
15340
+ /*
15341
+ * JSDOMの未実装対策
15342
+ * @see https://github.com/jsdom/jsdom/blob/main/lib/jsdom/living/nodes/HTMLSourceElement.webidl
15343
+ * Living Standardでは実装済み
15344
+ * @see https://html.spec.whatwg.org/multipage/embedded-content.html#htmlsourceelement
15345
+ */
15346
+ source: ['width', 'height'],
15347
+ };
15348
+ /**
15349
+ *
15350
+ * @param el
15351
+ * @param name
15352
+ */
15353
+ function propInElement(el, name) {
15354
+ const has = name in el;
15355
+ if (has) {
15356
+ return true;
15357
+ }
15358
+ return specificProp[el.localName]?.includes(name) ?? false;
14674
15359
  }
15360
+
15361
+ const cache = new Map();
14675
15362
  /**
14676
15363
  *
14677
- * @param str
15364
+ * @param query
14678
15365
  */
14679
- function camelCase(str) {
14680
- return str.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
15366
+ function parseFields(query) {
15367
+ const cached = cache.get(query);
15368
+ if (cached) {
15369
+ return cached;
15370
+ }
15371
+ const fields = query.split(',');
15372
+ const result = fields.map(parseField);
15373
+ cache.set(query, result);
15374
+ return result;
14681
15375
  }
14682
-
14683
15376
  /**
14684
15377
  *
14685
15378
  * @param field
14686
15379
  */
14687
- function fieldNameParser(field) {
15380
+ function parseField(field) {
14688
15381
  field = field.trim().toLowerCase();
14689
15382
  if (!field) {
14690
15383
  throw new Error('Field name is empty.');
14691
15384
  }
14692
- let [fieldName, propName] = field.split(':');
15385
+ let [fieldName, propName,
15386
+ // eslint-disable-next-line prefer-const
15387
+ ...rest] = field.split(':');
15388
+ if (rest.length > 0) {
15389
+ throw new Error('Invalid field format.');
15390
+ }
14693
15391
  propName = propName?.trim() || '';
14694
15392
  fieldName = fieldName?.trim() || propName || field;
14695
15393
  return {
@@ -14709,58 +15407,67 @@ function fieldNameParser(field) {
14709
15407
  * @param datum A datum of value
14710
15408
  * @param attr Data attribute name for specifying the node that FrozenPatty treats as a field
14711
15409
  * @param filter
15410
+ * @param xssSanitize Enable XSS protection
14712
15411
  */
14713
- function setValue(el, name, datum, attr = 'field', filter) {
15412
+ function setValue(el, name, datum, attr = 'field', filter, xssSanitize = true) {
14714
15413
  const rawValue = el.getAttribute(`data-${attr}`);
14715
- const fieldList = rawValue?.split(/\s*,\s*/) ?? [];
14716
- for (const field of fieldList) {
14717
- const { fieldName, propName } = fieldNameParser(field);
14718
- if (name !== fieldName) {
14719
- continue;
14720
- }
14721
- if (filter) {
14722
- datum = filter(datum);
14723
- }
14724
- // console.log({ name, field, fieldName, propName, datum, el: el.innerHTML });
14725
- if (propName) {
14726
- if (/^style\([a-z-]+\)$/i.test(propName)) {
14727
- const cssPropertyName = propName.replace(/^style\(([a-z-]+)\)$/i, '$1');
14728
- let cssValue;
14729
- switch (cssPropertyName) {
14730
- case 'background-image': {
14731
- //
14732
- // NGパターン
14733
- // $changeDom.css(cssPropertyName, 'url("' + value + '")');
14734
- //
14735
- // cssメソッドを経由すると styleAPIを使用するので URLがホストを含めた絶対パスになる
14736
- // デモサーバーから本番サーバーへの移行ができなくなってしまうので避ける
14737
- // 単純な文字列を流し込む(setAttributeを利用)
14738
- // urlはマルチバイト文字や空白記号を含まないはずであるがエスケープする
14739
- const url = encodeURI(`${datum}`);
14740
- cssValue = `url(${url})`;
14741
- break;
14742
- }
14743
- //
14744
- // TODO: 他にもvalueに単位が必要なケースなどに対応したい
14745
- //
14746
- default: {
14747
- cssValue = `${datum}`;
14748
- }
14749
- }
14750
- el.setAttribute('style', `${cssPropertyName}: ${cssValue}`);
14751
- }
14752
- else if (el instanceof HTMLElement) {
14753
- // HTMLElement
14754
- set$2(el, attr, propName, datum);
15414
+ const fields = parseFields(rawValue ?? '');
15415
+ const field = fields.find((field) => field.fieldName === name);
15416
+ if (!field) {
15417
+ return;
15418
+ }
15419
+ const { propName } = field;
15420
+ if (filter) {
15421
+ datum = filter(datum);
15422
+ }
15423
+ // console.log({ name, field, fieldName, propName, datum, el: el.innerHTML });
15424
+ if (!propName) {
15425
+ setContent(el, datum, undefined, xssSanitize);
15426
+ return;
15427
+ }
15428
+ if (propName === 'style' && el instanceof HTMLElement) {
15429
+ el.style.cssText = `${datum}`;
15430
+ return;
15431
+ }
15432
+ if (/^style\([a-z-]+\)$/i.test(propName)) {
15433
+ const cssPropertyName = propName.replace(/^style\(([a-z-]+)\)$/i, '$1');
15434
+ let cssValue;
15435
+ switch (cssPropertyName) {
15436
+ case 'background-image': {
15437
+ // Using CSS method would create absolute URLs with host
15438
+ // This would prevent migration from demo to production server
15439
+ // Using simple string insertion (setAttribute) instead
15440
+ // URL may not contain multibyte or whitespace characters, but escape anyway
15441
+ const url = encodeURI(`${datum}`);
15442
+ cssValue = `url(${url})`;
15443
+ break;
14755
15444
  }
14756
- else {
14757
- // SVGElement or more
14758
- el.setAttribute(propName, `${datum}`);
15445
+ //
15446
+ // TODO: Handle other cases where values need units
15447
+ //
15448
+ default: {
15449
+ cssValue = `${datum}`;
14759
15450
  }
14760
- return;
14761
15451
  }
14762
- setContent(el, datum);
15452
+ el.setAttribute('style', `${cssPropertyName}: ${cssValue}`);
15453
+ return;
15454
+ }
15455
+ if (el instanceof HTMLElement) {
15456
+ // HTMLElement
15457
+ set$1(el, attr, propName, datum, xssSanitize);
15458
+ return;
15459
+ }
15460
+ // SVGElement or more
15461
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
15462
+ // Check attribute value if XSS protection is enabled
15463
+ if (xssSanitize) {
15464
+ const safeValue = sanitizeAttributeValue(propName, `${datum}`);
15465
+ if (safeValue !== null) {
15466
+ el.setAttribute(propName, safeValue);
15467
+ }
15468
+ return;
14763
15469
  }
15470
+ el.setAttribute(propName, `${datum}`);
14764
15471
  }
14765
15472
  /**
14766
15473
  *
@@ -14768,41 +15475,54 @@ function setValue(el, name, datum, attr = 'field', filter) {
14768
15475
  * @param prefix
14769
15476
  * @param name
14770
15477
  * @param datum
15478
+ * @param xssSanitize
14771
15479
  */
14772
- function set$2(el, prefix, name, datum) {
15480
+ function set$1(el, prefix, name, datum, xssSanitize = true) {
14773
15481
  if (datum == null) {
14774
15482
  el.removeAttribute(name);
14775
15483
  return;
14776
15484
  }
14777
15485
  switch (name) {
14778
15486
  case 'text': {
14779
- setContent(el, datum, false);
15487
+ setContent(el, datum, false, xssSanitize);
14780
15488
  return;
14781
15489
  }
14782
15490
  case 'html': {
14783
- setContent(el, datum, true);
15491
+ // Sanitize HTML if XSS protection is enabled
15492
+ if (xssSanitize && typeof datum === 'string') {
15493
+ setContent(el, sanitizeHtml(datum), true, xssSanitize);
15494
+ }
15495
+ else {
15496
+ setContent(el, datum, true, xssSanitize);
15497
+ }
14784
15498
  return;
14785
15499
  }
14786
15500
  case 'node': {
14787
15501
  if (typeof datum !== 'string') {
14788
15502
  return;
14789
15503
  }
14790
- const nodeName = el.localName ?? el.nodeName.toLowerCase();
14791
- if (nodeName === datum.toLowerCase()) {
14792
- return;
14793
- }
14794
- const node = (el.ownerDocument ?? document).createElement(`${datum}`);
14795
- for (const child of el.childNodes) {
14796
- node.append(child.cloneNode(true));
14797
- }
14798
- for (const { name, value } of el.attributes) {
14799
- node.setAttribute(name, value);
14800
- }
14801
- el.replaceWith(node);
15504
+ replaceNode(el, datum, prefix, xssSanitize);
15505
+ return;
15506
+ }
15507
+ }
15508
+ // If XSS protection is enabled
15509
+ if (xssSanitize) {
15510
+ // Don't set event handler attributes (starting with 'on')
15511
+ if (name.toLowerCase().startsWith('on')) {
15512
+ return;
15513
+ }
15514
+ const beforeSanitize = datum;
15515
+ datum = sanitizeAttributeValue(name, datum);
15516
+ // If sanitization results in null value (except when original value was null), don't change the value
15517
+ if (datum === null && beforeSanitize !== null) {
14802
15518
  return;
14803
15519
  }
14804
15520
  }
14805
- if (!name.startsWith('data-') && !(name in el)) {
15521
+ if (datum == null) {
15522
+ el.removeAttribute(name);
15523
+ return;
15524
+ }
15525
+ if (!name.startsWith('data-') && !propInElement(el, name)) {
14806
15526
  const dataAttr = `data-${prefix}-${kebabCase(name)}`;
14807
15527
  if (el.hasAttribute(dataAttr)) {
14808
15528
  el.setAttribute(dataAttr, `${datum}`);
@@ -15374,8 +16094,9 @@ function set$2(el, prefix, name, datum) {
15374
16094
  * @param el
15375
16095
  * @param datum
15376
16096
  * @param asHtml
16097
+ * @param xssSanitize
15377
16098
  */
15378
- function setContent(el, datum, asHtml = true) {
16099
+ function setContent(el, datum, asHtml = true, xssSanitize = true) {
15379
16100
  if (el instanceof HTMLInputElement ||
15380
16101
  el instanceof HTMLSelectElement ||
15381
16102
  el instanceof HTMLTextAreaElement ||
@@ -15393,9 +16114,18 @@ function setContent(el, datum, asHtml = true) {
15393
16114
  return;
15394
16115
  }
15395
16116
  if (asHtml) {
15396
- el.innerHTML = datum == null ? '' : `${datum}`;
16117
+ // Sanitize HTML if XSS protection is enabled
16118
+ const htmlStr = datum == null ? '' : `${datum}`;
16119
+ if (xssSanitize && typeof htmlStr === 'string') {
16120
+ // sanitizeHtml will now replace dangerous elements instead of removing them
16121
+ el.innerHTML = sanitizeHtml(htmlStr);
16122
+ }
16123
+ else {
16124
+ el.innerHTML = htmlStr;
16125
+ }
15397
16126
  return;
15398
16127
  }
16128
+ // No need to sanitize textContent (automatically escaped)
15399
16129
  el.textContent = datum == null ? '' : `${datum}`;
15400
16130
  }
15401
16131
  /**
@@ -15469,6 +16199,16 @@ class ItemEditorDialog extends EditorDialog {
15469
16199
  }
15470
16200
  }
15471
16201
  }
16202
+ if ($ctrl.name.endsWith('[]')) {
16203
+ return $ctrl.value.split(',').map((value) => {
16204
+ try {
16205
+ return JSON.parse(value);
16206
+ }
16207
+ catch {
16208
+ return value;
16209
+ }
16210
+ });
16211
+ }
15472
16212
  try {
15473
16213
  return JSON.parse($ctrl.value);
15474
16214
  }
@@ -15520,7 +16260,11 @@ class ItemEditorDialog extends EditorDialog {
15520
16260
  const $data = this.findAll(`[name^="${prefix}"]`);
15521
16261
  const map = new Map();
15522
16262
  for (const $datum of $data) {
15523
- const name = camelCase($datum.name.slice(prefix.length));
16263
+ let name = camelCase($datum.name.slice(prefix.length));
16264
+ if (name.endsWith('[]')) {
16265
+ name = name.slice(0, -2);
16266
+ map.delete(name);
16267
+ }
15524
16268
  if (map.has(name)) {
15525
16269
  continue;
15526
16270
  }
@@ -15563,7 +16307,7 @@ class ItemEditorDialog extends EditorDialog {
15563
16307
  const propName = name.slice(1);
15564
16308
  let el;
15565
16309
  for (const n of [kebabCase(propName), propName]) {
15566
- el = this.findAll(`[name="bge-${n}"]`);
16310
+ el = this.findAll(`[name="bge-${n}"], [name="bge-${n}[]"]`);
15567
16311
  if (el.length > 0) {
15568
16312
  return [...el];
15569
16313
  }
@@ -15573,30 +16317,10 @@ class ItemEditorDialog extends EditorDialog {
15573
16317
  #setValues(data) {
15574
16318
  for (const [_name, datum] of Object.entries(data)) {
15575
16319
  const name = kebabCase(_name);
15576
- const inputSelector = `[name="bge-${name}"]`;
15577
- if (Array.isArray(datum)) {
15578
- const $targetEl = this.find(inputSelector);
15579
- if (!$targetEl) {
15580
- continue;
15581
- }
15582
- const $listRoot = $targetEl.closest('[data-bge-list]');
15583
- if (!$listRoot || $listRoot.children.length === 0) {
15584
- continue;
15585
- }
15586
- const $listItem = $listRoot.firstElementChild?.cloneNode(true);
15587
- if (!$listItem) {
15588
- continue;
15589
- }
15590
- while (datum.length > $listRoot.children.length) {
15591
- $listRoot.append($listItem.cloneNode(true));
15592
- }
15593
- for (const [i, targetEl] of $listRoot.querySelectorAll(inputSelector).entries()) {
15594
- setContent(targetEl, datum[i] || '');
15595
- }
15596
- continue;
15597
- }
16320
+ const inputSelector = `[name="bge-${name}"], [name="bge-${name}[]"]`;
16321
+ const value = Array.isArray(datum) ? datum.join(',') : datum;
15598
16322
  for (const targetEl of this.findAll(inputSelector)) {
15599
- setContent(targetEl, datum);
16323
+ setContent(targetEl, value);
15600
16324
  }
15601
16325
  }
15602
16326
  this.update(data);
@@ -15893,16 +16617,16 @@ class BurgerEditorEngine {
15893
16617
  */
15894
16618
  function getValues(el, convertType = false, attr = 'field', filter) {
15895
16619
  const result = [];
15896
- const rawValue = el.getAttribute(`data-${attr}`);
15897
- const listRoot = el.closest(`[data-${attr}-list]`);
15898
- const forceArray = !!listRoot;
15899
- if (rawValue == null) {
16620
+ const query = el.getAttribute(`data-${attr}`);
16621
+ if (query == null) {
15900
16622
  throw new Error(`data-${attr} attriblute is empty.`);
15901
16623
  }
15902
- const fieldList = rawValue.split(/\s*,\s*/);
15903
- for (const field of fieldList) {
16624
+ const listRoot = el.closest(`[data-${attr}-list]`);
16625
+ const forceArray = !!listRoot;
16626
+ const fields = parseFields(query);
16627
+ for (const field of fields) {
15904
16628
  let value;
15905
- const { fieldName, propName } = fieldNameParser(field);
16629
+ const { fieldName, propName } = field;
15906
16630
  if (propName) {
15907
16631
  switch (propName) {
15908
16632
  case 'node': {
@@ -15917,6 +16641,12 @@ function getValues(el, convertType = false, attr = 'field', filter) {
15917
16641
  value = el.innerHTML.trim();
15918
16642
  break;
15919
16643
  }
16644
+ case 'style': {
16645
+ if (el instanceof HTMLElement) {
16646
+ value = el.style.cssText;
16647
+ }
16648
+ break;
16649
+ }
15920
16650
  default: {
15921
16651
  if (/^style\([a-z-]+\)$/i.test(propName)) {
15922
16652
  const css = propName.replace(/^style\(([a-z-]+)\)$/i, '$1');
@@ -15933,7 +16663,7 @@ function getValues(el, convertType = false, attr = 'field', filter) {
15933
16663
  }
15934
16664
  break;
15935
16665
  }
15936
- value = getAttribute(el, attr, propName, convertType);
16666
+ value = getAttribute(el, propName, attr, convertType);
15937
16667
  }
15938
16668
  }
15939
16669
  }
@@ -15961,11 +16691,11 @@ function getValues(el, convertType = false, attr = 'field', filter) {
15961
16691
  /**
15962
16692
  *
15963
16693
  * @param el
15964
- * @param attr
15965
16694
  * @param keyAttr
16695
+ * @param attr
15966
16696
  * @param typeConvert
15967
16697
  */
15968
- function getAttribute(el, attr, keyAttr, typeConvert) {
16698
+ function getAttribute(el, keyAttr, attr, typeConvert) {
15969
16699
  switch (keyAttr) {
15970
16700
  case 'contenteditable': {
15971
16701
  if (el instanceof HTMLElement) {
@@ -15987,24 +16717,35 @@ function getAttribute(el, attr, keyAttr, typeConvert) {
15987
16717
  return el.hasAttribute('download') ? el.getAttribute('download') : null;
15988
16718
  }
15989
16719
  case 'href': {
15990
- // return (el as HTMLAnchorElement).href;
15991
- return el.getAttribute(keyAttr) ?? ''; // return plain string
16720
+ // Example: (el as HTMLAnchorElement).href;
16721
+ // Expected: return defined value of plain string
16722
+ return el.getAttribute(keyAttr) ?? '';
15992
16723
  }
15993
- default: {
15994
- let value;
15995
- const dataAttr = ['data', attr, kebabCase(keyAttr)].join('-');
15996
- if (el.hasAttribute(dataAttr)) {
15997
- value = el.getAttribute(dataAttr) || '';
15998
- }
15999
- else {
16000
- value = el.getAttribute(keyAttr) || '';
16001
- }
16002
- if (typeConvert) {
16003
- value = convert(value);
16004
- }
16005
- return value;
16724
+ }
16725
+ let value =
16726
+ // @ts-ignore
16727
+ el[keyAttr];
16728
+ if (value !== undefined) {
16729
+ if (typeConvert) {
16730
+ value = convert(value);
16006
16731
  }
16732
+ return value;
16733
+ }
16734
+ // For shorthand notation, get value from data-field-* attribute
16735
+ const dataAttr = ['data', attr, kebabCase(keyAttr)].join('-');
16736
+ if (el.hasAttribute(dataAttr)) {
16737
+ return el.getAttribute(dataAttr) ?? '';
16738
+ }
16739
+ value = el.getAttribute(keyAttr) ?? null;
16740
+ if ((value === '' || value == null) &&
16741
+ typeof keyAttr === 'string' &&
16742
+ keyAttr.startsWith('data-')) {
16743
+ return '';
16007
16744
  }
16745
+ if (typeConvert) {
16746
+ value = convert(value);
16747
+ }
16748
+ return value;
16008
16749
  }
16009
16750
  /**
16010
16751
  *
@@ -16012,8 +16753,9 @@ function getAttribute(el, attr, keyAttr, typeConvert) {
16012
16753
  */
16013
16754
  function convert(value) {
16014
16755
  value = parse(value);
16015
- if (URL.canParse(value)) {
16016
- const url = new URL(value, location.href);
16756
+ const str = `${value}`;
16757
+ if (URL.canParse(str)) {
16758
+ const url = new URL(str, location.href);
16017
16759
  if (url.origin === location.origin) {
16018
16760
  return url.pathname;
16019
16761
  }
@@ -16026,7 +16768,7 @@ function convert(value) {
16026
16768
  */
16027
16769
  function parse(value) {
16028
16770
  try {
16029
- return JSON.parse(value);
16771
+ return JSON.parse(`${value}`);
16030
16772
  }
16031
16773
  catch {
16032
16774
  return value;
@@ -16041,6 +16783,7 @@ function getBackgroundImagePath(value) {
16041
16783
  return decodeURI(value.replace(/^url\(["']?([^"']+)["']?\)$/i, '$1').replace(origin, ''));
16042
16784
  }
16043
16785
 
16786
+ const reverseListElementSelectors = ['picture'];
16044
16787
  /**
16045
16788
  *
16046
16789
  * @param el
@@ -16048,9 +16791,18 @@ function getBackgroundImagePath(value) {
16048
16791
  * @param typeConvert
16049
16792
  * @param filter
16050
16793
  */
16051
- function get$1 (el, attr, typeConvert, filter) {
16052
- // eslint-disable-next-line unicorn/prefer-spread
16053
- const filedElements = Array.from(el.querySelectorAll(`[data-${attr}]`));
16794
+ function getComponent(el, attr, typeConvert, filter) {
16795
+ el = el.cloneNode(true);
16796
+ const reverseListElements = el.querySelectorAll(`:is(${reverseListElementSelectors.join(',')})[data-${attr}-list]`);
16797
+ for (const reverseListElement of reverseListElements) {
16798
+ const children = [];
16799
+ while (reverseListElement.lastElementChild) {
16800
+ children.push(reverseListElement.lastElementChild);
16801
+ reverseListElement.lastElementChild.remove();
16802
+ }
16803
+ reverseListElement.append(...children);
16804
+ }
16805
+ const filedElements = el.querySelectorAll(`[data-${attr}]`);
16054
16806
  let values = [];
16055
16807
  for (const _el of filedElements) {
16056
16808
  values = [...values, ...getValues(_el, typeConvert, attr, filter)];
@@ -16059,59 +16811,105 @@ function get$1 (el, attr, typeConvert, filter) {
16059
16811
  return result;
16060
16812
  }
16061
16813
 
16814
+ /**
16815
+ * Combines multiple field definitions with comma separation
16816
+ * @param fields Array of field definitions
16817
+ * @returns Comma-separated field definition string
16818
+ */
16819
+ function stringifyFields(fields) {
16820
+ return fields
16821
+ .map((field) => stringifyField(field.fieldName, field.propName))
16822
+ .join(', ');
16823
+ }
16824
+ /**
16825
+ * Generates a field definition string from field name and attribute name
16826
+ * @param fieldName Field name
16827
+ * @param propName Attribute name (optional)
16828
+ * @returns Field definition string
16829
+ */
16830
+ function stringifyField(fieldName, propName) {
16831
+ if (!propName) {
16832
+ return fieldName;
16833
+ }
16834
+ // Use shorthand notation when fieldName and propName are the same
16835
+ if (fieldName === propName) {
16836
+ return `:${propName}`;
16837
+ }
16838
+ return `${fieldName}:${propName}`;
16839
+ }
16840
+
16062
16841
  /**
16063
16842
  *
16064
16843
  * @param el
16065
16844
  * @param data
16066
16845
  * @param attr
16067
16846
  * @param filter
16847
+ * @param xssSanitize Enable XSS protection
16068
16848
  */
16069
- function set$1 (el, data, attr, filter) {
16849
+ function setComponent(el, data, attr, filter, xssSanitize = true) {
16070
16850
  el = el.cloneNode(true);
16071
16851
  for (const dataKeyName in data) {
16072
- if (dataKeyName in data) {
16073
- const datum = data[dataKeyName];
16074
- const selector = `[data-${attr}*="${kebabCase(dataKeyName)}"]`;
16075
- // eslint-disable-next-line unicorn/prefer-spread
16076
- const targetList = Array.from(el.querySelectorAll(selector));
16077
- if (Array.isArray(datum)) {
16078
- const targetEl = targetList[0];
16079
- if (!targetEl) {
16080
- continue;
16081
- }
16082
- const listRoot = targetEl.closest(`[data-${attr}-list]`);
16083
- if (!listRoot || (listRoot && listRoot.children.length === 0)) {
16084
- continue;
16085
- }
16086
- const listItem = listRoot.children[0]?.cloneNode(true);
16087
- while (listItem && datum.length > listRoot.children.length) {
16088
- listRoot.append(listItem.cloneNode(true));
16089
- }
16090
- // eslint-disable-next-line unicorn/prefer-spread
16091
- const newChildren = Array.from(listRoot.querySelectorAll(selector));
16092
- // eslint-disable-next-line unicorn/prefer-spread
16093
- const oldChildList = Array.from(listRoot.children);
16094
- let deleteNodeList = [];
16095
- for (const [i, child] of [...newChildren].entries()) {
16096
- if (datum[i] == null) {
16097
- setValue(child, dataKeyName, '', attr, filter);
16098
- const oldChild = oldChildList[i];
16099
- if (oldChild) {
16100
- deleteNodeList.push(oldChild);
16101
- }
16852
+ const datum = data[dataKeyName];
16853
+ if (Array.isArray(datum)) {
16854
+ continue;
16855
+ }
16856
+ const targetList = [
16857
+ // Include self
16858
+ el,
16859
+ // Perform rough filtering for performance
16860
+ ...el.querySelectorAll(`[data-${attr}*="${kebabCase(dataKeyName)}"]`),
16861
+ ]
16862
+ // Perform more accurate filtering
16863
+ .filter((el) => hasField(el, attr, dataKeyName));
16864
+ for (const targetEl of targetList) {
16865
+ setValue(targetEl, dataKeyName, datum, attr, filter, xssSanitize);
16866
+ }
16867
+ }
16868
+ for (const listRoot of el.querySelectorAll(`[data-${attr}-list]`)) {
16869
+ const decendants = listRoot.querySelectorAll('*');
16870
+ const definedFieldsOfDecendants = new Set([...decendants].flatMap((el) => definedFields(el, attr)));
16871
+ const maxLength = maxLengthOf(data, [...definedFieldsOfDecendants]);
16872
+ const listItem = listRoot.children[0]?.cloneNode(true);
16873
+ if (!listItem) {
16874
+ continue;
16875
+ }
16876
+ while (listRoot.firstChild) {
16877
+ listRoot.firstChild.remove();
16878
+ }
16879
+ for (let i = 0; i < maxLength; i++) {
16880
+ let item = listItem.cloneNode(true);
16881
+ const itemData = flattenData(data, i);
16882
+ let reverse = false;
16883
+ switch (listRoot.localName) {
16884
+ case 'picture': {
16885
+ reverse = true;
16886
+ let fields = getFields(item, attr);
16887
+ if (i === 0) {
16888
+ // Convert first item to img element
16889
+ item = replaceNode(item, 'img', attr, xssSanitize);
16890
+ fields = replaceProp(fields, 'srcset', 'src');
16891
+ fields = removeProp(fields, 'sizes');
16892
+ const fieldQuery = stringifyFields(fields);
16893
+ item.setAttribute(`data-${attr}`, fieldQuery);
16102
16894
  }
16103
16895
  else {
16104
- setValue(child, dataKeyName, datum[i], attr, filter);
16105
- deleteNodeList = [];
16896
+ // Convert subsequent items to source elements
16897
+ item = replaceNode(item, 'source', attr, xssSanitize);
16898
+ fields = replaceProp(fields, 'src', 'srcset');
16899
+ fields = removeProp(fields, 'alt');
16900
+ fields = removeProp(fields, 'loading');
16901
+ const fieldQuery = stringifyFields(fields);
16902
+ item.setAttribute(`data-${attr}`, fieldQuery);
16106
16903
  }
16904
+ break;
16107
16905
  }
16108
- for (const node of deleteNodeList)
16109
- node.remove();
16906
+ }
16907
+ const newEl = setComponent(item, itemData, attr, filter, xssSanitize);
16908
+ if (reverse) {
16909
+ listRoot.prepend(newEl);
16110
16910
  }
16111
16911
  else {
16112
- for (const [, targetEl] of [...targetList].entries()) {
16113
- setValue(targetEl, dataKeyName, datum, attr, filter);
16114
- }
16912
+ listRoot.append(newEl);
16115
16913
  }
16116
16914
  }
16117
16915
  }
@@ -16126,6 +16924,10 @@ class FrozenPatty {
16126
16924
  */
16127
16925
  #filter;
16128
16926
  #typeConvert = false;
16927
+ /**
16928
+ * Enable XSS protection
16929
+ */
16930
+ #xssSanitize = true;
16129
16931
  /**
16130
16932
  *
16131
16933
  * @param html Original HTML
@@ -16133,7 +16935,14 @@ class FrozenPatty {
16133
16935
  */
16134
16936
  constructor(html, options) {
16135
16937
  this.#dom = document.createElement('fp-placeholer');
16136
- this.#dom.innerHTML = html;
16938
+ // Sanitize initial HTML if XSS protection is enabled
16939
+ if (options?.xssSanitize === false) {
16940
+ this.#dom.innerHTML = html;
16941
+ this.#xssSanitize = false;
16942
+ }
16943
+ else {
16944
+ this.#dom.innerHTML = sanitizeHtml(html);
16945
+ }
16137
16946
  if (options) {
16138
16947
  if (options.attr) {
16139
16948
  this.#attr = options.attr;
@@ -16145,7 +16954,8 @@ class FrozenPatty {
16145
16954
  merge(data) {
16146
16955
  const currentData = this.toJSON(false);
16147
16956
  const newData = Object.assign(currentData, data);
16148
- this.#dom = set$1(this.#dom, newData, this.#attr, this.#filter);
16957
+ // Pass XSS protection flag to set (default is true if not set)
16958
+ this.#dom = setComponent(this.#dom, newData, this.#attr, this.#filter, this.#xssSanitize);
16149
16959
  return this;
16150
16960
  }
16151
16961
  toDOM() {
@@ -16156,7 +16966,7 @@ class FrozenPatty {
16156
16966
  }
16157
16967
  toJSON(filtering = true) {
16158
16968
  const filter = filtering ? this.#filter : undefined;
16159
- return get$1(this.#dom, this.#attr, this.#typeConvert, filter);
16969
+ return getComponent(this.#dom, this.#attr, this.#typeConvert, filter);
16160
16970
  }
16161
16971
  }
16162
16972
 
@@ -17493,6 +18303,19 @@ function mutable_source(initial_value, immutable = false) {
17493
18303
  return s;
17494
18304
  }
17495
18305
 
18306
+ /**
18307
+ * @template V
18308
+ * @param {Value<V>} source
18309
+ * @param {V} value
18310
+ */
18311
+ function mutate(source, value) {
18312
+ set(
18313
+ source,
18314
+ untrack(() => get(source))
18315
+ );
18316
+ return value;
18317
+ }
18318
+
17496
18319
  /**
17497
18320
  * @template V
17498
18321
  * @param {Source<V>} source
@@ -18411,13 +19234,13 @@ function flush_queued_root_effects() {
18411
19234
  var collected_effects = process_effects(root_effects[i]);
18412
19235
  flush_queued_effects(collected_effects);
18413
19236
  }
19237
+ old_values.clear();
18414
19238
  }
18415
19239
  } finally {
18416
19240
  is_flushing = false;
18417
19241
  is_updating_effect = was_updating_effect;
18418
19242
 
18419
19243
  last_scheduled_effect = null;
18420
- old_values.clear();
18421
19244
  }
18422
19245
  }
18423
19246
 
@@ -19293,6 +20116,20 @@ function resume_children(effect, local) {
19293
20116
  }
19294
20117
  }
19295
20118
 
20119
+ /* This file is generated by scripts/process-messages/index.js. Do not edit! */
20120
+
20121
+
20122
+ /**
20123
+ * `%name%(...)` can only be used during component initialisation
20124
+ * @param {string} name
20125
+ * @returns {never}
20126
+ */
20127
+ function lifecycle_outside_component(name) {
20128
+ {
20129
+ throw new Error(`https://svelte.dev/e/lifecycle_outside_component`);
20130
+ }
20131
+ }
20132
+
19296
20133
  /** @import { ComponentContext } from '#client' */
19297
20134
 
19298
20135
 
@@ -21541,7 +22378,7 @@ function bind_this(element_or_component = {}, update, get_value, get_parts) {
21541
22378
  render_effect(() => {
21542
22379
  old_parts = parts;
21543
22380
  // We only track changes to the parts, not the value itself to avoid unnecessary reruns.
21544
- parts = [];
22381
+ parts = get_parts?.() || [];
21545
22382
 
21546
22383
  untrack(() => {
21547
22384
  if (element_or_component !== get_value(...parts)) {
@@ -21646,6 +22483,48 @@ function observe_all(context, props) {
21646
22483
  props();
21647
22484
  }
21648
22485
 
22486
+ /** @import { ComponentContext, ComponentContextLegacy } from '#client' */
22487
+ /** @import { EventDispatcher } from './index.js' */
22488
+ /** @import { NotFunction } from './internal/types.js' */
22489
+
22490
+ /**
22491
+ * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM.
22492
+ * Unlike `$effect`, the provided function only runs once.
22493
+ *
22494
+ * It must be called during the component's initialisation (but doesn't need to live _inside_ the component;
22495
+ * it can be called from an external module). If a function is returned _synchronously_ from `onMount`,
22496
+ * it will be called when the component is unmounted.
22497
+ *
22498
+ * `onMount` functions do not run during [server-side rendering](https://svelte.dev/docs/svelte/svelte-server#render).
22499
+ *
22500
+ * @template T
22501
+ * @param {() => NotFunction<T> | Promise<NotFunction<T>> | (() => any)} fn
22502
+ * @returns {void}
22503
+ */
22504
+ function onMount(fn) {
22505
+ if (component_context === null) {
22506
+ lifecycle_outside_component();
22507
+ }
22508
+
22509
+ if (legacy_mode_flag && component_context.l !== null) {
22510
+ init_update_callbacks(component_context).m.push(fn);
22511
+ } else {
22512
+ user_effect(() => {
22513
+ const cleanup = untrack(fn);
22514
+ if (typeof cleanup === 'function') return /** @type {() => void} */ (cleanup);
22515
+ });
22516
+ }
22517
+ }
22518
+
22519
+ /**
22520
+ * Legacy-mode: Init callbacks object for onMount/beforeUpdate/afterUpdate
22521
+ * @param {ComponentContext} context
22522
+ */
22523
+ function init_update_callbacks(context) {
22524
+ var l = /** @type {ComponentContextLegacy} */ (context).l;
22525
+ return (l.u ??= { a: [], b: [], m: [] });
22526
+ }
22527
+
21649
22528
  /** @import { StoreReferencesContainer } from '#client' */
21650
22529
  /** @import { Store } from '#shared' */
21651
22530
 
@@ -22015,8 +22894,8 @@ function prop(props, key, flags, fallback) {
22015
22894
  var root_4$3 = template(`<img alt="" loading="lazy" class="svelte-s2t554">`);
22016
22895
  var root_3$5 = template(`<figure class="svelte-s2t554"><div class="img svelte-s2t554"><!></div> <figcaption class="svelte-s2t554"> </figcaption></figure>`);
22017
22896
  var root_2$3 = template(`<dd class="svelte-s2t554"><button type="button" class="svelte-s2t554"><!></button></dd>`);
22018
- var root_1$5 = template(`<dt class="svelte-s2t554"> </dt> <div class="svelte-s2t554"></div>`, 1);
22019
- var root$a = template(`<div class="block-catalog"><dl class="svelte-s2t554"></dl></div>`);
22897
+ var root_1$6 = template(`<dt class="svelte-s2t554"> </dt> <div class="svelte-s2t554"></div>`, 1);
22898
+ var root$b = template(`<div class="block-catalog"><dl class="svelte-s2t554"></dl></div>`);
22020
22899
 
22021
22900
  function Block_catalog($$anchor, $$props) {
22022
22901
  push($$props, false);
@@ -22034,13 +22913,13 @@ function Block_catalog($$anchor, $$props) {
22034
22913
 
22035
22914
  init();
22036
22915
 
22037
- var div = root$a();
22916
+ var div = root$b();
22038
22917
  var dl = child(div);
22039
22918
 
22040
22919
  each(dl, 5, () => engine().blockCatalogDialog.catalog, ([category, blocks]) => category, ($$anchor, $$item) => {
22041
22920
  let category = () => get($$item)[0];
22042
22921
  let blocks = () => get($$item)[1];
22043
- var fragment = root_1$5();
22922
+ var fragment = root_1$6();
22044
22923
  var dt = first_child(fragment);
22045
22924
  var text$1 = child(dt);
22046
22925
 
@@ -22141,7 +23020,7 @@ const defaultAttributes = {
22141
23020
  },
22142
23021
  };
22143
23022
 
22144
- var root$9 = ns_template(`<svg><!><!></svg>`);
23023
+ var root$a = ns_template(`<svg><!><!></svg>`);
22145
23024
 
22146
23025
  function Icon($$anchor, $$props) {
22147
23026
  const $$sanitized_props = legacy_rest_props($$props, [
@@ -22171,7 +23050,7 @@ function Icon($$anchor, $$props) {
22171
23050
 
22172
23051
  init();
22173
23052
 
22174
- var svg = root$9();
23053
+ var svg = root$a();
22175
23054
  let attributes;
22176
23055
  var node = child(svg);
22177
23056
 
@@ -22535,11 +23414,11 @@ function Trash($$anchor, $$props) {
22535
23414
  }));
22536
23415
  }
22537
23416
 
22538
- var root$8 = template(`<div class="svelte-1m6pw9u"><button type="button" class="svelte-1m6pw9u"><!></button> <span class="svelte-1m6pw9u"> </span></div>`);
23417
+ var root$9 = template(`<div class="svelte-1m6pw9u"><button type="button" class="svelte-1m6pw9u"><!></button> <span class="svelte-1m6pw9u"> </span></div>`);
22539
23418
 
22540
23419
  function Block_menu_button($$anchor, $$props) {
22541
23420
  const uid = props_id();
22542
- var div = root$8();
23421
+ var div = root$9();
22543
23422
 
22544
23423
  set_style(div, '', {}, { '--name': `--anchor-${uid}` });
22545
23424
 
@@ -22628,7 +23507,7 @@ async function replaceElement(elA, elB, duration = 600) {
22628
23507
  }
22629
23508
 
22630
23509
  var root_5$3 = template(`<!> <!>`, 1);
22631
- var root$7 = template(`<div class="bge-menu-base svelte-15fci6"><div class="bge-menu svelte-15fci6"><div class="bge-move-group svelte-15fci6"><!> <!></div> <div class="bge-standard-group svelte-15fci6"><!> <!> <!> <!> <!> <!></div></div> <div class="bge-menu-margin svelte-15fci6"><span class="svelte-15fci6"> </span></div></div>`);
23510
+ var root$8 = template(`<div class="bge-menu-base svelte-15fci6"><div class="bge-menu svelte-15fci6"><div class="bge-move-group svelte-15fci6"><!> <!></div> <div class="bge-standard-group svelte-15fci6"><!> <!> <!> <!> <!> <!></div></div> <div class="bge-menu-margin svelte-15fci6"><span class="svelte-15fci6"> </span></div></div>`);
22632
23511
 
22633
23512
  function Block_menu($$anchor, $$props) {
22634
23513
  push($$props, false);
@@ -22780,7 +23659,7 @@ function Block_menu($$anchor, $$props) {
22780
23659
  legacy_pre_effect_reset();
22781
23660
  init();
22782
23661
 
22783
- var div = root$7();
23662
+ var div = root$8();
22784
23663
  var div_1 = child(div);
22785
23664
  var div_2 = child(div_1);
22786
23665
  var node = child(div_2);
@@ -22902,11 +23781,11 @@ var root_4$2 = template(`<div role="radiogroup" aria-labelledby="justify-group">
22902
23781
  var root_5$2 = template(`<label><span>列数</span> <output> </output> <input name="bge-options-columns" type="range" min="1" max="5"></label>`);
22903
23782
  var root_6$1 = template(`<div role="radiogroup" aria-labelledby="float-group"><div id="float-group">回り込み</div> <label><input type="radio" name="bge-options-float" value="start"><span>左寄せ</span></label> <label><input type="radio" name="bge-options-float" value="end"><span>右寄せ</span></label></div>`);
22904
23783
  var root_3$4 = template(`<!> <!> <!>`, 1);
22905
- var root_1$4 = template(`<fieldset><legend>コンテナ特性</legend> <label><span>コンテナタイプ</span> <output> </output></label> <!></fieldset>`);
23784
+ var root_1$5 = template(`<fieldset><legend>コンテナ特性</legend> <label><span>コンテナタイプ</span> <output> </output></label> <!></fieldset>`);
22906
23785
  var root_9$1 = template(`<option> </option>`);
22907
23786
  var root_8$1 = template(`<label><span> </span> <select></select></label>`);
22908
23787
  var root_7$2 = template(`<fieldset><legend>ブロックのスタイル拡張</legend> <!></fieldset>`);
22909
- var root$6 = template(`<!> <!> <label><span>独自class設定</span> <input type="text" name="bge-options-classes" aria-describedby="block-option-classes-desc"></label> <small id="block-option-classes-desc">複数指定する場合はスペース(空白文字)で区切ってください。</small> <label><span>ID設定: <code>bge-</code></span> <input name="bge-options-id" type="text" aria-describedby="block-option-id-desc"></label> <small id="block-option-id-desc">アンカーリンク用のID属性を設定します。実際のIDは<code>bge-</code>が自動的に先頭に付加されます。</small>`, 1);
23788
+ var root$7 = template(`<!> <!> <label><span>独自class設定</span> <input type="text" name="bge-options-classes" aria-describedby="block-option-classes-desc"></label> <small id="block-option-classes-desc">複数指定する場合はスペース(空白文字)で区切ってください。</small> <label><span>ID設定: <code>bge-</code></span> <input name="bge-options-id" type="text" aria-describedby="block-option-id-desc"></label> <small id="block-option-id-desc">アンカーリンク用のID属性を設定します。実際のIDは<code>bge-</code>が自動的に先頭に付加されます。</small>`, 1);
22910
23789
 
22911
23790
  function Block_options($$anchor, $$props) {
22912
23791
  push($$props, true);
@@ -22921,12 +23800,12 @@ function Block_options($$anchor, $$props) {
22921
23800
 
22922
23801
  const options = currentBlock.exportOptions();
22923
23802
  let currentColumns = state(proxy(options.props.columns ?? 1));
22924
- var fragment = root$6();
23803
+ var fragment = root$7();
22925
23804
  var node = first_child(fragment);
22926
23805
 
22927
23806
  {
22928
23807
  var consequent_4 = ($$anchor) => {
22929
- var fieldset = root_1$4();
23808
+ var fieldset = root_1$5();
22930
23809
  var label = sibling(child(fieldset), 2);
22931
23810
  var output = sibling(child(label), 2);
22932
23811
  var text = child(output);
@@ -23427,9 +24306,9 @@ function getExt(src) {
23427
24306
  };
23428
24307
  }
23429
24308
 
23430
- var root_1$2 = template(`<img alt="画像のプレビュー" loading="lazy" class="svelte-1zu56m">`);
24309
+ var root_1$3 = template(`<img alt="画像のプレビュー" loading="lazy" class="svelte-1zu56m">`);
23431
24310
  var root_3$2 = template(`<video playsinline="" class="svelte-1zu56m"><source> <track kind="captions" src=""> <!></video>`, 2);
23432
- var root$4 = template(`<span data-bge-editor-ui-component="thumbnail" class="svelte-1zu56m"><!></span>`);
24311
+ var root$5 = template(`<span data-bge-editor-ui-component="thumbnail" class="svelte-1zu56m"><!></span>`);
23433
24312
 
23434
24313
  function Thumbnail($$anchor, $$props) {
23435
24314
  push($$props, false);
@@ -23440,12 +24319,12 @@ function Thumbnail($$anchor, $$props) {
23440
24319
 
23441
24320
  init();
23442
24321
 
23443
- var span = root$4();
24322
+ var span = root$5();
23444
24323
  var node = child(span);
23445
24324
 
23446
24325
  {
23447
24326
  var consequent = ($$anchor) => {
23448
- var img = root_1$2();
24327
+ var img = root_1$3();
23449
24328
 
23450
24329
  template_effect(() => set_attribute(img, 'src', src()));
23451
24330
  event('load', img, () => set(isLoaded, true));
@@ -23581,7 +24460,7 @@ var root_5$1 = template(`<span class="svelte-15rcfwg">アップロード中... <
23581
24460
  var root_6 = template(`<span class="attr svelte-15rcfwg"><span class="svelte-15rcfwg">ID</span><span class="svelte-15rcfwg"><!></span> <span class="svelte-15rcfwg">名称</span><span class="svelte-15rcfwg"><!></span> <span class="svelte-15rcfwg">更新</span><span class="svelte-15rcfwg"> </span> <span class="svelte-15rcfwg">サイズ</span><span class="svelte-15rcfwg"> </span></span>`);
23582
24461
  var root_7$1 = template(`<button class="delete svelte-15rcfwg" type="button">削除</button>`);
23583
24462
  var root_4 = template(`<li class="svelte-15rcfwg"><button class="file svelte-15rcfwg" type="button"><span class="thumbnail svelte-15rcfwg"><!></span> <!></button> <!></li>`);
23584
- var root$3 = template(`<div class="ctrl svelte-15rcfwg"><div class="pagination svelte-15rcfwg"><button type="button" class="svelte-15rcfwg">前へ</button> <div class="page svelte-15rcfwg"><span><input type="number" min="1" aria-label="ページ番号"></span> <span>/</span> <span> </span></div> <button type="button" class="svelte-15rcfwg">次へ</button></div> <input type="search" placeholder="検索"></div> <ul class="list svelte-15rcfwg"></ul>`, 1);
24463
+ var root$4 = template(`<div class="ctrl svelte-15rcfwg"><div class="pagination svelte-15rcfwg"><button type="button" class="svelte-15rcfwg">前へ</button> <div class="page svelte-15rcfwg"><span><input type="number" min="1" aria-label="ページ番号"></span> <span>/</span> <span> </span></div> <button type="button" class="svelte-15rcfwg">次へ</button></div> <input type="search" placeholder="検索"></div> <ul class="list svelte-15rcfwg"></ul>`, 1);
23585
24464
 
23586
24465
  function File_list($$anchor, $$props) {
23587
24466
  push($$props, false);
@@ -23763,7 +24642,7 @@ function File_list($$anchor, $$props) {
23763
24642
 
23764
24643
  init();
23765
24644
 
23766
- var fragment_2 = root$3();
24645
+ var fragment_2 = root$4();
23767
24646
  var div = first_child(fragment_2);
23768
24647
  var div_1 = child(div);
23769
24648
  var button = child(div_1);
@@ -23887,7 +24766,7 @@ function File_list($$anchor, $$props) {
23887
24766
  pop();
23888
24767
  }
23889
24768
 
23890
- var root$2 = template(`<div><input type="file" class="svelte-1kuhunj"> <button type="button">ファイルを追加アップロードする</button></div>`);
24769
+ var root$3 = template(`<div><input type="file" class="svelte-1kuhunj"> <button type="button">ファイルを追加アップロードする</button></div>`);
23891
24770
 
23892
24771
  function File_uploader($$anchor, $$props) {
23893
24772
  push($$props, false);
@@ -23940,7 +24819,7 @@ function File_uploader($$anchor, $$props) {
23940
24819
 
23941
24820
  init();
23942
24821
 
23943
- var div = root$2();
24822
+ var div = root$3();
23944
24823
  var input = child(div);
23945
24824
 
23946
24825
  set_attribute(input, 'accept', accept);
@@ -23953,7 +24832,7 @@ function File_uploader($$anchor, $$props) {
23953
24832
  pop();
23954
24833
  }
23955
24834
 
23956
- var root_1$1 = template(`<img alt="画像プレビュー" class="svelte-1ie51m5">`);
24835
+ var root_1$2 = template(`<img alt="画像プレビュー" class="svelte-1ie51m5">`);
23957
24836
  var root_3 = template(`<video controls playsinline="" class="svelte-1ie51m5"><source> <track kind="captions" src=""></video>`, 2);
23958
24837
  var root_5 = template(`<audio controls class="svelte-1ie51m5"><source> <track kind="metadata" src=""></audio>`);
23959
24838
  var root_7 = template(`<object class="svelte-1ie51m5"><p class="svelte-1ie51m5">プレビューできません</p></object>`);
@@ -23962,7 +24841,7 @@ var root_9 = template(`<div class="progress svelte-1ie51m5"></div>`);
23962
24841
  var root_10 = template(`<li class="upload svelte-1ie51m5"><span>アップロード...</span> <span class="progress svelte-1ie51m5"> </span></li>`);
23963
24842
  var root_11 = template(`<li class="path svelte-1ie51m5"><a target="_blank" class="svelte-1ie51m5"> </a></li>`);
23964
24843
  var root_12 = template(`<li class="dimension svelte-1ie51m5"> </li>`);
23965
- var root$1 = template(`<div><div><!> <!></div> <ul class="svelte-1ie51m5"><!> <!></ul></div>`);
24844
+ var root$2 = template(`<div><div><!> <!></div> <ul class="svelte-1ie51m5"><!> <!></ul></div>`);
23966
24845
 
23967
24846
  function Preview($$anchor, $$props) {
23968
24847
  push($$props, false);
@@ -24028,14 +24907,14 @@ function Preview($$anchor, $$props) {
24028
24907
  legacy_pre_effect_reset();
24029
24908
  init();
24030
24909
 
24031
- var div = root$1();
24910
+ var div = root$2();
24032
24911
  var div_1 = child(div);
24033
24912
  let classes;
24034
24913
  var node = child(div_1);
24035
24914
 
24036
24915
  {
24037
24916
  var consequent = ($$anchor) => {
24038
- var img = root_1$1();
24917
+ var img = root_1$2();
24039
24918
 
24040
24919
  template_effect(() => set_attribute(img, 'src', get(selectedPath)));
24041
24920
  append($$anchor, img);
@@ -24247,8 +25126,8 @@ function Arrows_transfer_down($$anchor, $$props) {
24247
25126
  }));
24248
25127
  }
24249
25128
 
24250
- var root_1 = template(`<div class="row svelte-37o4yi"><div class="th"><textarea class="svelte-37o4yi"></textarea></div> <div class="td"><textarea class="svelte-37o4yi"></textarea></div> <div class="btn svelte-37o4yi"><ul class="svelte-37o4yi"><li class="svelte-37o4yi"><button type="button" title="下に追加"><!></button></li> <li class="svelte-37o4yi"><button type="button" title="削除"><!></button></li> <li class="svelte-37o4yi"><button type="button" title="下に移動"><!></button></li></ul></div></div>`);
24251
- var root = template(`<div class="table svelte-37o4yi"></div>`);
25129
+ var root_1$1 = template(`<div class="row svelte-37o4yi"><div class="th"><textarea class="svelte-37o4yi"></textarea></div> <div class="td"><textarea class="svelte-37o4yi"></textarea></div> <div class="btn svelte-37o4yi"><ul class="svelte-37o4yi"><li class="svelte-37o4yi"><button type="button" title="下に追加"><!></button></li> <li class="svelte-37o4yi"><button type="button" title="削除"><!></button></li> <li class="svelte-37o4yi"><button type="button" title="下に移動"><!></button></li></ul></div></div>`);
25130
+ var root$1 = template(`<div class="table svelte-37o4yi"></div>`);
24252
25131
 
24253
25132
  function Table_editor($$anchor, $$props) {
24254
25133
  push($$props, false);
@@ -24317,10 +25196,10 @@ function Table_editor($$anchor, $$props) {
24317
25196
 
24318
25197
  init();
24319
25198
 
24320
- var div = root();
25199
+ var div = root$1();
24321
25200
 
24322
25201
  each(div, 5, () => get(table), index, ($$anchor, row, i) => {
24323
- var div_1 = root_1();
25202
+ var div_1 = root_1$1();
24324
25203
  var div_2 = child(div_1);
24325
25204
  var textarea = child(div_2);
24326
25205
  set_attribute(textarea, 'aria-label', `${i}行目の見出しセル`);
@@ -24384,6 +25263,138 @@ function Table_editor($$anchor, $$props) {
24384
25263
  }
24385
25264
 
24386
25265
  delegate(['input', 'click']);
25266
+
25267
+ function Circle_plus($$anchor, $$props) {
25268
+ const $$sanitized_props = legacy_rest_props($$props, [
25269
+ 'children',
25270
+ '$$slots',
25271
+ '$$events',
25272
+ '$$legacy'
25273
+ ]);
25274
+
25275
+ const iconNode = [
25276
+ [
25277
+ "path",
25278
+ { "d": "M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" }
25279
+ ],
25280
+ ["path", { "d": "M9 12h6" }],
25281
+ ["path", { "d": "M12 9v6" }]
25282
+ ];
25283
+
25284
+ Icon($$anchor, spread_props({ type: 'outline', name: 'circle-plus' }, () => $$sanitized_props, {
25285
+ iconNode,
25286
+ children: ($$anchor, $$slotProps) => {
25287
+ var fragment_1 = comment();
25288
+ var node = first_child(fragment_1);
25289
+
25290
+ slot(node, $$props, 'default', {});
25291
+ append($$anchor, fragment_1);
25292
+ },
25293
+ $$slots: { default: true }
25294
+ }));
25295
+ }
25296
+
25297
+ const onKeyDown = (event, update, currentIndex, length) => {
25298
+ if (event.key === 'ArrowLeft') {
25299
+ update(Math.max(0, get(currentIndex) - 1));
25300
+ } else if (event.key === 'ArrowRight') {
25301
+ update(Math.min(length - 1, get(currentIndex) + 1));
25302
+ } else {
25303
+ return;
25304
+ }
25305
+ };
25306
+
25307
+ var on_click = (__1, onClick, index) => onClick(get(index));
25308
+ var root_1 = template(`<button type="button" role="tab" class="svelte-2oedib"> </button>`);
25309
+ var on_click_1 = () => {};
25310
+ var root = template(`<div role="tablist" class="svelte-2oedib"><!> <button class="add-tab svelte-2oedib" type="button" title="タブを追加"><!></button></div>`);
25311
+
25312
+ function Tabs($$anchor, $$props) {
25313
+ push($$props, false);
25314
+
25315
+ let engine = prop($$props, 'engine', 8);
25316
+ let contentId = prop($$props, 'contentId', 8);
25317
+ const refs = mutable_source([]);
25318
+ let tabPanel = null;
25319
+ const length = 2;
25320
+ let currentIndex = mutable_source(0);
25321
+
25322
+ engine().componentObserver.on('select-tab-in-item-editor', ({ index }) => {
25323
+ set(currentIndex, index);
25324
+ get(refs)[get(currentIndex)]?.focus();
25325
+ });
25326
+
25327
+ const onClick = (index) => {
25328
+ update(index);
25329
+ };
25330
+
25331
+ /**
25332
+ *
25333
+ * @param index
25334
+ */
25335
+ function createLabel(index) {
25336
+ return `画像${index + 1}`;
25337
+ }
25338
+
25339
+ /**
25340
+ *
25341
+ * @param index
25342
+ */
25343
+ function update(index) {
25344
+ set(currentIndex, index);
25345
+ get(refs)[get(currentIndex)]?.focus();
25346
+ engine().componentObserver.notify('select-tab-in-item-editor', { index: get(currentIndex) });
25347
+ tabPanel?.setAttribute('aria-label', createLabel(get(currentIndex)));
25348
+ }
25349
+
25350
+ onMount(() => {
25351
+ tabPanel = document.getElementById(contentId());
25352
+
25353
+ if (!tabPanel) {
25354
+ throw new Error('Tab panel not found');
25355
+ }
25356
+ });
25357
+
25358
+ init();
25359
+
25360
+ var div = root();
25361
+ var node = child(div);
25362
+
25363
+ each(node, 1, () => Array.from({ length }, (_, index) => index), (index) => index, ($$anchor, index) => {
25364
+ var button = root_1();
25365
+
25366
+ button.__click = [on_click, onClick, index];
25367
+ button.__keydown = [onKeyDown, update, currentIndex, length];
25368
+
25369
+ var text = child(button);
25370
+ bind_this(button, ($$value, index) => mutate(refs, get(refs)[index] = $$value), (index) => get(refs)?.[index], () => [get(index)]);
25371
+
25372
+ template_effect(
25373
+ ($0) => {
25374
+ set_attribute(button, 'aria-controls', contentId());
25375
+ set_attribute(button, 'aria-selected', get(currentIndex) === get(index));
25376
+ set_attribute(button, 'tabindex', get(currentIndex) === get(index) ? 0 : -1);
25377
+ set_text(text, $0);
25378
+ },
25379
+ [() => createLabel(get(index))],
25380
+ derived_safe_equal
25381
+ );
25382
+
25383
+ append($$anchor, button);
25384
+ });
25385
+
25386
+ var button_1 = sibling(node, 2);
25387
+
25388
+ button_1.__click = [on_click_1];
25389
+
25390
+ var node_1 = child(button_1);
25391
+
25392
+ Circle_plus(node_1, {});
25393
+ append($$anchor, div);
25394
+ pop();
25395
+ }
25396
+
25397
+ delegate(['click', 'keydown']);
24387
25398
  async function createBurgerEditorClient(options) {
24388
25399
  const engine = await BurgerEditorEngine.new({
24389
25400
  ...options,
@@ -24456,6 +25467,19 @@ async function createBurgerEditorClient(options) {
24456
25467
  }
24457
25468
  });
24458
25469
  },
25470
+ tabs: (el, engine2) => {
25471
+ const contentId = el.dataset.bgeEditorUiFor;
25472
+ if (!contentId) {
25473
+ throw new Error("Tab UI component requires contentId attribute");
25474
+ }
25475
+ return svelteMount(Tabs, {
25476
+ target: el,
25477
+ props: {
25478
+ engine: engine2,
25479
+ contentId
25480
+ }
25481
+ });
25482
+ },
24459
25483
  tableEditor: (el, engine2) => {
24460
25484
  return svelteMount(Table_editor, {
24461
25485
  target: el,