Add multiline support with YAML pipe symbol

Completes #10
This commit is contained in:
Mario Lurig
2026-01-31 23:50:17 -07:00
committed by GitHub
3 changed files with 772 additions and 32 deletions
+31
View File
@@ -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
File diff suppressed because it is too large Load Diff
+141 -32
View File
@@ -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 `
<div class="form-group" id="prop-${key}">
<label class="form-label">${def.label}</label>
<label class="form-checkbox" style="margin-bottom: 8px;">
<input type="checkbox" id="enable-default" ${hasDefault ? 'checked' : ''}>
Set default value
</label>
<input type="${def.type}" class="form-input" id="default-input" data-prop="${key}" value="${defaultValue}" placeholder="${def.placeholder || ''}" ${!hasDefault ? 'disabled' : ''} style="opacity: ${hasDefault ? '1' : '0.5'}">
${def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
</div>`;
}
// 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
? `<textarea class="form-textarea" id="default-input" data-prop="${key}" placeholder="${def.placeholder || ''}" ${!hasDefault ? 'disabled' : ''} style="opacity: ${hasDefault ? '1' : '0.5'}; resize: vertical;">${defaultValue}</textarea>`
: `<input type="${def.type}" class="form-input" id="default-input" data-prop="${key}" value="${defaultValue}" placeholder="${def.placeholder || ''}" ${!hasDefault ? 'disabled' : ''} style="opacity: ${hasDefault ? '1' : '0.5'}">`;
return `
<div class="form-group" id="prop-${key}">
<label class="form-label">${def.label}</label>
<input type="${def.type}" class="form-input" data-prop="${key}" value="${value}" placeholder="${def.placeholder || ''}">
<label class="form-checkbox" style="margin-bottom: 8px;">
<input type="checkbox" id="enable-default" ${hasDefault ? 'checked' : ''}>
Set default value
</label>
${inputElement}
${def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
</div>`;
}
// 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
? `<textarea class="form-textarea" data-prop="${key}" placeholder="${def.placeholder || ''}" style="resize: vertical;">${value}</textarea>`
: `<input type="${def.type}" class="form-input" data-prop="${key}" value="${value}" placeholder="${def.placeholder || ''}">`;
return `
<div class="form-group" id="prop-${key}">
<label class="form-label">${def.label}</label>
${inputElement}
${def.help ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">${def.help}</div>` : ''}
</div>`;
}
@@ -1725,21 +1740,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 +1888,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 +1929,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 +2172,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 +2227,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 +2321,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);