docs_layout.js 71 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662
  1. /*
  2. ArozOS Office - Docs layout engine
  3. ==================================
  4. Everything about how a document is laid out that CSS cannot decide on
  5. its own, so that the page the editor shows is the page a word processor
  6. would print - and, because the PDF exporter draws this very DOM, the
  7. page the PDF gets.
  8. Four passes over a rendered subtree (the editor, a header/footer copy,
  9. a footnote area):
  10. lineHeights Word and Google Docs size a line from the font's own
  11. ascent and descent, rounded to whole pixels one side at
  12. a time: ceil(ascent x size x spacing) + ceil(descent x
  13. size x spacing). CSS line-height is size x number and
  14. drifts by a pixel or two a line, which over a page is a
  15. different page break. Every block that holds text gets
  16. that exact line height in px, and inline elements get
  17. line-height 0 (docs.css) so a run in another font cannot
  18. make its line taller than the rule says; a run in a
  19. bigger size gets its own px height and grows its line.
  20. numbering list markers ("1.", "b)", bullets) are computed here into
  21. li[data-marker] and drawn by li::before - a real value
  22. the PDF exporter can read, formats CSS counters do not
  23. have ("%1.%2."), and numbering that carries on across a
  24. list interrupted by a paragraph (same data-num).
  25. tabs span.doc-tab is sized to reach the next tab stop: the
  26. paragraph's own stops (data-tabs, right/center/left, with
  27. leaders) or the default 36pt grid.
  28. footnotes references are numbered in document order.
  29. And pagination (paginate): the document is one contenteditable flow,
  30. and page boundaries are made real in it. What crosses a boundary is
  31. split into two elements - a paragraph at a line, the list or quote
  32. around it, a table row cell by cell - and a spacer element between the
  33. halves pushes the second one to the top of the next sheet, so long
  34. paragraphs, lists and tables break where a word processor breaks them
  35. and every half is a box of its own (borders and shading end at the
  36. page). Spacers carry .doc-autobreak; splits are undone, and spacers
  37. removed, on everything that is saved (docs.js cleanedHtml). Headings keep with the paragraph after
  38. them, data-keep-lines paragraphs do not split, data-widow paragraphs
  39. keep two lines on each side, footnotes take their space at the bottom of
  40. the page that references them.
  41. Coordinates are layout px relative to #page's padding box (offsetTop
  42. space, unaffected by the framework's CSS zoom).
  43. */
  44. var DocsLayout = (function () {
  45. "use strict";
  46. var PT = 96 / 72; // css px per point
  47. var MM = 96 / 25.4; // css px per millimetre
  48. var DEFAULT_TAB_PT = 36;
  49. /* ---------------- font metrics ---------------- */
  50. /* ascent/descent per em of the font a family list actually resolves to.
  51. A canvas at 2048px reports the font's own units exactly (ArialMT:
  52. 1854/434), and it resolves a font stack the same way the text did. */
  53. var ratioCache = {};
  54. var ratioCtx = null;
  55. /* Fonts a browser reports taller metrics for than the word processor
  56. lays their lines out with. Consolas is the one that matters: in
  57. Google Docs a Consolas code line is exactly as tall as an Arial line,
  58. while its OS/2 win metrics would overshoot that by a pixel a line. */
  59. var LINE_METRICS = {
  60. "consolas": { asc: 1854 / 2048, desc: 434 / 2048 }
  61. };
  62. function fontRatios(family) {
  63. var key = family || "";
  64. if (ratioCache[key]) return ratioCache[key];
  65. var out = { asc: 0.905, desc: 0.212 };
  66. try {
  67. if (!ratioCtx) ratioCtx = document.createElement("canvas").getContext("2d");
  68. ratioCtx.font = "2048px " + (family || "sans-serif");
  69. var m = ratioCtx.measureText("Hxg");
  70. if (m && m.fontBoundingBoxAscent > 0) {
  71. out = { asc: m.fontBoundingBoxAscent / 2048, desc: m.fontBoundingBoxDescent / 2048 };
  72. }
  73. var first = String(family || "").split(",")[0].trim().replace(/^["']|["']$/g, "").toLowerCase();
  74. if (LINE_METRICS[first] && Math.abs(m.fontBoundingBoxAscent - 1884) < 2) out = LINE_METRICS[first];
  75. } catch (e) { /* estimate */ }
  76. ratioCache[key] = out;
  77. return out;
  78. }
  79. // the word-processor line height rule, in px
  80. function lineHeightPx(sizePx, family, mult) {
  81. var r = fontRatios(family);
  82. var s = sizePx * (mult > 0 ? mult : 1);
  83. return Math.ceil(r.asc * s - 1e-6) + Math.ceil(r.desc * s - 1e-6);
  84. }
  85. /* ---------------- helpers ---------------- */
  86. var INLINE_TAGS = {
  87. A: 1, ABBR: 1, B: 1, BDI: 1, BDO: 1, BR: 1, CITE: 1, CODE: 1, DATA: 1, DEL: 1,
  88. DFN: 1, EM: 1, FONT: 1, I: 1, IMG: 1, INS: 1, KBD: 1, MARK: 1, Q: 1, S: 1,
  89. SAMP: 1, SMALL: 1, SPAN: 1, STRIKE: 1, STRONG: 1, SUB: 1, SUP: 1, TIME: 1,
  90. TT: 1, U: 1, VAR: 1, WBR: 1
  91. };
  92. function isInlineEl(el) {
  93. if (!INLINE_TAGS[el.tagName]) return false;
  94. // an anchored picture or a line spacer is display:block
  95. if (el.classList.contains("doc-anchor") || el.classList.contains("doc-autobreak")) return false;
  96. return true;
  97. }
  98. function isSpacer(el) {
  99. return el.nodeType === 1 && el.classList.contains("doc-autobreak");
  100. }
  101. // does this element hold inline content of its own (a "line block")?
  102. function holdsInline(el) {
  103. for (var c = el.firstChild; c; c = c.nextSibling) {
  104. if (c.nodeType === 3) {
  105. if (c.nodeValue && c.nodeValue.length) return true;
  106. } else if (c.nodeType === 1 && isInlineEl(c)) {
  107. return true;
  108. }
  109. }
  110. return false;
  111. }
  112. function hasBlockChild(el) {
  113. for (var c = el.firstElementChild; c; c = c.nextElementSibling) {
  114. if (!isInlineEl(c) && !isSpacer(c)) return true;
  115. }
  116. return false;
  117. }
  118. // multiple of single spacing stated on a block, or the legacy
  119. // unitless line-height an older document carries
  120. function blockSpacing(el, defLS) {
  121. var ls = parseFloat(el.getAttribute("data-ls"));
  122. if (ls > 0) return ls;
  123. // documents from before data-ls said it as a unitless line-height
  124. var inline = el.style.lineHeight;
  125. if (inline && /^[\d.]+$/.test(inline) && parseFloat(inline) > 0) {
  126. el.setAttribute("data-ls", inline);
  127. return parseFloat(inline);
  128. }
  129. return defLS;
  130. }
  131. function cssLenPx(v) {
  132. v = String(v || "").trim();
  133. var n = parseFloat(v);
  134. if (isNaN(n)) return 0;
  135. if (/pt$/.test(v)) return n * PT;
  136. if (/mm$/.test(v)) return n * MM;
  137. if (/in$/.test(v)) return n * 96;
  138. return n;
  139. }
  140. /* ---------------- line heights ---------------- */
  141. /* lineHeights sets the px line height of every block in root that
  142. holds text, and the own height of any run that is bigger than its
  143. block's smallest run. */
  144. function applyLineHeights(root, defLS) {
  145. if (!root) return;
  146. defLS = defLS > 0 ? defLS : 1.15;
  147. var blocks = [];
  148. if (holdsInline(root) || !root.firstElementChild) blocks.push(root);
  149. var all = root.getElementsByTagName("*");
  150. for (var i = 0; i < all.length; i++) {
  151. var el = all[i];
  152. if (isInlineEl(el) || isSpacer(el)) continue;
  153. if (el.tagName === "TABLE" || el.tagName === "TBODY" || el.tagName === "TR" ||
  154. el.tagName === "COLGROUP" || el.tagName === "COL") continue;
  155. if (holdsInline(el) || !el.firstElementChild) blocks.push(el);
  156. }
  157. blocks.forEach(function (b) { lineHeightFor(b, defLS); });
  158. }
  159. function lineHeightFor(block, defLS) {
  160. var bcs = window.getComputedStyle(block);
  161. if (bcs.display === "none") return;
  162. var exact = block.getAttribute("data-lsexact");
  163. if (exact) {
  164. block.style.lineHeight = cssLenPx(exact) + "px";
  165. return;
  166. }
  167. var mult = blockSpacing(block, defLS);
  168. var minPx = cssLenPx(block.getAttribute("data-lsmin"));
  169. // the runs directly in this block: their sizes decide the lines
  170. var runs = [];
  171. var walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT, {
  172. acceptNode: function (n) {
  173. // text of a nested block belongs to that block
  174. for (var p = n.parentNode; p && p !== block; p = p.parentNode) {
  175. if (p.nodeType === 1 && !isInlineEl(p)) return NodeFilter.FILTER_REJECT;
  176. }
  177. return NodeFilter.FILTER_ACCEPT;
  178. }
  179. });
  180. var preWs = bcs.whiteSpace.indexOf("pre") === 0 || bcs.whiteSpace === "break-spaces";
  181. var node;
  182. while ((node = walker.nextNode())) {
  183. var txt = node.nodeValue;
  184. if (!txt || (!preWs && !/\S/.test(txt))) continue;
  185. var host = node.parentNode;
  186. // superscript / subscript never make a line taller
  187. var inScript = false;
  188. for (var q = host; q && q !== block; q = q.parentNode) {
  189. if (q.tagName === "SUP" || q.tagName === "SUB") { inScript = true; break; }
  190. }
  191. if (inScript) continue;
  192. var cs = window.getComputedStyle(host);
  193. runs.push({ el: host, size: parseFloat(cs.fontSize) || 14.67, family: cs.fontFamily });
  194. }
  195. var baseSize = parseFloat(bcs.fontSize) || 14.67;
  196. var baseFamily = bcs.fontFamily;
  197. var strut;
  198. if (!runs.length) {
  199. strut = lineHeightPx(baseSize, baseFamily, mult);
  200. } else {
  201. var min = runs[0];
  202. runs.forEach(function (r) { if (r.size < min.size) min = r; });
  203. // the paragraph's own font (its mark) takes part in every line
  204. strut = Math.max(lineHeightPx(min.size, min.family, mult),
  205. baseSize <= min.size + 0.01 ? lineHeightPx(baseSize, baseFamily, mult) : 0);
  206. // bigger runs carry their own height so only their lines grow
  207. runs.forEach(function (r) {
  208. if (r.el === block) return;
  209. if (r.size > min.size + 0.01) {
  210. var own = Math.max(lineHeightPx(r.size, r.family, mult), minPx);
  211. if (r.el.style.lineHeight !== own + "px") r.el.style.lineHeight = own + "px";
  212. } else if (r.el.style.lineHeight) {
  213. r.el.style.lineHeight = "";
  214. }
  215. });
  216. }
  217. if (minPx > strut) strut = minPx;
  218. var v = strut + "px";
  219. if (block.style.lineHeight !== v) block.style.lineHeight = v;
  220. if (!runs.length) pictureLines(block, strut, baseSize, baseFamily, mult);
  221. }
  222. /* A line holding pictures is as tall as the tallest picture, plus 1.75pt
  223. above it and the part of a text line that hangs below the baseline
  224. under it - the pictures sit on the baseline. In a paragraph that holds
  225. only pictures that is set exactly: each picture is aligned to the
  226. bottom of its line and carries those two amounts as margins, so the
  227. line comes out the word processor's height to the fraction of a pixel
  228. (baseline alignment would round the picture's top to a whole pixel,
  229. which over a page of screenshots is a paragraph's worth of drift).
  230. Where the text baseline sits is the one thing CSS and a word
  231. processor disagree on: CSS splits the leading evenly around the
  232. glyphs, Word and Google Docs put nearly all the leading that 1.15
  233. spacing adds below them. The marks are undone before saving
  234. (data-picline). */
  235. var PICTURE_TOP_PX = 1.75 * PT;
  236. function pictureLines(block, strut, sizePx, family, mult) {
  237. var imgs = block.getElementsByTagName("img");
  238. if (!imgs.length) return;
  239. var r = fontRatios(family);
  240. var single = lineHeightPx(sizePx, family, 1);
  241. var baseline = Math.ceil(r.asc * sizePx - 1e-6) + 0.2 * (strut - single);
  242. var below = Math.max(0, strut - baseline);
  243. for (var i = 0; i < imgs.length; i++) {
  244. var im = imgs[i];
  245. if (im.classList.contains("doc-anchor")) continue;
  246. if (im.parentNode !== block && !isInlineEl(im.parentNode)) continue;
  247. im.setAttribute("data-picline", "1");
  248. im.style.verticalAlign = "bottom";
  249. im.style.marginTop = PICTURE_TOP_PX + "px";
  250. im.style.marginBottom = below + "px";
  251. }
  252. }
  253. function restorePictureLines(root) {
  254. var imgs = root.querySelectorAll("img[data-picline]");
  255. for (var i = 0; i < imgs.length; i++) {
  256. imgs[i].style.verticalAlign = "";
  257. imgs[i].style.marginTop = "";
  258. imgs[i].style.marginBottom = "";
  259. imgs[i].removeAttribute("data-picline");
  260. if (!imgs[i].getAttribute("style")) imgs[i].removeAttribute("style");
  261. }
  262. }
  263. /* ---------------- table borders ----------------
  264. A browser draws a border in whole device pixels, so a 1pt (1.33px)
  265. table rule takes 1px of layout - and a table of 40 rows comes out 13px
  266. shorter than on paper. The difference goes back in as cell padding,
  267. which is invisible and keeps the rows exactly as tall as the document
  268. says. The padding the document states is kept in data-pad0 and put
  269. back before anything is saved. */
  270. function applyTableBorders(root) {
  271. if (!root) return;
  272. var tables = root.getElementsByTagName("table");
  273. for (var t = 0; t < tables.length; t++) {
  274. var tbl = tables[t];
  275. // a table whose size has not moved since it was last fixed up
  276. // needs nothing (the signature is a property, never saved)
  277. if (tbl.__docsSig && tbl.__docsSig === tableSignature(tbl)) continue;
  278. fixTable(tbl);
  279. if (tbl.hasAttribute("data-docx")) roundRows(tbl);
  280. tbl.__docsSig = tableSignature(tbl);
  281. }
  282. }
  283. function tableSignature(tbl) {
  284. return tbl.rows.length + ":" + tbl.offsetHeight + ":" + tbl.offsetWidth + ":" + (scaleOf(tbl) || 1).toFixed(3);
  285. }
  286. function fixTable(tbl) {
  287. var rows = tbl.rows;
  288. var cells = [];
  289. // put back the stated padding first (writes only) ...
  290. for (var r = 0; r < rows.length; r++) {
  291. if (rows[r].classList.contains("doc-autobreak") || rows[r].closest("table") !== tbl) continue;
  292. for (var c = 0; c < rows[r].cells.length; c++) {
  293. var td = rows[r].cells[c];
  294. var orig = td.getAttribute("data-pad0");
  295. if (orig === null) {
  296. orig = td.style.padding || "";
  297. td.setAttribute("data-pad0", orig);
  298. } else if (td.style.padding !== orig) {
  299. td.style.padding = orig;
  300. }
  301. cells.push({ td: td, last: r + td.rowSpan >= rows.length });
  302. }
  303. }
  304. // ... then measure everything once ...
  305. cells.forEach(function (it) {
  306. var wantTop = declaredBorder(it.td, "Top");
  307. var wantBottom = declaredBorder(it.td, "Bottom");
  308. if (!(wantTop > 0) && !(wantBottom > 0)) return;
  309. var cs = window.getComputedStyle(it.td);
  310. it.padTop = parseFloat(cs.paddingTop) || 0;
  311. it.padBottom = parseFloat(cs.paddingBottom) || 0;
  312. it.dTop = wantTop > 0 ? wantTop - (parseFloat(cs.borderTopWidth) || 0) : 0;
  313. it.dBottom = it.last && wantBottom > 0 ? wantBottom - (parseFloat(cs.borderBottomWidth) || 0) : 0;
  314. });
  315. // ... and write the differences
  316. cells.forEach(function (it) {
  317. if (it.dTop > 0.01) it.td.style.paddingTop = (it.padTop + it.dTop) + "px";
  318. if (it.dBottom > 0.01) it.td.style.paddingBottom = (it.padBottom + it.dBottom) + "px";
  319. });
  320. }
  321. /* Google Docs makes every table row a whole number of pixels tall
  322. (rounding up), so a row of one 10pt line is 31px, not 30.67. Read all
  323. the rows first and write after, so this costs one layout, not one
  324. per row. */
  325. function roundRows(tbl) {
  326. var scale = scaleOf(tbl) || 1;
  327. var rows = [];
  328. for (var r = 0; r < tbl.rows.length; r++) {
  329. var row = tbl.rows[r];
  330. if (row.classList.contains("doc-autobreak") || row.closest("table") !== tbl) continue;
  331. rows.push({ row: row, h: row.getBoundingClientRect().height / scale });
  332. }
  333. var writes = [];
  334. rows.forEach(function (it) {
  335. var extra = Math.ceil(it.h - 0.05) - it.h;
  336. if (extra <= 0.01) return;
  337. for (var c = 0; c < it.row.cells.length; c++) {
  338. var td = it.row.cells[c];
  339. if (td.rowSpan > 1) continue;
  340. writes.push({ td: td, pb: (parseFloat(window.getComputedStyle(td).paddingBottom) || 0) + extra });
  341. }
  342. });
  343. writes.forEach(function (w) { w.td.style.paddingBottom = w.pb + "px"; });
  344. }
  345. // the border width the document states (the specified value, before the
  346. // browser rounds it to device pixels)
  347. function declaredBorder(td, side) {
  348. var st = td.style["border" + side + "Style"];
  349. if (!st || st === "none" || st === "hidden") return 0;
  350. return cssLenPx(td.style["border" + side + "Width"]);
  351. }
  352. function restoreTablePadding(root) {
  353. var cells = root.querySelectorAll("[data-pad0]");
  354. for (var i = 0; i < cells.length; i++) {
  355. cells[i].style.padding = cells[i].getAttribute("data-pad0");
  356. cells[i].removeAttribute("data-pad0");
  357. }
  358. }
  359. /* ---------------- list numbering ---------------- */
  360. var BULLETS = [String.fromCharCode(0x25CF), String.fromCharCode(0x25CB), String.fromCharCode(0x25A0)];
  361. var ORDERED = ["decimal", "lowerLetter", "lowerRoman"];
  362. function roman(n) {
  363. if (n < 1 || n > 3999) return String(n);
  364. var v = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
  365. var s = ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"];
  366. var out = "";
  367. for (var i = 0; i < v.length; i++) while (n >= v[i]) { out += s[i]; n -= v[i]; }
  368. return out;
  369. }
  370. function formatNumber(n, fmt) {
  371. switch (fmt) {
  372. case "lowerLetter":
  373. case "upperLetter":
  374. if (n < 1) return String(n);
  375. var ch = String.fromCharCode(97 + (n - 1) % 26);
  376. var s = new Array(Math.floor((n - 1) / 26) + 2).join(ch);
  377. return fmt === "upperLetter" ? s.toUpperCase() : s;
  378. case "lowerRoman": return roman(n);
  379. case "upperRoman": return roman(n).toUpperCase();
  380. case "decimalZero": return n < 10 ? "0" + n : String(n);
  381. case "none":
  382. case "bullet": return "";
  383. }
  384. return String(n);
  385. }
  386. function isList(el) { return el && (el.tagName === "OL" || el.tagName === "UL"); }
  387. /* depth of a list among its list ancestors (0 = outermost) */
  388. function listDepth(list, root) {
  389. var d = 0;
  390. for (var p = list.parentNode; p && p !== root; p = p.parentNode) {
  391. if (isList(p)) d++;
  392. }
  393. return d;
  394. }
  395. function applyNumbering(root) {
  396. if (!root) return;
  397. var lists = root.querySelectorAll("ol, ul");
  398. // continuation state per list instance and level
  399. var carried = {};
  400. // a list split across a page carries on in its tail, and the tail
  401. // of a split item is the same item (no marker, no count)
  402. var splitEnd = {}, splitItem = {};
  403. for (var i = 0; i < lists.length; i++) {
  404. var list = lists[i];
  405. if (list.classList.contains("of-checklist")) continue;
  406. var depth = listDepth(list, root);
  407. var ordered = list.tagName === "OL";
  408. var fmt = list.getAttribute("data-fmt") ||
  409. (ordered ? ORDERED[depth % ORDERED.length] : "bullet");
  410. var text = list.getAttribute("data-lvltext");
  411. if (text === null) {
  412. text = fmt === "bullet" ? BULLETS[depth % BULLETS.length] : "%" + (depth + 1) + ".";
  413. }
  414. var num = list.getAttribute("data-num");
  415. var key = num ? num + ":" + depth : null;
  416. var start = parseInt(list.getAttribute("start"), 10);
  417. var counter;
  418. var listOf = list.getAttribute("data-split-of");
  419. if (key && carried[key] !== undefined) counter = carried[key];
  420. else if (listOf && splitEnd[listOf] !== undefined) counter = splitEnd[listOf];
  421. else counter = (isNaN(start) ? 1 : start) - 1;
  422. for (var c = list.firstElementChild; c; c = c.nextElementSibling) {
  423. if (c.tagName !== "LI") continue;
  424. var itemOf = c.getAttribute("data-split-of");
  425. if (itemOf) {
  426. var headItem = splitItem[itemOf];
  427. c.setAttribute("data-n", headItem ? headItem.getAttribute("data-n") : counter);
  428. if (c.getAttribute("data-marker") !== "") c.setAttribute("data-marker", "");
  429. if (c.hasAttribute("data-split")) splitItem[c.getAttribute("data-split")] = headItem || c;
  430. continue;
  431. }
  432. if (c.hasAttribute("data-split")) splitItem[c.getAttribute("data-split")] = c;
  433. var v = parseInt(c.getAttribute("value"), 10);
  434. counter = isNaN(v) ? counter + 1 : v;
  435. c.setAttribute("data-n", counter);
  436. var marker = text.replace(/%(\d)/g, function (m, lvl) {
  437. var want = parseInt(lvl, 10) - 1;
  438. if (want === depth) return formatNumber(counter, fmt);
  439. var anc = ancestorItem(c, want, root);
  440. if (!anc) return "";
  441. var ancList = anc.parentNode;
  442. var ancFmt = ancList.getAttribute("data-fmt") ||
  443. (ancList.tagName === "OL" ? ORDERED[want % ORDERED.length] : "bullet");
  444. return formatNumber(parseInt(anc.getAttribute("data-n"), 10) || 1, ancFmt);
  445. });
  446. if (fmt === "none") marker = text.replace(/%\d/g, "");
  447. if (c.getAttribute("data-marker") !== marker) c.setAttribute("data-marker", marker);
  448. }
  449. if (key) carried[key] = counter;
  450. if (list.hasAttribute("data-split")) splitEnd[list.getAttribute("data-split")] = counter;
  451. // a deeper level restarts once a shallower item of the same list
  452. // comes along
  453. if (key) {
  454. for (var k in carried) {
  455. var parts = k.split(":");
  456. if (parts[0] === num && parseInt(parts[1], 10) > depth) delete carried[k];
  457. }
  458. }
  459. }
  460. }
  461. // the list item at a given depth that a nested list item hangs off
  462. function ancestorItem(li, depth, root) {
  463. var list = li.parentNode;
  464. while (list && list !== root) {
  465. var parent = list.parentNode;
  466. var d = listDepth(list, root);
  467. if (d === depth + 1 || (d > depth && isList(parent) && listDepth(parent, root) === depth)) {
  468. // nested directly in a list: the item before it
  469. if (isList(parent)) {
  470. for (var p = list.previousElementSibling; p; p = p.previousElementSibling) {
  471. if (p.tagName === "LI") return p;
  472. }
  473. return null;
  474. }
  475. if (parent && parent.tagName === "LI") return parent;
  476. }
  477. list = parent;
  478. }
  479. return null;
  480. }
  481. /* ---------------- tabs ---------------- */
  482. // the edge tab stops are measured from: a table cell's content box, or
  483. // the text column
  484. function tabOrigin(span, rootEl) {
  485. for (var p = span.parentNode; p && p !== rootEl; p = p.parentNode) {
  486. if (p.tagName === "TD" || p.tagName === "TH") {
  487. var r = p.getBoundingClientRect();
  488. var cs = window.getComputedStyle(p);
  489. return r.left + (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.paddingLeft) || 0) * scaleOf(p);
  490. }
  491. }
  492. var rr = rootEl.getBoundingClientRect();
  493. var rcs = window.getComputedStyle(rootEl);
  494. return rr.left + (parseFloat(rcs.paddingLeft) || 0) * scaleOf(rootEl);
  495. }
  496. function scaleOf(el) {
  497. var h = el.offsetWidth;
  498. var r = el.getBoundingClientRect().width;
  499. return h > 0 && r > 0 ? r / h : 1;
  500. }
  501. function parseTabs(block) {
  502. var out = [];
  503. for (var b = block; b; b = b.parentElement) {
  504. var raw = b.getAttribute && b.getAttribute("data-tabs");
  505. if (raw) {
  506. raw.split(";").forEach(function (t) {
  507. var p = t.split(":");
  508. var pos = parseFloat(p[1]);
  509. if (!isNaN(pos)) out.push({ align: p[0] || "left", pos: pos * PT, leader: p[2] || "none" });
  510. });
  511. break;
  512. }
  513. if (!isInlineEl(b) && !holdsInline(b)) break;
  514. }
  515. out.sort(function (a, b) { return a.pos - b.pos; });
  516. return out;
  517. }
  518. function lineBlockOf(el, rootEl) {
  519. for (var p = el.parentNode; p && p !== rootEl; p = p.parentNode) {
  520. if (p.nodeType === 1 && !isInlineEl(p)) return p;
  521. }
  522. return rootEl;
  523. }
  524. function applyTabs(root) {
  525. if (!root) return;
  526. // a tab's position only depends on what comes before it, and the
  527. // ones before it are sized first - so each tab can be measured as
  528. // it stands and written only when its width really changes, which
  529. // on an unchanged document means no layout work at all
  530. var spans = root.querySelectorAll("span.doc-tab");
  531. for (var i = 0; i < spans.length; i++) sizeTab(spans[i], root);
  532. }
  533. function sizeTab(span, root) {
  534. var block = lineBlockOf(span, root);
  535. var scale = scaleOf(root) || 1;
  536. if (span.style.display !== "inline-block") span.style.display = "inline-block";
  537. var origin = tabOrigin(span, root);
  538. var x = (span.getBoundingClientRect().left - origin) / scale;
  539. var stops = parseTabs(block);
  540. var stop = null;
  541. for (var i = 0; i < stops.length; i++) {
  542. if (stops[i].pos > x + 0.5) { stop = stops[i]; break; }
  543. }
  544. var w;
  545. if (!stop) {
  546. var grid = DEFAULT_TAB_PT * PT;
  547. w = (Math.floor(x / grid + 1e-6) + 1) * grid - x;
  548. if (w < 1) w += grid;
  549. } else if (stop.align === "right" || stop.align === "center" || stop.align === "decimal") {
  550. // the text after the tab, up to the next tab or the end of the line
  551. var follow = followingWidth(span, block) / scale;
  552. // a right stop sits on the margin more often than not; a hair of
  553. // slack keeps the number from wrapping to a line of its own
  554. w = stop.pos - x - (stop.align === "center" ? follow / 2 : follow) - 1;
  555. if (w < 0) w = 0;
  556. } else {
  557. w = stop.pos - x;
  558. }
  559. w = Math.max(0, w);
  560. if (Math.abs((parseFloat(span.style.width) || -1) - w) > 0.25) span.style.width = w + "px";
  561. var leader = stop && stop.leader && stop.leader !== "none" ? stop.leader : null;
  562. if (span.getAttribute("data-leader") !== leader) {
  563. if (leader) span.setAttribute("data-leader", leader);
  564. else span.removeAttribute("data-leader");
  565. }
  566. }
  567. function followingWidth(span, block) {
  568. var range = document.createRange();
  569. range.setStartAfter(span);
  570. var end = null;
  571. var walker = document.createTreeWalker(block, NodeFilter.SHOW_ELEMENT, null);
  572. walker.currentNode = span;
  573. var n;
  574. while ((n = walker.nextNode())) {
  575. if (n.classList && n.classList.contains("doc-tab")) { end = n; break; }
  576. if (n.tagName === "BR") { end = n; break; }
  577. }
  578. if (end) range.setEndBefore(end);
  579. else range.setEnd(block, block.childNodes.length);
  580. var rects = range.getClientRects();
  581. if (!rects.length) return 0;
  582. // only what stays on the tab's own line counts
  583. var top = span.getBoundingClientRect().top;
  584. var left = Infinity, right = -Infinity;
  585. for (var i = 0; i < rects.length; i++) {
  586. if (Math.abs(rects[i].top - top) > rects[i].height) continue;
  587. left = Math.min(left, rects[i].left);
  588. right = Math.max(right, rects[i].right);
  589. }
  590. return right > left ? right - left : 0;
  591. }
  592. /* ---------------- footnote references ---------------- */
  593. function numberFootnotes(root) {
  594. var order = [];
  595. if (!root) return order;
  596. var refs = root.querySelectorAll("sup.doc-fnref");
  597. for (var i = 0; i < refs.length; i++) {
  598. var id = refs[i].getAttribute("data-fn");
  599. var n = order.indexOf(id);
  600. if (n < 0) { order.push(id); n = order.length - 1; }
  601. var label = String(n + 1);
  602. if (refs[i].textContent !== label) refs[i].textContent = label;
  603. refs[i].setAttribute("contenteditable", "false");
  604. }
  605. return order;
  606. }
  607. /* ---------------- pagination ---------------- */
  608. function Paginator(o) {
  609. this.o = o;
  610. this.editor = o.editor;
  611. this.pageEl = o.pageEl;
  612. this.sheetH = o.sheetH;
  613. this.gap = o.gap;
  614. this.mTop = o.mTop;
  615. this.mBot = o.mBot;
  616. }
  617. // y of an element's border-box top in #page padding-box coordinates
  618. Paginator.prototype.top = function (el) {
  619. var y = 0;
  620. var n = el;
  621. while (n && n !== this.pageEl) {
  622. y += n.offsetTop;
  623. n = n.offsetParent;
  624. if (n === document.body || !n) {
  625. // #page is not the offset parent chain root: measure
  626. return this.rectTop(el.getBoundingClientRect());
  627. }
  628. }
  629. return y;
  630. };
  631. Paginator.prototype.bottom = function (el) {
  632. return this.top(el) + el.offsetHeight;
  633. };
  634. Paginator.prototype.scale = function () {
  635. var h = this.pageEl.offsetHeight;
  636. var r = this.pageEl.getBoundingClientRect().height;
  637. return h > 0 && r > 0 ? r / h : 1;
  638. };
  639. Paginator.prototype.rectTop = function (rect) {
  640. var pr = this.pageEl.getBoundingClientRect();
  641. return (rect.top - pr.top) / this._scale - this.pageEl.clientTop;
  642. };
  643. Paginator.prototype.rectBottom = function (rect) {
  644. var pr = this.pageEl.getBoundingClientRect();
  645. return (rect.bottom - pr.top) / this._scale - this.pageEl.clientTop;
  646. };
  647. Paginator.prototype.sheetTop = function (i) { return i * (this.sheetH + this.gap); };
  648. Paginator.prototype.contentTop = function (i) { return this.sheetTop(i) + this.mTop; };
  649. Paginator.prototype.contentBottom = function (i) { return this.sheetTop(i) + this.sheetH - this.mBot; };
  650. /* ---- splitting what crosses a page ----
  651. A page boundary is real in the DOM. Whatever runs across it - a
  652. paragraph, the list or quote around it, a table row and the cells of
  653. that row - is cut into two elements: the head keeps what fits on the
  654. page, a shallow copy (the tail) receives the rest, and a spacer
  655. between them pushes the tail to the top of the next sheet. A border,
  656. a shading or a cell rule therefore ends at the bottom of its page and
  657. starts again on the next one, and nothing is left painted across the
  658. gap between the sheets.
  659. The two halves are paired with a token: data-split on the head,
  660. data-split-of on the tail, data-pair on the spacer. Undoing a split
  661. (before a relayout, and on everything that is saved) moves the tail's
  662. content back into its head - an inner pair (a span or a cell cut in
  663. the same place) is joined along the way - and the text node that was
  664. divided is joined again. The caret is followed through all of it. */
  665. var splitSeq = 0;
  666. var splitBase = Math.floor(Math.random() * 46656).toString(36) + "-";
  667. function newToken() {
  668. splitSeq++;
  669. return splitBase + splitSeq.toString(36);
  670. }
  671. // the selection, followed through the node moves a split makes
  672. var track = null;
  673. function beginTrack(root) {
  674. var outer = track;
  675. var sel = window.getSelection ? window.getSelection() : null;
  676. if (!outer && sel && sel.rangeCount && sel.anchorNode && root.contains(sel.anchorNode)) {
  677. track = {
  678. sel: sel, a: sel.anchorNode, ao: sel.anchorOffset,
  679. f: sel.focusNode, fo: sel.focusOffset, moved: false
  680. };
  681. }
  682. return { outer: outer, mine: !outer && !!track };
  683. }
  684. function endTrack(t) {
  685. if (!t.mine) return;
  686. var tr = track;
  687. track = null;
  688. if (!tr || !tr.moved) return;
  689. if (!tr.a.isConnected || !tr.f.isConnected) return;
  690. var len = function (n) { return n.nodeType === 3 ? n.nodeValue.length : n.childNodes.length; };
  691. try {
  692. tr.sel.setBaseAndExtent(tr.a, Math.min(tr.ao, len(tr.a)), tr.f, Math.min(tr.fo, len(tr.f)));
  693. } catch (e) { /* a selection the browser will not take back: leave it */ }
  694. }
  695. function noteMoved() {
  696. if (track) track.moved = true;
  697. }
  698. function splitText(node, offset) {
  699. var tail = node.splitText(offset);
  700. if (track) {
  701. if (track.a === node && track.ao > offset) { track.a = tail; track.ao -= offset; }
  702. if (track.f === node && track.fo > offset) { track.f = tail; track.fo -= offset; }
  703. track.moved = true;
  704. }
  705. return tail;
  706. }
  707. function joinText(prev, next) {
  708. var n = prev.nodeValue.length;
  709. if (track) {
  710. if (track.a === next) { track.a = prev; track.ao += n; }
  711. if (track.f === next) { track.f = prev; track.fo += n; }
  712. track.moved = true;
  713. }
  714. prev.nodeValue += next.nodeValue;
  715. next.parentNode.removeChild(next);
  716. }
  717. var SPLIT_BOX = { TR: 1, TD: 1, TH: 1, TBODY: 1, THEAD: 1, TFOOT: 1 };
  718. function cloneShell(el) {
  719. var c = el.cloneNode(false);
  720. c.removeAttribute("id");
  721. c.removeAttribute("data-split");
  722. c.removeAttribute("data-split-of");
  723. c.removeAttribute("data-marker");
  724. c.classList.remove("doc-split-head", "doc-split-tail");
  725. if (!c.getAttribute("class")) c.removeAttribute("class");
  726. if (el.tagName === "OL") c.removeAttribute("start");
  727. if (el.tagName === "TR") {
  728. c.style.height = "";
  729. c.removeAttribute("height");
  730. if (!c.getAttribute("style")) c.removeAttribute("style");
  731. }
  732. if (el.tagName === "TABLE") {
  733. // the tail of a table needs the column widths too
  734. for (var k = el.firstElementChild; k; k = k.nextElementSibling) {
  735. if (k.tagName !== "COLGROUP") continue;
  736. var cg = k.cloneNode(true);
  737. cg.setAttribute("data-split-copy", "1");
  738. c.appendChild(cg);
  739. }
  740. }
  741. return c;
  742. }
  743. function pairUp(head, tail) {
  744. var k = newToken();
  745. head.setAttribute("data-split", k);
  746. tail.setAttribute("data-split-of", k);
  747. if (!SPLIT_BOX[head.tagName] && !isInlineEl(head)) {
  748. head.classList.add("doc-split-head");
  749. tail.classList.add("doc-split-tail");
  750. }
  751. return k;
  752. }
  753. /* unmark drops one role from an element - "head" or "tail" - or both.
  754. An element can be both at once (the middle of a row that spans three
  755. pages), so undoing one split must leave the other alone. deep strips
  756. everything below it too (a split that is being given up). */
  757. function unmark(el, role, deep) {
  758. var list = deep ? [el].concat(Array.prototype.slice.call(el.querySelectorAll("[data-split],[data-split-of]"))) : [el];
  759. list.forEach(function (n, i) {
  760. var r = i === 0 ? role : "both";
  761. if (r !== "tail") {
  762. n.removeAttribute("data-split");
  763. n.classList.remove("doc-split-head");
  764. }
  765. if (r !== "head") {
  766. n.removeAttribute("data-split-of");
  767. n.classList.remove("doc-split-tail");
  768. }
  769. if (!n.getAttribute("class")) n.removeAttribute("class");
  770. });
  771. }
  772. function meaningful(n) {
  773. if (n.nodeType === 3) return /\S/.test(n.nodeValue);
  774. return n.nodeType === 1 && !isSpacer(n) && !n.hasAttribute("data-split-copy");
  775. }
  776. function prevMeaningful(n) {
  777. for (var p = n.previousSibling; p; p = p.previousSibling) if (meaningful(p)) return p;
  778. return null;
  779. }
  780. function nextMeaningful(n) {
  781. for (var p = n.nextSibling; p; p = p.nextSibling) if (meaningful(p)) return p;
  782. return null;
  783. }
  784. function nextInOrder(node, stop) {
  785. for (var n = node; n && n !== stop; n = n.parentNode) {
  786. if (n.nextSibling) return n.nextSibling;
  787. }
  788. return null;
  789. }
  790. /* splitTree moves `start` and everything after it, up to the end of
  791. `boundary`, into shallow copies of the ancestors in between. Returns
  792. the node directly inside boundary that the moved content begins with.
  793. An ancestor whose content all moves is taken along whole instead of
  794. being copied, so no half is ever left empty. */
  795. function splitTree(start, boundary) {
  796. var cur = start;
  797. while (cur && cur.parentNode && cur.parentNode !== boundary) {
  798. var parent = cur.parentNode;
  799. if (!prevMeaningful(cur)) {
  800. cur = parent;
  801. continue;
  802. }
  803. var clone = cloneShell(parent);
  804. pairUp(parent, clone);
  805. parent.parentNode.insertBefore(clone, parent.nextSibling);
  806. var n = cur;
  807. while (n) {
  808. var nx = n.nextSibling;
  809. clone.appendChild(n);
  810. n = nx;
  811. }
  812. noteMoved();
  813. cur = clone;
  814. }
  815. return cur;
  816. }
  817. // the node a line cut starts the moved content with (text divided there)
  818. function lineStartNode(cut) {
  819. if (cut.beforeEl) return cut.beforeEl;
  820. var node = cut.node;
  821. var len = node.nodeValue.length;
  822. var at = cut.offset;
  823. // the space a line wrapped at stays at the end of the line above,
  824. // where it collapses; at the start of the next page it would not
  825. var ws = node.parentNode ? window.getComputedStyle(node.parentNode).whiteSpace : "normal";
  826. if (ws.indexOf("pre") !== 0 && ws !== "break-spaces") {
  827. while (at > 0 && at < len && /\s/.test(node.nodeValue.charAt(at))) at++;
  828. }
  829. cut = { el: cut.el, node: node, offset: at };
  830. if (cut.offset > 0 && cut.offset < len) return splitText(node, cut.offset);
  831. if (cut.offset >= len) return nextInOrder(node, cut.el);
  832. return node;
  833. }
  834. /* mergePair undoes one split: the tail's content goes back to the end
  835. of its head (for a table row, cell by cell) and the tail goes away */
  836. function mergePair(head, tail) {
  837. if (!head || !tail || !tail.parentNode) return;
  838. noteMoved();
  839. if (head.tagName === "TR" && tail.tagName === "TR") {
  840. var tcells = Array.prototype.slice.call(tail.cells);
  841. for (var i = 0; i < tcells.length; i++) {
  842. var hc = head.cells[i];
  843. if (hc) {
  844. mergeChildren(hc, tcells[i]);
  845. unmark(hc, "head");
  846. } else {
  847. unmark(tcells[i], "tail");
  848. head.appendChild(tcells[i]);
  849. }
  850. }
  851. } else {
  852. mergeChildren(head, tail);
  853. }
  854. if (tail.parentNode) tail.parentNode.removeChild(tail);
  855. unmark(head, "head");
  856. }
  857. function mergeChildren(head, tail) {
  858. var c, nx;
  859. for (c = tail.firstChild; c; c = nx) {
  860. nx = c.nextSibling;
  861. if (c.nodeType === 1 && c.hasAttribute("data-split-copy")) tail.removeChild(c);
  862. }
  863. // an inner pair cut at the same place (a span, a list, a nested
  864. // table) joins first, so the content meets inside it
  865. var last = head.lastChild;
  866. while (last && !meaningful(last)) last = last.previousSibling;
  867. var first = tail.firstChild;
  868. while (first && !meaningful(first)) first = first.nextSibling;
  869. if (last && first && last.nodeType === 1 && first.nodeType === 1) {
  870. var k = first.getAttribute("data-split-of");
  871. if (k && last.getAttribute("data-split") === k) mergePair(last, first);
  872. }
  873. var seam = head.lastChild;
  874. while (tail.firstChild) head.appendChild(tail.firstChild);
  875. if (seam && seam.nodeType === 3 && seam.nextSibling && seam.nextSibling.nodeType === 3) {
  876. joinText(seam, seam.nextSibling);
  877. }
  878. }
  879. // undo the split a spacer stands for (the spacer is already gone)
  880. function mergeAround(root, k, prev, next) {
  881. if (!k) return;
  882. var head = prev && prev.nodeType === 1 && prev.getAttribute("data-split") === k ? prev : null;
  883. var tail = next && next.nodeType === 1 && next.getAttribute("data-split-of") === k ? next : null;
  884. if (!head) {
  885. var hs = root.querySelectorAll('[data-split="' + k + '"]');
  886. head = hs.length ? hs[hs.length - 1] : null;
  887. }
  888. if (!tail) tail = root.querySelector('[data-split-of="' + k + '"]');
  889. if (head && tail) mergePair(head, tail);
  890. else {
  891. if (head) unmark(head, "head");
  892. if (tail) unmark(tail, "tail");
  893. }
  894. }
  895. function removeSpacerList(list, root) {
  896. for (var i = list.length - 1; i >= 0; i--) {
  897. var el = list[i];
  898. var parent = el.parentNode;
  899. if (!parent) continue;
  900. var k = el.getAttribute("data-pair");
  901. var prev = el.previousSibling, next = el.nextSibling;
  902. while (prev && !meaningful(prev) && !isSpacer(prev)) prev = prev.previousSibling;
  903. while (next && !meaningful(next) && !isSpacer(next)) next = next.nextSibling;
  904. parent.removeChild(el);
  905. noteMoved();
  906. if (k) {
  907. mergeAround(root, k, prev, next);
  908. } else if (prev && next && prev.nodeType === 3 && next.nodeType === 3 &&
  909. prev.nextSibling === next) {
  910. // a line spacer from an older layout divided this text
  911. joinText(prev, next);
  912. }
  913. }
  914. }
  915. function isInner(el) {
  916. var p = el.parentNode;
  917. return !!(p && p.nodeType === 1 && (p.hasAttribute("data-split") || p.hasAttribute("data-split-of")));
  918. }
  919. /* repairSplits puts right what editing did to a split: a tail whose
  920. spacer was deleted joins its head again, and a marker left without
  921. its partner (the tail typed away, a copy made by Enter) is dropped.
  922. With all, every remaining pair is undone - what is saved has none. */
  923. function repairSplits(root, all) {
  924. var tails = root.querySelectorAll("[data-split-of]");
  925. var i;
  926. for (i = tails.length - 1; i >= 0; i--) {
  927. var tl = tails[i];
  928. if (!tl.parentNode || !root.contains(tl) || isInner(tl)) continue;
  929. var k = tl.getAttribute("data-split-of");
  930. var prev = tl.previousSibling;
  931. while (prev && !meaningful(prev) && !isSpacer(prev)) prev = prev.previousSibling;
  932. if (!all && prev && isSpacer(prev) && prev.getAttribute("data-pair") === k) continue;
  933. var head = prev && prev.nodeType === 1 && prev.getAttribute("data-split") === k ? prev : null;
  934. if (head) mergePair(head, tl);
  935. else unmark(tl, "tail", true);
  936. }
  937. var heads = root.querySelectorAll("[data-split]");
  938. for (i = 0; i < heads.length; i++) {
  939. var h = heads[i];
  940. if (!h.isConnected || !h.hasAttribute("data-split") || isInner(h)) continue;
  941. var nx = h.nextSibling;
  942. while (nx && !meaningful(nx) && !isSpacer(nx)) nx = nx.nextSibling;
  943. if (!all && nx && isSpacer(nx) && nx.getAttribute("data-pair") === h.getAttribute("data-split")) continue;
  944. unmark(h, "head", true);
  945. }
  946. }
  947. function removeSpacers(root) {
  948. var t = beginTrack(root);
  949. try {
  950. removeSpacerList(root.querySelectorAll(".doc-autobreak"), root);
  951. repairSplits(root, true);
  952. var breaks = root.querySelectorAll(".doc-pagebreak");
  953. for (var i = 0; i < breaks.length; i++) breaks[i].style.height = "0px";
  954. } finally {
  955. endTrack(t);
  956. }
  957. }
  958. /* unsplitWithin undoes the splits inside an element (a table about to
  959. gain or lose a row or column) */
  960. function unsplitWithin(root, el) {
  961. if (!root || !el) return false;
  962. var list = el.querySelectorAll(".doc-autobreak");
  963. if (!list.length) return false;
  964. var t = beginTrack(root);
  965. try {
  966. removeSpacerList(list, root);
  967. repairSplits(root, false);
  968. } finally {
  969. endTrack(t);
  970. }
  971. return true;
  972. }
  973. /* unsplitAtCaret is called before a key edits the document. A
  974. Backspace at the start of what a page break moved, a Delete at the
  975. end of what it left behind, or any key over a selection that spans a
  976. page boundary would otherwise act on the spacer rather than on the
  977. text; the split in the way is undone first, and the edit then does
  978. what it would do in one continuous paragraph. */
  979. function edgeEmpty(el, caretNode, caretOffset, atStart) {
  980. var r = document.createRange();
  981. try {
  982. if (atStart) {
  983. r.setStart(el, 0);
  984. r.setEnd(caretNode, caretOffset);
  985. } else {
  986. r.setStart(caretNode, caretOffset);
  987. r.setEnd(el, el.childNodes.length);
  988. }
  989. } catch (e) { return false; }
  990. if (r.toString().replace(/\s/g, "").split(String.fromCharCode(0x200B)).join("") !== "") return false;
  991. var frag = r.cloneContents();
  992. return !frag.querySelector || !frag.querySelector("img,table,hr");
  993. }
  994. function unsplitAtCaret(root, backward) {
  995. var sel = window.getSelection ? window.getSelection() : null;
  996. if (!sel || !sel.rangeCount) return false;
  997. var r = sel.getRangeAt(0);
  998. if (!root.contains(r.commonAncestorContainer)) return false;
  999. var spacers = [];
  1000. var all, i;
  1001. if (!r.collapsed) {
  1002. all = root.querySelectorAll(".doc-autobreak");
  1003. for (i = 0; i < all.length; i++) {
  1004. if (r.intersectsNode(all[i])) spacers.push(all[i]);
  1005. }
  1006. } else {
  1007. var node = backward ? r.startContainer : r.endContainer;
  1008. var off = backward ? r.startOffset : r.endOffset;
  1009. var el = node.nodeType === 1 ? node : node.parentNode;
  1010. while (el && el !== root) {
  1011. if (el.tagName !== "TR" && !edgeEmpty(el, node, off, backward) &&
  1012. !((el.tagName === "TD" || el.tagName === "TH") && el.hasAttribute(backward ? "data-split-of" : "data-split"))) {
  1013. break;
  1014. }
  1015. var sib = backward ? el.previousSibling : el.nextSibling;
  1016. while (sib && !meaningful(sib) && !isSpacer(sib)) sib = backward ? sib.previousSibling : sib.nextSibling;
  1017. if (sib && isSpacer(sib)) {
  1018. spacers.push(sib);
  1019. break;
  1020. }
  1021. if (el.tagName === "TD" || el.tagName === "TH") {
  1022. el = el.parentNode;
  1023. continue;
  1024. }
  1025. if (sib) break;
  1026. el = el.parentNode;
  1027. }
  1028. }
  1029. if (!spacers.length) return false;
  1030. var t = beginTrack(root);
  1031. try {
  1032. removeSpacerList(spacers, root);
  1033. repairSplits(root, false);
  1034. } finally {
  1035. endTrack(t);
  1036. }
  1037. return true;
  1038. }
  1039. function blockKids(container) {
  1040. var out = [];
  1041. for (var c = container.firstElementChild; c; c = c.nextElementSibling) {
  1042. if (isSpacer(c)) continue;
  1043. if (c.tagName === "COLGROUP" || c.tagName === "COL") continue;
  1044. out.push(c);
  1045. }
  1046. return out;
  1047. }
  1048. function keepsWithNext(el) {
  1049. if (el.getAttribute("data-keep-next") === "1") return true;
  1050. // headings written in the editor keep with what follows, like the
  1051. // Heading styles of every word processor
  1052. return /^H[1-6]$/.test(el.tagName) && !el.hasAttribute("data-keep-next");
  1053. }
  1054. /* A cut says where page content stops:
  1055. { kind: "before", el } content from el moves (a block, li, row)
  1056. { kind: "line", el, node, offset | beforeEl } a block splits at a line
  1057. { kind: "row", tr, cells: [cut|null per cell] }
  1058. { kind: "explicit", el } a manual page break
  1059. { kind: "overflow" } nothing movable: content spills over
  1060. with y = where the moved content currently starts, and boundary = the
  1061. element the split reaches up to (the editor, or a table cell). */
  1062. Paginator.prototype.findCut = function (container, B, C, boundary) {
  1063. var kids = blockKids(container);
  1064. if (!kids.length) return null;
  1065. // explicit breaks and page-break-before on this level
  1066. for (var e = 0; e < kids.length; e++) {
  1067. var k = kids[e];
  1068. if (k.classList.contains("doc-pagebreak")) {
  1069. var ty = this.top(k);
  1070. if (ty >= C - 1 && ty <= B + 0.5) return { kind: "explicit", el: k, y: ty, boundary: boundary };
  1071. } else if (k.getAttribute("data-page-break-before") === "1") {
  1072. var py = this.top(k);
  1073. if (py > C + 1 && py <= B + 0.5) return this.beforeCut(k, C, boundary);
  1074. }
  1075. }
  1076. // first child whose bottom crosses B (children are in flow order)
  1077. var lo = 0, hi = kids.length - 1, idx = -1;
  1078. while (lo <= hi) {
  1079. var mid = (lo + hi) >> 1;
  1080. if (this.bottom(kids[mid]) > B + 0.5) { idx = mid; hi = mid - 1; }
  1081. else lo = mid + 1;
  1082. }
  1083. if (idx < 0) return null;
  1084. // a float or a negative margin can leave an earlier child lower;
  1085. // walk back over anything that also crosses
  1086. while (idx > 0 && this.bottom(kids[idx - 1]) > B + 0.5) idx--;
  1087. // a child that only hangs its spacing-after over the edge still
  1088. // fits - the cut belongs to whatever comes after it
  1089. for (; idx < kids.length; idx++) {
  1090. if (idx > 0 && this.bottom(kids[idx]) <= B + 0.5) continue;
  1091. var cut = this.splitChild(kids[idx], B, C, boundary);
  1092. if (cut && cut.kind !== "fit") return cut;
  1093. }
  1094. return null;
  1095. };
  1096. Paginator.prototype.splitChild = function (el, B, C, boundary) {
  1097. var top = this.top(el);
  1098. if (top >= B - 0.5) return this.beforeCut(el, C, boundary);
  1099. var tag = el.tagName;
  1100. if (tag === "TABLE") return this.splitTable(el, B, C, boundary);
  1101. if (tag === "IMG" || tag === "HR" || tag === "VIDEO" || tag === "IFRAME") {
  1102. return this.beforeCut(el, C, boundary);
  1103. }
  1104. if (el.classList.contains("doc-pagebreak")) return null;
  1105. if (isList(el) || tag === "TBODY") {
  1106. var inner = this.findCut(el, B, C, boundary);
  1107. return this.hoist(inner, el, C, boundary);
  1108. }
  1109. if (hasBlockChild(el)) {
  1110. if (el.getAttribute("data-keep-lines") === "1" && top > C + 1) return this.beforeCut(el, C, boundary);
  1111. var sub = this.findCut(el, B, C, boundary);
  1112. return this.hoist(sub, el, C, boundary);
  1113. }
  1114. return this.splitLines(el, B, C, boundary);
  1115. };
  1116. // a cut before the first child of a container is a cut before the
  1117. // container (so keep-with-next and page-top checks see the real block)
  1118. Paginator.prototype.hoist = function (cut, container, C, boundary) {
  1119. if (!cut) return { kind: "fit" };
  1120. if (cut.kind === "before" && container !== boundary) {
  1121. var kids = blockKids(container);
  1122. if (kids.length && kids[0] === cut.el && this.top(container) > C + 1) {
  1123. return this.beforeCut(container, C, boundary);
  1124. }
  1125. }
  1126. return cut;
  1127. };
  1128. Paginator.prototype.beforeCut = function (el, C, boundary) {
  1129. var target = el;
  1130. // keep-with-next: pull the blocks that must stay with el along
  1131. for (var guard = 0; guard < 20; guard++) {
  1132. var prev = target.previousElementSibling;
  1133. while (prev && isSpacer(prev)) prev = prev.previousElementSibling;
  1134. if (!prev || prev.classList.contains("doc-pagebreak")) break;
  1135. if (!keepsWithNext(prev)) break;
  1136. if (this.top(prev) <= C + 1) break;
  1137. target = prev;
  1138. }
  1139. var y = this.top(target);
  1140. if (y <= C + 1) {
  1141. if (target !== el) {
  1142. target = el;
  1143. y = this.top(el);
  1144. }
  1145. if (y <= C + 1) return { kind: "overflow", y: y };
  1146. }
  1147. // a cut before the first row of a table is a cut before the table
  1148. if (target.tagName === "TR") {
  1149. var tbl = target.closest("table");
  1150. var rows = rowsOf(tbl);
  1151. if (rows[0] === target && tbl !== boundary) return this.beforeCut(tbl, C, boundary);
  1152. }
  1153. return { kind: "before", el: target, y: y, boundary: boundary };
  1154. };
  1155. function rowsOf(tbl) {
  1156. var out = [];
  1157. for (var i = 0; i < tbl.rows.length; i++) {
  1158. if (!tbl.rows[i].classList.contains("doc-autobreak") &&
  1159. tbl.rows[i].closest("table") === tbl) out.push(tbl.rows[i]);
  1160. }
  1161. return out;
  1162. }
  1163. Paginator.prototype.splitTable = function (tbl, B, C, boundary) {
  1164. var rows = rowsOf(tbl);
  1165. var idx = -1;
  1166. for (var i = 0; i < rows.length; i++) {
  1167. if (this.bottom(rows[i]) > B + 0.5) { idx = i; break; }
  1168. }
  1169. if (idx < 0) return { kind: "fit" };
  1170. var row = rows[idx];
  1171. var rowTop = this.top(row);
  1172. if (rowTop >= B - 1) return this.beforeCut(idx === 0 ? tbl : row, C, boundary);
  1173. // a row that cannot split, or one a merged cell reaches into from
  1174. // above, moves whole - unless it already starts the page
  1175. var spanned = rowspanCrosses(tbl, rows, idx);
  1176. var cant = row.getAttribute("data-cant-split") === "1" || spanned || hasRowspan(row);
  1177. if (cant && rowTop > C + 1) {
  1178. var bc = this.beforeCut(idx === 0 ? tbl : row, C, boundary);
  1179. if (bc.kind !== "overflow") return bc;
  1180. }
  1181. if (spanned || hasRowspan(row)) return { kind: "overflow", y: rowTop };
  1182. var cells = [];
  1183. var any = false, allNothing = true;
  1184. for (var c = 0; c < row.cells.length; c++) {
  1185. var td = row.cells[c];
  1186. var cs = window.getComputedStyle(td);
  1187. var padB = (parseFloat(cs.paddingBottom) || 0) + (parseFloat(cs.borderBottomWidth) || 0);
  1188. var cut = hasBlockChild(td) ? this.findCut(td, B - padB, C, td)
  1189. : this.splitLines(td, B - padB, C, td);
  1190. if (cut && cut.kind === "fit") cut = null;
  1191. if (cut && cut.kind === "overflow") {
  1192. // this cell cannot give anything up: move the row if we can
  1193. cut = { kind: "cellstart", td: td, y: rowTop };
  1194. }
  1195. if (cut) {
  1196. any = true;
  1197. if (cut.kind !== "cellstart" && !(cut.kind === "before" && cut.el === blockKids(td)[0])) {
  1198. allNothing = false;
  1199. }
  1200. } else {
  1201. allNothing = false;
  1202. }
  1203. cells.push(cut);
  1204. }
  1205. if (!any) return { kind: "fit" };
  1206. if (allNothing && rowTop > C + 1) return this.beforeCut(idx === 0 ? tbl : row, C, boundary);
  1207. var y = Infinity;
  1208. cells.forEach(function (ct) { if (ct && ct.y < y) y = ct.y; });
  1209. return { kind: "row", tr: row, cells: cells, y: y, boundary: boundary };
  1210. };
  1211. function hasRowspan(row) {
  1212. for (var i = 0; i < row.cells.length; i++) {
  1213. if (row.cells[i].rowSpan > 1) return true;
  1214. }
  1215. return false;
  1216. }
  1217. function rowspanCrosses(tbl, rows, idx) {
  1218. for (var r = 0; r < idx; r++) {
  1219. for (var c = 0; c < rows[r].cells.length; c++) {
  1220. if (r + rows[r].cells[c].rowSpan > idx) return true;
  1221. }
  1222. }
  1223. return false;
  1224. }
  1225. /* the visual lines of a block that holds only inline content */
  1226. Paginator.prototype.lines = function (block) {
  1227. var frags = [];
  1228. var self = this;
  1229. var range = document.createRange();
  1230. function visit(parent) {
  1231. for (var n = parent.firstChild; n; n = n.nextSibling) {
  1232. if (n.nodeType === 3) {
  1233. if (!n.nodeValue) continue;
  1234. range.selectNodeContents(n);
  1235. var rects = range.getClientRects();
  1236. for (var i = 0; i < rects.length; i++) {
  1237. if (rects[i].height <= 0) continue;
  1238. frags.push({ node: n, index: i, top: self.rectTop(rects[i]), bottom: self.rectBottom(rects[i]) });
  1239. }
  1240. } else if (n.nodeType === 1) {
  1241. if (isSpacer(n)) continue;
  1242. var disp = n.tagName === "IMG" ? "inline" : window.getComputedStyle(n).display;
  1243. if (n.tagName === "IMG" || (disp === "inline-block" && !n.classList.contains("doc-tab"))) {
  1244. // a picture or an inline-block is one unbreakable box
  1245. var r = n.getBoundingClientRect();
  1246. if (r.height > 0) frags.push({ el: n, top: self.rectTop(r), bottom: self.rectBottom(r) });
  1247. } else if (disp !== "none") {
  1248. visit(n);
  1249. }
  1250. }
  1251. }
  1252. }
  1253. visit(block);
  1254. var lines = [];
  1255. frags.forEach(function (f) {
  1256. var cur = lines[lines.length - 1];
  1257. if (!cur || f.top >= cur.bottom - 1.5) {
  1258. lines.push({ top: f.top, bottom: f.bottom, first: f, text: !f.el });
  1259. } else {
  1260. cur.top = Math.min(cur.top, f.top);
  1261. cur.bottom = Math.max(cur.bottom, f.bottom);
  1262. if (!f.el) cur.text = true;
  1263. }
  1264. });
  1265. // a text rect is the glyphs, not the line: the line box reaches half
  1266. // the leading further down, and that is what has to fit the page
  1267. var lh = parseFloat(window.getComputedStyle(block).lineHeight) || 0;
  1268. lines.forEach(function (ln) {
  1269. var glyphs = ln.bottom - ln.top;
  1270. ln.boxBottom = ln.bottom + (ln.text && lh > glyphs ? (lh - glyphs) / 2 : 0);
  1271. });
  1272. return lines;
  1273. };
  1274. Paginator.prototype.splitLines = function (block, B, C, boundary) {
  1275. var lines = this.lines(block);
  1276. if (!lines.length) {
  1277. return this.bottom(block) > B + 0.5 && this.top(block) > C + 1 ?
  1278. this.beforeCut(block, C, boundary) : { kind: "fit" };
  1279. }
  1280. var k = -1;
  1281. for (var i = 0; i < lines.length; i++) {
  1282. if (lines[i].boxBottom > B + 0.5) { k = i; break; }
  1283. }
  1284. // only the spacing after the last line hangs over: that is allowed
  1285. if (k < 0) return { kind: "fit" };
  1286. var atTop = this.top(block) <= C + 1;
  1287. if (block.getAttribute("data-keep-lines") === "1" && !atTop) k = 0;
  1288. // widow/orphan control is on unless the paragraph turns it off
  1289. if (block.getAttribute("data-widow") !== "0" && lines.length > 1) {
  1290. // a widow (last line alone on the next page) pulls one more line
  1291. // over; an orphan (first line alone on this page) moves the
  1292. // whole paragraph
  1293. if (k > 0 && lines.length - k === 1) k = k - 1;
  1294. if (k === 1 && !atTop) k = 0;
  1295. }
  1296. if (k === 0) {
  1297. if (!atTop) return this.beforeCut(block, C, boundary);
  1298. if (lines.length === 1) return { kind: "overflow", y: lines[0].top };
  1299. k = 1;
  1300. }
  1301. var f = lines[k].first;
  1302. var cut = { kind: "line", el: block, y: lines[k].top, boundary: boundary };
  1303. if (f.el) {
  1304. cut.beforeEl = f.el;
  1305. } else {
  1306. cut.node = f.node;
  1307. cut.offset = f.index === 0 ? 0 : this.lineStartOffset(f.node, lines[k].top);
  1308. }
  1309. return cut;
  1310. };
  1311. // first character of a text node that sits on the line starting at y
  1312. Paginator.prototype.lineStartOffset = function (node, lineTop) {
  1313. var len = node.nodeValue.length;
  1314. var range = document.createRange();
  1315. var self = this;
  1316. function topAt(o) {
  1317. for (var i = o; i < len; i++) {
  1318. range.setStart(node, i);
  1319. range.setEnd(node, i + 1);
  1320. var rr = range.getClientRects();
  1321. if (rr.length && rr[0].height > 0) return { top: self.rectTop(rr[rr.length - 1]), at: i };
  1322. }
  1323. return null;
  1324. }
  1325. var lo = 0, hi = len - 1, ans = len;
  1326. while (lo <= hi) {
  1327. var mid = (lo + hi) >> 1;
  1328. var t = topAt(mid);
  1329. if (!t) { hi = mid - 1; continue; }
  1330. if (t.top >= lineTop - 1.5) { ans = Math.min(ans, t.at); hi = mid - 1; }
  1331. else lo = t.at + 1;
  1332. }
  1333. return Math.min(ans, len);
  1334. };
  1335. /* ---- applying a cut ---- */
  1336. function makeSpacer(kind, cols) {
  1337. var el;
  1338. if (kind === "row") {
  1339. el = document.createElement("tr");
  1340. var td = document.createElement("td");
  1341. td.colSpan = Math.max(1, cols);
  1342. td.className = "doc-autobreak-cell";
  1343. el.appendChild(td);
  1344. } else {
  1345. el = document.createElement("div");
  1346. }
  1347. el.className = "doc-autobreak";
  1348. el.setAttribute("contenteditable", "false");
  1349. el.setAttribute("aria-hidden", "true");
  1350. return el;
  1351. }
  1352. function marginTopOf(el) {
  1353. return parseFloat(window.getComputedStyle(el).marginTop) || 0;
  1354. }
  1355. // stretch a spacer so that the content after it starts at targetY
  1356. Paginator.prototype.land = function (spacer, targetY, probe) {
  1357. var box = spacer.tagName === "TR" ? spacer.firstChild : spacer;
  1358. box.style.height = "0px";
  1359. for (var i = 0; i < 3; i++) {
  1360. var at = probe ? probe() : this.bottom(spacer);
  1361. var cur = parseFloat(box.style.height) || 0;
  1362. var want = Math.max(0, cur + (targetY - at));
  1363. if (Math.abs(want - cur) < 0.05) break;
  1364. box.style.height = want + "px";
  1365. }
  1366. };
  1367. Paginator.prototype.apply = function (cut, nextC) {
  1368. var self = this;
  1369. var boundary = cut.boundary || this.editor;
  1370. var top, dv;
  1371. switch (cut.kind) {
  1372. case "explicit":
  1373. cut.el.style.height = "0px";
  1374. this.land(cut.el, nextC);
  1375. return;
  1376. case "before":
  1377. var el = cut.el;
  1378. if (el.tagName === "TR") {
  1379. var cols = 0;
  1380. for (var c = 0; c < el.cells.length; c++) cols += el.cells[c].colSpan;
  1381. var sp = makeSpacer("row", cols);
  1382. el.parentNode.insertBefore(sp, el);
  1383. var bw = parseFloat(window.getComputedStyle(el.cells[0] || el).borderTopWidth) || 0;
  1384. this.land(sp, nextC + bw / 2, function () { return self.top(el); });
  1385. return;
  1386. }
  1387. // what holds el (a list, a quote) splits with it, so the
  1388. // spacer sits between two whole boxes
  1389. top = el.parentNode === boundary ? el : splitTree(el, boundary);
  1390. dv = makeSpacer("block");
  1391. if (top !== el && top.getAttribute("data-split-of")) dv.setAttribute("data-pair", top.getAttribute("data-split-of"));
  1392. top.parentNode.insertBefore(dv, top);
  1393. // spacing-before still applies at the top of a page
  1394. this.land(dv, nextC + marginTopOf(el), function () { return self.top(el); });
  1395. return;
  1396. case "line":
  1397. top = splitTree(lineStartNode(cut), boundary);
  1398. if (!top) return;
  1399. dv = makeSpacer("block");
  1400. if (top.getAttribute && top.getAttribute("data-split-of")) dv.setAttribute("data-pair", top.getAttribute("data-split-of"));
  1401. top.parentNode.insertBefore(dv, top);
  1402. this.land(dv, nextC, function () { return self.top(top); });
  1403. return;
  1404. case "row":
  1405. this.splitRow(cut, nextC, true);
  1406. return;
  1407. }
  1408. };
  1409. /* A row that breaks across the page becomes two rows: the head keeps
  1410. what fits in every cell, a copy of the row takes the rest of each
  1411. cell, and a spacer row between them carries the copy to the next
  1412. sheet. Each half has its own cell borders and shading. */
  1413. Paginator.prototype.splitRow = function (cut, nextC, withSpacer) {
  1414. var self = this;
  1415. var tr = cut.tr;
  1416. var tailTr = cloneShell(tr);
  1417. var k = pairUp(tr, tailTr);
  1418. tr.parentNode.insertBefore(tailTr, tr.nextSibling);
  1419. noteMoved();
  1420. var cells = Array.prototype.slice.call(tr.cells);
  1421. cells.forEach(function (td, i) {
  1422. var tailTd = cloneShell(td);
  1423. pairUp(td, tailTd);
  1424. tailTr.appendChild(tailTd);
  1425. var ct = cut.cells[i];
  1426. if (!ct) return;
  1427. var start = null;
  1428. if (ct.kind === "cellstart") {
  1429. start = td.firstChild;
  1430. } else if (ct.kind === "line") {
  1431. start = lineStartNode(ct);
  1432. } else if (ct.kind === "before" || ct.kind === "explicit") {
  1433. start = ct.el;
  1434. } else if (ct.kind === "row") {
  1435. // a nested table splits in the same place; its tail rows go
  1436. // with the rest of this cell
  1437. start = self.splitRow(ct, nextC, false);
  1438. }
  1439. if (!start) return;
  1440. var from = start.parentNode === td ? start : splitTree(start, td);
  1441. while (from) {
  1442. var nx = from.nextSibling;
  1443. tailTd.appendChild(from);
  1444. from = nx;
  1445. }
  1446. });
  1447. if (withSpacer) {
  1448. var cols = 0;
  1449. for (var c = 0; c < cells.length; c++) cols += cells[c].colSpan;
  1450. var sp = makeSpacer("row", cols);
  1451. sp.setAttribute("data-pair", k);
  1452. tr.parentNode.insertBefore(sp, tailTr);
  1453. var bw = parseFloat(window.getComputedStyle(tailTr.cells[0] || tailTr).borderTopWidth) || 0;
  1454. this.land(sp, nextC + bw / 2, function () { return self.top(tailTr); });
  1455. }
  1456. return tailTr;
  1457. };
  1458. /* ---- footnotes ---- */
  1459. Paginator.prototype.refsBetween = function (y0, y1) {
  1460. var out = [];
  1461. var refs = this.o.fnRefs || [];
  1462. for (var i = 0; i < refs.length; i++) {
  1463. var r = refs[i];
  1464. var rect = r.getBoundingClientRect();
  1465. if (!rect.height) continue;
  1466. var y = this.rectTop(rect);
  1467. if (y >= y0 - 1 && y < y1) {
  1468. var id = r.getAttribute("data-fn");
  1469. if (out.indexOf(id) < 0) out.push(id);
  1470. }
  1471. }
  1472. return out;
  1473. };
  1474. /* ---- the page loop ---- */
  1475. /* Where to start. An edit cannot move a page break that comes before
  1476. it, so when the caller says where the change is (fromY) and hands in
  1477. the previous page records, the pages up to the one before the change
  1478. are kept as they are - with their spacers and splits - and only the
  1479. rest is laid out again. Starting one page early leaves room for a
  1480. widow or a keep-with-next that pulls a line back across the page
  1481. above. */
  1482. Paginator.prototype.resume = function () {
  1483. var o = this.o;
  1484. var prev = o.prevPages;
  1485. if (!(o.fromY >= 0) || !prev || prev.length < 3) return 0;
  1486. var p = 0;
  1487. for (var k = 0; k < prev.length; k++) {
  1488. if (prev[k].sheetTop <= o.fromY) p = k;
  1489. }
  1490. var start = p - 1;
  1491. if (start < 1) return 0;
  1492. var threshold = this.contentTop(start);
  1493. var self = this;
  1494. var stale = [];
  1495. var spacers = this.editor.querySelectorAll(".doc-autobreak");
  1496. for (var i = 0; i < spacers.length; i++) {
  1497. if (self.top(spacers[i]) >= threshold - 0.5) stale.push(spacers[i]);
  1498. }
  1499. var breaks = this.editor.querySelectorAll(".doc-pagebreak");
  1500. var staleBreaks = [];
  1501. for (i = 0; i < breaks.length; i++) {
  1502. if (self.top(breaks[i]) >= threshold - 0.5) staleBreaks.push(breaks[i]);
  1503. }
  1504. removeSpacerList(stale, this.editor);
  1505. repairSplits(this.editor, false);
  1506. staleBreaks.forEach(function (b) { b.style.height = "0px"; });
  1507. return start;
  1508. };
  1509. Paginator.prototype.run = function () {
  1510. var o = this.o;
  1511. this._scale = this.scale();
  1512. var i = this.resume();
  1513. var pages;
  1514. if (i > 0) {
  1515. pages = o.prevPages.slice(0, i).map(function (pg) {
  1516. var copy = {};
  1517. for (var k in pg) copy[k] = pg[k];
  1518. return copy;
  1519. });
  1520. } else {
  1521. removeSpacers(this.editor);
  1522. pages = [];
  1523. }
  1524. var contentEnd = function (self) { return self.bottom(self.editor); };
  1525. for (var guard = 0; guard < 3000; guard++) {
  1526. this._scale = this.scale();
  1527. var C = this.contentTop(i);
  1528. var Bfull = this.contentBottom(i);
  1529. var B = Bfull;
  1530. var ids = [];
  1531. var fnH = 0;
  1532. var cut = null;
  1533. for (var iter = 0; iter < 4; iter++) {
  1534. cut = this.findCut(this.editor, B, C, this.editor);
  1535. if (cut && cut.kind === "fit") cut = null;
  1536. var endY = cut ? cut.y : contentEnd(this);
  1537. var got = o.measureFootnotes ? this.refsBetween(C, Math.min(endY, B + 0.5)) : [];
  1538. var h = got.length ? o.measureFootnotes(got) : 0;
  1539. if (got.join(",") === ids.join(",") && Math.abs(h - fnH) < 0.5) break;
  1540. ids = got;
  1541. fnH = h;
  1542. B = Bfull - fnH;
  1543. }
  1544. var page = {
  1545. index: i, sheetTop: this.sheetTop(i), contentTop: C, contentBottom: B,
  1546. footnotes: ids, footnoteTop: Bfull - fnH
  1547. };
  1548. pages.push(page);
  1549. if (!cut) break;
  1550. var nextC = this.contentTop(i + 1);
  1551. if (cut.kind !== "overflow") this.apply(cut, nextC);
  1552. i++;
  1553. }
  1554. return pages;
  1555. };
  1556. function paginate(opts) {
  1557. var t = beginTrack(opts.editor);
  1558. try {
  1559. return new Paginator(opts).run();
  1560. } finally {
  1561. endTrack(t);
  1562. }
  1563. }
  1564. return {
  1565. PT: PT,
  1566. MM: MM,
  1567. fontRatios: fontRatios,
  1568. lineHeightPx: lineHeightPx,
  1569. applyLineHeights: applyLineHeights,
  1570. applyTableBorders: applyTableBorders,
  1571. restoreTablePadding: restoreTablePadding,
  1572. restorePictureLines: restorePictureLines,
  1573. applyNumbering: applyNumbering,
  1574. applyTabs: applyTabs,
  1575. numberFootnotes: numberFootnotes,
  1576. removeSpacers: removeSpacers,
  1577. unsplitWithin: unsplitWithin,
  1578. unsplitAtCaret: unsplitAtCaret,
  1579. paginate: paginate,
  1580. formatNumber: formatNumber
  1581. };
  1582. })();