ui: redesign Tab 2 as text-paste form merger (Option A)
Workflow: 1. Load template JSON (defines section structure) 2. Paste multiple text-format forms into textarea 3. Parser splits by title mark, collects tokens per section key 4. Merged output in text or JSON format Key changes: - splitIntoInstances(): split at each title-mark line boundary - parseInstance(): match section headers by mark+name, tokenize value lines - mergedToText(): walk template tree, output tokens with per_line chunking - mergedToJSON(): reconstruct ROOT/SECTION/VALUE tree with merged tokens - Template view: compact inline chip display of section structure - Instance counter badge shows how many forms were detected - JSON export: each token becomes a separate VALUE node (data_type:string) - per_line attribute on SECTION controls line wrapping in text output Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
319
ui/builder.html
319
ui/builder.html
@@ -219,6 +219,24 @@ input,textarea,button,select{font-family:inherit}
|
|||||||
.fval-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
|
.fval-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
|
||||||
.fval-inp{width:100%;font-size:13px;border:1.5px solid var(--border);border-radius:6px;padding:6px 8px;outline:none;font-family:inherit;background:var(--surface-alt);resize:vertical;line-height:1.5}
|
.fval-inp{width:100%;font-size:13px;border:1.5px solid var(--border);border-radius:6px;padding:6px 8px;outline:none;font-family:inherit;background:var(--surface-alt);resize:vertical;line-height:1.5}
|
||||||
.fval-inp:focus{border-color:var(--border-focus);background:#fff}
|
.fval-inp:focus{border-color:var(--border-focus);background:#fff}
|
||||||
|
|
||||||
|
/* ── 양식 합치기: 입력 패널 ──────────────── */
|
||||||
|
.filler-input-panel{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--bg)}
|
||||||
|
.ftpl-view{padding:8px 14px;border-bottom:1px solid var(--border);background:var(--surface-alt);flex-shrink:0;display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;min-height:38px;max-height:90px;overflow-y:auto}
|
||||||
|
.ftpl-view::-webkit-scrollbar{height:4px}
|
||||||
|
.ftpl-empty{font-size:11px;color:var(--muted);font-style:italic}
|
||||||
|
.ftpl-title{display:flex;align-items:center;gap:4px;font-size:12px;font-weight:700;color:var(--text);padding-right:8px;border-right:1px solid var(--border);margin-right:4px}
|
||||||
|
.ftpl-sec{display:inline-flex;align-items:center;gap:3px;font-size:11px;background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:2px 7px;white-space:nowrap}
|
||||||
|
.ftpl-mark{font-size:13px;line-height:1}
|
||||||
|
.ftpl-name{font-weight:500;color:var(--text)}
|
||||||
|
.ftpl-arr{color:var(--muted);font-size:9px}
|
||||||
|
.ftpl-badge{font-size:9px;background:var(--accent-bg);color:var(--accent);border-radius:3px;padding:1px 5px;font-weight:700;margin-left:2px;letter-spacing:.04em}
|
||||||
|
.filler-textarea-wrap{flex:1;display:flex;flex-direction:column;padding:12px;overflow:hidden}
|
||||||
|
.filler-input-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);margin-bottom:7px;display:flex;align-items:center;gap:7px}
|
||||||
|
.filler-inst-count{font-size:11px;font-weight:600;color:var(--accent);background:var(--accent-bg);border-radius:99px;padding:1px 9px;text-transform:none;letter-spacing:0}
|
||||||
|
.filler-textarea{flex:1;width:100%;font-size:13px;font-family:inherit;border:1.5px solid var(--border);border-radius:var(--r);padding:10px 12px;outline:none;resize:none;line-height:1.75;background:var(--surface);color:var(--text)}
|
||||||
|
.filler-textarea:focus{border-color:var(--border-focus)}
|
||||||
|
.filler-textarea::placeholder{color:var(--muted)}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -286,18 +304,29 @@ input,textarea,button,select{font-family:inherit}
|
|||||||
<div class="filler-toolbar">
|
<div class="filler-toolbar">
|
||||||
<span style="font-size:16px">📋</span>
|
<span style="font-size:16px">📋</span>
|
||||||
<span class="filler-title" id="fillerTitle">템플릿 없음</span>
|
<span class="filler-title" id="fillerTitle">템플릿 없음</span>
|
||||||
<button class="hdr-btn" onclick="openFillerImport()">📂 JSON 불러오기</button>
|
<button class="hdr-btn" onclick="openFillerImport()">📂 템플릿 불러오기</button>
|
||||||
<button class="hdr-btn" onclick="clearFiller()">🗑 지우기</button>
|
<button class="hdr-btn" onclick="clearFiller()">🗑 초기화</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filler-main">
|
<div class="filler-main">
|
||||||
<div class="filler-form-wrap" id="fillerForm">
|
|
||||||
<div class="filler-empty">
|
<div class="filler-input-panel">
|
||||||
<span style="font-size:40px">📋</span>
|
<!-- 템플릿 섹션 구조 표시 -->
|
||||||
<p style="font-size:14px;font-weight:600">템플릿이 없습니다</p>
|
<div class="ftpl-view" id="ftplView">
|
||||||
<p style="font-size:12px">위의 📂 JSON 불러오기 버튼으로<br>Builder에서 만든 양식 JSON을 가져오세요</p>
|
<span class="ftpl-empty">← 템플릿을 먼저 불러오세요</span>
|
||||||
|
</div>
|
||||||
|
<!-- 텍스트 입력 -->
|
||||||
|
<div class="filler-textarea-wrap">
|
||||||
|
<div class="filler-input-lbl">
|
||||||
|
양식 붙여넣기
|
||||||
|
<span class="filler-inst-count" id="instCount"></span>
|
||||||
|
</div>
|
||||||
|
<textarea class="filler-textarea" id="fillerInputText"
|
||||||
|
placeholder="여러 양식을 여기에 붙여넣으세요. 제목 구분자(예: 📝)가 있으면 자동으로 인스턴스를 구분합니다."
|
||||||
|
oninput="parseAndMerge()"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filler-preview">
|
<div class="filler-preview">
|
||||||
<div class="preview-head">
|
<div class="preview-head">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
@@ -308,9 +337,10 @@ input,textarea,button,select{font-family:inherit}
|
|||||||
<button class="copy-btn" id="fCopyBtn" onclick="copyFillerOutput()">📋 복사</button>
|
<button class="copy-btn" id="fCopyBtn" onclick="copyFillerOutput()">📋 복사</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-body">
|
<div class="preview-body">
|
||||||
<pre class="preview-text dim" id="fillerOut">내용을 채우면 미리보기가 표시됩니다</pre>
|
<pre class="preview-text dim" id="fillerOut">양식을 붙여넣으면 합치기 결과가 표시됩니다</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div><!-- /panel-filler -->
|
</div><!-- /panel-filler -->
|
||||||
@@ -1110,11 +1140,12 @@ function newDoc(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
// Tab 2: 내용 채우기
|
// Tab 2: 양식 합치기 (텍스트 붙여넣기 방식)
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
let FT = null; // filler template (parsed JSON root)
|
let FT = null; // filler template (parsed JSON root)
|
||||||
let FV = {}; // filler values: nodeId → string
|
|
||||||
let fillerTab = 'text';
|
let fillerTab = 'text';
|
||||||
|
let fillerMerged = {}; // sectionKey → string[]
|
||||||
|
let fillerInstCount = 0;
|
||||||
|
|
||||||
function openFillerImport(){ document.getElementById('fillerImportModal').classList.add('open') }
|
function openFillerImport(){ document.getElementById('fillerImportModal').classList.add('open') }
|
||||||
function closeFillerImport(){ document.getElementById('fillerImportModal').classList.remove('open') }
|
function closeFillerImport(){ document.getElementById('fillerImportModal').classList.remove('open') }
|
||||||
@@ -1124,91 +1155,169 @@ function doLoadFillerTemplate(){
|
|||||||
let obj;
|
let obj;
|
||||||
try{ obj=JSON.parse(raw); }catch(e){ alert('JSON 파싱 오류:\n'+e.message); return; }
|
try{ obj=JSON.parse(raw); }catch(e){ alert('JSON 파싱 오류:\n'+e.message); return; }
|
||||||
if(obj.type!=='ROOT'){ alert('최상위 타입이 ROOT가 아닙니다.'); return; }
|
if(obj.type!=='ROOT'){ alert('최상위 타입이 ROOT가 아닙니다.'); return; }
|
||||||
FT=obj; FV={};
|
FT=obj;
|
||||||
initFillerValues(FT);
|
|
||||||
closeFillerImport();
|
closeFillerImport();
|
||||||
document.getElementById('fillerImportTxt').value='';
|
document.getElementById('fillerImportTxt').value='';
|
||||||
renderFiller();
|
const mark=FT.attributes?.mark??'';
|
||||||
}
|
const title=FT.attributes?.title??'(제목 없음)';
|
||||||
|
document.getElementById('fillerTitle').textContent=(mark?mark+' ':'')+title;
|
||||||
function initFillerValues(node){
|
renderFillerTemplateView();
|
||||||
for(const child of node.children??[]){
|
parseAndMerge();
|
||||||
if(child.type==='VALUE'){
|
|
||||||
if(!child.id) child.id='fv_'+Math.random().toString(36).slice(2);
|
|
||||||
FV[child.id]=String(child.value??'');
|
|
||||||
} else if(child.type==='SECTION'){
|
|
||||||
if(!child.id) child.id='fs_'+Math.random().toString(36).slice(2);
|
|
||||||
initFillerValues(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearFiller(){
|
function clearFiller(){
|
||||||
if(!confirm('템플릿을 지우고 처음부터 시작할까요?')) return;
|
if(!confirm('템플릿과 입력 내용을 모두 지울까요?')) return;
|
||||||
FT=null; FV={};
|
FT=null; fillerMerged={}; fillerInstCount=0;
|
||||||
renderFiller();
|
document.getElementById('fillerTitle').textContent='템플릿 없음';
|
||||||
}
|
document.getElementById('fillerInputText').value='';
|
||||||
|
document.getElementById('instCount').textContent='';
|
||||||
function renderFiller(){
|
renderFillerTemplateView();
|
||||||
const form=document.getElementById('fillerForm');
|
|
||||||
const titleEl=document.getElementById('fillerTitle');
|
|
||||||
const outEl=document.getElementById('fillerOut');
|
|
||||||
if(!FT){
|
|
||||||
titleEl.textContent='템플릿 없음';
|
|
||||||
form.innerHTML=`<div class="filler-empty">
|
|
||||||
<span style="font-size:40px">📋</span>
|
|
||||||
<p style="font-size:14px;font-weight:600">템플릿이 없습니다</p>
|
|
||||||
<p style="font-size:12px">위의 📂 JSON 불러오기 버튼으로<br>Builder에서 만든 양식 JSON을 가져오세요</p>
|
|
||||||
</div>`;
|
|
||||||
outEl.className='preview-text dim'; outEl.textContent='내용을 채우면 미리보기가 표시됩니다';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const title=FT.attributes?.title??'(제목 없음)';
|
|
||||||
const mark=FT.attributes?.mark??'';
|
|
||||||
titleEl.textContent=(mark?mark+' ':'')+title;
|
|
||||||
let html='';
|
|
||||||
for(const child of FT.children??[]) if(child.type==='SECTION') html+=renderFillerSection(child);
|
|
||||||
form.innerHTML=html||'<div class="filler-empty"><p style="font-size:12px">양식에 섹션이 없습니다</p></div>';
|
|
||||||
updateFillerPreview();
|
updateFillerPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFillerSection(sec){
|
// ── 템플릿 구조 표시 ───────────────────────────────────
|
||||||
const mark=sec.attributes?.mark??'';
|
function renderFillerTemplateView(){
|
||||||
const name=sec.content??'';
|
const el=document.getElementById('ftplView');
|
||||||
let body='';
|
if(!FT){ el.innerHTML='<span class="ftpl-empty">← 템플릿을 먼저 불러오세요</span>'; return; }
|
||||||
for(const child of sec.children??[]){
|
function walkSec(sec, depth){
|
||||||
if(child.type==='VALUE'){
|
const m=sec.attributes?.mark??'';
|
||||||
const type=child.data_type??'string';
|
const n=sec.content??'';
|
||||||
const id=child.id;
|
const pl=sec.attributes?.per_line;
|
||||||
const val=FV[id]??'';
|
let html=`<span class="ftpl-sec">${depth?'<span class="ftpl-arr">▸</span>':''}<span class="ftpl-mark">${esc(m)}</span><span class="ftpl-name">${esc(n)}</span>${pl?`<span class="ftpl-badge">${esc(pl)}개/줄</span>`:''}</span>`;
|
||||||
body+=`<div class="fval-row">
|
for(const c of sec.children??[]) if(c.type==='SECTION') html+=walkSec(c,depth+1);
|
||||||
<span class="fval-lbl">${esc(type)}</span>
|
return html;
|
||||||
${type==='string'
|
|
||||||
?`<textarea class="fval-inp" rows="2" oninput="FV['${id}']=this.value;updateFillerPreview()">${esc(val)}</textarea>`
|
|
||||||
:`<input class="fval-inp" value="${esc(val)}" oninput="FV['${id}']=this.value;updateFillerPreview()">`}
|
|
||||||
</div>`;
|
|
||||||
} else if(child.type==='SECTION'){
|
|
||||||
body+=renderFillerSection(child);
|
|
||||||
}
|
}
|
||||||
}
|
const tm=FT.attributes?.mark??'';
|
||||||
return `<div class="fsec">
|
const tt=FT.attributes?.title??'';
|
||||||
<div class="fsec-head${!body?' alone':''}">
|
let html=tt?`<span class="ftpl-title"><span class="ftpl-mark">${esc(tm)}</span><span class="ftpl-name">${esc(tt)}</span></span>`:'';
|
||||||
<span class="fsec-mark">${esc(mark||'·')}</span>
|
for(const c of FT.children??[]) if(c.type==='SECTION') html+=walkSec(c,0);
|
||||||
<span class="fsec-name">${esc(name||'이름 없음')}</span>
|
el.innerHTML=html;
|
||||||
</div>
|
|
||||||
${body?`<div class="fsec-body">${body}</div>`:''}
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFillerPreview(){
|
// ── 파싱 & 합치기 ──────────────────────────────────────
|
||||||
const el=document.getElementById('fillerOut'); if(!FT) return;
|
function extractSectionMarks(node){
|
||||||
if(fillerTab==='text'){
|
const marks=new Set();
|
||||||
const t=fillerToText();
|
function walk(n){ for(const c of n.children??[]){ if(c.type==='SECTION'){ const m=c.attributes?.mark??''; if(m) marks.add(m); walk(c); } } }
|
||||||
el.className=t.trim()?'preview-text':'preview-text dim';
|
walk(node);
|
||||||
el.textContent=t.trim()||'내용을 채우면 미리보기가 표시됩니다';
|
return [...marks];
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitIntoInstances(rawText, titleMark){
|
||||||
|
if(!titleMark) return rawText.split(/\n{3,}/).map(s=>s.trim()).filter(Boolean);
|
||||||
|
const lines=rawText.split('\n');
|
||||||
|
const instances=[]; let current=[];
|
||||||
|
for(const line of lines){
|
||||||
|
if(line.trim().startsWith(titleMark)&¤t.some(l=>l.trim())){
|
||||||
|
instances.push(current.join('\n')); current=[];
|
||||||
|
}
|
||||||
|
current.push(line);
|
||||||
|
}
|
||||||
|
if(current.some(l=>l.trim())) instances.push(current.join('\n'));
|
||||||
|
return instances;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseInstance(text, titleMark, sectionMarks){
|
||||||
|
const sorted=sectionMarks.slice().sort((a,b)=>b.length-a.length);
|
||||||
|
const result={sections:{}};
|
||||||
|
let curKey=null;
|
||||||
|
for(const rawLine of text.split('\n')){
|
||||||
|
const line=rawLine.trim(); if(!line) continue;
|
||||||
|
if(titleMark&&line.startsWith(titleMark)) continue;
|
||||||
|
let matched=false;
|
||||||
|
for(const mark of sorted){
|
||||||
|
if(line.startsWith(mark)){
|
||||||
|
curKey=mark+line.slice(mark.length).trim();
|
||||||
|
if(!result.sections[curKey]) result.sections[curKey]=[];
|
||||||
|
matched=true; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!matched&&curKey){
|
||||||
|
result.sections[curKey].push(...line.split(/\s+/).filter(Boolean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAndMerge(){
|
||||||
|
const rawText=document.getElementById('fillerInputText').value;
|
||||||
|
const instEl=document.getElementById('instCount');
|
||||||
|
if(!FT||!rawText.trim()){
|
||||||
|
fillerMerged={}; fillerInstCount=0;
|
||||||
|
instEl.textContent=''; updateFillerPreview(); return;
|
||||||
|
}
|
||||||
|
const titleMark=FT.attributes?.mark??'';
|
||||||
|
const sectionMarks=extractSectionMarks(FT);
|
||||||
|
const instances=splitIntoInstances(rawText,titleMark);
|
||||||
|
fillerInstCount=instances.length;
|
||||||
|
fillerMerged={};
|
||||||
|
for(const text of instances){
|
||||||
|
const parsed=parseInstance(text,titleMark,sectionMarks);
|
||||||
|
for(const [key,tokens] of Object.entries(parsed.sections)){
|
||||||
|
if(!fillerMerged[key]) fillerMerged[key]=[];
|
||||||
|
fillerMerged[key].push(...tokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instEl.textContent=instances.length?`(${instances.length}개 양식)`:'';
|
||||||
|
updateFillerPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 텍스트 출력 ────────────────────────────────────────
|
||||||
|
function mergedToText(){
|
||||||
|
if(!FT) return '';
|
||||||
|
function walkSec(sec){
|
||||||
|
const mark=sec.attributes?.mark??'';
|
||||||
|
const content=sec.content??'';
|
||||||
|
const key=mark+content;
|
||||||
|
const perLine=parseInt(sec.attributes?.per_line??'0')||0;
|
||||||
|
const tokens=fillerMerged[key]??[];
|
||||||
|
let t=mark+content+'\n';
|
||||||
|
if(tokens.length){
|
||||||
|
if(perLine>0){
|
||||||
|
for(let i=0;i<tokens.length;i+=perLine) t+=tokens.slice(i,i+perLine).join(' ')+'\n';
|
||||||
} else {
|
} else {
|
||||||
el.className='preview-text';
|
t+=tokens.join(' ')+'\n';
|
||||||
el.innerHTML=highlight(fillerToJSON());
|
}
|
||||||
|
}
|
||||||
|
for(const c of sec.children??[]) if(c.type==='SECTION') t+=walkSec(c);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
const titleMark=FT.attributes?.mark??'';
|
||||||
|
const title=FT.attributes?.title??'';
|
||||||
|
let out=title?`${titleMark}${title}\n`:'';
|
||||||
|
for(const c of FT.children??[]) if(c.type==='SECTION') out+=walkSec(c);
|
||||||
|
return out.replace(/\n{3,}/g,'\n\n').trimEnd()+'\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JSON 출력 ───────────────────────────────────────────
|
||||||
|
function mergedToJSON(){
|
||||||
|
if(!FT) return '{}';
|
||||||
|
function buildSec(sec){
|
||||||
|
const mark=sec.attributes?.mark??'';
|
||||||
|
const content=sec.content??'';
|
||||||
|
const key=mark+content;
|
||||||
|
const tokens=fillerMerged[key]??[];
|
||||||
|
const valChildren=tokens.map((t,i)=>({
|
||||||
|
id:'mv'+i,type:'VALUE',data_type:'string',value:t
|
||||||
|
}));
|
||||||
|
const secChildren=(sec.children??[]).filter(c=>c.type==='SECTION').map(buildSec);
|
||||||
|
return {...sec, children:[...valChildren,...secChildren]};
|
||||||
|
}
|
||||||
|
const merged={...FT, children:(FT.children??[]).map(c=>c.type==='SECTION'?buildSec(c):c)};
|
||||||
|
return JSON.stringify(merged,null,2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 미리보기 & 공통 ────────────────────────────────────
|
||||||
|
function updateFillerPreview(){
|
||||||
|
const el=document.getElementById('fillerOut');
|
||||||
|
if(!FT||!fillerInstCount){
|
||||||
|
el.className='preview-text dim';
|
||||||
|
el.textContent='양식을 붙여넣으면 합치기 결과가 표시됩니다';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(fillerTab==='text'){
|
||||||
|
el.className='preview-text'; el.textContent=mergedToText();
|
||||||
|
} else {
|
||||||
|
el.className='preview-text'; el.innerHTML=highlight(mergedToJSON());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1220,7 +1329,7 @@ function setFillerTab(t){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function copyFillerOutput(){
|
function copyFillerOutput(){
|
||||||
const txt=fillerTab==='text'?fillerToText():fillerToJSON();
|
const txt=fillerTab==='text'?mergedToText():mergedToJSON();
|
||||||
navigator.clipboard.writeText(txt).then(()=>{
|
navigator.clipboard.writeText(txt).then(()=>{
|
||||||
const btn=document.getElementById('fCopyBtn');
|
const btn=document.getElementById('fCopyBtn');
|
||||||
btn.textContent='✅ 복사됨'; btn.classList.add('ok');
|
btn.textContent='✅ 복사됨'; btn.classList.add('ok');
|
||||||
@@ -1228,48 +1337,6 @@ function copyFillerOutput(){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillerToText(){
|
|
||||||
if(!FT) return '';
|
|
||||||
function rSec(sec){
|
|
||||||
const mark=sec.attributes?.mark??'';
|
|
||||||
const content=sec.content??'';
|
|
||||||
const prefix=sec.prefix??'';
|
|
||||||
const suffix=sec.suffix??'';
|
|
||||||
const nl=sec.nameNewline!==false?'\n':'';
|
|
||||||
let t=prefix+mark+content;
|
|
||||||
let first=true;
|
|
||||||
for(const child of sec.children??[]){
|
|
||||||
if(child.type==='SECTION'){
|
|
||||||
t+=rSec(child); first=false;
|
|
||||||
} else if(child.type==='VALUE'){
|
|
||||||
const val=FV[child.id]??'';
|
|
||||||
if(val.trim()){ t+=(first?nl:'\n')+val; first=false; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t+=suffix;
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
const title=FT.attributes?.title??'';
|
|
||||||
const titleMark=FT.attributes?.mark??'';
|
|
||||||
let out=title?`${titleMark}${title}\n`:'';
|
|
||||||
for(const child of FT.children??[]) if(child.type==='SECTION') out+=rSec(child);
|
|
||||||
return out.replace(/\n{3,}/g,'\n\n').trimEnd()+'\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillerToJSON(){
|
|
||||||
if(!FT) return '{}';
|
|
||||||
function fill(node){
|
|
||||||
const r={...node};
|
|
||||||
r.children=(node.children??[]).map(child=>{
|
|
||||||
if(child.type==='VALUE') return {...child, value:FV[child.id]??child.value??null};
|
|
||||||
if(child.type==='SECTION') return fill(child);
|
|
||||||
return child;
|
|
||||||
});
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
return JSON.stringify(fill(FT),null,2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
// 탭 전환
|
// 탭 전환
|
||||||
// ════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user