refactor(ui): 구조 대폭 간소화
- 자료형: node/string/int/bool/date 5개만 유지, integer→int, boolean→bool - 리스트 타입 완전 제거 (기존 데이터 → string으로 자동 마이그레이션) - nextSep/childSep 제거 (노드 간 구분은 항상 줄바꿈) - header/footer 제거 - prefix = 노드 전체 앞 문자열, suffix = 노드 전체 뒤 문자열로 재정의 - toText(): prefix/suffix 렌더링, list 로직 제거, 구조 단순화 - toJSON(): list/nextSep/childSep/header/footer 제거 - parseJsonSection: 동일하게 단순화, 구버전 LIST → string 호환 - doImport: flush() → string value 생성 - CSS: list 전용 스타일 제거 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
275
ui/builder.html
275
ui/builder.html
@@ -81,29 +81,12 @@ input,textarea,button,select{font-family:inherit}
|
|||||||
.val-item.val-node{border:none;background:transparent;padding:0}
|
.val-item.val-node{border:none;background:transparent;padding:0}
|
||||||
.val-head{display:flex;align-items:center;gap:5px;padding:4px 6px;background:var(--surface);border-bottom:1px solid var(--border)}
|
.val-head{display:flex;align-items:center;gap:5px;padding:4px 6px;background:var(--surface);border-bottom:1px solid var(--border)}
|
||||||
.val-type-badge{font-size:10px;font-weight:700;color:var(--muted);background:var(--surface-alt);border:1px solid var(--border);border-radius:3px;padding:1px 5px;text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}
|
.val-type-badge{font-size:10px;font-weight:700;color:var(--muted);background:var(--surface-alt);border:1px solid var(--border);border-radius:3px;padding:1px 5px;text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}
|
||||||
.val-cat-inp{flex:1;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:2px 6px;background:var(--surface);outline:none;min-width:0}
|
|
||||||
.val-cat-inp:focus{border-color:var(--border-focus)}
|
|
||||||
.val-cat-inp::placeholder{color:var(--muted)}
|
|
||||||
.val-body{padding:6px}
|
.val-body{padding:6px}
|
||||||
.val-names{width:100%;min-height:42px;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:4px 6px;resize:vertical;background:var(--surface);line-height:1.55;outline:none}
|
|
||||||
.val-names:focus{border-color:var(--border-focus)}
|
|
||||||
.val-data-inp{width:100%;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:4px 6px;background:var(--surface);outline:none}
|
.val-data-inp{width:100%;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:4px 6px;background:var(--surface);outline:none}
|
||||||
.val-data-inp:focus{border-color:var(--border-focus)}
|
.val-data-inp:focus{border-color:var(--border-focus)}
|
||||||
.val-data-ta{width:100%;min-height:60px;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:4px 6px;resize:vertical;background:var(--surface);font-family:monospace;line-height:1.5;outline:none}
|
.sep-chip{font-size:11px;padding:2px 8px;border-radius:4px;border:1px solid var(--border);background:var(--surface);color:var(--muted);cursor:pointer;font-weight:500;transition:all .1s;white-space:nowrap}
|
||||||
.val-data-ta:focus{border-color:var(--border-focus)}
|
|
||||||
.val-opts{display:flex;flex-direction:column;gap:4px;margin-top:5px;padding-top:5px;border-top:1px dashed var(--border)}
|
|
||||||
.opt-row{display:flex;align-items:center;gap:5px}
|
|
||||||
.opt-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);width:36px;flex-shrink:0}
|
|
||||||
.sep-chips{display:flex;gap:3px;flex-wrap:wrap}
|
|
||||||
.sep-chip{font-size:11px;padding:2px 6px;border-radius:4px;border:1px solid var(--border);background:var(--surface);color:var(--muted);cursor:pointer;font-weight:500;transition:all .1s;white-space:nowrap}
|
|
||||||
.sep-chip:hover{border-color:var(--accent);color:var(--accent)}
|
.sep-chip:hover{border-color:var(--accent);color:var(--accent)}
|
||||||
.sep-chip.on{background:var(--accent);color:#fff;border-color:var(--accent)}
|
.sep-chip.on{background:var(--accent);color:#fff;border-color:var(--accent)}
|
||||||
.sep-cust{width:48px;font-size:11px;border:1px solid var(--border);border-radius:4px;padding:2px 4px;background:var(--surface);text-align:center;outline:none}
|
|
||||||
.sep-cust:focus{border-color:var(--border-focus)}
|
|
||||||
.npl-inp{width:44px;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:2px 4px;text-align:center;background:var(--surface);outline:none}
|
|
||||||
.npl-inp:focus{border-color:var(--border-focus)}
|
|
||||||
.npl-inp:disabled{background:var(--surface-alt);color:var(--muted)}
|
|
||||||
.opt-hint{font-size:10px;color:var(--muted)}
|
|
||||||
|
|
||||||
/* ── 구분자/이름줄바꿈 설정 (노드 설정 내부) ─ */
|
/* ── 구분자/이름줄바꿈 설정 (노드 설정 내부) ─ */
|
||||||
.struct-sep{display:flex;flex-direction:column;gap:4px;padding-bottom:6px;margin-bottom:6px;border-bottom:1px dashed var(--border)}
|
.struct-sep{display:flex;flex-direction:column;gap:4px;padding-bottom:6px;margin-bottom:6px;border-bottom:1px dashed var(--border)}
|
||||||
@@ -254,25 +237,14 @@ input,textarea,button,select{font-family:inherit}
|
|||||||
const VALUE_TYPES = [
|
const VALUE_TYPES = [
|
||||||
{v:'node', lbl:'노드 (Node)'},
|
{v:'node', lbl:'노드 (Node)'},
|
||||||
{v:'string', lbl:'문자열 (String)'},
|
{v:'string', lbl:'문자열 (String)'},
|
||||||
{v:'integer', lbl:'정수 (Integer)'},
|
{v:'int', lbl:'정수 (Int)'},
|
||||||
{v:'float', lbl:'실수 (Float)'},
|
{v:'bool', lbl:'불리언 (Bool)'},
|
||||||
{v:'boolean', lbl:'불리언 (Boolean)'},
|
|
||||||
{v:'null', lbl:'Null'},
|
|
||||||
{v:'date', lbl:'날짜 (Date)'},
|
{v:'date', lbl:'날짜 (Date)'},
|
||||||
{v:'datetime', lbl:'날짜+시간 (Datetime)'},
|
|
||||||
{v:'time', lbl:'시간 (Time)'},
|
|
||||||
{v:'markdown', lbl:'Markdown'},
|
|
||||||
{v:'html', lbl:'HTML'},
|
|
||||||
{v:'json', lbl:'JSON'},
|
|
||||||
{v:'code', lbl:'코드 (Code)'},
|
|
||||||
{v:'custom', lbl:'사용자 정의 (Custom)'},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const SETTING_OPTIONS = [
|
const SETTING_OPTIONS = [
|
||||||
{v:'header', lbl:'머리글 (header)'},
|
{v:'prefix', lbl:'앞 문자열 (prefix)'},
|
||||||
{v:'footer', lbl:'꼬리글 (footer)'},
|
{v:'suffix', lbl:'뒤 문자열 (suffix)'},
|
||||||
{v:'prefix', lbl:'접두사 (prefix)'},
|
|
||||||
{v:'suffix', lbl:'접미사 (suffix)'},
|
|
||||||
{v:'attributes', lbl:'어트리뷰트'},
|
{v:'attributes', lbl:'어트리뷰트'},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -291,13 +263,6 @@ const ATTR_KEYS = [
|
|||||||
{v:'_custom', lbl:'직접 입력...'},
|
{v:'_custom', lbl:'직접 입력...'},
|
||||||
];
|
];
|
||||||
|
|
||||||
const SEP_PRESETS = [
|
|
||||||
{v:' ', lbl:'공백'},
|
|
||||||
{v:', ', lbl:', '},
|
|
||||||
{v:' / ', lbl:'/'},
|
|
||||||
{v:'\n', lbl:'↵'},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
// 팩토리
|
// 팩토리
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
@@ -305,10 +270,7 @@ function uid(){ return Math.random().toString(36).slice(2,9) }
|
|||||||
|
|
||||||
function mkNode(mark='', content=''){
|
function mkNode(mark='', content=''){
|
||||||
return { id:uid(), mark, content, hidden:false, values:[], settings:{},
|
return { id:uid(), mark, content, hidden:false, values:[], settings:{},
|
||||||
nextSep:'blank', childSep:'blank', nameNewline:true };
|
nameNewline:true };
|
||||||
}
|
|
||||||
function mkListVal(names='', category=''){
|
|
||||||
return { id:uid(), type:'list', names, sep:' ', namesPerLine:0, category };
|
|
||||||
}
|
}
|
||||||
function mkNodeVal(childNode){
|
function mkNodeVal(childNode){
|
||||||
return { id:uid(), type:'node', node: childNode ?? mkNode() };
|
return { id:uid(), type:'node', node: childNode ?? mkNode() };
|
||||||
@@ -353,11 +315,20 @@ function load(){
|
|||||||
S=buildExample();
|
S=buildExample();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VALID_TYPES=new Set(['node','string','int','bool','date']);
|
||||||
function normalizeNode(n){
|
function normalizeNode(n){
|
||||||
if(n.nextSep===undefined) n.nextSep='blank';
|
|
||||||
if(n.childSep===undefined) n.childSep='blank';
|
|
||||||
if(n.nameNewline===undefined) n.nameNewline=true;
|
if(n.nameNewline===undefined) n.nameNewline=true;
|
||||||
for(const v of n.values||[]) if(v.type==='node') normalizeNode(v.node);
|
// list → string, 지원하지 않는 타입 → string
|
||||||
|
n.values=(n.values||[]).map(v=>{
|
||||||
|
if(v.type==='list'){
|
||||||
|
const sv=mkScalarVal('string'); sv.data=(v.names??'').trim(); return sv;
|
||||||
|
}
|
||||||
|
if(v.type==='node'){ normalizeNode(v.node); return v; }
|
||||||
|
if(v.type==='integer') v.type='int';
|
||||||
|
if(v.type==='boolean') v.type='bool';
|
||||||
|
if(!VALID_TYPES.has(v.type)) v.type='string';
|
||||||
|
return v;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
function normalizeState(s){
|
function normalizeState(s){
|
||||||
for(const n of s.nodes||[]) normalizeNode(n);
|
for(const n of s.nodes||[]) normalizeNode(n);
|
||||||
@@ -376,35 +347,28 @@ function migrateOld(o){
|
|||||||
function migSec(s, oldMarks){
|
function migSec(s, oldMarks){
|
||||||
const mark = s.mark ?? oldMarks[(s.lv??1)-1] ?? '';
|
const mark = s.mark ?? oldMarks[(s.lv??1)-1] ?? '';
|
||||||
const n = mkNode(mark, s.content ?? s.name ?? '');
|
const n = mkNode(mark, s.content ?? s.name ?? '');
|
||||||
n.hidden = false;
|
// groups → string values (names 합치기)
|
||||||
// groups → list values
|
|
||||||
for(const g of s.groups??[]){
|
for(const g of s.groups??[]){
|
||||||
const lv = mkListVal(g.names??'', g.attributes?.category??'');
|
const names=(g.names??'').trim(); if(!names) continue;
|
||||||
lv.sep = g.sep??' '; lv.namesPerLine = g.namesPerLine??0;
|
const sv=mkScalarVal('string'); sv.data=names; n.values.push(sv);
|
||||||
n.values.push(lv);
|
|
||||||
}
|
}
|
||||||
// children → node values
|
|
||||||
for(const c of s.children??[]) n.values.push(mkNodeVal(migSec(c, oldMarks)));
|
for(const c of s.children??[]) n.values.push(mkNodeVal(migSec(c, oldMarks)));
|
||||||
// scalar value
|
|
||||||
if(s.value!=null && s.data_type && s.data_type!=='unset'){
|
if(s.value!=null && s.data_type && s.data_type!=='unset'){
|
||||||
const sv=mkScalarVal(s.data_type); sv.data=String(s.value??''); n.values.push(sv);
|
const sv=mkScalarVal('string'); sv.data=String(s.value??''); n.values.push(sv);
|
||||||
}
|
}
|
||||||
// settings
|
|
||||||
if(s.header) n.settings.header=s.header;
|
|
||||||
if(s.footer) n.settings.footer=s.footer;
|
|
||||||
if(s.prefix) n.settings.prefix=s.prefix;
|
if(s.prefix) n.settings.prefix=s.prefix;
|
||||||
if(s.suffix) n.settings.suffix=s.suffix;
|
if(s.suffix) n.settings.suffix=s.suffix;
|
||||||
const filtAttrs=Object.entries(s.attributes??{}).filter(([k])=>k!=='level'&&k!=='category');
|
const filtAttrs=Object.entries(s.attributes??{}).filter(([k])=>k!=='level'&&k!=='category'&&k!=='mark');
|
||||||
if(filtAttrs.length) n.settings.attributes=Object.fromEntries(filtAttrs);
|
if(filtAttrs.length) n.settings.attributes=Object.fromEntries(filtAttrs);
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildExample(){
|
function buildExample(){
|
||||||
const n1=mkNode('🔹','1번 항목');
|
const n1=mkNode('🔹','1번 항목');
|
||||||
n1.values.push(mkListVal('홍길동 홍길순 홍길영 홍길준\n홍길호 홍길윤',''));
|
const sv1=mkScalarVal('string'); sv1.data='홍길동 홍길순 홍길영 홍길준'; n1.values.push(sv1);
|
||||||
const n2=mkNode('🔹','2번 항목');
|
const n2=mkNode('🔹','2번 항목');
|
||||||
const c1=mkNode('🔸','2-1 항목'); c1.values.push(mkListVal('홍길찬 홍길만','건강'));
|
const c1=mkNode('🔸','2-1 항목'); const sv2=mkScalarVal('string'); sv2.data='홍길찬 홍길만'; c1.values.push(sv2);
|
||||||
const c2=mkNode('🔸','2-2 항목'); c2.values.push(mkListVal('홍길주','직장'));
|
const c2=mkNode('🔸','2-2 항목'); const sv3=mkScalarVal('string'); sv3.data='홍길주'; c2.values.push(sv3);
|
||||||
n2.values.push(mkNodeVal(c1)); n2.values.push(mkNodeVal(c2));
|
n2.values.push(mkNodeVal(c1)); n2.values.push(mkNodeVal(c2));
|
||||||
return {titleMark:'✅', title:'예시 양식', nodes:[n1,n2]};
|
return {titleMark:'✅', title:'예시 양식', nodes:[n1,n2]};
|
||||||
}
|
}
|
||||||
@@ -440,12 +404,8 @@ function toggleHidden(nid){
|
|||||||
function addValue(nid, type){
|
function addValue(nid, type){
|
||||||
if(!type) return;
|
if(!type) return;
|
||||||
const n=nodeMap[nid]; if(!n) return;
|
const n=nodeMap[nid]; if(!n) return;
|
||||||
let v;
|
const v = type==='node' ? mkNodeVal() : mkScalarVal(type);
|
||||||
if(type==='list') v=mkListVal();
|
|
||||||
else if(type==='node') v=mkNodeVal();
|
|
||||||
else v=mkScalarVal(type);
|
|
||||||
n.values.push(v); refresh();
|
n.values.push(v); refresh();
|
||||||
if(type==='list') focusNew('.val-names');
|
|
||||||
}
|
}
|
||||||
function removeValue(vid){
|
function removeValue(vid){
|
||||||
const e=valueMap[vid]; if(!e) return;
|
const e=valueMap[vid]; if(!e) return;
|
||||||
@@ -590,47 +550,7 @@ function renderValue(nid, val){
|
|||||||
return `<div class="val-item val-node" data-vid="${val.id}">${renderNode(val.node,true)}</div>`;
|
return `<div class="val-item val-node" data-vid="${val.id}">${renderNode(val.node,true)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(val.type==='list'){
|
|
||||||
const sep=val.sep??' ', npl=val.namesPerLine??0, isNl=sep==='\n';
|
|
||||||
const chips=SEP_PRESETS.map(p=>`
|
|
||||||
<span class="sep-chip${sep===p.v?' on':''}"
|
|
||||||
onclick="valueMap['${val.id}'].val.sep=${JSON.stringify(p.v)};refresh()"
|
|
||||||
>${p.lbl}</span>`).join('');
|
|
||||||
const customVal=SEP_PRESETS.find(p=>p.v===sep)?'':esc(sep);
|
|
||||||
return `
|
|
||||||
<div class="val-item" data-vid="${val.id}">
|
|
||||||
<div class="val-head">
|
|
||||||
<span class="val-type-badge">LIST</span>
|
|
||||||
<input class="val-cat-inp" placeholder="카테고리 (선택)"
|
|
||||||
value="${esc(val.category??'')}"
|
|
||||||
oninput="valueMap['${val.id}'].val.category=this.value;updatePreview();save()">
|
|
||||||
${mvDel}
|
|
||||||
</div>
|
|
||||||
<div class="val-body">
|
|
||||||
<textarea class="val-names" placeholder="이름 입력 (공백·줄바꿈·쉼표로 구분)"
|
|
||||||
oninput="valueMap['${val.id}'].val.names=this.value;updatePreview();save()"
|
|
||||||
>${esc(val.names)}</textarea>
|
|
||||||
<div class="val-opts">
|
|
||||||
<div class="opt-row">
|
|
||||||
<span class="opt-lbl">구분</span>
|
|
||||||
<div class="sep-chips">${chips}</div>
|
|
||||||
<input class="sep-cust" placeholder="직접" value="${customVal}"
|
|
||||||
oninput="valueMap['${val.id}'].val.sep=this.value||' ';updatePreview();save()">
|
|
||||||
</div>
|
|
||||||
<div class="opt-row">
|
|
||||||
<span class="opt-lbl">줄당</span>
|
|
||||||
<input class="npl-inp" type="number" min="0" value="${npl}" ${isNl?'disabled':''}
|
|
||||||
oninput="valueMap['${val.id}'].val.namesPerLine=Math.max(0,+this.value||0);updatePreview();save()">
|
|
||||||
<span class="opt-hint">${isNl?'(줄바꿈 모드)':'개 (0=무제한)'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 스칼라 타입
|
|
||||||
const typeLbl=VALUE_TYPES.find(t=>t.v===val.type)?.lbl??val.type.toUpperCase();
|
const typeLbl=VALUE_TYPES.find(t=>t.v===val.type)?.lbl??val.type.toUpperCase();
|
||||||
const isMulti=['markdown','html','json','code'].includes(val.type);
|
|
||||||
return `
|
return `
|
||||||
<div class="val-item" data-vid="${val.id}">
|
<div class="val-item" data-vid="${val.id}">
|
||||||
<div class="val-head">
|
<div class="val-head">
|
||||||
@@ -639,60 +559,22 @@ function renderValue(nid, val){
|
|||||||
${mvDel}
|
${mvDel}
|
||||||
</div>
|
</div>
|
||||||
<div class="val-body">
|
<div class="val-body">
|
||||||
${isMulti
|
<input class="val-data-inp" placeholder="${esc(typeLbl)} 입력"
|
||||||
?`<textarea class="val-data-ta" placeholder="${esc(typeLbl)} 입력"
|
|
||||||
oninput="valueMap['${val.id}'].val.data=this.value;updatePreview();save()"
|
|
||||||
>${esc(val.data??'')}</textarea>`
|
|
||||||
:`<input class="val-data-inp" placeholder="${esc(typeLbl)} 입력"
|
|
||||||
value="${esc(val.data??'')}"
|
value="${esc(val.data??'')}"
|
||||||
oninput="valueMap['${val.id}'].val.data=this.value;updatePreview();save()">`
|
oninput="valueMap['${val.id}'].val.data=this.value;updatePreview();save()">
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEP_CFG = [
|
|
||||||
{v:'none', lbl:'없음'},
|
|
||||||
{v:'newline', lbl:'줄바꿈'},
|
|
||||||
{v:'blank', lbl:'빈 줄'},
|
|
||||||
];
|
|
||||||
|
|
||||||
function setSep(nid, field, val){
|
|
||||||
const n=nodeMap[nid]; if(!n) return;
|
|
||||||
n[field]=val; refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSepChips(nid, field, current){
|
|
||||||
const isCustom = !SEP_CFG.find(p=>p.v===current);
|
|
||||||
const chips = SEP_CFG.map(p=>`
|
|
||||||
<span class="sep-chip${current===p.v?' on':''}"
|
|
||||||
onclick="setSep('${nid}','${field}','${p.v}')">${p.lbl}</span>`).join('');
|
|
||||||
return `
|
|
||||||
${chips}
|
|
||||||
<input class="sep-cust" placeholder="직접" title="직접 입력"
|
|
||||||
value="${isCustom?esc(current):''}"
|
|
||||||
oninput="setSep('${nid}','${field}',this.value||'none')"
|
|
||||||
style="width:56px">`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNameNL(nid, val){
|
function setNameNL(nid, val){
|
||||||
const n=nodeMap[nid]; if(!n) return;
|
const n=nodeMap[nid]; if(!n) return;
|
||||||
n.nameNewline=val; refresh();
|
n.nameNewline=val; refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStructSettings(node){
|
function renderStructSettings(node){
|
||||||
const nid=node.id;
|
const nid=node.id, nl=node.nameNewline??true;
|
||||||
const nl=node.nameNewline??true;
|
|
||||||
return `
|
return `
|
||||||
<div class="struct-sep">
|
<div class="struct-sep">
|
||||||
<div class="sep-bar-row">
|
|
||||||
<span class="sep-bar-lbl">다음 노드</span>
|
|
||||||
${renderSepChips(nid,'nextSep', node.nextSep??'blank')}
|
|
||||||
</div>
|
|
||||||
<div class="sep-bar-row">
|
|
||||||
<span class="sep-bar-lbl">하위 노드</span>
|
|
||||||
${renderSepChips(nid,'childSep', node.childSep??'blank')}
|
|
||||||
</div>
|
|
||||||
<div class="sep-bar-row">
|
<div class="sep-bar-row">
|
||||||
<span class="sep-bar-lbl">이름 뒤</span>
|
<span class="sep-bar-lbl">이름 뒤</span>
|
||||||
<span class="sep-chip${nl?' on':''}" onclick="setNameNL('${nid}',true)">줄바꿈</span>
|
<span class="sep-chip${nl?' on':''}" onclick="setNameNL('${nid}',true)">줄바꿈</span>
|
||||||
@@ -746,65 +628,25 @@ function renderSetting(nid, key, val){
|
|||||||
function parseNames(raw){ return (raw??'').trim().split(/[\s,\n]+/).filter(Boolean) }
|
function parseNames(raw){ return (raw??'').trim().split(/[\s,\n]+/).filter(Boolean) }
|
||||||
|
|
||||||
function toText(){
|
function toText(){
|
||||||
// 구분자 값 → 실제 문자열
|
|
||||||
function ss(sep){
|
|
||||||
const s=sep??'blank';
|
|
||||||
if(s==='none') return '';
|
|
||||||
if(s==='newline') return '\n';
|
|
||||||
if(s==='blank') return '\n\n';
|
|
||||||
return String(s); // 직접 입력값 (예: '/', ', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 노드 하나를 문자열로 렌더링
|
|
||||||
function rNode(node){
|
function rNode(node){
|
||||||
let t=`${node.mark??''}${node.content||''}`;
|
const pre=node.settings.prefix??'';
|
||||||
const nl=(node.nameNewline??true)?'\n':''; // 이름 뒤 줄바꿈 여부
|
const suf=node.settings.suffix??'';
|
||||||
let pendingSep=null;
|
const nl=(node.nameNewline??true)?'\n':'';
|
||||||
let prevWasList=false;
|
let t=pre+`${node.mark??''}${node.content||''}`;
|
||||||
let firstContent=true; // 첫 번째 list/scalar 여부 (nameNewline 적용 대상)
|
let first=true;
|
||||||
|
for(const v of node.values){
|
||||||
for(let i=0;i<node.values.length;i++){
|
if(v.type==='node'){
|
||||||
const v=node.values[i];
|
t+='\n'+rNode(v.node); first=false;
|
||||||
const nextV=node.values[i+1]??null;
|
} else if(v.data!==undefined&&v.data!==null&&String(v.data).length){
|
||||||
|
t+=(first?nl:'\n')+String(v.data); first=false;
|
||||||
if(v.type==='list'){
|
|
||||||
const ns=parseNames(v.names); if(!ns.length) continue;
|
|
||||||
const cat=(v.category??'').trim(), pre=cat?`(${cat}) `:'';
|
|
||||||
const lsep=v.sep??' ', npl=v.namesPerLine??0;
|
|
||||||
let lt;
|
|
||||||
if(lsep==='\n') lt=ns.map((n,j)=>(j===0?pre:'')+n).join('\n');
|
|
||||||
else if(npl>0) lt=Array.from({length:Math.ceil(ns.length/npl)},(_,j)=>(j===0?pre:'')+ns.slice(j*npl,(j+1)*npl).join(lsep)).join('\n');
|
|
||||||
else lt=pre+ns.join(lsep);
|
|
||||||
const pre2=firstContent?nl:'\n'; // 첫 list만 nameNewline 적용
|
|
||||||
t+=pre2+lt+'\n';
|
|
||||||
prevWasList=true; pendingSep=null; firstContent=false;
|
|
||||||
|
|
||||||
} else if(v.type==='node'){
|
|
||||||
if(!prevWasList){
|
|
||||||
t+=(pendingSep!==null) ? pendingSep : ss(node.childSep??'blank');
|
|
||||||
}
|
|
||||||
t+=rNode(v.node);
|
|
||||||
prevWasList=false; firstContent=false;
|
|
||||||
pendingSep=(nextV&&nextV.type==='node') ? ss(v.node.nextSep??'blank') : null;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if(v.data){
|
|
||||||
const pre2=firstContent?nl:'\n';
|
|
||||||
t+=pre2+String(v.data);
|
|
||||||
firstContent=false;
|
|
||||||
}
|
|
||||||
prevWasList=false; pendingSep=null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
t+=suf;
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title=S.title.trim();
|
const title=S.title.trim();
|
||||||
let out=title?`${S.titleMark}${title}\n\n`:'';
|
let out=title?`${S.titleMark}${title}\n`:'';
|
||||||
for(let i=0;i<S.nodes.length;i++){
|
out+=S.nodes.map(rNode).join('\n');
|
||||||
out+=rNode(S.nodes[i]);
|
|
||||||
if(i<S.nodes.length-1) out+=ss(S.nodes[i].nextSep??'blank');
|
|
||||||
}
|
|
||||||
return out.replace(/\n{3,}/g,'\n\n').trimEnd()+'\n';
|
return out.replace(/\n{3,}/g,'\n\n').trimEnd()+'\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -836,25 +678,15 @@ function toJSON(slim=false){
|
|||||||
function serNode(node){
|
function serNode(node){
|
||||||
depth++;
|
depth++;
|
||||||
const children=node.values.map(v=>{
|
const children=node.values.map(v=>{
|
||||||
if(v.type==='list'){
|
|
||||||
const ns=parseNames(v.names);
|
|
||||||
return buildNode({id:v.id,type:'LIST',
|
|
||||||
attributes:v.category?{category:v.category}:{},
|
|
||||||
children:ns.map(n=>buildNode({id:uid(),type:'LIST_ITEM',content:n}))});
|
|
||||||
}
|
|
||||||
if(v.type==='node') return serNode(v.node);
|
if(v.type==='node') return serNode(v.node);
|
||||||
return buildNode({id:v.id,type:'VALUE',data_type:v.type,value:v.data??null});
|
return buildNode({id:v.id,type:'VALUE',data_type:v.type,value:v.data??null});
|
||||||
});
|
});
|
||||||
const attrs={mark:node.mark,level:depth,...(node.settings.attributes??{})};
|
const attrs={mark:node.mark,level:depth,...(node.settings.attributes??{})};
|
||||||
const r=buildNode({
|
const r=buildNode({
|
||||||
id:node.id,type:'SECTION',content:node.content,
|
id:node.id,type:'SECTION',content:node.content,
|
||||||
header:node.settings.header??null,
|
|
||||||
footer:node.settings.footer??null,
|
|
||||||
prefix:node.settings.prefix??null,
|
prefix:node.settings.prefix??null,
|
||||||
suffix:node.settings.suffix??null,
|
suffix:node.settings.suffix??null,
|
||||||
attributes:attrs,children,
|
attributes:attrs,children,
|
||||||
nextSep:node.nextSep??'blank',
|
|
||||||
childSep:node.childSep??'blank',
|
|
||||||
nameNewline:node.nameNewline??true,
|
nameNewline:node.nameNewline??true,
|
||||||
});
|
});
|
||||||
depth--;
|
depth--;
|
||||||
@@ -959,29 +791,22 @@ function doImportJson(){
|
|||||||
function parseJsonSection(sec){
|
function parseJsonSection(sec){
|
||||||
const mark=sec.attributes?.mark??'';
|
const mark=sec.attributes?.mark??'';
|
||||||
const n=mkNode(mark, sec.content??'');
|
const n=mkNode(mark, sec.content??'');
|
||||||
|
|
||||||
if(sec.nextSep!==undefined) n.nextSep=sec.nextSep;
|
|
||||||
if(sec.childSep!==undefined) n.childSep=sec.childSep;
|
|
||||||
if(sec.nameNewline!==undefined) n.nameNewline=sec.nameNewline;
|
if(sec.nameNewline!==undefined) n.nameNewline=sec.nameNewline;
|
||||||
|
|
||||||
if(sec.header) n.settings.header=sec.header;
|
|
||||||
if(sec.footer) n.settings.footer=sec.footer;
|
|
||||||
if(sec.prefix) n.settings.prefix=sec.prefix;
|
if(sec.prefix) n.settings.prefix=sec.prefix;
|
||||||
if(sec.suffix) n.settings.suffix=sec.suffix;
|
if(sec.suffix) n.settings.suffix=sec.suffix;
|
||||||
|
|
||||||
const filtAttrs=Object.entries(sec.attributes??{}).filter(([k])=>k!=='mark'&&k!=='level');
|
const filtAttrs=Object.entries(sec.attributes??{}).filter(([k])=>k!=='mark'&&k!=='level');
|
||||||
if(filtAttrs.length) n.settings.attributes=Object.fromEntries(filtAttrs);
|
if(filtAttrs.length) n.settings.attributes=Object.fromEntries(filtAttrs);
|
||||||
|
|
||||||
for(const child of sec.children??[]){
|
for(const child of sec.children??[]){
|
||||||
if(child.type==='LIST'){
|
if(child.type==='SECTION'){
|
||||||
const names=(child.children??[]).filter(c=>c.type==='LIST_ITEM').map(c=>c.content??'').join(' ');
|
|
||||||
n.values.push(mkListVal(names, child.attributes?.category??''));
|
|
||||||
} else if(child.type==='SECTION'){
|
|
||||||
n.values.push(mkNodeVal(parseJsonSection(child)));
|
n.values.push(mkNodeVal(parseJsonSection(child)));
|
||||||
} else if(child.type==='VALUE'){
|
} else if(child.type==='VALUE'){
|
||||||
const sv=mkScalarVal(child.data_type??'string');
|
const sv=mkScalarVal(child.data_type??'string');
|
||||||
sv.data=String(child.value??'');
|
sv.data=String(child.value??'');
|
||||||
n.values.push(sv);
|
n.values.push(sv);
|
||||||
|
} else if(child.type==='LIST'){
|
||||||
|
// 구버전 호환: LIST → string value
|
||||||
|
const names=(child.children??[]).filter(c=>c.type==='LIST_ITEM').map(c=>c.content??'').join(' ');
|
||||||
|
const sv=mkScalarVal('string'); sv.data=names; n.values.push(sv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return n;
|
return n;
|
||||||
@@ -998,8 +823,8 @@ function doImport(){
|
|||||||
function flush(){
|
function flush(){
|
||||||
if(!pend||!pend.names.length){ pend=null; return; }
|
if(!pend||!pend.names.length){ pend=null; return; }
|
||||||
const parent=pend.parentId?findNodeInList(nodes,pend.parentId):null;
|
const parent=pend.parentId?findNodeInList(nodes,pend.parentId):null;
|
||||||
const lv=mkListVal(pend.names.join(' '),pend.cat||'');
|
const sv=mkScalarVal('string'); sv.data=pend.names.join(' ');
|
||||||
if(parent) parent.values.push(lv); pend=null;
|
if(parent) parent.values.push(sv); pend=null;
|
||||||
}
|
}
|
||||||
function findNodeInList(list,id){ for(const n of list){ if(n.id===id) return n; for(const v of n.values) if(v.type==='node'){ const f=findNodeInList([v.node],id); if(f) return f; } } return null; }
|
function findNodeInList(list,id){ for(const n of list){ if(n.id===id) return n; for(const v of n.values) if(v.type==='node'){ const f=findNodeInList([v.node],id); if(f) return f; } } return null; }
|
||||||
function topId(){ return stack.length?stack[stack.length-1].id:null }
|
function topId(){ return stack.length?stack[stack.length-1].id:null }
|
||||||
|
|||||||
Reference in New Issue
Block a user