rtf.js 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001
  1. /*
  2. rtf.js — self-contained RTF reader / writer for the Text editor.
  3. TextRTF.toHTML(src)
  4. src is the RTF source as a "binary string" (one char per byte, as read
  5. from the file); characters above U+00FF are accepted as already-decoded
  6. text. Returns { html, baseSize, baseFont } where baseSize is the
  7. document's dominant font size in half-points and baseFont its dominant
  8. font name. Runs at the base size / font carry no inline style, so they
  9. follow the editor's own typography.
  10. TextRTF.fromHTML(root, opts)
  11. Serialises the editor DOM (a detached clone) back to RTF. opts may give
  12. { baseSize, baseFont } so a document keeps the defaults it was loaded
  13. with. Images must be data: URLs (run inlineImages() first).
  14. TextRTF.inlineImages(root)
  15. Promise. Turns every <img> under root into a PNG/JPEG data: URL and
  16. records its natural size, so fromHTML() can embed it as \pict.
  17. Supported: paragraphs, alignment, indents, headings (\outlinelevel), bold,
  18. italic, underline, strikethrough, super/subscript, fonts, sizes, colours,
  19. highlight, hyperlinks, bullet / numbered lists, tables, horizontal rules,
  20. PNG/JPEG pictures, \u unicode and code-page (\'hh) text incl. DBCS (Big5,
  21. GBK, Shift_JIS, EUC-KR).
  22. */
  23. (function(){
  24. "use strict";
  25. // ── Code pages ───────────────────────────────────────────────────────────
  26. var CHARSET_CP = {
  27. 0:1252, 77:10000, 128:932, 129:949, 130:949, 134:936, 136:950, 161:1253,
  28. 162:1254, 163:1258, 177:1255, 178:1256, 186:1257, 204:1251, 222:874, 238:1250
  29. };
  30. var CP_LABEL = {
  31. 874:"windows-874", 932:"shift_jis", 936:"gbk", 949:"euc-kr", 950:"big5",
  32. 1250:"windows-1250", 1251:"windows-1251", 1252:"windows-1252", 1253:"windows-1253",
  33. 1254:"windows-1254", 1255:"windows-1255", 1256:"windows-1256", 1257:"windows-1257",
  34. 1258:"windows-1258", 10000:"macintosh", 65001:"utf-8"
  35. };
  36. var decoders = {};
  37. function decoderFor(cp){
  38. if (decoders[cp]) return decoders[cp];
  39. var d = null;
  40. try { d = new TextDecoder(CP_LABEL[cp] || "windows-1252"); } catch(e){}
  41. if (!d) d = new TextDecoder("windows-1252");
  42. decoders[cp] = d;
  43. return d;
  44. }
  45. // destinations whose whole group is dropped
  46. var SKIP_DEST = {
  47. stylesheet:1, info:1, header:1, headerl:1, headerr:1, headerf:1, footer:1, footerl:1,
  48. footerr:1, footerf:1, footnote:1, nonshppict:1, listtable:1, listoverridetable:1,
  49. revtbl:1, rsidtbl:1, xe:1, tc:1, txe:1, themedata:1, colorschememapping:1,
  50. latentstyles:1, datastore:1, xmlnstbl:1, mmathPr:1, pnseclvl:1, filetbl:1,
  51. pgdsctbl:1, author:1, operator:1, title:1, comment:1, company:1, doccomm:1,
  52. keywords:1, subject:1, upr:1, "private":1, annotation:1, atnid:1, atnauthor:1,
  53. objdata:1, objclass:1, bkmkstart:1, bkmkend:1, template:1, docvar:1, userprops:1
  54. };
  55. // "\*"-marked destinations we understand (every other one is skipped)
  56. var KNOWN_STAR = { fldinst:1, shppict:1, pn:1, listtext:1 };
  57. var SYMBOLS = {
  58. emdash:"\u2014", endash:"\u2013", bullet:"\u2022", lquote:"\u2018", rquote:"\u2019",
  59. ldblquote:"\u201C", rdblquote:"\u201D", emspace:"\u2003", enspace:"\u2002",
  60. qmspace:"\u2005", zwj:"\u200D", zwnj:"\u200C", ltrmark:"", rtlmark:""
  61. };
  62. function defaultChar(){
  63. return { b:false, i:false, u:false, s:false, sup:false, sub:false, v:false,
  64. fs:24, f:-1, cf:0, bg:0, link:null };
  65. }
  66. function defaultPara(){
  67. return { align:"", li:0, fi:0, intbl:false, outline:-1, ilvl:-1, ls:0, brdrb:false };
  68. }
  69. function copy(o){ var r = {}; for (var k in o) r[k] = o[k]; return r; }
  70. // ════════════════════════════════════════════════════════════════════════
  71. // Reader
  72. // ════════════════════════════════════════════════════════════════════════
  73. function parse(src){
  74. var fonts = {}, colors = [], deff = 0, ansicp = 1252;
  75. var blocks = []; // finished paragraphs / tables
  76. var table = null, row = null, cell = null;
  77. var para = newPara();
  78. var st = { dest:"main", chr:defaultChar(), par:defaultPara(), uc:1, field:null,
  79. font:null, pict:null };
  80. var stack = [];
  81. var bytes = [], bytesCP = 1252; // pending code-page bytes
  82. var skipChars = 0; // \u fallback characters still to skip
  83. var star = false; // saw "\*" for the next control word
  84. var color = null; // colortbl entry being built
  85. function newPara(){ return { runs:[], marker:"", listType:"" }; }
  86. function fontCP(fi){ return cpOf(fonts[fi]); }
  87. function cpOf(f){
  88. if (!f) return ansicp;
  89. if (f.cpg) return f.cpg;
  90. if (f.charset === 1 || f.charset === undefined) return ansicp;
  91. if (f.charset === 2) return 1252;
  92. return CHARSET_CP[f.charset] || ansicp;
  93. }
  94. function curCP(){ return fontCP(st.chr.f < 0 ? deff : st.chr.f); }
  95. function flush(){
  96. if (!bytes.length) return;
  97. var txt = decoderFor(bytesCP).decode(new Uint8Array(bytes));
  98. bytes = [];
  99. emitText(txt);
  100. }
  101. function emitByte(b){
  102. if (skipChars > 0){ skipChars--; return; }
  103. var cp = st.dest === "fonttbl" ? cpOf(st.font) : curCP();
  104. if (bytes.length && cp !== bytesCP) flush();
  105. bytesCP = cp;
  106. bytes.push(b);
  107. }
  108. function emitText(txt){
  109. if (!txt) return;
  110. switch (st.dest){
  111. case "main":
  112. if (st.chr.v) return; // hidden text
  113. var runs = para.runs, last = runs[runs.length - 1];
  114. if (last && last.type === "text" && sameChr(last.chr, st.chr)) last.text += txt;
  115. else runs.push({ type:"text", text:txt, chr:copy(st.chr) });
  116. return;
  117. case "fonttbl":
  118. if (st.font) st.font.raw += txt;
  119. return;
  120. case "fldinst":
  121. if (st.field) st.field.inst += txt;
  122. return;
  123. case "listtext":
  124. para.marker += txt;
  125. return;
  126. }
  127. }
  128. function sameChr(a, b){
  129. for (var k in a) if (a[k] !== b[k]) return false;
  130. return true;
  131. }
  132. function emitObj(o){
  133. if (st.dest !== "main" || st.chr.v) return;
  134. o.chr = copy(st.chr);
  135. para.runs.push(o);
  136. }
  137. function endPara(){
  138. flush();
  139. var p = para;
  140. p.pp = copy(st.par);
  141. para = newPara();
  142. if (p.pp.intbl){
  143. if (!cell) cell = [];
  144. cell.push(p);
  145. } else {
  146. closeTable();
  147. blocks.push(p);
  148. }
  149. }
  150. function endCell(){
  151. flush();
  152. if (para.runs.length || !cell || !cell.length){
  153. para.pp = copy(st.par);
  154. if (!cell) cell = [];
  155. cell.push(para);
  156. }
  157. para = newPara();
  158. if (!row) row = [];
  159. row.push(cell);
  160. cell = null;
  161. }
  162. function endRow(){
  163. flush();
  164. if (cell) endCell();
  165. if (!table) table = { type:"table", rows:[] };
  166. if (row) table.rows.push(row);
  167. row = null;
  168. }
  169. function closeTable(){
  170. if (cell || row) endRow();
  171. if (table){ blocks.push(table); table = null; }
  172. }
  173. function endGroup(){
  174. flush();
  175. var child = st;
  176. st = stack.pop() || st;
  177. if (child.dest === "pict" && child.pict && st.dest !== "skip") finishPict(child);
  178. if (child.dest === "fonttbl" && child.font && child.font !== st.font) finishFont(child.font);
  179. skipChars = 0;
  180. }
  181. function finishFont(f){
  182. if (f.done) return;
  183. f.done = true;
  184. var name = f.raw.replace(/;.*$/, "").trim();
  185. fonts[f.idx] = { name:name, charset:f.charset, cpg:f.cpg, family:f.family };
  186. }
  187. function finishPict(s){
  188. var p = s.pict, mime = null;
  189. if (p.type === "png") mime = "image/png";
  190. else if (p.type === "jpeg") mime = "image/jpeg";
  191. if (!mime) return; // metafiles / bitmaps can't be shown
  192. var bin = p.bin;
  193. if (!bin){
  194. var hex = p.hex.replace(/[^0-9a-fA-F]/g, "");
  195. var out = [];
  196. for (var i = 0; i + 1 < hex.length; i += 2) out.push(String.fromCharCode(parseInt(hex.substr(i, 2), 16)));
  197. bin = out.join("");
  198. }
  199. if (!bin) return;
  200. var w = p.wgoal ? p.wgoal * (p.scalex || 100) / 100 / 15 : 0;
  201. var h = p.hgoal ? p.hgoal * (p.scaley || 100) / 100 / 15 : 0;
  202. if (st.dest === "main"){ // st is the enclosing group again
  203. para.runs.push({ type:"img", src:"data:" + mime + ";base64," + btoa(bin),
  204. w:Math.round(w), h:Math.round(h), chr:copy(st.chr) });
  205. }
  206. }
  207. function control(word, param, hasParam){
  208. // "\*" handling: unknown ignorable destinations are skipped wholesale
  209. if (star){
  210. star = false;
  211. if (!KNOWN_STAR[word]){ st.dest = "skip"; return; }
  212. }
  213. if (st.dest === "skip") return;
  214. if (skipChars > 0 && word !== "u"){ skipChars--; return; }
  215. if (SKIP_DEST[word]){ st.dest = "skip"; return; }
  216. // unicode text is valid in every text-bearing destination (incl. font names)
  217. if (word === "u"){
  218. flush();
  219. emitText(String.fromCharCode(param < 0 ? param + 65536 : param));
  220. skipChars = st.uc;
  221. return;
  222. }
  223. if (word === "uc"){ st.uc = param; return; }
  224. // ── destinations ──
  225. switch (word){
  226. case "fonttbl": st.dest = "fonttbl"; st.font = null; return;
  227. case "colortbl": st.dest = "colortbl"; color = null; return;
  228. case "pict": st.dest = "pict"; st.pict = { type:"", hex:"", bin:"", wgoal:0, hgoal:0, scalex:100, scaley:100 }; return;
  229. case "field": st.field = { inst:"" }; return;
  230. case "fldinst": st.dest = "fldinst"; return;
  231. case "fldrslt":
  232. st.dest = "main";
  233. if (st.field){
  234. var m = st.field.inst.match(/HYPERLINK\s+(?:\\l\s+)?"([^"]*)"/i) ||
  235. st.field.inst.match(/HYPERLINK\s+(\S+)/i);
  236. if (m){
  237. var url = m[1];
  238. if (/\\l\s/.test(st.field.inst) && url.charAt(0) !== "#") url = "#" + url;
  239. st.chr.link = url;
  240. }
  241. }
  242. return;
  243. case "listtext": case "pntext":
  244. st.dest = "listtext"; return;
  245. case "pn": st.dest = "pn"; return;
  246. case "shppict": return; // its \pict is the real picture
  247. }
  248. if (st.dest === "fonttbl"){
  249. if (word === "f"){
  250. if (st.font && !st.font.done && stack.length && stack[stack.length-1].font !== st.font) finishFont(st.font);
  251. st.font = { idx:param, raw:"", charset:undefined, cpg:0, family:"" };
  252. } else if (st.font){
  253. if (word === "fcharset") st.font.charset = param;
  254. else if (word === "cpg") st.font.cpg = param;
  255. else if (/^f(roman|swiss|modern|script|decor|tech|bidi|nil)$/.test(word)) st.font.family = word.substr(1);
  256. }
  257. return;
  258. }
  259. if (st.dest === "colortbl"){
  260. if (!color) color = { r:0, g:0, b:0 };
  261. if (word === "red") color.r = param;
  262. else if (word === "green") color.g = param;
  263. else if (word === "blue") color.b = param;
  264. return;
  265. }
  266. if (st.dest === "pict"){
  267. var p = st.pict;
  268. if (word === "pngblip") p.type = "png";
  269. else if (word === "jpegblip") p.type = "jpeg";
  270. else if (word === "emfblip" || word === "wmetafile" || word === "macpict" || word === "dibitmap" || word === "wbitmap") p.type = p.type || word;
  271. else if (word === "picwgoal") p.wgoal = param;
  272. else if (word === "pichgoal") p.hgoal = param;
  273. else if (word === "picscalex") p.scalex = param;
  274. else if (word === "picscaley") p.scaley = param;
  275. return;
  276. }
  277. if (st.dest === "pn"){
  278. if (word === "pnlvlblt") para.listType = "ul";
  279. else if (/^pn(lvlbody|dec|ucltr|lcltr|ucrm|lcrm)$/.test(word)) para.listType = para.listType || "ol";
  280. return;
  281. }
  282. var c = st.chr, pp = st.par;
  283. switch (word){
  284. // header
  285. case "ansicpg": ansicp = param || 1252; return;
  286. case "deff": deff = param; return;
  287. // breaks
  288. case "par": case "sect": case "page":
  289. endPara(); return;
  290. case "line":
  291. flush(); emitObj({ type:"br" }); return;
  292. case "tab":
  293. flush(); emitText("\t"); return;
  294. case "cell": case "nestcell":
  295. endCell(); return;
  296. case "row": case "nestrow":
  297. endRow(); return;
  298. // paragraph formatting
  299. case "pard": st.par = defaultPara(); return;
  300. case "ql": pp.align = ""; return;
  301. case "qc": pp.align = "center"; return;
  302. case "qr": pp.align = "right"; return;
  303. case "qj": pp.align = "justify"; return;
  304. case "li": pp.li = param; return;
  305. case "fi": pp.fi = param; return;
  306. case "intbl": pp.intbl = true; return;
  307. case "outlinelevel": pp.outline = param; return;
  308. case "ilvl": pp.ilvl = param; return;
  309. case "ls": pp.ls = param; return;
  310. case "brdrb": pp.brdrb = true; return;
  311. // character formatting
  312. case "plain":
  313. var link = c.link;
  314. st.chr = defaultChar(); st.chr.link = link; return;
  315. case "b": c.b = !hasParam || param !== 0; return;
  316. case "i": c.i = !hasParam || param !== 0; return;
  317. case "strike": case "striked":
  318. c.s = !hasParam || param !== 0; return;
  319. case "ulnone": c.u = false; return;
  320. case "super": c.sup = true; c.sub = false; return;
  321. case "sub": c.sub = true; c.sup = false; return;
  322. case "nosupersub": c.sup = c.sub = false; return;
  323. case "v": c.v = !hasParam || param !== 0; return;
  324. case "fs": c.fs = hasParam ? param : 24; return;
  325. case "f": c.f = param; return;
  326. case "cf": c.cf = param; return;
  327. case "cb": case "highlight": case "chcbpat":
  328. c.bg = param; return;
  329. }
  330. if (/^ul(d|dash|dashd|dashdd|db|hwave|ldash|th|thd|thdash|thdashd|thdashdd|thldash|w|wave)?$/.test(word)){
  331. c.u = !hasParam || param !== 0;
  332. return;
  333. }
  334. if (SYMBOLS.hasOwnProperty(word)){ flush(); emitText(SYMBOLS[word]); }
  335. }
  336. // ── tokenizer ──
  337. var i = 0, n = src.length;
  338. while (i < n){
  339. var ch = src.charAt(i), code = src.charCodeAt(i);
  340. if (ch === "{"){
  341. flush();
  342. stack.push(st);
  343. st = { dest:st.dest, chr:copy(st.chr), par:copy(st.par), uc:st.uc, field:st.field,
  344. font:st.font, pict:st.pict };
  345. i++;
  346. continue;
  347. }
  348. if (ch === "}"){
  349. endGroup(); star = false; i++;
  350. continue;
  351. }
  352. if (ch === "\\"){
  353. var nx = src.charAt(i + 1);
  354. if (/[a-zA-Z]/.test(nx)){
  355. var j = i + 1;
  356. while (j < n && /[a-zA-Z]/.test(src.charAt(j))) j++;
  357. var word = src.substring(i + 1, j);
  358. var k = j;
  359. if (src.charAt(k) === "-" && /[0-9]/.test(src.charAt(k + 1))) k++;
  360. while (k < n && /[0-9]/.test(src.charAt(k))) k++;
  361. var hasParam = k > j;
  362. var param = hasParam ? parseInt(src.substring(j, k), 10) : 0;
  363. if (src.charAt(k) === " ") k++;
  364. i = k;
  365. if (word !== "u") flush();
  366. if (word === "bin"){ // raw binary data follows
  367. var data = src.substr(i, param);
  368. if (st.dest === "pict" && st.pict) st.pict.bin = data;
  369. i += param;
  370. continue;
  371. }
  372. control(word, param, hasParam);
  373. continue;
  374. }
  375. i += 2;
  376. switch (nx){
  377. case "'":
  378. var hb = parseInt(src.substr(i, 2), 16);
  379. i += 2;
  380. if (st.dest === "skip" || st.dest === "pn" || st.dest === "pict" || isNaN(hb)) break;
  381. if (st.dest === "colortbl") break;
  382. emitByte(hb);
  383. break;
  384. case "*": star = true; break;
  385. case "~": flush(); if (skipChars > 0) skipChars--; else emitText("\u00A0"); break;
  386. case "_": flush(); if (skipChars > 0) skipChars--; else emitText("\u2011"); break;
  387. case "-": break; // optional hyphen
  388. case "\n": case "\r": flush(); if (st.dest === "main") endPara(); break;
  389. case "\\": case "{": case "}":
  390. if (st.dest === "skip" || st.dest === "pn") break;
  391. emitByte(nx.charCodeAt(0)); break;
  392. }
  393. continue;
  394. }
  395. i++;
  396. if (ch === "\r" || ch === "\n") continue;
  397. if (st.dest === "skip" || st.dest === "pn") continue;
  398. if (st.dest === "pict"){ st.pict.hex += ch; continue; }
  399. if (st.dest === "colortbl"){
  400. if (ch === ";"){ colors.push(color); color = null; }
  401. continue;
  402. }
  403. if (st.dest === "fonttbl" && ch === ";"){
  404. flush();
  405. if (st.font){ st.font.raw += ";"; finishFont(st.font); }
  406. continue;
  407. }
  408. if (code > 0xFF){ flush(); if (skipChars > 0) skipChars--; else emitText(ch); continue; }
  409. emitByte(code);
  410. }
  411. flush();
  412. if (para.runs.length) endPara();
  413. closeTable();
  414. return { blocks:blocks, fonts:fonts, colors:colors, deff:deff };
  415. }
  416. // ── model → HTML ─────────────────────────────────────────────────────────
  417. function esc(s){ return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); }
  418. function escAttr(s){ return esc(s).replace(/"/g, "&quot;"); }
  419. function colorCss(c){
  420. if (!c) return "";
  421. function h(v){ return ("0" + (v & 255).toString(16)).slice(-2); }
  422. return "#" + h(c.r) + h(c.g) + h(c.b);
  423. }
  424. var GENERIC = { roman:"serif", swiss:"sans-serif", modern:"monospace", script:"cursive", decor:"fantasy" };
  425. function toHTML(src){
  426. src = src || "";
  427. if (!/^\s*\{\\rtf/.test(src)){
  428. // not RTF (e.g. a brand-new empty file) — show it as plain paragraphs
  429. var lines = src.replace(/\r\n?/g, "\n").split("\n");
  430. return { html: lines.map(function(l){ return "<p>" + (l ? esc(l) : "<br>") + "</p>"; }).join(""),
  431. baseSize:24, baseFont:"" };
  432. }
  433. var doc = parse(src);
  434. // dominant size / font become the document base (no inline style)
  435. var sizeW = {}, fontW = {};
  436. doc.blocks.forEach(function scan(b){
  437. if (b.type === "table"){ b.rows.forEach(function(r){ r.forEach(function(c){ c.forEach(scan); }); }); return; }
  438. if (b.pp && b.pp.outline >= 0 && b.pp.outline <= 5) return;
  439. b.runs.forEach(function(r){
  440. if (r.type !== "text") return;
  441. var len = r.text.length, f = r.chr.f < 0 ? doc.deff : r.chr.f;
  442. sizeW[r.chr.fs] = (sizeW[r.chr.fs] || 0) + len;
  443. fontW[f] = (fontW[f] || 0) + len;
  444. });
  445. });
  446. 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; }
  447. var baseSize = top(sizeW, 24), baseFontIdx = top(fontW, doc.deff);
  448. var baseFont = doc.fonts[baseFontIdx] ? doc.fonts[baseFontIdx].name : "";
  449. function fontCss(idx){
  450. var f = doc.fonts[idx];
  451. if (!f || !f.name) return "";
  452. var gen = GENERIC[f.family];
  453. return "'" + f.name.replace(/'/g, "") + "'" + (gen ? ", " + gen : "");
  454. }
  455. function runsHTML(runs, heading){
  456. var html = "", openLink = null, buf = "";
  457. function closeLink(){
  458. if (openLink !== null){ html += '<a href="' + escAttr(openLink) + '">' + buf + "</a>"; buf = ""; openLink = null; }
  459. }
  460. runs.forEach(function(r){
  461. var c = r.chr, piece;
  462. if (r.type === "br") piece = "<br>";
  463. else if (r.type === "img"){
  464. piece = '<img src="' + r.src + '" alt=""' + (r.w ? ' width="' + r.w + '"' : "") + ">";
  465. } else {
  466. piece = esc(r.text);
  467. var style = [];
  468. var fi = c.f < 0 ? doc.deff : c.f;
  469. if (!heading && c.fs !== baseSize) style.push("font-size:" + (c.fs / 2) + "pt");
  470. if (fi !== baseFontIdx){ var fc = fontCss(fi); if (fc) style.push("font-family:" + fc); }
  471. if (c.cf > 0 && doc.colors[c.cf]) style.push("color:" + colorCss(doc.colors[c.cf]));
  472. if (c.bg > 0 && doc.colors[c.bg]) style.push("background-color:" + colorCss(doc.colors[c.bg]));
  473. if (style.length) piece = '<span style="' + escAttr(style.join(";")) + '">' + piece + "</span>";
  474. if (c.sup) piece = "<sup>" + piece + "</sup>";
  475. if (c.sub) piece = "<sub>" + piece + "</sub>";
  476. if (c.s) piece = "<s>" + piece + "</s>";
  477. if (c.u && !c.link) piece = "<u>" + piece + "</u>";
  478. if (c.i) piece = "<i>" + piece + "</i>";
  479. if (c.b && !heading) piece = "<b>" + piece + "</b>";
  480. }
  481. if (c.link !== openLink){ closeLink(); if (c.link) openLink = c.link; }
  482. if (openLink !== null) buf += piece; else html += piece;
  483. });
  484. closeLink();
  485. return html;
  486. }
  487. function blockStyle(pp, list){
  488. var s = [];
  489. if (pp.align) s.push("text-align:" + pp.align);
  490. if (!list){
  491. if (pp.li > 0) s.push("margin-left:" + Math.round(pp.li / 15) + "px");
  492. if (pp.fi) s.push("text-indent:" + Math.round(pp.fi / 15) + "px");
  493. }
  494. return s.length ? ' style="' + s.join(";") + '"' : "";
  495. }
  496. function isBlank(p){
  497. return !p.runs.some(function(r){ return r.type !== "text" || /\S/.test(r.text.replace(/\u00A0/g, "x")); });
  498. }
  499. function paraHTML(p){
  500. var pp = p.pp || defaultPara();
  501. if (pp.brdrb && isBlank(p)) return "<hr>";
  502. var tag = (pp.outline >= 0 && pp.outline <= 5) ? "h" + (pp.outline + 1) : "p";
  503. var inner = runsHTML(p.runs, tag !== "p");
  504. return "<" + tag + blockStyle(pp) + ">" + (inner || "<br>") + "</" + tag + ">";
  505. }
  506. function isList(p){ return p.type !== "table" && (p.marker || p.listType || (p.pp && p.pp.ls > 0)); }
  507. function listLevel(p){
  508. if (p.pp.ilvl >= 0) return p.pp.ilvl;
  509. return Math.max(0, Math.round(p.pp.li / 720) - 1);
  510. }
  511. function listKind(p){
  512. if (p.listType) return p.listType;
  513. return /^\s*([0-9]+|[a-zA-Z]|[ivxlcIVXLC]+)[.)]/.test(p.marker) ? "ol" : "ul";
  514. }
  515. function tableHTML(t){
  516. var html = "<table><tbody>";
  517. t.rows.forEach(function(r){
  518. html += "<tr>";
  519. r.forEach(function(c){
  520. var cellHTML = c.map(function(p){ return runsHTML(p.runs, false); }).join("<br>");
  521. html += "<td>" + (cellHTML || "<br>") + "</td>";
  522. });
  523. html += "</tr>";
  524. });
  525. return html + "</tbody></table>";
  526. }
  527. var out = "", bl = doc.blocks;
  528. for (var i = 0; i < bl.length; i++){
  529. var b = bl[i];
  530. if (b.type === "table"){ out += tableHTML(b); continue; }
  531. if (!isList(b)){ out += paraHTML(b); continue; }
  532. // a run of list paragraphs → nested <ul>/<ol>
  533. var stackL = []; // open list tags
  534. var j = i;
  535. for (; j < bl.length && isList(bl[j]); j++){
  536. var p = bl[j], lvl = Math.min(listLevel(p), 8), kind = listKind(p);
  537. while (stackL.length > lvl + 1){ out += "</li></" + stackL.pop() + ">"; }
  538. if (stackL.length === lvl + 1 && stackL[lvl] !== kind){ out += "</li></" + stackL.pop() + ">"; }
  539. if (stackL.length === lvl + 1) out += "</li>";
  540. while (stackL.length < lvl + 1){ out += "<" + kind + ">"; stackL.push(kind); }
  541. out += "<li" + blockStyle(p.pp, true) + ">" + (runsHTML(p.runs, false) || "<br>");
  542. }
  543. while (stackL.length) out += "</li></" + stackL.pop() + ">";
  544. i = j - 1;
  545. }
  546. return { html: out || "<p><br></p>", baseSize: baseSize, baseFont: baseFont };
  547. }
  548. // ════════════════════════════════════════════════════════════════════════
  549. // Writer
  550. // ════════════════════════════════════════════════════════════════════════
  551. var HEAD_SCALE = { H1:2, H2:1.6, H3:1.3, H4:1.1, H5:1, H6:0.9 };
  552. var FONT_SIZE_ATTR = { 1:8, 2:10, 3:12, 4:14, 5:18, 6:24, 7:36 };
  553. 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)$/;
  554. var MONO = "Courier New";
  555. function parseColor(v){
  556. if (!v) return null;
  557. v = String(v).trim().toLowerCase();
  558. if (v === "transparent" || v === "inherit" || v === "initial" || v === "currentcolor") return null;
  559. var m = v.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
  560. if (m){
  561. var h = m[1];
  562. if (h.length === 3) h = h.charAt(0)+h.charAt(0)+h.charAt(1)+h.charAt(1)+h.charAt(2)+h.charAt(2);
  563. return { r:parseInt(h.substr(0,2),16), g:parseInt(h.substr(2,2),16), b:parseInt(h.substr(4,2),16) };
  564. }
  565. m = v.match(/^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)(?:[\s,/]+([\d.]+%?))?\s*\)$/);
  566. if (m){
  567. if (m[4] !== undefined && parseFloat(m[4]) === 0) return null;
  568. return { r:+m[1], g:+m[2], b:+m[3] };
  569. }
  570. // named colours: let the browser resolve them
  571. try {
  572. var cv = document.createElement("canvas").getContext("2d");
  573. cv.fillStyle = "#000001"; cv.fillStyle = v;
  574. if (cv.fillStyle !== "#000001") return parseColor(cv.fillStyle);
  575. } catch(e){}
  576. return null;
  577. }
  578. function firstFamily(v){
  579. if (!v) return "";
  580. var f = String(v).split(",")[0].trim().replace(/^['"]|['"]$/g, "");
  581. var g = f.toLowerCase();
  582. if (g === "monospace") return MONO;
  583. if (g === "serif") return "Times New Roman";
  584. if (g === "sans-serif" || g === "system-ui" || g === "-apple-system" || g === "inherit") return "";
  585. return f;
  586. }
  587. // CSS font-size → half-points (relative sizes resolve against the current size)
  588. function sizeHalfPts(v, cur){
  589. if (!v) return 0;
  590. var m = String(v).trim().match(/^([\d.]+)(pt|px|em|rem|%)?$/i);
  591. if (!m) return 0;
  592. var n = parseFloat(m[1]), unit = (m[2] || "px").toLowerCase();
  593. var pt = unit === "pt" ? n : unit === "px" ? n * 0.75 : unit === "%" ? cur / 2 * n / 100 : cur / 2 * n;
  594. return Math.max(2, Math.round(pt * 2));
  595. }
  596. function fromHTML(root, opts){
  597. opts = opts || {};
  598. var base = opts.baseSize || 24;
  599. var fonts = [opts.baseFont || "Calibri"], fontIdx = {};
  600. fontIdx[fonts[0].toLowerCase()] = 0;
  601. var colors = [], colorIdx = {};
  602. var out = [];
  603. function fontNo(name){
  604. if (!name) return 0;
  605. var k = name.toLowerCase();
  606. if (fontIdx[k] === undefined){ fontIdx[k] = fonts.length; fonts.push(name); }
  607. return fontIdx[k];
  608. }
  609. function colorNo(c){
  610. if (!c) return 0;
  611. var k = c.r + "," + c.g + "," + c.b;
  612. if (colorIdx[k] === undefined){ colors.push(c); colorIdx[k] = colors.length; }
  613. return colorIdx[k];
  614. }
  615. function escText(s){
  616. var r = "";
  617. for (var i = 0; i < s.length; i++){
  618. var c = s.charCodeAt(i), ch = s.charAt(i);
  619. if (ch === "\\" || ch === "{" || ch === "}") r += "\\" + ch;
  620. else if (ch === "\t") r += "\\tab ";
  621. else if (ch === "\n") r += "\\line ";
  622. else if (c === 0xA0) r += "\\~";
  623. else if (c === 0x200B || c === 0xFEFF || c === 0x0D) continue;
  624. else if (c < 0x20) continue;
  625. else if (c < 0x80) r += ch;
  626. else r += "\\u" + (c > 32767 ? c - 65536 : c) + "?";
  627. }
  628. return r;
  629. }
  630. // ── inline content → list of items ──
  631. function fmtOf(el, f){
  632. var n = copy(f), tag = el.tagName;
  633. if (tag === "B" || tag === "STRONG") n.b = true;
  634. else if (tag === "I" || tag === "EM" || tag === "CITE") n.i = true;
  635. else if (tag === "U" || tag === "INS") n.u = true;
  636. else if (tag === "S" || tag === "STRIKE" || tag === "DEL") n.s = true;
  637. else if (tag === "SUP"){ n.sup = true; n.sub = false; }
  638. else if (tag === "SUB"){ n.sub = true; n.sup = false; }
  639. else if (tag === "CODE" || tag === "KBD" || tag === "TT" || tag === "SAMP") n.font = MONO;
  640. else if (tag === "MARK") n.bg = n.bg || { r:255, g:255, b:0 };
  641. if (tag === "FONT"){
  642. var fc = parseColor(el.getAttribute("color")); if (fc) n.color = fc;
  643. var ff = firstFamily(el.getAttribute("face")); if (ff) n.font = ff;
  644. var fz = FONT_SIZE_ATTR[el.getAttribute("size")]; if (fz) n.size = fz * 2;
  645. }
  646. var s = el.style;
  647. if (s){
  648. if (s.fontWeight){ n.b = s.fontWeight === "bold" || s.fontWeight === "bolder" || parseInt(s.fontWeight, 10) >= 600; }
  649. if (s.fontStyle){ n.i = s.fontStyle === "italic" || s.fontStyle === "oblique"; }
  650. var td = s.textDecorationLine || s.textDecoration || "";
  651. if (td.indexOf("underline") >= 0) n.u = true;
  652. if (td.indexOf("line-through") >= 0) n.s = true;
  653. if (td === "none"){ n.u = false; n.s = false; }
  654. if (s.verticalAlign === "super"){ n.sup = true; n.sub = false; }
  655. if (s.verticalAlign === "sub"){ n.sub = true; n.sup = false; }
  656. if (s.fontSize){ var hp = sizeHalfPts(s.fontSize, n.size || base); if (hp) n.size = hp; }
  657. if (s.fontFamily){ var fam = firstFamily(s.fontFamily); if (fam) n.font = fam; }
  658. if (s.color){ var col = parseColor(s.color); if (col) n.color = col; }
  659. if (s.backgroundColor){
  660. var bg = parseColor(s.backgroundColor);
  661. n.bg = bg; // transparent clears it
  662. }
  663. }
  664. return n;
  665. }
  666. function collect(nodes, f, items){
  667. for (var i = 0; i < nodes.length; i++){
  668. var nd = nodes[i];
  669. if (nd.nodeType === 3){
  670. var t = nd.nodeValue.replace(/\u200B/g, "");
  671. if (t) items.push({ t:"text", text:t, f:f });
  672. continue;
  673. }
  674. if (nd.nodeType !== 1) continue;
  675. var tag = nd.tagName;
  676. if (tag === "BR"){ items.push({ t:"br" }); continue; }
  677. if (tag === "IMG"){ items.push({ t:"img", el:nd, f:f }); continue; }
  678. if (tag === "SCRIPT" || tag === "STYLE" || tag === "SELECT") continue;
  679. if (tag === "A" && nd.getAttribute("href")){
  680. var sub = [];
  681. collect(nd.childNodes, fmtOf(nd, f), sub);
  682. items.push({ t:"link", url:nd.getAttribute("href"), items:sub });
  683. continue;
  684. }
  685. if (BLOCK_TAGS.test(tag)){ // stray block inside inline flow
  686. collect(nd.childNodes, f, items);
  687. items.push({ t:"br" });
  688. continue;
  689. }
  690. collect(nd.childNodes, fmtOf(nd, f), items);
  691. }
  692. }
  693. function fmtCodes(f, extra){
  694. var c = "";
  695. if (f.b) c += "\\b";
  696. if (f.i) c += "\\i";
  697. if (f.u) c += "\\ul";
  698. if (f.s) c += "\\strike";
  699. if (f.sup) c += "\\super";
  700. if (f.sub) c += "\\sub";
  701. if (f.font) c += "\\f" + fontNo(f.font);
  702. if (f.size && f.size !== base) c += "\\fs" + f.size;
  703. if (f.color) c += "\\cf" + colorNo(f.color);
  704. if (f.bg) c += "\\highlight" + colorNo(f.bg);
  705. return c + (extra || "");
  706. }
  707. function itemsRTF(items){
  708. // drop the trailing placeholder <br> contenteditable leaves in a line
  709. while (items.length && items[items.length - 1].t === "br") items.pop();
  710. var r = "", i = 0;
  711. while (i < items.length){
  712. var it = items[i];
  713. if (it.t === "br"){ r += "\\line "; i++; continue; }
  714. if (it.t === "img"){ r += pictRTF(it.el); i++; continue; }
  715. if (it.t === "link"){
  716. r += "{\\field{\\*\\fldinst{HYPERLINK \"" + escText(it.url).replace(/"/g, "%22") + "\"}}{\\fldrslt{\\ul\\cf" +
  717. colorNo({ r:5, g:99, b:193 }) + " " + itemsRTF(it.items.slice()) + "}}}";
  718. i++; continue;
  719. }
  720. // merge consecutive text items with the same formatting
  721. var codes = fmtCodes(it.f), text = it.text;
  722. i++;
  723. while (i < items.length && items[i].t === "text" && fmtCodes(items[i].f) === codes){ text += items[i].text; i++; }
  724. r += codes ? "{" + codes + " " + escText(text) + "}" : escText(text);
  725. }
  726. return r;
  727. }
  728. function pictRTF(img){
  729. var src = img.getAttribute("src") || "";
  730. var m = src.match(/^data:image\/(png|jpe?g);base64,(.*)$/i);
  731. if (!m) return "";
  732. var bin;
  733. try { bin = atob(m[2]); } catch(e){ return ""; }
  734. var hex = [];
  735. for (var i = 0; i < bin.length; i++){
  736. hex.push(("0" + bin.charCodeAt(i).toString(16)).slice(-2));
  737. if (i % 64 === 63) hex.push("\r\n");
  738. }
  739. var natW = +img.getAttribute("data-natw") || +img.getAttribute("width") || 300;
  740. var natH = +img.getAttribute("data-nath") || +img.getAttribute("height") || 150;
  741. var dispW = +img.getAttribute("width") || parseFloat(img.style.width) || natW;
  742. var dispH = +img.getAttribute("height") || parseFloat(img.style.height) || Math.round(natH * dispW / natW);
  743. if (dispW > 624){ dispH = Math.round(dispH * 624 / dispW); dispW = 624; }
  744. var type = /png/i.test(m[1]) ? "\\pngblip" : "\\jpegblip";
  745. return "{\\pict" + type + "\\picw" + natW + "\\pich" + natH + "\\picwgoal" + Math.round(dispW * 15) +
  746. "\\pichgoal" + Math.round(dispH * 15) + "\r\n" + hex.join("") + "}";
  747. }
  748. // ── blocks ──
  749. function alignOf(el, inherited){
  750. var a = (el.style && el.style.textAlign) || el.getAttribute("align") || "";
  751. a = a.toLowerCase();
  752. if (a === "center") return "\\qc";
  753. if (a === "right" || a === "end") return "\\qr";
  754. if (a === "justify") return "\\qj";
  755. if (a === "left" || a === "start") return "";
  756. return inherited || "";
  757. }
  758. function indentOf(el){
  759. var ml = el.style ? el.style.marginLeft : "";
  760. var m = ml && ml.match(/^([\d.]+)(px|pt)?$/);
  761. return m ? Math.round(parseFloat(m[1]) * (m[2] === "pt" ? 20 : 15)) : 0;
  762. }
  763. function textIndentOf(el){
  764. var ti = el.style ? el.style.textIndent : "";
  765. var m = ti && ti.match(/^(-?[\d.]+)(px|pt)?$/);
  766. return m ? Math.round(parseFloat(m[1]) * (m[2] === "pt" ? 20 : 15)) : 0;
  767. }
  768. function para(nodes, ctx, opt){
  769. opt = opt || {};
  770. var f = { b:false, i:false, u:false, s:false, sup:false, sub:false };
  771. if (opt.font) f.font = opt.font;
  772. if (ctx.bold) f.b = true;
  773. if (opt.heading){ f.b = true; f.size = Math.round(base * HEAD_SCALE[opt.heading] / 2) * 2; }
  774. var items = [];
  775. collect(nodes, f, items);
  776. var head = "\\pard" + (ctx.intbl ? "\\intbl" : "") + (opt.align || "");
  777. var li = (ctx.indent || 0) + (opt.li || 0);
  778. if (li) head += "\\li" + li;
  779. if (opt.fi) head += "\\fi" + opt.fi;
  780. if (opt.heading) head += "\\outlinelevel" + (parseInt(opt.heading.charAt(1), 10) - 1) + "\\sb240";
  781. head += "\\sa" + (opt.sa === undefined ? 120 : opt.sa) + "\\plain\\f0\\fs" + base + " ";
  782. out.push(head + (opt.prefix || "") + itemsRTF(items) + (opt.end || "\\par") + "\r\n");
  783. }
  784. function hasContent(nodes){
  785. for (var i = 0; i < nodes.length; i++){
  786. var n = nodes[i];
  787. if (n.nodeType === 3 && n.nodeValue.replace(/[\u200B\s]/g, "") !== "") return true;
  788. if (n.nodeType === 1) return true;
  789. }
  790. return false;
  791. }
  792. function container(el, ctx){
  793. var buf = [];
  794. function flushInline(){
  795. if (buf.length && hasContent(buf)) para(buf, ctx, { align:ctx.align });
  796. buf = [];
  797. }
  798. for (var i = 0; i < el.childNodes.length; i++){
  799. var c = el.childNodes[i];
  800. if (c.nodeType === 1 && BLOCK_TAGS.test(c.tagName)){ flushInline(); block(c, ctx); }
  801. else buf.push(c);
  802. }
  803. flushInline();
  804. }
  805. function hasBlockChild(el){
  806. for (var i = 0; i < el.children.length; i++) if (BLOCK_TAGS.test(el.children[i].tagName)) return true;
  807. return false;
  808. }
  809. function block(el, ctx){
  810. var tag = el.tagName, align = alignOf(el, ctx.align);
  811. if (/^H[1-6]$/.test(tag)){ para(el.childNodes, ctx, { align:align, heading:tag }); return; }
  812. if (tag === "HR"){ out.push("\\pard" + (ctx.intbl ? "\\intbl" : "") + "\\brdrb\\brdrs\\brdrw10\\brsp20\\sa120\\plain\\f0\\fs" + base + " \\par\r\n"); return; }
  813. if (tag === "PRE"){
  814. var lines = (el.textContent || "").replace(/\u200B/g, "").replace(/\n$/, "").split("\n");
  815. lines.forEach(function(ln, k){
  816. var tn = document.createTextNode(ln);
  817. para([tn], ctx, { font:MONO, sa:(k === lines.length - 1 ? 120 : 0) });
  818. });
  819. return;
  820. }
  821. if (tag === "UL" || tag === "OL"){ list(el, ctx); return; }
  822. if (tag === "BLOCKQUOTE"){
  823. container(el, { indent:(ctx.indent || 0) + 720, align:align, intbl:ctx.intbl, level:ctx.level });
  824. return;
  825. }
  826. if (tag === "TABLE"){ table(el, ctx); return; }
  827. if (tag === "P" || tag === "DT" || tag === "DD" || !hasBlockChild(el)){
  828. para(el.childNodes, ctx, { align:align, li:indentOf(el), fi:textIndentOf(el) });
  829. return;
  830. }
  831. container(el, { indent:(ctx.indent || 0) + indentOf(el), align:align, intbl:ctx.intbl, level:ctx.level });
  832. }
  833. function list(el, ctx){
  834. var ordered = el.tagName === "OL";
  835. var level = (ctx.level === undefined ? -1 : ctx.level) + 1;
  836. var num = parseInt(el.getAttribute("start"), 10) || 1;
  837. var li = 720 * (level + 1);
  838. for (var i = 0; i < el.children.length; i++){
  839. var item = el.children[i];
  840. if (item.tagName !== "LI"){ block(item, ctx); continue; }
  841. var marker = ordered ? (num++) + "." : "\\u8226?";
  842. var pn = ordered
  843. ? "{\\*\\pn\\pnlvlbody\\pndec\\pnstart1\\pnindent360{\\pntxta.}}"
  844. : "{\\*\\pn\\pnlvlblt\\pnf" + fontNo("Symbol") + "\\pnindent360{\\pntxtb\\'B7}}";
  845. var prefix = "{\\listtext\\pard\\plain\\f0\\fs" + base + " " + marker + "\\tab}" + pn;
  846. var sub = { indent:ctx.indent, align:ctx.align, intbl:ctx.intbl, level:level };
  847. // the item's own text runs up to its first block child
  848. var inline = [], rest = [];
  849. for (var k = 0; k < item.childNodes.length; k++){
  850. var c = item.childNodes[k];
  851. if (rest.length || (c.nodeType === 1 && BLOCK_TAGS.test(c.tagName))) rest.push(c);
  852. else inline.push(c);
  853. }
  854. if (!hasContent(inline) && rest.length && /^(P|DIV)$/.test(rest[0].tagName)){
  855. inline = Array.prototype.slice.call(rest.shift().childNodes);
  856. }
  857. para(inline, ctx, { align:alignOf(item, ctx.align), li:li, fi:-360, sa:40, prefix:prefix });
  858. rest.forEach(function(c){
  859. if (c.tagName === "UL" || c.tagName === "OL") list(c, sub);
  860. else block(c, { indent:(ctx.indent || 0) + li, align:ctx.align, intbl:ctx.intbl, level:level });
  861. });
  862. }
  863. }
  864. function table(el, ctx){
  865. var rows = el.querySelectorAll("tr");
  866. for (var r = 0; r < rows.length; r++){
  867. var cells = [];
  868. for (var c = 0; c < rows[r].children.length; c++){
  869. if (/^T[DH]$/.test(rows[r].children[c].tagName)) cells.push(rows[r].children[c]);
  870. }
  871. if (!cells.length) continue;
  872. var w = Math.floor(9000 / cells.length), def = "\\trowd\\trgaph108\\trleft0";
  873. for (var k = 0; k < cells.length; k++){
  874. def += "\\clbrdrt\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10\\clbrdrb\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10\\cellx" + (w * (k + 1));
  875. }
  876. out.push(def + "\r\n");
  877. for (var k2 = 0; k2 < cells.length; k2++){
  878. var cell = cells[k2], isHead = cell.tagName === "TH";
  879. var start = out.length;
  880. var cctx = { intbl:true, align:alignOf(cell, ""), bold:isHead };
  881. if (hasBlockChild(cell)) container(cell, cctx);
  882. else para(cell.childNodes, cctx, { align:cctx.align, sa:0 });
  883. if (out.length === start) para([], cctx, { sa:0 });
  884. // the cell's last paragraph ends with \cell instead of \par
  885. out[out.length - 1] = out[out.length - 1].replace(/\\par\r\n$/, "\\cell\r\n");
  886. }
  887. out.push(def + "\\row\r\n");
  888. }
  889. }
  890. container(root, { indent:0, align:"" });
  891. var ft = "{\\fonttbl";
  892. fonts.forEach(function(name, i){
  893. var fam = name === MONO ? "\\fmodern" : name === "Symbol" ? "\\ftech" : "\\fnil";
  894. var cs = name === "Symbol" ? 2 : (/[^\x00-\x7f]/.test(name) ? 1 : 0);
  895. ft += "{\\f" + i + fam + "\\fcharset" + cs + " " + escText(name) + ";}";
  896. });
  897. ft += "}";
  898. var ct = "{\\colortbl ;";
  899. colors.forEach(function(c){ ct += "\\red" + c.r + "\\green" + c.g + "\\blue" + c.b + ";"; });
  900. ct += "}";
  901. return "{\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat" + ft + "\r\n" + ct + "\r\n" +
  902. "{\\*\\generator ArozOS Text;}\\viewkind4\\uc1\r\n" + out.join("") + "}\r\n";
  903. }
  904. // ════════════════════════════════════════════════════════════════════════
  905. // Image preparation (async)
  906. // ════════════════════════════════════════════════════════════════════════
  907. function blobToDataURL(blob){
  908. return new Promise(function(res, rej){
  909. var fr = new FileReader();
  910. fr.onload = function(){ res(fr.result); };
  911. fr.onerror = rej;
  912. fr.readAsDataURL(blob);
  913. });
  914. }
  915. function loadImage(src){
  916. return new Promise(function(res, rej){
  917. var im = new Image();
  918. im.onload = function(){ res(im); };
  919. im.onerror = function(){ rej(new Error("image load failed")); };
  920. im.src = src;
  921. });
  922. }
  923. function inlineOne(img){
  924. var src = img.getAttribute("src") || "";
  925. var p = /^data:image\/(png|jpe?g);/i.test(src)
  926. ? Promise.resolve(src)
  927. : fetch(src, { credentials:"same-origin" }).then(function(r){
  928. if (!r.ok) throw new Error("HTTP " + r.status);
  929. return r.blob();
  930. }).then(function(b){
  931. if (/^image\/(png|jpe?g)$/i.test(b.type)) return blobToDataURL(b);
  932. // other formats (gif/webp/svg/bmp) are re-encoded as PNG
  933. var url = URL.createObjectURL(b);
  934. return loadImage(url).then(function(im){
  935. URL.revokeObjectURL(url);
  936. var cv = document.createElement("canvas");
  937. cv.width = im.naturalWidth || 300; cv.height = im.naturalHeight || 150;
  938. cv.getContext("2d").drawImage(im, 0, 0);
  939. return cv.toDataURL("image/png");
  940. });
  941. }).then(function(d){
  942. // sniff the real format: a mislabelled blob still needs png/jpeg bytes
  943. return d.replace(/^data:[^;,]*;/, function(){
  944. var head = atob(d.split(",")[1].substr(0, 8));
  945. return head.charCodeAt(0) === 0xFF ? "data:image/jpeg;" : "data:image/png;";
  946. });
  947. });
  948. return p.then(function(d){
  949. img.setAttribute("src", d);
  950. return loadImage(d).then(function(im){
  951. img.setAttribute("data-natw", im.naturalWidth);
  952. img.setAttribute("data-nath", im.naturalHeight);
  953. });
  954. }).catch(function(){
  955. img.parentNode && img.parentNode.removeChild(img); // unreachable image: leave it out
  956. });
  957. }
  958. function inlineImages(root){
  959. var imgs = Array.prototype.slice.call(root.querySelectorAll("img"));
  960. return Promise.all(imgs.map(inlineOne)).then(function(){ return root; });
  961. }
  962. window.TextRTF = { toHTML:toHTML, fromHTML:fromHTML, inlineImages:inlineImages, parse:parse };
  963. })();