| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001 |
- /*
- rtf.js — self-contained RTF reader / writer for the Text editor.
- TextRTF.toHTML(src)
- src is the RTF source as a "binary string" (one char per byte, as read
- from the file); characters above U+00FF are accepted as already-decoded
- text. Returns { html, baseSize, baseFont } where baseSize is the
- document's dominant font size in half-points and baseFont its dominant
- font name. Runs at the base size / font carry no inline style, so they
- follow the editor's own typography.
- TextRTF.fromHTML(root, opts)
- Serialises the editor DOM (a detached clone) back to RTF. opts may give
- { baseSize, baseFont } so a document keeps the defaults it was loaded
- with. Images must be data: URLs (run inlineImages() first).
- TextRTF.inlineImages(root)
- Promise. Turns every <img> under root into a PNG/JPEG data: URL and
- records its natural size, so fromHTML() can embed it as \pict.
- Supported: paragraphs, alignment, indents, headings (\outlinelevel), bold,
- italic, underline, strikethrough, super/subscript, fonts, sizes, colours,
- highlight, hyperlinks, bullet / numbered lists, tables, horizontal rules,
- PNG/JPEG pictures, \u unicode and code-page (\'hh) text incl. DBCS (Big5,
- GBK, Shift_JIS, EUC-KR).
- */
- (function(){
- "use strict";
- // ── Code pages ───────────────────────────────────────────────────────────
- var CHARSET_CP = {
- 0:1252, 77:10000, 128:932, 129:949, 130:949, 134:936, 136:950, 161:1253,
- 162:1254, 163:1258, 177:1255, 178:1256, 186:1257, 204:1251, 222:874, 238:1250
- };
- var CP_LABEL = {
- 874:"windows-874", 932:"shift_jis", 936:"gbk", 949:"euc-kr", 950:"big5",
- 1250:"windows-1250", 1251:"windows-1251", 1252:"windows-1252", 1253:"windows-1253",
- 1254:"windows-1254", 1255:"windows-1255", 1256:"windows-1256", 1257:"windows-1257",
- 1258:"windows-1258", 10000:"macintosh", 65001:"utf-8"
- };
- var decoders = {};
- function decoderFor(cp){
- if (decoders[cp]) return decoders[cp];
- var d = null;
- try { d = new TextDecoder(CP_LABEL[cp] || "windows-1252"); } catch(e){}
- if (!d) d = new TextDecoder("windows-1252");
- decoders[cp] = d;
- return d;
- }
- // destinations whose whole group is dropped
- var SKIP_DEST = {
- stylesheet:1, info:1, header:1, headerl:1, headerr:1, headerf:1, footer:1, footerl:1,
- footerr:1, footerf:1, footnote:1, nonshppict:1, listtable:1, listoverridetable:1,
- revtbl:1, rsidtbl:1, xe:1, tc:1, txe:1, themedata:1, colorschememapping:1,
- latentstyles:1, datastore:1, xmlnstbl:1, mmathPr:1, pnseclvl:1, filetbl:1,
- pgdsctbl:1, author:1, operator:1, title:1, comment:1, company:1, doccomm:1,
- keywords:1, subject:1, upr:1, "private":1, annotation:1, atnid:1, atnauthor:1,
- objdata:1, objclass:1, bkmkstart:1, bkmkend:1, template:1, docvar:1, userprops:1
- };
- // "\*"-marked destinations we understand (every other one is skipped)
- var KNOWN_STAR = { fldinst:1, shppict:1, pn:1, listtext:1 };
- var SYMBOLS = {
- emdash:"\u2014", endash:"\u2013", bullet:"\u2022", lquote:"\u2018", rquote:"\u2019",
- ldblquote:"\u201C", rdblquote:"\u201D", emspace:"\u2003", enspace:"\u2002",
- qmspace:"\u2005", zwj:"\u200D", zwnj:"\u200C", ltrmark:"", rtlmark:""
- };
- function defaultChar(){
- return { b:false, i:false, u:false, s:false, sup:false, sub:false, v:false,
- fs:24, f:-1, cf:0, bg:0, link:null };
- }
- function defaultPara(){
- return { align:"", li:0, fi:0, intbl:false, outline:-1, ilvl:-1, ls:0, brdrb:false };
- }
- function copy(o){ var r = {}; for (var k in o) r[k] = o[k]; return r; }
- // ════════════════════════════════════════════════════════════════════════
- // Reader
- // ════════════════════════════════════════════════════════════════════════
- function parse(src){
- var fonts = {}, colors = [], deff = 0, ansicp = 1252;
- var blocks = []; // finished paragraphs / tables
- var table = null, row = null, cell = null;
- var para = newPara();
- var st = { dest:"main", chr:defaultChar(), par:defaultPara(), uc:1, field:null,
- font:null, pict:null };
- var stack = [];
- var bytes = [], bytesCP = 1252; // pending code-page bytes
- var skipChars = 0; // \u fallback characters still to skip
- var star = false; // saw "\*" for the next control word
- var color = null; // colortbl entry being built
- function newPara(){ return { runs:[], marker:"", listType:"" }; }
- function fontCP(fi){ return cpOf(fonts[fi]); }
- function cpOf(f){
- if (!f) return ansicp;
- if (f.cpg) return f.cpg;
- if (f.charset === 1 || f.charset === undefined) return ansicp;
- if (f.charset === 2) return 1252;
- return CHARSET_CP[f.charset] || ansicp;
- }
- function curCP(){ return fontCP(st.chr.f < 0 ? deff : st.chr.f); }
- function flush(){
- if (!bytes.length) return;
- var txt = decoderFor(bytesCP).decode(new Uint8Array(bytes));
- bytes = [];
- emitText(txt);
- }
- function emitByte(b){
- if (skipChars > 0){ skipChars--; return; }
- var cp = st.dest === "fonttbl" ? cpOf(st.font) : curCP();
- if (bytes.length && cp !== bytesCP) flush();
- bytesCP = cp;
- bytes.push(b);
- }
- function emitText(txt){
- if (!txt) return;
- switch (st.dest){
- case "main":
- if (st.chr.v) return; // hidden text
- var runs = para.runs, last = runs[runs.length - 1];
- if (last && last.type === "text" && sameChr(last.chr, st.chr)) last.text += txt;
- else runs.push({ type:"text", text:txt, chr:copy(st.chr) });
- return;
- case "fonttbl":
- if (st.font) st.font.raw += txt;
- return;
- case "fldinst":
- if (st.field) st.field.inst += txt;
- return;
- case "listtext":
- para.marker += txt;
- return;
- }
- }
- function sameChr(a, b){
- for (var k in a) if (a[k] !== b[k]) return false;
- return true;
- }
- function emitObj(o){
- if (st.dest !== "main" || st.chr.v) return;
- o.chr = copy(st.chr);
- para.runs.push(o);
- }
- function endPara(){
- flush();
- var p = para;
- p.pp = copy(st.par);
- para = newPara();
- if (p.pp.intbl){
- if (!cell) cell = [];
- cell.push(p);
- } else {
- closeTable();
- blocks.push(p);
- }
- }
- function endCell(){
- flush();
- if (para.runs.length || !cell || !cell.length){
- para.pp = copy(st.par);
- if (!cell) cell = [];
- cell.push(para);
- }
- para = newPara();
- if (!row) row = [];
- row.push(cell);
- cell = null;
- }
- function endRow(){
- flush();
- if (cell) endCell();
- if (!table) table = { type:"table", rows:[] };
- if (row) table.rows.push(row);
- row = null;
- }
- function closeTable(){
- if (cell || row) endRow();
- if (table){ blocks.push(table); table = null; }
- }
- function endGroup(){
- flush();
- var child = st;
- st = stack.pop() || st;
- if (child.dest === "pict" && child.pict && st.dest !== "skip") finishPict(child);
- if (child.dest === "fonttbl" && child.font && child.font !== st.font) finishFont(child.font);
- skipChars = 0;
- }
- function finishFont(f){
- if (f.done) return;
- f.done = true;
- var name = f.raw.replace(/;.*$/, "").trim();
- fonts[f.idx] = { name:name, charset:f.charset, cpg:f.cpg, family:f.family };
- }
- function finishPict(s){
- var p = s.pict, mime = null;
- if (p.type === "png") mime = "image/png";
- else if (p.type === "jpeg") mime = "image/jpeg";
- if (!mime) return; // metafiles / bitmaps can't be shown
- var bin = p.bin;
- if (!bin){
- var hex = p.hex.replace(/[^0-9a-fA-F]/g, "");
- var out = [];
- for (var i = 0; i + 1 < hex.length; i += 2) out.push(String.fromCharCode(parseInt(hex.substr(i, 2), 16)));
- bin = out.join("");
- }
- if (!bin) return;
- var w = p.wgoal ? p.wgoal * (p.scalex || 100) / 100 / 15 : 0;
- var h = p.hgoal ? p.hgoal * (p.scaley || 100) / 100 / 15 : 0;
- if (st.dest === "main"){ // st is the enclosing group again
- para.runs.push({ type:"img", src:"data:" + mime + ";base64," + btoa(bin),
- w:Math.round(w), h:Math.round(h), chr:copy(st.chr) });
- }
- }
- function control(word, param, hasParam){
- // "\*" handling: unknown ignorable destinations are skipped wholesale
- if (star){
- star = false;
- if (!KNOWN_STAR[word]){ st.dest = "skip"; return; }
- }
- if (st.dest === "skip") return;
- if (skipChars > 0 && word !== "u"){ skipChars--; return; }
- if (SKIP_DEST[word]){ st.dest = "skip"; return; }
- // unicode text is valid in every text-bearing destination (incl. font names)
- if (word === "u"){
- flush();
- emitText(String.fromCharCode(param < 0 ? param + 65536 : param));
- skipChars = st.uc;
- return;
- }
- if (word === "uc"){ st.uc = param; return; }
- // ── destinations ──
- switch (word){
- case "fonttbl": st.dest = "fonttbl"; st.font = null; return;
- case "colortbl": st.dest = "colortbl"; color = null; return;
- case "pict": st.dest = "pict"; st.pict = { type:"", hex:"", bin:"", wgoal:0, hgoal:0, scalex:100, scaley:100 }; return;
- case "field": st.field = { inst:"" }; return;
- case "fldinst": st.dest = "fldinst"; return;
- case "fldrslt":
- st.dest = "main";
- if (st.field){
- var m = st.field.inst.match(/HYPERLINK\s+(?:\\l\s+)?"([^"]*)"/i) ||
- st.field.inst.match(/HYPERLINK\s+(\S+)/i);
- if (m){
- var url = m[1];
- if (/\\l\s/.test(st.field.inst) && url.charAt(0) !== "#") url = "#" + url;
- st.chr.link = url;
- }
- }
- return;
- case "listtext": case "pntext":
- st.dest = "listtext"; return;
- case "pn": st.dest = "pn"; return;
- case "shppict": return; // its \pict is the real picture
- }
- if (st.dest === "fonttbl"){
- if (word === "f"){
- if (st.font && !st.font.done && stack.length && stack[stack.length-1].font !== st.font) finishFont(st.font);
- st.font = { idx:param, raw:"", charset:undefined, cpg:0, family:"" };
- } else if (st.font){
- if (word === "fcharset") st.font.charset = param;
- else if (word === "cpg") st.font.cpg = param;
- else if (/^f(roman|swiss|modern|script|decor|tech|bidi|nil)$/.test(word)) st.font.family = word.substr(1);
- }
- return;
- }
- if (st.dest === "colortbl"){
- if (!color) color = { r:0, g:0, b:0 };
- if (word === "red") color.r = param;
- else if (word === "green") color.g = param;
- else if (word === "blue") color.b = param;
- return;
- }
- if (st.dest === "pict"){
- var p = st.pict;
- if (word === "pngblip") p.type = "png";
- else if (word === "jpegblip") p.type = "jpeg";
- else if (word === "emfblip" || word === "wmetafile" || word === "macpict" || word === "dibitmap" || word === "wbitmap") p.type = p.type || word;
- else if (word === "picwgoal") p.wgoal = param;
- else if (word === "pichgoal") p.hgoal = param;
- else if (word === "picscalex") p.scalex = param;
- else if (word === "picscaley") p.scaley = param;
- return;
- }
- if (st.dest === "pn"){
- if (word === "pnlvlblt") para.listType = "ul";
- else if (/^pn(lvlbody|dec|ucltr|lcltr|ucrm|lcrm)$/.test(word)) para.listType = para.listType || "ol";
- return;
- }
- var c = st.chr, pp = st.par;
- switch (word){
- // header
- case "ansicpg": ansicp = param || 1252; return;
- case "deff": deff = param; return;
- // breaks
- case "par": case "sect": case "page":
- endPara(); return;
- case "line":
- flush(); emitObj({ type:"br" }); return;
- case "tab":
- flush(); emitText("\t"); return;
- case "cell": case "nestcell":
- endCell(); return;
- case "row": case "nestrow":
- endRow(); return;
- // paragraph formatting
- case "pard": st.par = defaultPara(); return;
- case "ql": pp.align = ""; return;
- case "qc": pp.align = "center"; return;
- case "qr": pp.align = "right"; return;
- case "qj": pp.align = "justify"; return;
- case "li": pp.li = param; return;
- case "fi": pp.fi = param; return;
- case "intbl": pp.intbl = true; return;
- case "outlinelevel": pp.outline = param; return;
- case "ilvl": pp.ilvl = param; return;
- case "ls": pp.ls = param; return;
- case "brdrb": pp.brdrb = true; return;
- // character formatting
- case "plain":
- var link = c.link;
- st.chr = defaultChar(); st.chr.link = link; return;
- case "b": c.b = !hasParam || param !== 0; return;
- case "i": c.i = !hasParam || param !== 0; return;
- case "strike": case "striked":
- c.s = !hasParam || param !== 0; return;
- case "ulnone": c.u = false; return;
- case "super": c.sup = true; c.sub = false; return;
- case "sub": c.sub = true; c.sup = false; return;
- case "nosupersub": c.sup = c.sub = false; return;
- case "v": c.v = !hasParam || param !== 0; return;
- case "fs": c.fs = hasParam ? param : 24; return;
- case "f": c.f = param; return;
- case "cf": c.cf = param; return;
- case "cb": case "highlight": case "chcbpat":
- c.bg = param; return;
- }
- if (/^ul(d|dash|dashd|dashdd|db|hwave|ldash|th|thd|thdash|thdashd|thdashdd|thldash|w|wave)?$/.test(word)){
- c.u = !hasParam || param !== 0;
- return;
- }
- if (SYMBOLS.hasOwnProperty(word)){ flush(); emitText(SYMBOLS[word]); }
- }
- // ── tokenizer ──
- var i = 0, n = src.length;
- while (i < n){
- var ch = src.charAt(i), code = src.charCodeAt(i);
- if (ch === "{"){
- flush();
- stack.push(st);
- st = { dest:st.dest, chr:copy(st.chr), par:copy(st.par), uc:st.uc, field:st.field,
- font:st.font, pict:st.pict };
- i++;
- continue;
- }
- if (ch === "}"){
- endGroup(); star = false; i++;
- continue;
- }
- if (ch === "\\"){
- var nx = src.charAt(i + 1);
- if (/[a-zA-Z]/.test(nx)){
- var j = i + 1;
- while (j < n && /[a-zA-Z]/.test(src.charAt(j))) j++;
- var word = src.substring(i + 1, j);
- var k = j;
- if (src.charAt(k) === "-" && /[0-9]/.test(src.charAt(k + 1))) k++;
- while (k < n && /[0-9]/.test(src.charAt(k))) k++;
- var hasParam = k > j;
- var param = hasParam ? parseInt(src.substring(j, k), 10) : 0;
- if (src.charAt(k) === " ") k++;
- i = k;
- if (word !== "u") flush();
- if (word === "bin"){ // raw binary data follows
- var data = src.substr(i, param);
- if (st.dest === "pict" && st.pict) st.pict.bin = data;
- i += param;
- continue;
- }
- control(word, param, hasParam);
- continue;
- }
- i += 2;
- switch (nx){
- case "'":
- var hb = parseInt(src.substr(i, 2), 16);
- i += 2;
- if (st.dest === "skip" || st.dest === "pn" || st.dest === "pict" || isNaN(hb)) break;
- if (st.dest === "colortbl") break;
- emitByte(hb);
- break;
- case "*": star = true; break;
- case "~": flush(); if (skipChars > 0) skipChars--; else emitText("\u00A0"); break;
- case "_": flush(); if (skipChars > 0) skipChars--; else emitText("\u2011"); break;
- case "-": break; // optional hyphen
- case "\n": case "\r": flush(); if (st.dest === "main") endPara(); break;
- case "\\": case "{": case "}":
- if (st.dest === "skip" || st.dest === "pn") break;
- emitByte(nx.charCodeAt(0)); break;
- }
- continue;
- }
- i++;
- if (ch === "\r" || ch === "\n") continue;
- if (st.dest === "skip" || st.dest === "pn") continue;
- if (st.dest === "pict"){ st.pict.hex += ch; continue; }
- if (st.dest === "colortbl"){
- if (ch === ";"){ colors.push(color); color = null; }
- continue;
- }
- if (st.dest === "fonttbl" && ch === ";"){
- flush();
- if (st.font){ st.font.raw += ";"; finishFont(st.font); }
- continue;
- }
- if (code > 0xFF){ flush(); if (skipChars > 0) skipChars--; else emitText(ch); continue; }
- emitByte(code);
- }
- flush();
- if (para.runs.length) endPara();
- closeTable();
- return { blocks:blocks, fonts:fonts, colors:colors, deff:deff };
- }
- // ── model → HTML ─────────────────────────────────────────────────────────
- function esc(s){ return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); }
- function escAttr(s){ return esc(s).replace(/"/g, """); }
- function colorCss(c){
- if (!c) return "";
- function h(v){ return ("0" + (v & 255).toString(16)).slice(-2); }
- return "#" + h(c.r) + h(c.g) + h(c.b);
- }
- var GENERIC = { roman:"serif", swiss:"sans-serif", modern:"monospace", script:"cursive", decor:"fantasy" };
- function toHTML(src){
- src = src || "";
- if (!/^\s*\{\\rtf/.test(src)){
- // not RTF (e.g. a brand-new empty file) — show it as plain paragraphs
- var lines = src.replace(/\r\n?/g, "\n").split("\n");
- return { html: lines.map(function(l){ return "<p>" + (l ? esc(l) : "<br>") + "</p>"; }).join(""),
- baseSize:24, baseFont:"" };
- }
- var doc = parse(src);
- // dominant size / font become the document base (no inline style)
- var sizeW = {}, fontW = {};
- doc.blocks.forEach(function scan(b){
- if (b.type === "table"){ b.rows.forEach(function(r){ r.forEach(function(c){ c.forEach(scan); }); }); return; }
- if (b.pp && b.pp.outline >= 0 && b.pp.outline <= 5) return;
- b.runs.forEach(function(r){
- if (r.type !== "text") return;
- var len = r.text.length, f = r.chr.f < 0 ? doc.deff : r.chr.f;
- sizeW[r.chr.fs] = (sizeW[r.chr.fs] || 0) + len;
- fontW[f] = (fontW[f] || 0) + len;
- });
- });
- function top(w, d){ var best = d, bw = -1; for (var k in w) if (w[k] > bw){ bw = w[k]; best = +k; } return best; }
- var baseSize = top(sizeW, 24), baseFontIdx = top(fontW, doc.deff);
- var baseFont = doc.fonts[baseFontIdx] ? doc.fonts[baseFontIdx].name : "";
- function fontCss(idx){
- var f = doc.fonts[idx];
- if (!f || !f.name) return "";
- var gen = GENERIC[f.family];
- return "'" + f.name.replace(/'/g, "") + "'" + (gen ? ", " + gen : "");
- }
- function runsHTML(runs, heading){
- var html = "", openLink = null, buf = "";
- function closeLink(){
- if (openLink !== null){ html += '<a href="' + escAttr(openLink) + '">' + buf + "</a>"; buf = ""; openLink = null; }
- }
- runs.forEach(function(r){
- var c = r.chr, piece;
- if (r.type === "br") piece = "<br>";
- else if (r.type === "img"){
- piece = '<img src="' + r.src + '" alt=""' + (r.w ? ' width="' + r.w + '"' : "") + ">";
- } else {
- piece = esc(r.text);
- var style = [];
- var fi = c.f < 0 ? doc.deff : c.f;
- if (!heading && c.fs !== baseSize) style.push("font-size:" + (c.fs / 2) + "pt");
- if (fi !== baseFontIdx){ var fc = fontCss(fi); if (fc) style.push("font-family:" + fc); }
- if (c.cf > 0 && doc.colors[c.cf]) style.push("color:" + colorCss(doc.colors[c.cf]));
- if (c.bg > 0 && doc.colors[c.bg]) style.push("background-color:" + colorCss(doc.colors[c.bg]));
- if (style.length) piece = '<span style="' + escAttr(style.join(";")) + '">' + piece + "</span>";
- if (c.sup) piece = "<sup>" + piece + "</sup>";
- if (c.sub) piece = "<sub>" + piece + "</sub>";
- if (c.s) piece = "<s>" + piece + "</s>";
- if (c.u && !c.link) piece = "<u>" + piece + "</u>";
- if (c.i) piece = "<i>" + piece + "</i>";
- if (c.b && !heading) piece = "<b>" + piece + "</b>";
- }
- if (c.link !== openLink){ closeLink(); if (c.link) openLink = c.link; }
- if (openLink !== null) buf += piece; else html += piece;
- });
- closeLink();
- return html;
- }
- function blockStyle(pp, list){
- var s = [];
- if (pp.align) s.push("text-align:" + pp.align);
- if (!list){
- if (pp.li > 0) s.push("margin-left:" + Math.round(pp.li / 15) + "px");
- if (pp.fi) s.push("text-indent:" + Math.round(pp.fi / 15) + "px");
- }
- return s.length ? ' style="' + s.join(";") + '"' : "";
- }
- function isBlank(p){
- return !p.runs.some(function(r){ return r.type !== "text" || /\S/.test(r.text.replace(/\u00A0/g, "x")); });
- }
- function paraHTML(p){
- var pp = p.pp || defaultPara();
- if (pp.brdrb && isBlank(p)) return "<hr>";
- var tag = (pp.outline >= 0 && pp.outline <= 5) ? "h" + (pp.outline + 1) : "p";
- var inner = runsHTML(p.runs, tag !== "p");
- return "<" + tag + blockStyle(pp) + ">" + (inner || "<br>") + "</" + tag + ">";
- }
- function isList(p){ return p.type !== "table" && (p.marker || p.listType || (p.pp && p.pp.ls > 0)); }
- function listLevel(p){
- if (p.pp.ilvl >= 0) return p.pp.ilvl;
- return Math.max(0, Math.round(p.pp.li / 720) - 1);
- }
- function listKind(p){
- if (p.listType) return p.listType;
- return /^\s*([0-9]+|[a-zA-Z]|[ivxlcIVXLC]+)[.)]/.test(p.marker) ? "ol" : "ul";
- }
- function tableHTML(t){
- var html = "<table><tbody>";
- t.rows.forEach(function(r){
- html += "<tr>";
- r.forEach(function(c){
- var cellHTML = c.map(function(p){ return runsHTML(p.runs, false); }).join("<br>");
- html += "<td>" + (cellHTML || "<br>") + "</td>";
- });
- html += "</tr>";
- });
- return html + "</tbody></table>";
- }
- var out = "", bl = doc.blocks;
- for (var i = 0; i < bl.length; i++){
- var b = bl[i];
- if (b.type === "table"){ out += tableHTML(b); continue; }
- if (!isList(b)){ out += paraHTML(b); continue; }
- // a run of list paragraphs → nested <ul>/<ol>
- var stackL = []; // open list tags
- var j = i;
- for (; j < bl.length && isList(bl[j]); j++){
- var p = bl[j], lvl = Math.min(listLevel(p), 8), kind = listKind(p);
- while (stackL.length > lvl + 1){ out += "</li></" + stackL.pop() + ">"; }
- if (stackL.length === lvl + 1 && stackL[lvl] !== kind){ out += "</li></" + stackL.pop() + ">"; }
- if (stackL.length === lvl + 1) out += "</li>";
- while (stackL.length < lvl + 1){ out += "<" + kind + ">"; stackL.push(kind); }
- out += "<li" + blockStyle(p.pp, true) + ">" + (runsHTML(p.runs, false) || "<br>");
- }
- while (stackL.length) out += "</li></" + stackL.pop() + ">";
- i = j - 1;
- }
- return { html: out || "<p><br></p>", baseSize: baseSize, baseFont: baseFont };
- }
- // ════════════════════════════════════════════════════════════════════════
- // Writer
- // ════════════════════════════════════════════════════════════════════════
- var HEAD_SCALE = { H1:2, H2:1.6, H3:1.3, H4:1.1, H5:1, H6:0.9 };
- var FONT_SIZE_ATTR = { 1:8, 2:10, 3:12, 4:14, 5:18, 6:24, 7:36 };
- var BLOCK_TAGS = /^(P|DIV|H[1-6]|UL|OL|LI|BLOCKQUOTE|PRE|TABLE|THEAD|TBODY|TFOOT|TR|TD|TH|HR|SECTION|ARTICLE|FIGURE|HEADER|FOOTER|ASIDE|NAV|ADDRESS|DL|DT|DD)$/;
- var MONO = "Courier New";
- function parseColor(v){
- if (!v) return null;
- v = String(v).trim().toLowerCase();
- if (v === "transparent" || v === "inherit" || v === "initial" || v === "currentcolor") return null;
- var m = v.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
- if (m){
- var h = m[1];
- if (h.length === 3) h = h.charAt(0)+h.charAt(0)+h.charAt(1)+h.charAt(1)+h.charAt(2)+h.charAt(2);
- return { r:parseInt(h.substr(0,2),16), g:parseInt(h.substr(2,2),16), b:parseInt(h.substr(4,2),16) };
- }
- m = v.match(/^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)(?:[\s,/]+([\d.]+%?))?\s*\)$/);
- if (m){
- if (m[4] !== undefined && parseFloat(m[4]) === 0) return null;
- return { r:+m[1], g:+m[2], b:+m[3] };
- }
- // named colours: let the browser resolve them
- try {
- var cv = document.createElement("canvas").getContext("2d");
- cv.fillStyle = "#000001"; cv.fillStyle = v;
- if (cv.fillStyle !== "#000001") return parseColor(cv.fillStyle);
- } catch(e){}
- return null;
- }
- function firstFamily(v){
- if (!v) return "";
- var f = String(v).split(",")[0].trim().replace(/^['"]|['"]$/g, "");
- var g = f.toLowerCase();
- if (g === "monospace") return MONO;
- if (g === "serif") return "Times New Roman";
- if (g === "sans-serif" || g === "system-ui" || g === "-apple-system" || g === "inherit") return "";
- return f;
- }
- // CSS font-size → half-points (relative sizes resolve against the current size)
- function sizeHalfPts(v, cur){
- if (!v) return 0;
- var m = String(v).trim().match(/^([\d.]+)(pt|px|em|rem|%)?$/i);
- if (!m) return 0;
- var n = parseFloat(m[1]), unit = (m[2] || "px").toLowerCase();
- var pt = unit === "pt" ? n : unit === "px" ? n * 0.75 : unit === "%" ? cur / 2 * n / 100 : cur / 2 * n;
- return Math.max(2, Math.round(pt * 2));
- }
- function fromHTML(root, opts){
- opts = opts || {};
- var base = opts.baseSize || 24;
- var fonts = [opts.baseFont || "Calibri"], fontIdx = {};
- fontIdx[fonts[0].toLowerCase()] = 0;
- var colors = [], colorIdx = {};
- var out = [];
- function fontNo(name){
- if (!name) return 0;
- var k = name.toLowerCase();
- if (fontIdx[k] === undefined){ fontIdx[k] = fonts.length; fonts.push(name); }
- return fontIdx[k];
- }
- function colorNo(c){
- if (!c) return 0;
- var k = c.r + "," + c.g + "," + c.b;
- if (colorIdx[k] === undefined){ colors.push(c); colorIdx[k] = colors.length; }
- return colorIdx[k];
- }
- function escText(s){
- var r = "";
- for (var i = 0; i < s.length; i++){
- var c = s.charCodeAt(i), ch = s.charAt(i);
- if (ch === "\\" || ch === "{" || ch === "}") r += "\\" + ch;
- else if (ch === "\t") r += "\\tab ";
- else if (ch === "\n") r += "\\line ";
- else if (c === 0xA0) r += "\\~";
- else if (c === 0x200B || c === 0xFEFF || c === 0x0D) continue;
- else if (c < 0x20) continue;
- else if (c < 0x80) r += ch;
- else r += "\\u" + (c > 32767 ? c - 65536 : c) + "?";
- }
- return r;
- }
- // ── inline content → list of items ──
- function fmtOf(el, f){
- var n = copy(f), tag = el.tagName;
- if (tag === "B" || tag === "STRONG") n.b = true;
- else if (tag === "I" || tag === "EM" || tag === "CITE") n.i = true;
- else if (tag === "U" || tag === "INS") n.u = true;
- else if (tag === "S" || tag === "STRIKE" || tag === "DEL") n.s = true;
- else if (tag === "SUP"){ n.sup = true; n.sub = false; }
- else if (tag === "SUB"){ n.sub = true; n.sup = false; }
- else if (tag === "CODE" || tag === "KBD" || tag === "TT" || tag === "SAMP") n.font = MONO;
- else if (tag === "MARK") n.bg = n.bg || { r:255, g:255, b:0 };
- if (tag === "FONT"){
- var fc = parseColor(el.getAttribute("color")); if (fc) n.color = fc;
- var ff = firstFamily(el.getAttribute("face")); if (ff) n.font = ff;
- var fz = FONT_SIZE_ATTR[el.getAttribute("size")]; if (fz) n.size = fz * 2;
- }
- var s = el.style;
- if (s){
- if (s.fontWeight){ n.b = s.fontWeight === "bold" || s.fontWeight === "bolder" || parseInt(s.fontWeight, 10) >= 600; }
- if (s.fontStyle){ n.i = s.fontStyle === "italic" || s.fontStyle === "oblique"; }
- var td = s.textDecorationLine || s.textDecoration || "";
- if (td.indexOf("underline") >= 0) n.u = true;
- if (td.indexOf("line-through") >= 0) n.s = true;
- if (td === "none"){ n.u = false; n.s = false; }
- if (s.verticalAlign === "super"){ n.sup = true; n.sub = false; }
- if (s.verticalAlign === "sub"){ n.sub = true; n.sup = false; }
- if (s.fontSize){ var hp = sizeHalfPts(s.fontSize, n.size || base); if (hp) n.size = hp; }
- if (s.fontFamily){ var fam = firstFamily(s.fontFamily); if (fam) n.font = fam; }
- if (s.color){ var col = parseColor(s.color); if (col) n.color = col; }
- if (s.backgroundColor){
- var bg = parseColor(s.backgroundColor);
- n.bg = bg; // transparent clears it
- }
- }
- return n;
- }
- function collect(nodes, f, items){
- for (var i = 0; i < nodes.length; i++){
- var nd = nodes[i];
- if (nd.nodeType === 3){
- var t = nd.nodeValue.replace(/\u200B/g, "");
- if (t) items.push({ t:"text", text:t, f:f });
- continue;
- }
- if (nd.nodeType !== 1) continue;
- var tag = nd.tagName;
- if (tag === "BR"){ items.push({ t:"br" }); continue; }
- if (tag === "IMG"){ items.push({ t:"img", el:nd, f:f }); continue; }
- if (tag === "SCRIPT" || tag === "STYLE" || tag === "SELECT") continue;
- if (tag === "A" && nd.getAttribute("href")){
- var sub = [];
- collect(nd.childNodes, fmtOf(nd, f), sub);
- items.push({ t:"link", url:nd.getAttribute("href"), items:sub });
- continue;
- }
- if (BLOCK_TAGS.test(tag)){ // stray block inside inline flow
- collect(nd.childNodes, f, items);
- items.push({ t:"br" });
- continue;
- }
- collect(nd.childNodes, fmtOf(nd, f), items);
- }
- }
- function fmtCodes(f, extra){
- var c = "";
- if (f.b) c += "\\b";
- if (f.i) c += "\\i";
- if (f.u) c += "\\ul";
- if (f.s) c += "\\strike";
- if (f.sup) c += "\\super";
- if (f.sub) c += "\\sub";
- if (f.font) c += "\\f" + fontNo(f.font);
- if (f.size && f.size !== base) c += "\\fs" + f.size;
- if (f.color) c += "\\cf" + colorNo(f.color);
- if (f.bg) c += "\\highlight" + colorNo(f.bg);
- return c + (extra || "");
- }
- function itemsRTF(items){
- // drop the trailing placeholder <br> contenteditable leaves in a line
- while (items.length && items[items.length - 1].t === "br") items.pop();
- var r = "", i = 0;
- while (i < items.length){
- var it = items[i];
- if (it.t === "br"){ r += "\\line "; i++; continue; }
- if (it.t === "img"){ r += pictRTF(it.el); i++; continue; }
- if (it.t === "link"){
- r += "{\\field{\\*\\fldinst{HYPERLINK \"" + escText(it.url).replace(/"/g, "%22") + "\"}}{\\fldrslt{\\ul\\cf" +
- colorNo({ r:5, g:99, b:193 }) + " " + itemsRTF(it.items.slice()) + "}}}";
- i++; continue;
- }
- // merge consecutive text items with the same formatting
- var codes = fmtCodes(it.f), text = it.text;
- i++;
- while (i < items.length && items[i].t === "text" && fmtCodes(items[i].f) === codes){ text += items[i].text; i++; }
- r += codes ? "{" + codes + " " + escText(text) + "}" : escText(text);
- }
- return r;
- }
- function pictRTF(img){
- var src = img.getAttribute("src") || "";
- var m = src.match(/^data:image\/(png|jpe?g);base64,(.*)$/i);
- if (!m) return "";
- var bin;
- try { bin = atob(m[2]); } catch(e){ return ""; }
- var hex = [];
- for (var i = 0; i < bin.length; i++){
- hex.push(("0" + bin.charCodeAt(i).toString(16)).slice(-2));
- if (i % 64 === 63) hex.push("\r\n");
- }
- var natW = +img.getAttribute("data-natw") || +img.getAttribute("width") || 300;
- var natH = +img.getAttribute("data-nath") || +img.getAttribute("height") || 150;
- var dispW = +img.getAttribute("width") || parseFloat(img.style.width) || natW;
- var dispH = +img.getAttribute("height") || parseFloat(img.style.height) || Math.round(natH * dispW / natW);
- if (dispW > 624){ dispH = Math.round(dispH * 624 / dispW); dispW = 624; }
- var type = /png/i.test(m[1]) ? "\\pngblip" : "\\jpegblip";
- return "{\\pict" + type + "\\picw" + natW + "\\pich" + natH + "\\picwgoal" + Math.round(dispW * 15) +
- "\\pichgoal" + Math.round(dispH * 15) + "\r\n" + hex.join("") + "}";
- }
- // ── blocks ──
- function alignOf(el, inherited){
- var a = (el.style && el.style.textAlign) || el.getAttribute("align") || "";
- a = a.toLowerCase();
- if (a === "center") return "\\qc";
- if (a === "right" || a === "end") return "\\qr";
- if (a === "justify") return "\\qj";
- if (a === "left" || a === "start") return "";
- return inherited || "";
- }
- function indentOf(el){
- var ml = el.style ? el.style.marginLeft : "";
- var m = ml && ml.match(/^([\d.]+)(px|pt)?$/);
- return m ? Math.round(parseFloat(m[1]) * (m[2] === "pt" ? 20 : 15)) : 0;
- }
- function textIndentOf(el){
- var ti = el.style ? el.style.textIndent : "";
- var m = ti && ti.match(/^(-?[\d.]+)(px|pt)?$/);
- return m ? Math.round(parseFloat(m[1]) * (m[2] === "pt" ? 20 : 15)) : 0;
- }
- function para(nodes, ctx, opt){
- opt = opt || {};
- var f = { b:false, i:false, u:false, s:false, sup:false, sub:false };
- if (opt.font) f.font = opt.font;
- if (ctx.bold) f.b = true;
- if (opt.heading){ f.b = true; f.size = Math.round(base * HEAD_SCALE[opt.heading] / 2) * 2; }
- var items = [];
- collect(nodes, f, items);
- var head = "\\pard" + (ctx.intbl ? "\\intbl" : "") + (opt.align || "");
- var li = (ctx.indent || 0) + (opt.li || 0);
- if (li) head += "\\li" + li;
- if (opt.fi) head += "\\fi" + opt.fi;
- if (opt.heading) head += "\\outlinelevel" + (parseInt(opt.heading.charAt(1), 10) - 1) + "\\sb240";
- head += "\\sa" + (opt.sa === undefined ? 120 : opt.sa) + "\\plain\\f0\\fs" + base + " ";
- out.push(head + (opt.prefix || "") + itemsRTF(items) + (opt.end || "\\par") + "\r\n");
- }
- function hasContent(nodes){
- for (var i = 0; i < nodes.length; i++){
- var n = nodes[i];
- if (n.nodeType === 3 && n.nodeValue.replace(/[\u200B\s]/g, "") !== "") return true;
- if (n.nodeType === 1) return true;
- }
- return false;
- }
- function container(el, ctx){
- var buf = [];
- function flushInline(){
- if (buf.length && hasContent(buf)) para(buf, ctx, { align:ctx.align });
- buf = [];
- }
- for (var i = 0; i < el.childNodes.length; i++){
- var c = el.childNodes[i];
- if (c.nodeType === 1 && BLOCK_TAGS.test(c.tagName)){ flushInline(); block(c, ctx); }
- else buf.push(c);
- }
- flushInline();
- }
- function hasBlockChild(el){
- for (var i = 0; i < el.children.length; i++) if (BLOCK_TAGS.test(el.children[i].tagName)) return true;
- return false;
- }
- function block(el, ctx){
- var tag = el.tagName, align = alignOf(el, ctx.align);
- if (/^H[1-6]$/.test(tag)){ para(el.childNodes, ctx, { align:align, heading:tag }); return; }
- if (tag === "HR"){ out.push("\\pard" + (ctx.intbl ? "\\intbl" : "") + "\\brdrb\\brdrs\\brdrw10\\brsp20\\sa120\\plain\\f0\\fs" + base + " \\par\r\n"); return; }
- if (tag === "PRE"){
- var lines = (el.textContent || "").replace(/\u200B/g, "").replace(/\n$/, "").split("\n");
- lines.forEach(function(ln, k){
- var tn = document.createTextNode(ln);
- para([tn], ctx, { font:MONO, sa:(k === lines.length - 1 ? 120 : 0) });
- });
- return;
- }
- if (tag === "UL" || tag === "OL"){ list(el, ctx); return; }
- if (tag === "BLOCKQUOTE"){
- container(el, { indent:(ctx.indent || 0) + 720, align:align, intbl:ctx.intbl, level:ctx.level });
- return;
- }
- if (tag === "TABLE"){ table(el, ctx); return; }
- if (tag === "P" || tag === "DT" || tag === "DD" || !hasBlockChild(el)){
- para(el.childNodes, ctx, { align:align, li:indentOf(el), fi:textIndentOf(el) });
- return;
- }
- container(el, { indent:(ctx.indent || 0) + indentOf(el), align:align, intbl:ctx.intbl, level:ctx.level });
- }
- function list(el, ctx){
- var ordered = el.tagName === "OL";
- var level = (ctx.level === undefined ? -1 : ctx.level) + 1;
- var num = parseInt(el.getAttribute("start"), 10) || 1;
- var li = 720 * (level + 1);
- for (var i = 0; i < el.children.length; i++){
- var item = el.children[i];
- if (item.tagName !== "LI"){ block(item, ctx); continue; }
- var marker = ordered ? (num++) + "." : "\\u8226?";
- var pn = ordered
- ? "{\\*\\pn\\pnlvlbody\\pndec\\pnstart1\\pnindent360{\\pntxta.}}"
- : "{\\*\\pn\\pnlvlblt\\pnf" + fontNo("Symbol") + "\\pnindent360{\\pntxtb\\'B7}}";
- var prefix = "{\\listtext\\pard\\plain\\f0\\fs" + base + " " + marker + "\\tab}" + pn;
- var sub = { indent:ctx.indent, align:ctx.align, intbl:ctx.intbl, level:level };
- // the item's own text runs up to its first block child
- var inline = [], rest = [];
- for (var k = 0; k < item.childNodes.length; k++){
- var c = item.childNodes[k];
- if (rest.length || (c.nodeType === 1 && BLOCK_TAGS.test(c.tagName))) rest.push(c);
- else inline.push(c);
- }
- if (!hasContent(inline) && rest.length && /^(P|DIV)$/.test(rest[0].tagName)){
- inline = Array.prototype.slice.call(rest.shift().childNodes);
- }
- para(inline, ctx, { align:alignOf(item, ctx.align), li:li, fi:-360, sa:40, prefix:prefix });
- rest.forEach(function(c){
- if (c.tagName === "UL" || c.tagName === "OL") list(c, sub);
- else block(c, { indent:(ctx.indent || 0) + li, align:ctx.align, intbl:ctx.intbl, level:level });
- });
- }
- }
- function table(el, ctx){
- var rows = el.querySelectorAll("tr");
- for (var r = 0; r < rows.length; r++){
- var cells = [];
- for (var c = 0; c < rows[r].children.length; c++){
- if (/^T[DH]$/.test(rows[r].children[c].tagName)) cells.push(rows[r].children[c]);
- }
- if (!cells.length) continue;
- var w = Math.floor(9000 / cells.length), def = "\\trowd\\trgaph108\\trleft0";
- for (var k = 0; k < cells.length; k++){
- def += "\\clbrdrt\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10\\clbrdrb\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10\\cellx" + (w * (k + 1));
- }
- out.push(def + "\r\n");
- for (var k2 = 0; k2 < cells.length; k2++){
- var cell = cells[k2], isHead = cell.tagName === "TH";
- var start = out.length;
- var cctx = { intbl:true, align:alignOf(cell, ""), bold:isHead };
- if (hasBlockChild(cell)) container(cell, cctx);
- else para(cell.childNodes, cctx, { align:cctx.align, sa:0 });
- if (out.length === start) para([], cctx, { sa:0 });
- // the cell's last paragraph ends with \cell instead of \par
- out[out.length - 1] = out[out.length - 1].replace(/\\par\r\n$/, "\\cell\r\n");
- }
- out.push(def + "\\row\r\n");
- }
- }
- container(root, { indent:0, align:"" });
- var ft = "{\\fonttbl";
- fonts.forEach(function(name, i){
- var fam = name === MONO ? "\\fmodern" : name === "Symbol" ? "\\ftech" : "\\fnil";
- var cs = name === "Symbol" ? 2 : (/[^\x00-\x7f]/.test(name) ? 1 : 0);
- ft += "{\\f" + i + fam + "\\fcharset" + cs + " " + escText(name) + ";}";
- });
- ft += "}";
- var ct = "{\\colortbl ;";
- colors.forEach(function(c){ ct += "\\red" + c.r + "\\green" + c.g + "\\blue" + c.b + ";"; });
- ct += "}";
- return "{\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat" + ft + "\r\n" + ct + "\r\n" +
- "{\\*\\generator ArozOS Text;}\\viewkind4\\uc1\r\n" + out.join("") + "}\r\n";
- }
- // ════════════════════════════════════════════════════════════════════════
- // Image preparation (async)
- // ════════════════════════════════════════════════════════════════════════
- function blobToDataURL(blob){
- return new Promise(function(res, rej){
- var fr = new FileReader();
- fr.onload = function(){ res(fr.result); };
- fr.onerror = rej;
- fr.readAsDataURL(blob);
- });
- }
- function loadImage(src){
- return new Promise(function(res, rej){
- var im = new Image();
- im.onload = function(){ res(im); };
- im.onerror = function(){ rej(new Error("image load failed")); };
- im.src = src;
- });
- }
- function inlineOne(img){
- var src = img.getAttribute("src") || "";
- var p = /^data:image\/(png|jpe?g);/i.test(src)
- ? Promise.resolve(src)
- : fetch(src, { credentials:"same-origin" }).then(function(r){
- if (!r.ok) throw new Error("HTTP " + r.status);
- return r.blob();
- }).then(function(b){
- if (/^image\/(png|jpe?g)$/i.test(b.type)) return blobToDataURL(b);
- // other formats (gif/webp/svg/bmp) are re-encoded as PNG
- var url = URL.createObjectURL(b);
- return loadImage(url).then(function(im){
- URL.revokeObjectURL(url);
- var cv = document.createElement("canvas");
- cv.width = im.naturalWidth || 300; cv.height = im.naturalHeight || 150;
- cv.getContext("2d").drawImage(im, 0, 0);
- return cv.toDataURL("image/png");
- });
- }).then(function(d){
- // sniff the real format: a mislabelled blob still needs png/jpeg bytes
- return d.replace(/^data:[^;,]*;/, function(){
- var head = atob(d.split(",")[1].substr(0, 8));
- return head.charCodeAt(0) === 0xFF ? "data:image/jpeg;" : "data:image/png;";
- });
- });
- return p.then(function(d){
- img.setAttribute("src", d);
- return loadImage(d).then(function(im){
- img.setAttribute("data-natw", im.naturalWidth);
- img.setAttribute("data-nath", im.naturalHeight);
- });
- }).catch(function(){
- img.parentNode && img.parentNode.removeChild(img); // unreachable image: leave it out
- });
- }
- function inlineImages(root){
- var imgs = Array.prototype.slice.call(root.querySelectorAll("img"));
- return Promise.all(imgs.map(inlineOne)).then(function(){ return root; });
- }
- window.TextRTF = { toHTML:toHTML, fromHTML:fromHTML, inlineImages:inlineImages, parse:parse };
- })();
|