From cd82819d679cdc21f12769337fe65e2ab33d058a Mon Sep 17 00:00:00 2001 From: Mario Lurig Date: Sat, 31 Jan 2026 09:43:29 -0700 Subject: [PATCH 1/3] add pipe symbol tests created by Claude --- tests/pipe-symbol.test.js | 280 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 tests/pipe-symbol.test.js diff --git a/tests/pipe-symbol.test.js b/tests/pipe-symbol.test.js new file mode 100644 index 0000000..9a7c6bd --- /dev/null +++ b/tests/pipe-symbol.test.js @@ -0,0 +1,280 @@ +/** + * Test Suite for TRMNL Form Builder - Pipe Symbol YAML Support + * + * Tests validation of the pipe symbol (|) for multiline string values in YAML, + * particularly for the 'code' field_type with default values. + */ + +import { describe, test, expect, beforeEach, afterEach } from 'vitest'; +import '../trmnl-form-builder.js'; + +describe('TRMLYamlForm - Pipe Symbol YAML Support', () => { + let formBuilder; + + beforeEach(() => { + // Create a fresh instance of the form builder + formBuilder = document.createElement('trmnl-form-builder'); + document.body.appendChild(formBuilder); + }); + + afterEach(() => { + // Clean up + document.body.removeChild(formBuilder); + }); + + describe('parseYaml with pipe symbol multiline strings', () => { + test('should parse code field with pipe symbol multiline default value', () => { + const yamlInput = `- keyname: example_field + field_type: code + rows: 8 + name: Example Field + default: | + { + "Example": "Replace" + }`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(1); + expect(result[0].keyname).toBe('example_field'); + expect(result[0].field_type).toBe('code'); + expect(result[0].rows).toBe(8); + expect(result[0].name).toBe('Example Field'); + + // The default value should preserve the multiline formatting + const expectedDefault = '{\n "Example": "Replace" \n}'; + expect(result[0].default).toBe(expectedDefault); + }); + + test('should parse multiple code fields with pipe symbol defaults', () => { + const yamlInput = `- keyname: json_config + field_type: code + rows: 5 + name: JSON Configuration + default: | + { + "key": "value" + } +- keyname: html_template + field_type: code + rows: 10 + name: HTML Template + default: | +
+

Title

+
`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(2); + + // First field + expect(result[0].keyname).toBe('json_config'); + expect(result[0].default).toBe('{\n "key": "value"\n}'); + + // Second field + expect(result[1].keyname).toBe('html_template'); + expect(result[1].default).toBe('
\n

Title

\n
'); + }); + + test('should handle pipe symbol with various indentation levels', () => { + const yamlInput = `- keyname: nested_example + field_type: code + name: Nested Example + default: | + function example() { + if (true) { + console.log("nested"); + } + }`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(1); + expect(result[0].default).toContain('function example()'); + expect(result[0].default).toContain(' if (true)'); + expect(result[0].default).toContain(' console.log("nested")'); + }); + + test('should handle empty lines within pipe symbol content', () => { + const yamlInput = `- keyname: code_with_blank_lines + field_type: code + name: Code With Blanks + default: | + line 1 + + line 3 + + line 5`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(1); + const lines = result[0].default.split('\n'); + expect(lines).toHaveLength(5); + expect(lines[0]).toBe('line 1'); + expect(lines[1]).toBe(''); + expect(lines[2]).toBe('line 3'); + expect(lines[3]).toBe(''); + expect(lines[4]).toBe('line 5'); + }); + + test('should handle pipe symbol for text field type', () => { + const yamlInput = `- keyname: multiline_text + field_type: text + name: Multiline Text + default: | + This is line 1 + This is line 2 + This is line 3`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(1); + expect(result[0].field_type).toBe('text'); + expect(result[0].default).toBe('This is line 1\nThis is line 2\nThis is line 3'); + }); + + test('should preserve special characters in pipe symbol content', () => { + const yamlInput = `- keyname: special_chars + field_type: code + name: Special Characters + default: | + {"key": "value with 'quotes' and \\"escapes\\""} + + symbols: @ # $ % & * ( )`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(1); + expect(result[0].default).toContain('\'quotes\''); + expect(result[0].default).toContain(''); + expect(result[0].default).toContain('@ # $ % & *'); + }); + + test('should handle pipe symbol mixed with other field properties', () => { + const yamlInput = `- keyname: mixed_field + field_type: code + rows: 12 + name: Mixed Properties + help_text: This is a helpful description + optional: true + default: | + // Default code here + const x = 1; + placeholder: Enter your code here`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(1); + expect(result[0].keyname).toBe('mixed_field'); + expect(result[0].rows).toBe(12); + expect(result[0].optional).toBe(true); + expect(result[0].default).toBe('// Default code here\nconst x = 1;'); + expect(result[0].placeholder).toBe('Enter your code here'); + }); + + test('should handle pipe symbol with trailing spaces', () => { + const yamlInput = `- keyname: trailing_spaces + field_type: code + name: Trailing Spaces + default: | + line with trailing space + another line`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(1); + // Trailing spaces should be preserved in the multiline string + expect(result[0].default).toBe('line with trailing space \nanother line'); + }); + + test('should differentiate between pipe symbol and regular single-line values', () => { + const yamlInput = `- keyname: single_line + field_type: string + name: Single Line + default: simple value +- keyname: multi_line + field_type: code + name: Multi Line + default: | + multi + line + value`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(2); + expect(result[0].default).toBe('simple value'); + expect(result[1].default).toBe('multi\nline\nvalue'); + }); + }); + + describe('generateYaml with pipe symbol output', () => { + test('should generate YAML with pipe symbol for multiline code defaults', () => { + const fields = [{ + id: 'field_1', + keyname: 'example_field', + field_type: 'code', + rows: 8, + name: 'Example Field', + default: '{\n "Example": "Replace"\n}' + }]; + + formBuilder.setFields(fields); + const yamlOutput = formBuilder.getYaml(); + + expect(yamlOutput).toContain('- keyname: example_field'); + expect(yamlOutput).toContain('field_type: code'); + expect(yamlOutput).toContain('default: |'); + expect(yamlOutput).toContain(' {'); + expect(yamlOutput).toContain(' "Example": "Replace"'); + expect(yamlOutput).toContain(' }'); + }); + + test('should not use pipe symbol for single-line defaults', () => { + const fields = [{ + id: 'field_1', + keyname: 'simple_field', + field_type: 'string', + name: 'Simple Field', + default: 'single line value' + }]; + + formBuilder.setFields(fields); + const yamlOutput = formBuilder.getYaml(); + + expect(yamlOutput).toContain('default: single line value'); + expect(yamlOutput).not.toContain('default: |'); + }); + }); + + describe('round-trip parsing (parse -> generate -> parse)', () => { + test('should maintain pipe symbol multiline values through round-trip', () => { + const originalYaml = `- keyname: round_trip_test + field_type: code + rows: 6 + name: Round Trip Test + default: | + { + "test": "value", + "number": 42 + }`; + + // Parse the YAML + const parsed = formBuilder.parseYaml(originalYaml); + + // Set fields and generate new YAML + formBuilder.setFields(parsed); + const generatedYaml = formBuilder.getYaml(); + + // Parse the generated YAML + const reparsed = formBuilder.parseYaml(generatedYaml); + + // The default value should remain the same + expect(reparsed[0].default).toBe(parsed[0].default); + expect(reparsed[0].default).toContain('\n'); + expect(reparsed[0].default).toContain('"test": "value"'); + }); + }); +}); \ No newline at end of file From 9d40c03888ec1e03fac6c9892fb4a082db538cb2 Mon Sep 17 00:00:00 2001 From: Mario Lurig Date: Sat, 31 Jan 2026 23:27:36 -0700 Subject: [PATCH 2/3] Support Pipe Symbol Supported in: - default - help_text - description - description i18n - placeholder Add tests and sample YAML --- test-fixtures/multiline.yml | 31 ++++ tests/pipe-symbol.test.js | 320 ++++++++++++++++++++++++++++++++++++ trmnl-form-builder.js | 122 ++++++++++++-- 3 files changed, 459 insertions(+), 14 deletions(-) create mode 100644 test-fixtures/multiline.yml diff --git a/test-fixtures/multiline.yml b/test-fixtures/multiline.yml new file mode 100644 index 0000000..21b55ce --- /dev/null +++ b/test-fixtures/multiline.yml @@ -0,0 +1,31 @@ +- keyname: example_default + field_type: code + name: Example Default + default: | + { + "Example": "Replace" + } + rows: 8 +- keyname: example_help + field_type: code + name: Example Help + help_text: | + { + "Example": "Replace" + } +- keyname: example_placeholder + field_type: text + name: Example Placeholder + placeholder: | + { + "Place": "Holder" + } +- keyname: example_desc + field_type: code + name: Example Desc + description: | + hello + world + description-es: | + hola + mundo \ No newline at end of file diff --git a/tests/pipe-symbol.test.js b/tests/pipe-symbol.test.js index 9a7c6bd..27b1a45 100644 --- a/tests/pipe-symbol.test.js +++ b/tests/pipe-symbol.test.js @@ -6,6 +6,8 @@ */ import { describe, test, expect, beforeEach, afterEach } from 'vitest'; +import { readFileSync } from 'fs' +import { join } from 'path' import '../trmnl-form-builder.js'; describe('TRMLYamlForm - Pipe Symbol YAML Support', () => { @@ -46,6 +48,128 @@ describe('TRMLYamlForm - Pipe Symbol YAML Support', () => { expect(result[0].default).toBe(expectedDefault); }); + test('should parse multiline description field', () => { + const yamlInput = `- keyname: desc_field + field_type: code + name: Description Field + description: | + hello + world`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(1); + expect(result[0].description).toBe('hello\nworld'); + }); + + test('should parse multiline help_text field', () => { + const yamlInput = `- keyname: help_field + field_type: code + name: Help Field + help_text: | + { + "Example": "Replace" + }`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(1); + expect(result[0].help_text).toBe('{\n "Example": "Replace" \n}'); + }); + + test('should parse multiline placeholder field', () => { + const yamlInput = `- keyname: placeholder_field + field_type: text + name: Placeholder Field + placeholder: | + { + "Place": "Holder" + }`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(1); + expect(result[0].placeholder).toBe('{\n "Place": "Holder" \n}'); + }); + + test('should parse multiline i18n description fields', () => { + const yamlInput = `- keyname: i18n_field + field_type: code + name: i18n Field + description: | + hello + world + description-es: | + hola + mundo + description-fr: | + bonjour + monde`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(1); + expect(result[0].description).toBe('hello\nworld'); + expect(result[0]['description-es']).toBe('hola\nmundo'); + expect(result[0]['description-fr']).toBe('bonjour\nmonde'); + }); + + test('should parse the multiline.yml test fixture', async () => { + // Read the actual fixture file + const yamlInput = readFileSync( + join(process.cwd(), 'test-fixtures', 'multiline.yml'), + 'utf-8' + ) + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(4); + + // Field 1: example_default + expect(result[0].keyname).toBe('example_default'); + expect(result[0].default).toBe('{\n "Example": "Replace" \n}'); + expect(result[0].rows).toBe(8); + + // Field 2: example_help + expect(result[1].keyname).toBe('example_help'); + expect(result[1].help_text).toBe('{\n "Example": "Replace" \n}'); + + // Field 3: example_placeholder + expect(result[2].keyname).toBe('example_placeholder'); + expect(result[2].placeholder).toBe('{\n "Place": "Holder" \n}'); + + // Field 4: example_desc + expect(result[3].keyname).toBe('example_desc'); + expect(result[3].description).toBe('hello\nworld'); + expect(result[3]['description-es']).toBe('hola\nmundo'); + }); + + test('should handle multiple multiline fields in same field definition', () => { + const yamlInput = `- keyname: multi_multiline + field_type: text + name: Multiple Multiline + description: | + First multiline + description + help_text: | + Second multiline + help text + placeholder: | + Third multiline + placeholder + default: | + Fourth multiline + default`; + + const result = formBuilder.parseYaml(yamlInput); + + expect(result).toHaveLength(1); + expect(result[0].description).toBe('First multiline\ndescription'); + expect(result[0].help_text).toBe('Second multiline\nhelp text'); + expect(result[0].placeholder).toBe('Third multiline\nplaceholder'); + expect(result[0].default).toBe('Fourth multiline\ndefault'); + }); + test('should parse multiple code fields with pipe symbol defaults', () => { const yamlInput = `- keyname: json_config field_type: code @@ -232,6 +356,83 @@ describe('TRMLYamlForm - Pipe Symbol YAML Support', () => { expect(yamlOutput).toContain(' }'); }); + test('should generate YAML with pipe symbol for multiline description', () => { + const fields = [{ + id: 'field_1', + keyname: 'desc_field', + field_type: 'string', + name: 'Description Field', + description: 'hello\nworld' + }]; + + formBuilder.setFields(fields); + const yamlOutput = formBuilder.getYaml(); + + expect(yamlOutput).toContain('description: |'); + expect(yamlOutput).toContain(' hello'); + expect(yamlOutput).toContain(' world'); + }); + + test('should generate YAML with pipe symbol for multiline help_text', () => { + const fields = [{ + id: 'field_1', + keyname: 'help_field', + field_type: 'string', + name: 'Help Field', + help_text: 'Line 1\nLine 2\nLine 3' + }]; + + formBuilder.setFields(fields); + const yamlOutput = formBuilder.getYaml(); + + expect(yamlOutput).toContain('help_text: |'); + expect(yamlOutput).toContain(' Line 1'); + expect(yamlOutput).toContain(' Line 2'); + expect(yamlOutput).toContain(' Line 3'); + }); + + test('should generate YAML with pipe symbol for multiline placeholder', () => { + const fields = [{ + id: 'field_1', + keyname: 'placeholder_field', + field_type: 'text', + name: 'Placeholder Field', + placeholder: 'First line\nSecond line' + }]; + + formBuilder.setFields(fields); + const yamlOutput = formBuilder.getYaml(); + + expect(yamlOutput).toContain('placeholder: |'); + expect(yamlOutput).toContain(' First line'); + expect(yamlOutput).toContain(' Second line'); + }); + + test('should generate YAML with pipe symbol for i18n descriptions', () => { + const fields = [{ + id: 'field_1', + keyname: 'i18n_field', + field_type: 'string', + name: 'i18n Field', + description: 'hello\nworld', + 'description-es': 'hola\nmundo', + 'description-fr': 'bonjour\nmonde' + }]; + + formBuilder.setFields(fields); + const yamlOutput = formBuilder.getYaml(); + + expect(yamlOutput).toContain('description: |'); + expect(yamlOutput).toContain(' hello'); + expect(yamlOutput).toContain(' world'); + expect(yamlOutput).toContain('description-es: |'); + expect(yamlOutput).toContain(' hola'); + expect(yamlOutput).toContain(' mundo'); + expect(yamlOutput).toContain('description-fr: |'); + expect(yamlOutput).toContain(' bonjour'); + expect(yamlOutput).toContain(' monde'); + }); + test('should not use pipe symbol for single-line defaults', () => { const fields = [{ id: 'field_1', @@ -276,5 +477,124 @@ describe('TRMLYamlForm - Pipe Symbol YAML Support', () => { expect(reparsed[0].default).toContain('\n'); expect(reparsed[0].default).toContain('"test": "value"'); }); + + test('should maintain multiline description through round-trip', () => { + const originalYaml = `- keyname: desc_test + field_type: string + name: Description Test + description: | + Line one + Line two + Line three`; + + const parsed = formBuilder.parseYaml(originalYaml); + formBuilder.setFields(parsed); + const generatedYaml = formBuilder.getYaml(); + const reparsed = formBuilder.parseYaml(generatedYaml); + + expect(reparsed[0].description).toBe(parsed[0].description); + expect(reparsed[0].description).toBe('Line one\nLine two\nLine three'); + }); + + test('should maintain multiline help_text through round-trip', () => { + const originalYaml = `- keyname: help_test + field_type: string + name: Help Test + help_text: | + Help line 1 + Help line 2`; + + const parsed = formBuilder.parseYaml(originalYaml); + formBuilder.setFields(parsed); + const generatedYaml = formBuilder.getYaml(); + const reparsed = formBuilder.parseYaml(generatedYaml); + + expect(reparsed[0].help_text).toBe(parsed[0].help_text); + expect(reparsed[0].help_text).toBe('Help line 1\nHelp line 2'); + }); + + test('should maintain multiline placeholder through round-trip', () => { + const originalYaml = `- keyname: placeholder_test + field_type: text + name: Placeholder Test + placeholder: | + Placeholder line 1 + Placeholder line 2`; + + const parsed = formBuilder.parseYaml(originalYaml); + formBuilder.setFields(parsed); + const generatedYaml = formBuilder.getYaml(); + const reparsed = formBuilder.parseYaml(generatedYaml); + + expect(reparsed[0].placeholder).toBe(parsed[0].placeholder); + expect(reparsed[0].placeholder).toBe('Placeholder line 1\nPlaceholder line 2'); + }); + + test('should maintain multiline i18n descriptions through round-trip', () => { + const originalYaml = `- keyname: i18n_test + field_type: string + name: i18n Test + description: | + English line 1 + English line 2 + description-es: | + Spanish line 1 + Spanish line 2 + description-fr: | + French line 1 + French line 2`; + + const parsed = formBuilder.parseYaml(originalYaml); + formBuilder.setFields(parsed); + const generatedYaml = formBuilder.getYaml(); + const reparsed = formBuilder.parseYaml(generatedYaml); + + expect(reparsed[0].description).toBe(parsed[0].description); + expect(reparsed[0]['description-es']).toBe(parsed[0]['description-es']); + expect(reparsed[0]['description-fr']).toBe(parsed[0]['description-fr']); + }); + + test('should maintain multiline.yml fixture through round-trip', async () => { + // Read the actual fixture file + const originalYaml = readFileSync( + join(process.cwd(), 'test-fixtures', 'multiline.yml'), + 'utf-8' + ) + const parsed = formBuilder.parseYaml(originalYaml); + formBuilder.setFields(parsed); + const generatedYaml = formBuilder.getYaml(); + const reparsed = formBuilder.parseYaml(generatedYaml); + + // Verify all fields maintained their multiline values + expect(reparsed[0].default).toBe(parsed[0].default); + expect(reparsed[1].help_text).toBe(parsed[1].help_text); + expect(reparsed[2].placeholder).toBe(parsed[2].placeholder); + expect(reparsed[3].description).toBe(parsed[3].description); + expect(reparsed[3]['description-es']).toBe(parsed[3]['description-es']); + }); + + test('should handle mixed single-line and multiline fields in round-trip', () => { + const originalYaml = `- keyname: mixed_test + field_type: text + name: Mixed Test + description: single line description + help_text: | + multiline + help text + placeholder: single line placeholder + default: | + multiline + default`; + + const parsed = formBuilder.parseYaml(originalYaml); + formBuilder.setFields(parsed); + const generatedYaml = formBuilder.getYaml(); + const reparsed = formBuilder.parseYaml(generatedYaml); + + expect(reparsed[0].description).toBe('single line description'); + expect(reparsed[0].help_text).toBe('multiline\nhelp text'); + expect(reparsed[0].placeholder).toBe('single line placeholder'); + expect(reparsed[0].default).toBe('multiline\ndefault'); + }); }); }); \ No newline at end of file diff --git a/trmnl-form-builder.js b/trmnl-form-builder.js index a9cff0a..6b1dfee 100644 --- a/trmnl-form-builder.js +++ b/trmnl-form-builder.js @@ -1725,21 +1725,50 @@ class TRMLYamlForm extends HTMLElement { lines.push(` name: ${this.escapeYaml(field.name)}`); if (field.description) { + const formatted = this.formatYamlValue(field.description); + if (formatted.includes('\n')) { + lines.push(` description: |`); + field.description.split('\n').forEach(line => { + lines.push(` ${line}`); + }); + } else { lines.push(` description: ${this.escapeYaml(field.description)}`); + } } - + if (field.help_text) { + if (typeof field.help_text === 'string' && field.help_text.includes('\n')) { + lines.push(` help_text: |`); + field.help_text.split('\n').forEach(line => { + lines.push(` ${line}`); + }); + } else { lines.push(` help_text: ${this.escapeYaml(field.help_text)}`); + } } - + if (field.placeholder) { + if (typeof field.placeholder === 'string' && field.placeholder.includes('\n')) { + lines.push(` placeholder: |`); + field.placeholder.split('\n').forEach(line => { + lines.push(` ${line}`); + }); + } else { lines.push(` placeholder: ${this.escapeYaml(field.placeholder)}`); + } } - + if (field.hasOwnProperty('default')) { - const quoteLiterals = (field.field_type == 'boolean'); - lines.push(` default: ${this.escapeYaml(field.default, quoteLiterals)}`); - } + if (typeof field.default === 'string' && field.default.includes('\n')) { + lines.push(` default: |`); + field.default.split('\n').forEach(line => { + lines.push(` ${line}`); + }); + } else { + const quoteLiterals = (field.field_type == 'boolean'); + lines.push(` default: ${this.escapeYaml(field.default, quoteLiterals)}`); + } + } if (field.optional) { lines.push(` optional: true`); @@ -1844,11 +1873,17 @@ if (field.max !== undefined) { } // Capture any i18n description keys - Object.keys(field) - .filter(k => k.startsWith('description-')) - .sort() - .forEach(k => { - lines.push(` ${k}: ${this.escapeYaml(field[k])}`); + Object.keys(field).forEach(k => { + if (k.startsWith('description-')) { + if (typeof field[k] === 'string' && field[k].includes('\n')) { + lines.push(` ${k}: |`); + field[k].split('\n').forEach(line => { + lines.push(` ${line}`); + }); + } else { + lines.push(` ${k}: "${field[k]}"`); + } + } }); }); @@ -1879,6 +1914,20 @@ if (field.max !== undefined) { return value; } + formatYamlValue(value, indent = ' ') { + // Check if value contains newlines (multiline) + if (typeof value === 'string' && value.includes('\n')) { + const lines = [`${indent}|`]; + // Split and indent each line + value.split('\n').forEach(line => { + lines.push(`${indent} ${line}`); + }); + return lines.join('\n'); + } else { + return `${indent}${this.escapeYaml(value)}`; + } + } + reorderFields(draggedId, targetId) { const draggedIndex = this.fields.findIndex(f => f.id === draggedId); const targetIndex = this.fields.findIndex(f => f.id === targetId); @@ -2108,9 +2157,41 @@ if (field.max !== undefined) { let currentArrayKey = null; let currentCondition = null; let currentNestedKey = null; // Helper to track if we are in hidden/required + let multilineKey = null; // Track which property is receiving multiline content + let multilineContent = []; // Collect multiline content + let multilineIndent = 0; // Track base indentation of multiline content - lines.forEach(line => { - // Skip comments and empty lines + + lines.forEach((line, index) => { + // Handle multiline content (pipe symbol continuation) + if (multilineKey) { + // Check if this line is part of the multiline content + // It should be indented more than the property declaration + const lineIndent = line.search(/\S/); // Find first non-whitespace character + + // If line is empty, add to multiline content + if (line.trim() === '') { + multilineContent.push(''); + return; + } + + // If line has proper indentation (more than property line), it's part of multiline + if (lineIndent > multilineIndent) { + // Remove the base indentation from the content line + const contentLine = line.substring(multilineIndent + 2); // +2 for the base indentation after | + multilineContent.push(contentLine); + return; + } else { + // End of multiline content - save it and continue processing this line + currentField[multilineKey] = multilineContent.join('\n'); + multilineKey = null; + multilineContent = []; + multilineIndent = 0; + // Don't return - fall through to process this line normally + } + } + + // Skip comments and empty lines (when not in multiline mode) if (line.trim().startsWith('#') || !line.trim()) { return; } @@ -2131,11 +2212,19 @@ if (field.max !== undefined) { // 2. Field properties (2 spaces indentation) else if (currentField && line.match(/^\s{2}[a-zA-Z_-]+:/)) { - const match = line.match(/^\s{2}([a-zA-Z_-]+):\s*(.*)$/); + const match = line.match(/^\s{2}([a-zA-Z_-]+):\s*(.*)$/); if (match) { const key = match[1]; const value = match[2].trim(); + // Check for pipe symbol (multiline string indicator) + if (value === '|') { + multilineKey = key; + multilineContent = []; + multilineIndent = 2; // Base indentation for field properties + return; + } + // Handle array start (value is empty) if (!value || value === '') { currentArrayKey = key; @@ -2217,6 +2306,11 @@ if (field.max !== undefined) { } }); + // Save any remaining multiline content + if (multilineKey && currentField) { + currentField[multilineKey] = multilineContent.join('\n'); + } + // Don't forget the last field if (currentField) { fields.push(currentField); From 4a643dead8b614465f7912696b67b1fdd1a09e93 Mon Sep 17 00:00:00 2001 From: Mario Lurig Date: Sat, 31 Jan 2026 23:43:25 -0700 Subject: [PATCH 3/3] expand code and text subfields placeholder and default switch from 'text' to 'textarea' under code and text field types. --- trmnl-form-builder.js | 51 ++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/trmnl-form-builder.js b/trmnl-form-builder.js index 6b1dfee..3d8f6c8 100644 --- a/trmnl-form-builder.js +++ b/trmnl-form-builder.js @@ -1430,28 +1430,43 @@ class TRMLYamlForm extends HTMLElement { // --- Special handling for 'default' field --- if (key === 'default') { - const hasDefault = field.hasOwnProperty('default'); - const defaultValue = hasDefault ? (field.default || '') : ''; - - return ` -
- - - - ${def.help ? `
${def.help}
` : ''} -
`; - } - - // Default: Text, Number, URL inputs - return ` + const hasDefault = field.hasOwnProperty('default'); + const defaultValue = hasDefault ? (field.default || '') : ''; + + // Check if parent field type should use textarea + const shouldUseTextarea = field.field_type === 'text' || field.field_type === 'code'; + + const inputElement = shouldUseTextarea + ? `` + : ``; + + return `
- + + ${inputElement} ${def.help ? `
${def.help}
` : ''}
`; + } + + + // Default: Text, Number, URL inputs + // Check if this is 'placeholder' and parent field type should use textarea + const shouldUseTextarea = (key === 'placeholder') && (field.field_type === 'text' || field.field_type === 'code'); + + const inputElement = shouldUseTextarea + ? `` + : ``; + + return ` +
+ + ${inputElement} + ${def.help ? `
${def.help}
` : ''} +
`; }