From 8fe2928ea1f273053d5159e846414d1053a495c6 Mon Sep 17 00:00:00 2001 From: "J. Hathaway" Date: Wed, 5 Aug 2020 17:06:08 -0600 Subject: [PATCH 01/20] first draft of CSS from pagedown --- media/pagedown-resume.css | 434 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 media/pagedown-resume.css diff --git a/media/pagedown-resume.css b/media/pagedown-resume.css new file mode 100644 index 0000000000..659f959ad4 --- /dev/null +++ b/media/pagedown-resume.css @@ -0,0 +1,434 @@ +@page{ + size: letter portrait; + margin: 1in 0.5in 1in 0.25in; +} + +*{ + box-sizing: border-box; +} + +:root{ + --page-width: 8.5in; + --margin-right: 0.5in; + --margin-left: 0.25in; + --content-width: calc(var(--page-width) - var(--margin-right) - var(--margin-left)); + --root-font-size: 12pt; + --sidebar-width: 15rem; + --sidebar-background-color: #f2f2f2; + --main-width: calc(var(--content-width) - var(--sidebar-width)); + --decorator-horizontal-margin: 0.2in; + + --sidebar-horizontal-padding: 0.2in; + + /* XXX: using px for very good precision control */ + --decorator-outer-offset-top: 10px; + --decorator-outer-offset-left: -5.5px; + --decorator-border-width: 1px; + --decorator-outer-dim: 9px; + --decorator-border: 1px solid #ccc; + + --row-blocks-padding-top: 0.5rem; + --date-block-width: 0.7in; + + --main-blocks-title-icon-offset-left: calc(-17pt - 0.25 * var(--root-font-size)); + + --viewer-background-color: #dcdcdc; + --viewer-pages-spacing: 12px; + --viewer-shadow-color: #313131; +} + +.pagedjs_page { + --content-area-height: calc(var(--pagedjs-height) - var(--pagedjs-margin-top) - var(--pagedjs-margin-bottom)); + --sidebar-background-width: calc(var(--pagedjs-margin-right) + var(--sidebar-width)); +} + +@page{ + background: linear-gradient(to left, var(--sidebar-background-color), var(--sidebar-background-color) var(--sidebar-background-width), white var(--sidebar-background-width)); +} + +html { + font-size: var(--root-font-size); +} + +body{ + width: var(--content-width); + font-family: "Open Sans", sans-serif; + font-weight: 300; + line-height: 1.3; + color: #444; + hyphens: auto; +} + +h1, h2, h3{ + margin: 0; + color: #000; +} + +#main > h1, #aside > h1, #disclaimer > h2 { + display: none; +} + +li{ + list-style-type: none; +} + +a{ + text-decoration: none; +} + +img{ + max-width: 100%; +} + +#main{ + width: var(--main-width); + padding: 0 0.25in 0 0.25in; + font-size: 0.7rem; + float: left; +} + +#aside{ + position: relative; /* for disclaimer */ + height: var(--content-area-height); +} + +.aside{ + width: var(--sidebar-width); + padding: 0.6in var(--sidebar-horizontal-padding); + font-size: 0.8rem; + float: right; + position: absolute; + right: 0; +} + +/* main */ + +/** big title **/ +h1, h2{ + text-transform: uppercase; +} + +#title{ + position: relative; + left: 0.55in; + margin: auto 0.55in 0.3in auto; + line-height: 1.2; +} + +#title h1{ + font-weight: 300; + font-size: 1.8rem; + line-height: 1.5; +} + +#title h3{ + font-size: 0.8rem; +} + + +/*** categorial blocks ***/ + +.main-block{ + margin-top: 0.1in; +} + +#main h2{ + position: relative; + top: var(--row-blocks-padding-top); + /* XXX: 0.5px for aligning fx printing */ + left: calc((var(--date-block-width) + var(--decorator-horizontal-margin))); + font-weight: 400; + font-size: 1.1rem; + color: #555; +} + +#main h2 > i{ + /* use absolute position to prevent icon's width from misaligning title */ + /* assigning a fixed width here is no better due to needing to align decorator + line too */ + position: absolute; + left: var(--main-blocks-title-icon-offset-left); + z-index: 1; /* over decorator line */ + color: #444; +} + +#main h2::after{ /* extends the decorator line */ + height: calc(var(--row-blocks-padding-top) * 2); + position: relative; + top: calc(-1 * var(--row-blocks-padding-top)); + /* XXX: 0.5px for aligning fx printing */ + left: calc(-1 * var(--decorator-horizontal-margin)); + display: block; + border-left: var(--decorator-border); + z-index: 0; + line-height: 0; + font-size: 0; + content: ' '; +} + +/**** minor tweaks on the icon fonts ****/ +#main h2 > .fa-graduation-cap{ + left: calc(var(--main-blocks-title-icon-offset-left) - 2pt); + top: 2pt; +} + +#main h2 > .fa-suitcase{ + top: 1pt; +} + +#main h2 > .fa-folder-open{ + top: 1.5pt; +} + +/**** individual row blocks (date - decorator - details) ****/ + +.blocks{ + display: flex; + flex-flow: row nowrap; +} + +.blocks > div{ + padding-top: var(--row-blocks-padding-top); +} + +.date{ + flex: 0 0 var(--date-block-width); + padding-top: calc(var(--row-blocks-padding-top) + 0.25rem) !important; + padding-right: var(--decorator-horizontal-margin); + font-size: 0.7rem; + text-align: right; + line-height: 1; + max-width: var(--date-block-width); +} + +.date span{ + display: block; + text-align: center; +} + +.date span:nth-child(2)::before{ + position: relative; + top: 0.1rem; + right: 0; + display: block; + height: 1rem; + content: '|'; +} + +.decorator{ + flex: 0 0 0; + position: relative; + width: 2pt; + min-height: 100%; + border-left: var(--decorator-border); +} + +/* + * XXX: Use two filled circles to achieve the circle-with-white-border effect. + * The normal technique of only using one pseudo element and + * border: 1px solid white; style makes firefox erroneously either: + * 1) overflows the grayshade background (if no background-clip is set), or + * 2) shows decorator line which should've been masked by the white border + * + */ + +.decorator::before{ + position: absolute; + top: var(--decorator-outer-offset-top); + left: var(--decorator-outer-offset-left); + content: ' '; + display: block; + width: var(--decorator-outer-dim); + height: var(--decorator-outer-dim); + border-radius: calc(var(--decorator-outer-dim) / 2); + background-color: #fff; +} + +.decorator::after{ + position: absolute; + top: calc(var(--decorator-outer-offset-top) + var(--decorator-border-width)); + left: calc(var(--decorator-outer-offset-left) + var(--decorator-border-width)); + content: ' '; + display: block; + width: calc(var(--decorator-outer-dim) - (var(--decorator-border-width) * 2)); + height: calc(var(--decorator-outer-dim) - (var(--decorator-border-width) * 2)); + border-radius: calc((var(--decorator-outer-dim) - (var(--decorator-border-width) * 2)) / 2); + background-color: #555; +} + +.blocks:last-child .decorator{ /* slightly shortens it */ + margin-bottom: 0.25in; +} + +/***** fine-tunes on the details block where the real juice is *****/ + +.details{ + flex: 1 0 0; + padding-left: var(--decorator-horizontal-margin); + padding-top: calc(var(--row-blocks-padding-top) - 0.05rem) !important; /* not sure why but this is needed for better alignment */ +} + +.details header{ + color: #000; +} + +.details h3{ + font-size: 0.8rem; +} + +.main-block:not(.concise) .details div{ + margin: 0.18in 0 0.1in 0; +} + +.main-block:not(.concise) .details div:empty { + margin: 0; +} + +.main-block:not(.concise) .blocks:last-child .details div{ + margin-bottom: 0; +} + +.main-block.concise .details div:not(.concise){ + /* use padding to work around the fact that margin doesn't affect floated + neighboring elements */ + padding: 0.05in 0 0.07in 0; +} + +.details .place{ + float: left; + font-size: 0.75rem; +} + +.details .location{ + float: right; +} + +.details div{ + clear: both; +} + +/***** fine-tunes on the lists... *****/ + +#main ul{ + padding-left: 0.07in; + margin: 0.08in 0; +} + +#main li{ + margin: 0 0 0.025in 0; +} + +/****** customize list symbol style ******/ +#main li::before{ + position: relative; + margin-left: -4.25pt; + content: '• '; +} + +.aside li::before { + content: none; +} + +.details .concise ul{ + margin: 0 !important; + -webkit-columns: 2; + -moz-columns: 2; + columns: 2; +} + +.details .concise li{ + margin: 0 !important; +} + +.details .concise li{ + margin-left: 0 !important; +} + + + +/* sidebar */ + +.aside h2{ + font-weight: 400; + font-size: 1.1rem; +} + +.aside .level2{ + margin-top: 0.5in; +} + +#contact ul{ + margin-top: 0.05in; + padding-left: 0; + font-weight: 400; + line-height: 1.75; +} + +#contact li > i{ + width: 0.9rem; /* for text alignment */ + text-align: right; +} + +#skills{ + line-height: 1.5; +} + +#skills ul{ + margin: 0.05in 0 0.15in; + padding: 0; +} + +#disclaimer{ + position: absolute; + bottom: 0; + right: var(--sidebar-horizontal-padding); + font-size: 0.75rem; + font-style: italic; + line-height: 1.1; + text-align: right; + color: #777; +} + +#disclaimer code{ + color: #666; + font-family: "Source Code Pro"; + font-weight: 400; + font-style: normal; +} + +/* Page breaks */ + +h2 { + break-after: avoid; +} + +.blocks { + break-inside: avoid; +} + +/* Paged.js viewer */ + +@media screen { + body { + background-color: var(--viewer-background-color); + margin: 0; /* for mobile */ + width: calc(var(--pagedjs-width) + 2 * var(--viewer-pages-spacing)); /* for mobile */ + } + .pagedjs_pages { + max-width: var(--pagedjs-width); + margin: 0 auto; + display: flex; + flex-direction: column; + } + .pagedjs_page { + box-shadow: 0 0 calc(0.66667 * var(--viewer-pages-spacing)) var(--viewer-shadow-color); + margin: var(--viewer-pages-spacing) 0; + } +} + @media screen and (min-width: 8.5in) { + /* not a mobile */ + body { + margin: auto; + width: unset; + } +} From e899af342c5c7a89582f32940419f38cd8734c27 Mon Sep 17 00:00:00 2001 From: "J. Hathaway" Date: Wed, 5 Aug 2020 17:11:35 -0600 Subject: [PATCH 02/20] changed to resume --- README.md | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 99d2d44506..7841129115 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# markdown-cv +# md-resume -A curriculum vitae maintained in plain text and rendered to HTML and PDF using CSS. +A resume maintained in plain text and rendered to HTML and PDF using CSS. -For more details, see the [project page](http://elipapa.github.io/markdown-cv), or the blog post on [why I switched to markdown for my CV](http://elipapa.github.io/blog/why-i-switched-to-markdown-for-my-cv.html). +For more details, see the [markdown-csv project page](http://elipapa.github.io/markdown-cv), or the blog post on [why I switched to markdown for my CV](http://elipapa.github.io/blog/why-i-switched-to-markdown-for-my-cv.html). *** @@ -22,24 +22,15 @@ adding your skills, jobs and education. ## Distribution -To transform your plain text CV into a beautiful and shareable HTML page, you have two options: +To transform your plain text resume into a beautiful and shareable HTML page, you have two options: ### I. Use Github Pages to publish it online -1. Delete the existing `gh-pages` branch from your fork. It will only contain this webpage. You can either use git or [the Github web interface](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/#deleting-a-branch). -2. Create a new branch called `gh-pages`. -3. Head to *yourusername*.github.io/markdown-cv to see your CV live. +1. Create a new branch called `gh-pages`. +2. Head to *yourusername*.github.io/mdresume to see your resume live. Any change you want to make to your CV from then on would have to be done on the `gh-pages` branch and will be immediately rendered by Github Pages. -### II. Build it locally and print a PDF - -1. To [install jekyll](https://jekyllrb.com/docs/installation/), run `gem install bundler jekyll` from the command line. -3. [Clone](https://help.github.com/en/articles/cloning-a-repository) your fork of markdown-cv to your local machine. -3. Type `jekyll serve` to render your CV at http://localhost:4000. -4. You can edit the `index.md` file and see the changes live in your browser. -5. To print a PDF, press + p. Print and web CSS media queries should take care of the styling. - ## Styling The included CSS will render your CV in two styles: From 1fc3014964c668e46c3dc5bfb5e17eca2f4f289f Mon Sep 17 00:00:00 2001 From: "J. Hathaway" Date: Fri, 7 Aug 2020 08:01:34 -0600 Subject: [PATCH 03/20] put stuff into docs folder --- .DS_Store | Bin 0 -> 6148 bytes docs/.DS_Store | Bin 0 -> 6148 bytes _config.yml => docs/_config.yml | 0 docs/_layouts/.DS_Store | Bin 0 -> 6148 bytes {_layouts => docs/_layouts}/cv.html | 0 docs/index.md | 91 +++++++++++++++++++++ {media => docs/media}/davewhipp-print.css | 0 {media => docs/media}/davewhipp-screen.css | 0 {media => docs/media}/kjhealy-print.css | 0 {media => docs/media}/kjhealy-screen.css | 0 {media => docs/media}/pagedown-resume.css | 0 11 files changed, 91 insertions(+) create mode 100644 .DS_Store create mode 100644 docs/.DS_Store rename _config.yml => docs/_config.yml (100%) create mode 100644 docs/_layouts/.DS_Store rename {_layouts => docs/_layouts}/cv.html (100%) create mode 100644 docs/index.md rename {media => docs/media}/davewhipp-print.css (100%) rename {media => docs/media}/davewhipp-screen.css (100%) rename {media => docs/media}/kjhealy-print.css (100%) rename {media => docs/media}/kjhealy-screen.css (100%) rename {media => docs/media}/pagedown-resume.css (100%) diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f411205a9a9841e8c19799adf40055296ebc8344 GIT binary patch literal 6148 zcmeHK%}N6?5Kh{v*^1bMV2^w8)U#X?*7o zr4RkLrD$^eMF#NO6S5T0$TZYp9Af<5Nq%|k_7PeO=#@Fqm`pi&cBY#?T(NiA9{c@2FdpTO5~ zW_MdG)r$v_G6S>U?ELJqU&8(Xfba*M9e^4DNL0d-gUvTWand;{Sr4JmV<;Fv4p}tJ z?&q@E@gEtWZzrJ*354(nU%p?Is0d^4(R~tUd86?mO6AJZ@`{vISzW7M2R$_m@-QD{ ze%QaH-f5WkB5R*1l}zn0I*+@J2IhH1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 +isaac@applesdofall.org +| My wikipedia page + + + +## Currently + +Standing on the shoulders of giants + +### Specialized in + +Laws of motion, gravitation, minting coins, disliking [Robert Hooke](http://en.wikipedia.org/wiki/Robert_Hooke) + + +### Research interests + +Cooling, power series, optics, alchemy, planetary motions, apples. + + +## Education + +`1654-1660` +__The King's School, Grantham.__ + +`June 1661 - now` +__Trinity College, Cambridge__ + +- Sizar + +`1667 - death` +__Trinity College, Cambridge__ + +- Fellow + + + +## Awards + +`2012` +President, *Royal Society*, London, UK + +Associate, *French Academy of Science*, Paris, France + + + +## Publications + + + +### Journals + +`1669` +Newton Sir I, De analysi per æquationes numero terminorum infinitas. + +`1669` +Lectiones opticæ. + +etc. etc. etc. + +### Patents + +`2012` +Infinitesimal calculus for solutions to physics problems, [SMBC](http://www.techdirt.com/articles/20121011/09312820678/if-patents-had-been-around-time-newton.shtml) patent 001 + + +## Occupation + +`1600` +__Royal Mint__, London + +- Warden +- Minted coins + +`1600` +__Lucasian professor of Mathematics__, Cambridge University + + + + + + diff --git a/media/davewhipp-print.css b/docs/media/davewhipp-print.css similarity index 100% rename from media/davewhipp-print.css rename to docs/media/davewhipp-print.css diff --git a/media/davewhipp-screen.css b/docs/media/davewhipp-screen.css similarity index 100% rename from media/davewhipp-screen.css rename to docs/media/davewhipp-screen.css diff --git a/media/kjhealy-print.css b/docs/media/kjhealy-print.css similarity index 100% rename from media/kjhealy-print.css rename to docs/media/kjhealy-print.css diff --git a/media/kjhealy-screen.css b/docs/media/kjhealy-screen.css similarity index 100% rename from media/kjhealy-screen.css rename to docs/media/kjhealy-screen.css diff --git a/media/pagedown-resume.css b/docs/media/pagedown-resume.css similarity index 100% rename from media/pagedown-resume.css rename to docs/media/pagedown-resume.css From 5309f4bc1dd6ddc2bbcac77a555b483d7e957e88 Mon Sep 17 00:00:00 2001 From: "J. Hathaway" Date: Fri, 7 Aug 2020 08:14:19 -0600 Subject: [PATCH 04/20] changed name to resume --- docs/_layouts/{cv.html => resume.html} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/_layouts/{cv.html => resume.html} (82%) diff --git a/docs/_layouts/cv.html b/docs/_layouts/resume.html similarity index 82% rename from docs/_layouts/cv.html rename to docs/_layouts/resume.html index 991939d51b..803209ee62 100644 --- a/docs/_layouts/cv.html +++ b/docs/_layouts/resume.html @@ -2,7 +2,7 @@ - {% if page.title %} {{ page.title }} | {% endif %} CV + {% if page.title %} {{ page.title }} | {% endif %} Resume From e88707cd68690cee5313fe05ea20df1a4aec7ecd Mon Sep 17 00:00:00 2001 From: "J. Hathaway" Date: Fri, 7 Aug 2020 08:15:19 -0600 Subject: [PATCH 05/20] tried the other theme --- docs/_config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_config.yml b/docs/_config.yml index daba7398ab..1c4d9f3178 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,2 +1,2 @@ markdown: kramdown -style: kjhealy \ No newline at end of file +style: davewhipp \ No newline at end of file From 489ab1c95eb4a2e8818cdb28d07ddaf8503923fa Mon Sep 17 00:00:00 2001 From: "J. Hathaway" Date: Fri, 7 Aug 2020 08:17:01 -0600 Subject: [PATCH 06/20] back --- docs/_config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_config.yml b/docs/_config.yml index 1c4d9f3178..c9412b420e 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,2 +1,2 @@ markdown: kramdown -style: davewhipp \ No newline at end of file +style: kjhealy From 0f2733f89d3cd66dbe18bd6fe1467729df25459b Mon Sep 17 00:00:00 2001 From: "J. Hathaway" Date: Fri, 7 Aug 2020 08:20:51 -0600 Subject: [PATCH 07/20] change to resume --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index aa6c0cb838..c649285e10 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,5 @@ --- -layout: cv +layout: resume title: Isaac Newtons's CV --- # Isaac Newton From 62fdb9546e93cefe7a567dec07b377bf6d41a973 Mon Sep 17 00:00:00 2001 From: "J. Hathaway" Date: Fri, 7 Aug 2020 08:21:43 -0600 Subject: [PATCH 08/20] added CV format --- docs/_layouts/cv.html | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/_layouts/cv.html diff --git a/docs/_layouts/cv.html b/docs/_layouts/cv.html new file mode 100644 index 0000000000..991939d51b --- /dev/null +++ b/docs/_layouts/cv.html @@ -0,0 +1,16 @@ + + + + + {% if page.title %} {{ page.title }} | {% endif %} CV + + + + +
+
+ {{ content }} +
+
+ + \ No newline at end of file From a71188c33a86a9436e976a79fc7c3e738ab6838f Mon Sep 17 00:00:00 2001 From: "J. Hathaway" Date: Fri, 7 Aug 2020 08:22:56 -0600 Subject: [PATCH 09/20] cv --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index c649285e10..aa6c0cb838 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,5 @@ --- -layout: resume +layout: cv title: Isaac Newtons's CV --- # Isaac Newton From 411c8846edd1acdc91d36cb9d846f6e97a29ad89 Mon Sep 17 00:00:00 2001 From: "J. Hathaway" Date: Fri, 7 Aug 2020 08:26:47 -0600 Subject: [PATCH 10/20] changed theme --- docs/_config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_config.yml b/docs/_config.yml index c9412b420e..823f4de072 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,2 +1,2 @@ markdown: kramdown -style: kjhealy +style: davewhipp From c7aaf9aa02fb34b44dc7282a9c986d971225c297 Mon Sep 17 00:00:00 2001 From: "J. Hathaway" Date: Fri, 7 Aug 2020 08:34:04 -0600 Subject: [PATCH 11/20] test new style --- docs/_config.yml | 2 +- docs/_layouts/resume.html | 2 +- ...wn-resume.css => pagedownresume-print.css} | 0 docs/media/pagedownresume-screen.css | 434 ++++++++++++++++++ 4 files changed, 436 insertions(+), 2 deletions(-) rename docs/media/{pagedown-resume.css => pagedownresume-print.css} (100%) create mode 100644 docs/media/pagedownresume-screen.css diff --git a/docs/_config.yml b/docs/_config.yml index 823f4de072..0f1d8e1c92 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,2 +1,2 @@ markdown: kramdown -style: davewhipp +style: pagedownresume diff --git a/docs/_layouts/resume.html b/docs/_layouts/resume.html index 803209ee62..d72786c4eb 100644 --- a/docs/_layouts/resume.html +++ b/docs/_layouts/resume.html @@ -2,7 +2,7 @@ - {% if page.title %} {{ page.title }} | {% endif %} Resume + {% if page.title %} {{ page.title }} | {% endif %} RESUME diff --git a/docs/media/pagedown-resume.css b/docs/media/pagedownresume-print.css similarity index 100% rename from docs/media/pagedown-resume.css rename to docs/media/pagedownresume-print.css diff --git a/docs/media/pagedownresume-screen.css b/docs/media/pagedownresume-screen.css new file mode 100644 index 0000000000..659f959ad4 --- /dev/null +++ b/docs/media/pagedownresume-screen.css @@ -0,0 +1,434 @@ +@page{ + size: letter portrait; + margin: 1in 0.5in 1in 0.25in; +} + +*{ + box-sizing: border-box; +} + +:root{ + --page-width: 8.5in; + --margin-right: 0.5in; + --margin-left: 0.25in; + --content-width: calc(var(--page-width) - var(--margin-right) - var(--margin-left)); + --root-font-size: 12pt; + --sidebar-width: 15rem; + --sidebar-background-color: #f2f2f2; + --main-width: calc(var(--content-width) - var(--sidebar-width)); + --decorator-horizontal-margin: 0.2in; + + --sidebar-horizontal-padding: 0.2in; + + /* XXX: using px for very good precision control */ + --decorator-outer-offset-top: 10px; + --decorator-outer-offset-left: -5.5px; + --decorator-border-width: 1px; + --decorator-outer-dim: 9px; + --decorator-border: 1px solid #ccc; + + --row-blocks-padding-top: 0.5rem; + --date-block-width: 0.7in; + + --main-blocks-title-icon-offset-left: calc(-17pt - 0.25 * var(--root-font-size)); + + --viewer-background-color: #dcdcdc; + --viewer-pages-spacing: 12px; + --viewer-shadow-color: #313131; +} + +.pagedjs_page { + --content-area-height: calc(var(--pagedjs-height) - var(--pagedjs-margin-top) - var(--pagedjs-margin-bottom)); + --sidebar-background-width: calc(var(--pagedjs-margin-right) + var(--sidebar-width)); +} + +@page{ + background: linear-gradient(to left, var(--sidebar-background-color), var(--sidebar-background-color) var(--sidebar-background-width), white var(--sidebar-background-width)); +} + +html { + font-size: var(--root-font-size); +} + +body{ + width: var(--content-width); + font-family: "Open Sans", sans-serif; + font-weight: 300; + line-height: 1.3; + color: #444; + hyphens: auto; +} + +h1, h2, h3{ + margin: 0; + color: #000; +} + +#main > h1, #aside > h1, #disclaimer > h2 { + display: none; +} + +li{ + list-style-type: none; +} + +a{ + text-decoration: none; +} + +img{ + max-width: 100%; +} + +#main{ + width: var(--main-width); + padding: 0 0.25in 0 0.25in; + font-size: 0.7rem; + float: left; +} + +#aside{ + position: relative; /* for disclaimer */ + height: var(--content-area-height); +} + +.aside{ + width: var(--sidebar-width); + padding: 0.6in var(--sidebar-horizontal-padding); + font-size: 0.8rem; + float: right; + position: absolute; + right: 0; +} + +/* main */ + +/** big title **/ +h1, h2{ + text-transform: uppercase; +} + +#title{ + position: relative; + left: 0.55in; + margin: auto 0.55in 0.3in auto; + line-height: 1.2; +} + +#title h1{ + font-weight: 300; + font-size: 1.8rem; + line-height: 1.5; +} + +#title h3{ + font-size: 0.8rem; +} + + +/*** categorial blocks ***/ + +.main-block{ + margin-top: 0.1in; +} + +#main h2{ + position: relative; + top: var(--row-blocks-padding-top); + /* XXX: 0.5px for aligning fx printing */ + left: calc((var(--date-block-width) + var(--decorator-horizontal-margin))); + font-weight: 400; + font-size: 1.1rem; + color: #555; +} + +#main h2 > i{ + /* use absolute position to prevent icon's width from misaligning title */ + /* assigning a fixed width here is no better due to needing to align decorator + line too */ + position: absolute; + left: var(--main-blocks-title-icon-offset-left); + z-index: 1; /* over decorator line */ + color: #444; +} + +#main h2::after{ /* extends the decorator line */ + height: calc(var(--row-blocks-padding-top) * 2); + position: relative; + top: calc(-1 * var(--row-blocks-padding-top)); + /* XXX: 0.5px for aligning fx printing */ + left: calc(-1 * var(--decorator-horizontal-margin)); + display: block; + border-left: var(--decorator-border); + z-index: 0; + line-height: 0; + font-size: 0; + content: ' '; +} + +/**** minor tweaks on the icon fonts ****/ +#main h2 > .fa-graduation-cap{ + left: calc(var(--main-blocks-title-icon-offset-left) - 2pt); + top: 2pt; +} + +#main h2 > .fa-suitcase{ + top: 1pt; +} + +#main h2 > .fa-folder-open{ + top: 1.5pt; +} + +/**** individual row blocks (date - decorator - details) ****/ + +.blocks{ + display: flex; + flex-flow: row nowrap; +} + +.blocks > div{ + padding-top: var(--row-blocks-padding-top); +} + +.date{ + flex: 0 0 var(--date-block-width); + padding-top: calc(var(--row-blocks-padding-top) + 0.25rem) !important; + padding-right: var(--decorator-horizontal-margin); + font-size: 0.7rem; + text-align: right; + line-height: 1; + max-width: var(--date-block-width); +} + +.date span{ + display: block; + text-align: center; +} + +.date span:nth-child(2)::before{ + position: relative; + top: 0.1rem; + right: 0; + display: block; + height: 1rem; + content: '|'; +} + +.decorator{ + flex: 0 0 0; + position: relative; + width: 2pt; + min-height: 100%; + border-left: var(--decorator-border); +} + +/* + * XXX: Use two filled circles to achieve the circle-with-white-border effect. + * The normal technique of only using one pseudo element and + * border: 1px solid white; style makes firefox erroneously either: + * 1) overflows the grayshade background (if no background-clip is set), or + * 2) shows decorator line which should've been masked by the white border + * + */ + +.decorator::before{ + position: absolute; + top: var(--decorator-outer-offset-top); + left: var(--decorator-outer-offset-left); + content: ' '; + display: block; + width: var(--decorator-outer-dim); + height: var(--decorator-outer-dim); + border-radius: calc(var(--decorator-outer-dim) / 2); + background-color: #fff; +} + +.decorator::after{ + position: absolute; + top: calc(var(--decorator-outer-offset-top) + var(--decorator-border-width)); + left: calc(var(--decorator-outer-offset-left) + var(--decorator-border-width)); + content: ' '; + display: block; + width: calc(var(--decorator-outer-dim) - (var(--decorator-border-width) * 2)); + height: calc(var(--decorator-outer-dim) - (var(--decorator-border-width) * 2)); + border-radius: calc((var(--decorator-outer-dim) - (var(--decorator-border-width) * 2)) / 2); + background-color: #555; +} + +.blocks:last-child .decorator{ /* slightly shortens it */ + margin-bottom: 0.25in; +} + +/***** fine-tunes on the details block where the real juice is *****/ + +.details{ + flex: 1 0 0; + padding-left: var(--decorator-horizontal-margin); + padding-top: calc(var(--row-blocks-padding-top) - 0.05rem) !important; /* not sure why but this is needed for better alignment */ +} + +.details header{ + color: #000; +} + +.details h3{ + font-size: 0.8rem; +} + +.main-block:not(.concise) .details div{ + margin: 0.18in 0 0.1in 0; +} + +.main-block:not(.concise) .details div:empty { + margin: 0; +} + +.main-block:not(.concise) .blocks:last-child .details div{ + margin-bottom: 0; +} + +.main-block.concise .details div:not(.concise){ + /* use padding to work around the fact that margin doesn't affect floated + neighboring elements */ + padding: 0.05in 0 0.07in 0; +} + +.details .place{ + float: left; + font-size: 0.75rem; +} + +.details .location{ + float: right; +} + +.details div{ + clear: both; +} + +/***** fine-tunes on the lists... *****/ + +#main ul{ + padding-left: 0.07in; + margin: 0.08in 0; +} + +#main li{ + margin: 0 0 0.025in 0; +} + +/****** customize list symbol style ******/ +#main li::before{ + position: relative; + margin-left: -4.25pt; + content: '• '; +} + +.aside li::before { + content: none; +} + +.details .concise ul{ + margin: 0 !important; + -webkit-columns: 2; + -moz-columns: 2; + columns: 2; +} + +.details .concise li{ + margin: 0 !important; +} + +.details .concise li{ + margin-left: 0 !important; +} + + + +/* sidebar */ + +.aside h2{ + font-weight: 400; + font-size: 1.1rem; +} + +.aside .level2{ + margin-top: 0.5in; +} + +#contact ul{ + margin-top: 0.05in; + padding-left: 0; + font-weight: 400; + line-height: 1.75; +} + +#contact li > i{ + width: 0.9rem; /* for text alignment */ + text-align: right; +} + +#skills{ + line-height: 1.5; +} + +#skills ul{ + margin: 0.05in 0 0.15in; + padding: 0; +} + +#disclaimer{ + position: absolute; + bottom: 0; + right: var(--sidebar-horizontal-padding); + font-size: 0.75rem; + font-style: italic; + line-height: 1.1; + text-align: right; + color: #777; +} + +#disclaimer code{ + color: #666; + font-family: "Source Code Pro"; + font-weight: 400; + font-style: normal; +} + +/* Page breaks */ + +h2 { + break-after: avoid; +} + +.blocks { + break-inside: avoid; +} + +/* Paged.js viewer */ + +@media screen { + body { + background-color: var(--viewer-background-color); + margin: 0; /* for mobile */ + width: calc(var(--pagedjs-width) + 2 * var(--viewer-pages-spacing)); /* for mobile */ + } + .pagedjs_pages { + max-width: var(--pagedjs-width); + margin: 0 auto; + display: flex; + flex-direction: column; + } + .pagedjs_page { + box-shadow: 0 0 calc(0.66667 * var(--viewer-pages-spacing)) var(--viewer-shadow-color); + margin: var(--viewer-pages-spacing) 0; + } +} + @media screen and (min-width: 8.5in) { + /* not a mobile */ + body { + margin: auto; + width: unset; + } +} From 2a01bf84b874d91312e175b6843bb86140401146 Mon Sep 17 00:00:00 2001 From: "J. Hathaway" Date: Fri, 7 Aug 2020 09:04:16 -0600 Subject: [PATCH 12/20] testing pagedown stuff --- .DS_Store | Bin 6148 -> 6148 bytes .gitignore | 5 +- docs/.DS_Store | Bin 6148 -> 8196 bytes docs/html/resume.html | 144 + docs/js/chrome_print.js | 68 + docs/js/config.js | 81 + docs/js/hooks.js | 510 + docs/js/paged-latest.js | 23 + docs/js/paged.js | 27610 ++++++++++++++++++++++++++++++++++++++ docs/lua/footnotes.lua | 27 + docs/lua/jss.lua | 99 + docs/lua/loft.lua | 121 + docs/lua/uri-to-fn.lua | 59 + index.md | 10 +- 14 files changed, 28751 insertions(+), 6 deletions(-) create mode 100644 docs/html/resume.html create mode 100644 docs/js/chrome_print.js create mode 100644 docs/js/config.js create mode 100644 docs/js/hooks.js create mode 100644 docs/js/paged-latest.js create mode 100644 docs/js/paged.js create mode 100644 docs/lua/footnotes.lua create mode 100644 docs/lua/jss.lua create mode 100644 docs/lua/loft.lua create mode 100644 docs/lua/uri-to-fn.lua diff --git a/.DS_Store b/.DS_Store index f411205a9a9841e8c19799adf40055296ebc8344..9e1fa1373a6b10c432317f6004fb78a753f18349 100644 GIT binary patch delta 339 zcmZoMXfc@JFUZBfz`)4BAi$85ZWx@LpIfk5ka;y@JxG{^L60GwA(Np5CIu8@U^t_` z0a-3L-^C>S18GasfoLFr)}E9%MSpR>sBWK delta 149 zcmZoMXfc@JFUrKgz`)4BAi$7RUR;orlb;0SpWK+ZoN=;&2#Y8SLkdGaLo!1#vh3tJ zObwnA)zv1ZraB5Hre?J|3f1Q320*s4S#2#Rhq$Vtt!F}RWmR=eZQV?uc|gDjv;z$I Xp)`z|wfPRSHp|3<_nX-{{_+C=!}%iK diff --git a/.gitignore b/.gitignore index 2b8ee31c0d..8d3a04857d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ _site/ -.sass-cache/ \ No newline at end of file +.sass-cache/ +.DS_Store +*.DS_Store +.DS_Store? diff --git a/docs/.DS_Store b/docs/.DS_Store index 5e793e66dadc72110e2fd64147bf2a75cc26c898..5d00259ba084c8c9251ab46407959bbb97812882 100644 GIT binary patch literal 8196 zcmeHMzmF0@6n+DoE)e4tmmG-}n;5MqAi!NLG%I3ZXR<~MHQcUC0JH26SS06)+5bTc zwX!wVCN?%!XQ8pPv-dBsF~&DP+}nl4Tw3F!Htp*eWiUGxdVn8vV7+3`c@R`kuw&b}l zP}M316a)Vy1MGazu`+2$84Agd4s3V|fHH@}ykH;m0B)m8X-F9g$xU%ikv$M;N@R*b zggN3mQ%7k?844-Pfe3RTa%Cbj6v9_WJDsZoF@#jDVn8vl$N2TQ*|L9~Qa9>b_KkjL z+TPr{Og-P~dz}$C?mHb+xqY+mIds~j6VDm*c}xLlT3*YyH%mcKZ#2q!y|FVZ>%s1B z1^cy~*(|SZpRHcFx;NN=5ImecdM11$@_c}kbG|-LbJXot&AW6UFDp(tX*vViYDqG* zGjG3K{NTO2l&Pa_X7QMrfR$Js&b`6aC9-7w2u%<>cq3gEVd|{KT@Dor6P=8OQ?y0d64S3evu@@H_Klei=uQ`3#H<3@jj;31R_Q KY;!!%9A*GIAr>D1 diff --git a/docs/html/resume.html b/docs/html/resume.html new file mode 100644 index 0000000000..dec8192053 --- /dev/null +++ b/docs/html/resume.html @@ -0,0 +1,144 @@ + + + + + + + + + $for(author-meta)$$endfor$ + $if(date-meta)$$endif$ + $if(title-prefix)$$title-prefix$ - $endif$$pagetitle$ + + $if(math)$ + $if(mathjax)$ + + + $endif$ + $endif$ + + $for(header-includes)$ + $header-includes$ + $endfor$ + + $for(css)$ + + $endfor$ + + + +$for(include-before)$ +$include-before$ +$endfor$ + +$body$ + + + +$for(include-after)$ +$include-after$ +$endfor$ + + + diff --git a/docs/js/chrome_print.js b/docs/js/chrome_print.js new file mode 100644 index 0000000000..581687b80e --- /dev/null +++ b/docs/js/chrome_print.js @@ -0,0 +1,68 @@ +{ + let RevealReady = new Promise((resolve) => { + window.addEventListener( + 'DOMContentLoaded', + () => { + if (window.Reveal) { + if (!window.location.search.match( /print-pdf/gi )) { + window.location.search = 'print-pdf'; + return; + } + Reveal.addEventListener('ready', resolve); + if (Reveal.isReady()) resolve(); + } else { + resolve(); + } + }, + {capture: true, once: true} + ); + }); + + let HTMLWidgetsReady = new Promise((resolve) => { + window.addEventListener( + 'DOMContentLoaded', + () => { + if (window.HTMLWidgets) { + HTMLWidgets.addPostRenderHandler(resolve); + } else { + resolve(); + } + }, + {capture: true, once: true} + ); + }); + + let MathJaxReady = new Promise((resolve) => { + window.addEventListener( + 'load', + () => { + if (window.MathJax) { + if (window.MathJax.Hub) { + window.MathJax.Hub.Register.StartupHook('Begin', () => { + window.MathJax.Hub.Queue(resolve); + }); + } else { + let previousAuthorInit = window.MathJax.AuthorInit || function () {}; + window.MathJax.AuthorInit = function() { + previousAuthorInit(); + MathJax.Hub.Register.StartupHook('Begin', () => { + MathJax.Hub.Queue(resolve); + }); + }; + } + } + else { + resolve(); + } + }, + {capture: true, once: true} + ); + }); + + window.pagedownReady = Promise.all([ + RevealReady, + MathJaxReady, + HTMLWidgetsReady, + document.fonts.ready + ]); +} diff --git a/docs/js/config.js b/docs/js/config.js new file mode 100644 index 0000000000..e926a7fdae --- /dev/null +++ b/docs/js/config.js @@ -0,0 +1,81 @@ +// Configuration script for paged.js + +(function() { + // Retrieve previous config object if defined + window.PagedConfig = window.PagedConfig || {}; + const {before: beforePaged, after: afterPaged} = window.PagedConfig; + + // utils + const insertCSS = text => { + let style = document.createElement('style'); + style.type = 'text/css'; + style.appendChild(document.createTextNode(text)); + document.head.appendChild(style); + }; + + // Util function for front and back covers images + const insertCSSForCover = type => { + const links = document.querySelectorAll('link[id^=' + type + ']'); + if (!links.length) return; + const re = new RegExp(type + '-\\d+'); + let text = ':root {--' + type + ': var(--' + type + '-1);'; + for (const link of links) { + text += '--' + re.exec(link.id)[0] + ': url("' + link.href + '");'; + } + text += '}'; + insertCSS(text); + }; + + const insertPageBreaksCSS = () => { + insertCSS(` + .page-break-after {break-after: page;} + .page-break-before {break-before: page;} + `); + }; + + window.PagedConfig.before = async () => { + // Front and back covers support + let frontCover = document.querySelector('.front-cover'); + let backCover = document.querySelector('.back-cover'); + if (frontCover) document.body.prepend(frontCover); + if (backCover) document.body.append(backCover); + insertCSSForCover('front-cover'); + insertCSSForCover('back-cover'); + insertPageBreaksCSS(); + + if (beforePaged) await beforePaged(); + }; + + window.PagedConfig.after = (flow) => { + // force redraw, see https://github.com/rstudio/pagedown/issues/35#issuecomment-475905361 + // and https://stackoverflow.com/a/24753578/6500804 + document.body.style.display = 'none'; + document.body.offsetHeight; + document.body.style.display = ''; + + // run previous PagedConfig.after function if defined + if (afterPaged) afterPaged(flow); + + // pagedownListener is a binding added by the chrome_print function + // this binding exists only when chrome_print opens the html file + if (window.pagedownListener) { + // the html file is opened for printing + // call the binding to signal to the R session that Paged.js has finished + pagedownListener(JSON.stringify({ + pagedjs: true, + pages: flow.total, + elapsedtime: flow.performance + })); + return; + } + if (sessionStorage.getItem('pagedown-scroll')) { + // scroll to the last position before the page is reloaded + window.scrollTo(0, sessionStorage.getItem('pagedown-scroll')); + return; + } + if (window.location.hash) { + const id = window.location.hash.replace(/^#/, ''); + document.getElementById(id).scrollIntoView({behavior: 'smooth'}); + } + }; +})(); diff --git a/docs/js/hooks.js b/docs/js/hooks.js new file mode 100644 index 0000000000..76f00f0549 --- /dev/null +++ b/docs/js/hooks.js @@ -0,0 +1,510 @@ +// Hooks for paged.js +{ + // Utils + let pandocMeta, pandocMetaToString; + { + let el = document.getElementById('pandoc-meta'); + pandocMeta = el ? JSON.parse(el.firstChild.data) : {}; + } + + pandocMetaToString = meta => { + let el = document.createElement('div'); + el.innerHTML = meta; + return el.innerText; + }; + + let isString = value => { + return typeof value === 'string' || value instanceof String; + }; + + let isArray = value => { + return value && typeof value === 'object' && value.constructor === Array; + }; + + // This hook is an attempt to fix https://github.com/rstudio/pagedown/issues/131 + // Sometimes, the {break-after: avoid;} declaration applied on headers + // lead to duplicated headers. I hate this bug. + // This is linked to the way the HTML source is written + // When we have the \n character like this:
\n

...

+ // the header may be duplicated. + // But, if we have

...

without any \n, the problem disappear + // I think this handler can fix most of cases + // Obviously, we cannot suppress all the \n in the HTML document + // because carriage returns are important in
 elements.
+  // Tested with Chrome 76.0.3809.100/Windows
+  Paged.registerHandlers(class extends Paged.Handler {
+    constructor(chunker, polisher, caller) {
+      super(chunker, polisher, caller);
+      this.carriageReturn = String.fromCharCode(10);
+    }
+
+    checkNode(node) {
+      if (!node) return;
+      if (node.nodeType !== 3) return;
+      if (node.textContent === this.carriageReturn) {
+        node.remove();
+      }
+    }
+
+    afterParsed(parsed) {
+      let template = document.querySelector('template').content;
+      const breakAfterAvoidElements = template.querySelectorAll('[data-break-after="avoid"], [data-break-before="avoid"]');
+      for (let el of breakAfterAvoidElements) {
+        this.checkNode(el.previousSibling);
+        this.checkNode(el.nextSibling);
+      }
+    }
+  });
+
+  // This hook creates a list of abbreviations
+  // Note: we also could implement this feature using a Pandoc filter
+  Paged.registerHandlers(class extends Paged.Handler {
+    constructor(chunker, polisher, caller) {
+      super(chunker, polisher, caller);
+    }
+    beforeParsed(content) {
+      const abbreviations = content.querySelectorAll('abbr');
+      if(abbreviations.length === 0) return;
+      const loaTitle = 'List of Abbreviations';
+      const loaId = 'LOA';
+      const tocList = content.querySelector('.toc ul');
+      let listOfAbbreviations = document.createElement('div');
+      let descriptionList = document.createElement('dl');
+      content.appendChild(listOfAbbreviations);
+      listOfAbbreviations.id = loaId;
+      listOfAbbreviations.classList.add('section', 'front-matter', 'level1', 'loa');
+      listOfAbbreviations.innerHTML = '

' + loaTitle + '

'; + listOfAbbreviations.appendChild(descriptionList); + for(let abbr of abbreviations) { + if(!abbr.title) continue; + let term = document.createElement('dt'); + let definition = document.createElement('dd'); + descriptionList.appendChild(term); + descriptionList.appendChild(definition); + term.innerHTML = abbr.innerHTML; + definition.innerText = abbr.title; + } + if (tocList) { + const loaTOCItem = document.createElement('li'); + loaTOCItem.innerHTML = '' + loaTitle + ''; + tocList.appendChild(loaTOCItem); + } + } + }); + + // This hook moves the sections of class front-matter in the div.front-matter-container + Paged.registerHandlers(class extends Paged.Handler { + constructor(chunker, polisher, caller) { + super(chunker, polisher, caller); + } + + beforeParsed(content) { + const frontMatter = content.querySelector('.front-matter-container'); + if (!frontMatter) return; + + // move front matter sections in the front matter container + const frontMatterSections = content.querySelectorAll('.level1.front-matter'); + for (const section of frontMatterSections) { + frontMatter.appendChild(section); + } + + // add the class front-matter-ref to any element + // referring to an entry in the front matter + const anchors = content.querySelectorAll('a[href^="#"]:not([href*=":"])'); + for (const a of anchors) { + const ref = a.getAttribute('href').replace(/^#/, ''); + const element = content.getElementById(ref); + if (frontMatter.contains(element)) a.classList.add('front-matter-ref'); + } + + // update the toc, lof and lot for front matter sections + const frontMatterSectionsLinks = content.querySelectorAll('.toc .front-matter-ref, .lof .front-matter-ref, .lot .front-matter-ref'); + for (let i = frontMatterSectionsLinks.length - 1; i >= 0; i--) { + const listItem = frontMatterSectionsLinks[i].parentNode; + const list = listItem.parentNode; + list.insertBefore(listItem, list.firstChild); + } + } + }); + + // This hook expands the links in the lists of figures and tables + Paged.registerHandlers(class extends Paged.Handler { + constructor(chunker, polisher, caller) { + super(chunker, polisher, caller); + } + + beforeParsed(content) { + const items = content.querySelectorAll('.lof li, .lot li'); + for (const item of items) { + const anchor = item.firstChild; + anchor.innerText = item.innerText; + item.innerText = ''; + item.append(anchor); + } + } + }); + + // This hook adds spans for leading symbols + Paged.registerHandlers(class extends Paged.Handler { + constructor(chunker, polisher, caller) { + super(chunker, polisher, caller); + } + + beforeParsed(content) { + const anchors = content.querySelectorAll('.toc a, .lof a, .lot a'); + for (const a of anchors) { + a.innerHTML = a.innerHTML + ''; + } + } + }); + + // This hook appends short titles spans + Paged.registerHandlers(class extends Paged.Handler { + constructor(chunker, polisher, caller) { + super(chunker, polisher, caller); + } + + beforeParsed(content) { + /* A factory returning a function that appends short titles spans. + The text content of these spans are reused for running titles (see default.css). + Argument: level - An integer between 1 and 6. + */ + function appendShortTitleSpans(level) { + return () => { + const divs = Array.from(content.querySelectorAll('.level' + level)); + + function addSpan(div) { + const mainHeader = div.getElementsByTagName('h' + level)[0]; + if (!mainHeader) return; + const mainTitle = mainHeader.textContent; + const spanSectionNumber = mainHeader.getElementsByClassName('header-section-number')[0]; + const mainNumber = !!spanSectionNumber ? spanSectionNumber.textContent : ''; + const runningTitle = 'shortTitle' in div.dataset ? mainNumber + ' ' + div.dataset.shortTitle : mainTitle; + const span = document.createElement('span'); + span.className = 'shorttitle' + level; + span.innerText = runningTitle; + span.style.display = "none"; + mainHeader.appendChild(span); + if (level == 1 && div.querySelector('.level2') === null) { + let span2 = document.createElement('span'); + span2.className = 'shorttitle2'; + span2.innerText = ' '; + span2.style.display = "none"; + span.insertAdjacentElement('afterend', span2); + } + } + + for (const div of divs) { + addSpan(div); + } + }; + } + + appendShortTitleSpans(1)(); + appendShortTitleSpans(2)(); + } + }); + + // Footnotes support + Paged.registerHandlers(class extends Paged.Handler { + constructor(chunker, polisher, caller) { + super(chunker, polisher, caller); + + this.splittedParagraphRefs = []; + } + + beforeParsed(content) { + // remove footnotes in toc, lof, lot + // see https://github.com/rstudio/pagedown/issues/54 + let removeThese = content.querySelectorAll('.toc .footnote, .lof .footnote, .lot .footnote'); + for (const el of removeThese) { + el.remove(); + } + + let footnotes = content.querySelectorAll('.footnote'); + + for (let footnote of footnotes) { + let parentElement = footnote.parentElement; + let footnoteCall = document.createElement('a'); + let footnoteNumber = footnote.dataset.pagedownFootnoteNumber; + + footnoteCall.className = 'footnote-ref'; // same class as Pandoc + footnoteCall.setAttribute('id', 'fnref' + footnoteNumber); // same notation as Pandoc + footnoteCall.setAttribute('href', '#' + footnote.id); + footnoteCall.innerHTML = '' + footnoteNumber +''; + parentElement.insertBefore(footnoteCall, footnote); + + // Here comes a hack. Fortunately, it works with Chrome and FF. + let handler = document.createElement('p'); + handler.className = 'footnoteHandler'; + parentElement.insertBefore(handler, footnote); + handler.appendChild(footnote); + handler.style.display = 'inline-block'; + handler.style.width = '100%'; + handler.style.float = 'right'; + handler.style.pageBreakInside = 'avoid'; + } + } + + afterPageLayout(pageFragment, page, breakToken) { + function hasItemParent(node) { + if (node.parentElement === null) { + return false; + } else { + if (node.parentElement.tagName === 'LI') { + return true; + } else { + return hasItemParent(node.parentElement); + } + } + } + // If a li item is broken, we store the reference of the p child element + // see https://github.com/rstudio/pagedown/issues/23#issue-376548000 + if (breakToken !== undefined) { + if (breakToken.node.nodeName === "#text" && hasItemParent(breakToken.node)) { + this.splittedParagraphRefs.push(breakToken.node.parentElement.dataset.ref); + } + } + } + + afterRendered(pages) { + for (let page of pages) { + const footnotes = page.element.querySelectorAll('.footnote'); + if (footnotes.length === 0) { + continue; + } + + const pageContent = page.element.querySelector('.pagedjs_page_content'); + let hr = document.createElement('hr'); + let footnoteArea = document.createElement('div'); + + pageContent.style.display = 'flex'; + pageContent.style.flexDirection = 'column'; + + hr.className = 'footnote-break'; + hr.style.marginTop = 'auto'; + hr.style.marginBottom = 0; + hr.style.marginLeft = 0; + hr.style.marginRight = 'auto'; + pageContent.appendChild(hr); + + footnoteArea.className = 'footnote-area'; + pageContent.appendChild(footnoteArea); + + for (let footnote of footnotes) { + let handler = footnote.parentElement; + + footnoteArea.appendChild(footnote); + handler.parentNode.removeChild(handler); + + footnote.innerHTML = '' + footnote.dataset.pagedownFootnoteNumber + '' + footnote.innerHTML; + footnote.style.fontSize = 'x-small'; + footnote.style.marginTop = 0; + footnote.style.marginBottom = 0; + footnote.style.paddingTop = 0; + footnote.style.paddingBottom = 0; + footnote.style.display = 'block'; + } + } + + for (let ref of this.splittedParagraphRefs) { + let paragraphFirstPage = document.querySelector('[data-split-to="' + ref + '"]'); + // We test whether the paragraph is empty + // see https://github.com/rstudio/pagedown/issues/23#issue-376548000 + if (paragraphFirstPage.innerText === "") { + paragraphFirstPage.parentElement.style.display = "none"; + let paragraphSecondPage = document.querySelector('[data-split-from="' + ref + '"]'); + paragraphSecondPage.parentElement.style.setProperty('list-style', 'inherit', 'important'); + } + } + } + }); + + // Support for "Chapter " label on section with class `.chapter` + Paged.registerHandlers(class extends Paged.Handler { + constructor(chunker, polisher, caller) { + super(chunker, polisher, caller); + + this.options = pandocMeta['chapter_name']; + + let styles; + if (isString(this.options)) { + this.options = pandocMetaToString(this.options); + styles = ` + :root { + --chapter-name-before: "${this.options}"; + } + `; + } + if (isArray(this.options)) { + this.options = this.options.map(pandocMetaToString); + styles = ` + :root { + --chapter-name-before: "${this.options[0]}"; + --chapter-name-after: "${this.options[1]}"; + } + `; + } + if (styles) polisher.insert(styles); + } + + beforeParsed(content) { + const tocAnchors = content.querySelectorAll('.toc a[href^="#"]:not([href*=":"]'); + for (const anchor of tocAnchors) { + const ref = anchor.getAttribute('href').replace(/^#/, ''); + const element = content.getElementById(ref); + if (element.classList.contains('chapter')) { + anchor.classList.add('chapter-ref'); + } + } + } + }); + + // Main text line numbering, + // see https://github.com/rstudio/pagedown/issues/115 + // Original idea: Julien Taquet, thanks! + Paged.registerHandlers(class extends Paged.Handler { + constructor(chunker, polisher, caller) { + super(chunker, polisher, caller); + + // get the number-lines option from Pandoc metavariables + this.options = pandocMeta['number-lines']; + // quit early if the "number-lines" option is false or missing + if (!this.options) return; + // retrieve the selector if provided, otherwise use the default selector + this.selector = this.options.selector ? pandocMetaToString(this.options.selector) : '.level1:not(.front-matter) h1, .level1 h2, .level1 h3, .level1 p'; + + const styles = ` + :root { + --line-numbers-padding-right: 10px; + --line-numbers-font-size: 8pt; + } + .pagedown-linenumbers-container { + position: absolute; + margin-top: var(--pagedjs-margin-top); + right: calc(var(--pagedjs-width) - var(--pagedjs-margin-left)); + } + .maintextlinenumbers { + position: absolute; + right: 0; + text-align: right; + padding-right: var(--line-numbers-padding-right); + font-size: var(--line-numbers-font-size); + } + `; + polisher.insert(styles); + + this.resetLinesCounter(); + } + + appendLineNumbersContainer(page) { + const pagebox = page.element.querySelector('.pagedjs_pagebox'); + const lineNumbersContainer = document.createElement('div'); + lineNumbersContainer.classList.add('pagedown-linenumbers-container'); + + return pagebox.appendChild(lineNumbersContainer); + } + + lineHeight(element) { + // If the document stylesheet does not define a value for line-height, + // Blink returns "normal". Therefore, parseInt may return NaN. + return parseInt(getComputedStyle(element).lineHeight); + } + + innerHeight(element) { + let outerHeight = element.getBoundingClientRect().height; + let {borderTopWidth, + borderBottomWidth, + paddingTop, + paddingBottom} = getComputedStyle(element); + + borderTopWidth = parseFloat(borderTopWidth); + borderBottomWidth = parseFloat(borderBottomWidth); + paddingTop = parseFloat(paddingTop); + paddingBottom = parseFloat(paddingBottom); + + return Math.round(outerHeight - borderTopWidth - borderBottomWidth - paddingTop - paddingBottom); + } + + arrayOfInt(from, length) { + // adapted from https://stackoverflow.com/a/50234108/6500804 + return Array.from(Array(length).keys(), n => n + from); + } + + incrementLinesCounter(value) { + this.linesCounter = this.linesCounter + value; + } + + resetLinesCounter() { + this.linesCounter = 0; + } + + isDisplayMath(element) { + const nodes = element.childNodes; + if (nodes.length != 1) return false; + return (nodes[0].nodeName === 'SPAN') && (nodes[0].classList.value === 'math display'); + } + + afterRendered(pages) { + if (!this.options) return; + + for (let page of pages) { + const lineNumbersContainer = this.appendLineNumbersContainer(page); + const pageAreaY = page.area.getBoundingClientRect().y; + let elementsToNumber = page.area.querySelectorAll(this.selector); + + for (let element of elementsToNumber) { + // Do not add line numbers for display math environment + if (this.isDisplayMath(element)) continue; + + // Try to retrieve line height + const lineHeight = this.lineHeight(element); + // Test against lineHeight method returns NaN + if (!lineHeight) { + console.warn('Failed to compute line height value on "' + page.id + '".'); + continue; + } + + const innerHeight = this.innerHeight(element); + + // Number of lines estimation + // There is no built-in method to detect the number of lines in a block. + // The main caveat is that an actual line height can differ from + // the line-height CSS property. + // Mixed fonts, subscripts, superscripts, inline math... can increase + // the actual line height. + // Here, we divide the inner height of the block by the line-height + // computed property and round to the floor to take into account that + // sometimes the actual line height is greater than its property value. + // This is far from perfect and can be easily broken especially by + // inline math. + const nLines = Math.floor(innerHeight / lineHeight); + + // do not add line numbers for void paragraphs + if (nLines <= 0) continue; + + const linenumbers = document.createElement('div'); + lineNumbersContainer.appendChild(linenumbers); + linenumbers.classList.add('maintextlinenumbers'); + + const elementY = element.getBoundingClientRect().y; + linenumbers.style.top = (elementY - pageAreaY) + 'px'; + + const cs = getComputedStyle(element); + const paddingTop = parseFloat(cs.paddingTop) + parseFloat(cs.borderTopWidth); + linenumbers.style.paddingTop = paddingTop + 'px'; + + linenumbers.style.lineHeight = cs.lineHeight; + + linenumbers.innerHTML = this.arrayOfInt(this.linesCounter + 1, nLines) + .reduce((t, v) => t + '
' + v); + this.incrementLinesCounter(nLines); + } + + if (this.options['reset-page']) { + this.resetLinesCounter(); + } + } + } + }); +} diff --git a/docs/js/paged-latest.js b/docs/js/paged-latest.js new file mode 100644 index 0000000000..731af820c0 --- /dev/null +++ b/docs/js/paged-latest.js @@ -0,0 +1,23 @@ +{ + config = window.PagedConfig || {}; + window.PagedConfig = {auto: false}; + + let pagedjs = document.createElement('script'); + pagedjs.src = 'https://unpkg.com/pagedjs/dist/paged.polyfill.js'; + let pagedjsLoaded = new Promise(_ => pagedjs.onload = _); + + let hooks = document.createElement('script'); + hooks.src = document.head + .querySelector('script[src*="config.js"]') + .src + .replace('config', 'hooks'); + let hooksLoaded = new Promise(_ => hooks.onload = _); + + document.head.appendChild(pagedjs); + pagedjsLoaded.then(() => document.head.appendChild(hooks)); + hooksLoaded.then(async () => { + if (config.before) await config.before(); + await PagedPolyfill.preview(); + if (config.after) config.after(); + }); +} diff --git a/docs/js/paged.js b/docs/js/paged.js new file mode 100644 index 0000000000..b013e14d52 --- /dev/null +++ b/docs/js/paged.js @@ -0,0 +1,27610 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + global.PagedPolyfill = factory(); +}(typeof self !== 'undefined' ? self : this, function () { 'use strict'; + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + function getCjsExportFromNamespace (n) { + return n && n.default || n; + } + + var isImplemented = function () { + var assign = Object.assign, obj; + if (typeof assign !== "function") return false; + obj = { foo: "raz" }; + assign(obj, { bar: "dwa" }, { trzy: "trzy" }); + return (obj.foo + obj.bar + obj.trzy) === "razdwatrzy"; + }; + + var isImplemented$1 = function () { + try { + return true; + } catch (e) { + return false; + } + }; + + // eslint-disable-next-line no-empty-function + var noop = function () {}; + + var _undefined = noop(); // Support ES3 engines + + var isValue = function (val) { + return (val !== _undefined) && (val !== null); + }; + + var keys = Object.keys; + + var shim = function (object) { + return keys(isValue(object) ? Object(object) : object); + }; + + var keys$1 = isImplemented$1() + ? Object.keys + : shim; + + var validValue = function (value) { + if (!isValue(value)) throw new TypeError("Cannot use null or undefined"); + return value; + }; + + var max = Math.max; + + var shim$1 = function (dest, src /*, …srcn*/) { + var error, i, length = max(arguments.length, 2), assign; + dest = Object(validValue(dest)); + assign = function (key) { + try { + dest[key] = src[key]; + } catch (e) { + if (!error) error = e; + } + }; + for (i = 1; i < length; ++i) { + src = arguments[i]; + keys$1(src).forEach(assign); + } + if (error !== undefined) throw error; + return dest; + }; + + var assign = isImplemented() + ? Object.assign + : shim$1; + + var forEach = Array.prototype.forEach, create = Object.create; + + var process = function (src, obj) { + var key; + for (key in src) obj[key] = src[key]; + }; + + // eslint-disable-next-line no-unused-vars + var normalizeOptions = function (opts1 /*, …options*/) { + var result = create(null); + forEach.call(arguments, function (options) { + if (!isValue(options)) return; + process(Object(options), result); + }); + return result; + }; + + // Deprecated + + var isCallable = function (obj) { + return typeof obj === "function"; + }; + + var str = "razdwatrzy"; + + var isImplemented$2 = function () { + if (typeof str.contains !== "function") return false; + return (str.contains("dwa") === true) && (str.contains("foo") === false); + }; + + var indexOf = String.prototype.indexOf; + + var shim$2 = function (searchString/*, position*/) { + return indexOf.call(this, searchString, arguments[1]) > -1; + }; + + var contains = isImplemented$2() + ? String.prototype.contains + : shim$2; + + var d_1 = createCommonjsModule(function (module) { + + var d; + + d = module.exports = function (dscr, value/*, options*/) { + var c, e, w, options, desc; + if ((arguments.length < 2) || (typeof dscr !== 'string')) { + options = value; + value = dscr; + dscr = null; + } else { + options = arguments[2]; + } + if (dscr == null) { + c = w = true; + e = false; + } else { + c = contains.call(dscr, 'c'); + e = contains.call(dscr, 'e'); + w = contains.call(dscr, 'w'); + } + + desc = { value: value, configurable: c, enumerable: e, writable: w }; + return !options ? desc : assign(normalizeOptions(options), desc); + }; + + d.gs = function (dscr, get, set/*, options*/) { + var c, e, options, desc; + if (typeof dscr !== 'string') { + options = set; + set = get; + get = dscr; + dscr = null; + } else { + options = arguments[3]; + } + if (get == null) { + get = undefined; + } else if (!isCallable(get)) { + options = get; + get = set = undefined; + } else if (set == null) { + set = undefined; + } else if (!isCallable(set)) { + options = set; + set = undefined; + } + if (dscr == null) { + c = true; + e = false; + } else { + c = contains.call(dscr, 'c'); + e = contains.call(dscr, 'e'); + } + + desc = { get: get, set: set, configurable: c, enumerable: e }; + return !options ? desc : assign(normalizeOptions(options), desc); + }; + }); + + var validCallable = function (fn) { + if (typeof fn !== "function") throw new TypeError(fn + " is not a function"); + return fn; + }; + + var eventEmitter = createCommonjsModule(function (module, exports) { + + var apply = Function.prototype.apply, call = Function.prototype.call + , create = Object.create, defineProperty = Object.defineProperty + , defineProperties = Object.defineProperties + , hasOwnProperty = Object.prototype.hasOwnProperty + , descriptor = { configurable: true, enumerable: false, writable: true } + + , on, once, off, emit, methods, descriptors, base; + + on = function (type, listener) { + var data; + + validCallable(listener); + + if (!hasOwnProperty.call(this, '__ee__')) { + data = descriptor.value = create(null); + defineProperty(this, '__ee__', descriptor); + descriptor.value = null; + } else { + data = this.__ee__; + } + if (!data[type]) data[type] = listener; + else if (typeof data[type] === 'object') data[type].push(listener); + else data[type] = [data[type], listener]; + + return this; + }; + + once = function (type, listener) { + var once, self; + + validCallable(listener); + self = this; + on.call(this, type, once = function () { + off.call(self, type, once); + apply.call(listener, this, arguments); + }); + + once.__eeOnceListener__ = listener; + return this; + }; + + off = function (type, listener) { + var data, listeners, candidate, i; + + validCallable(listener); + + if (!hasOwnProperty.call(this, '__ee__')) return this; + data = this.__ee__; + if (!data[type]) return this; + listeners = data[type]; + + if (typeof listeners === 'object') { + for (i = 0; (candidate = listeners[i]); ++i) { + if ((candidate === listener) || + (candidate.__eeOnceListener__ === listener)) { + if (listeners.length === 2) data[type] = listeners[i ? 0 : 1]; + else listeners.splice(i, 1); + } + } + } else { + if ((listeners === listener) || + (listeners.__eeOnceListener__ === listener)) { + delete data[type]; + } + } + + return this; + }; + + emit = function (type) { + var i, l, listener, listeners, args; + + if (!hasOwnProperty.call(this, '__ee__')) return; + listeners = this.__ee__[type]; + if (!listeners) return; + + if (typeof listeners === 'object') { + l = arguments.length; + args = new Array(l - 1); + for (i = 1; i < l; ++i) args[i - 1] = arguments[i]; + + listeners = listeners.slice(); + for (i = 0; (listener = listeners[i]); ++i) { + apply.call(listener, this, args); + } + } else { + switch (arguments.length) { + case 1: + call.call(listeners, this); + break; + case 2: + call.call(listeners, this, arguments[1]); + break; + case 3: + call.call(listeners, this, arguments[1], arguments[2]); + break; + default: + l = arguments.length; + args = new Array(l - 1); + for (i = 1; i < l; ++i) { + args[i - 1] = arguments[i]; + } + apply.call(listeners, this, args); + } + } + }; + + methods = { + on: on, + once: once, + off: off, + emit: emit + }; + + descriptors = { + on: d_1(on), + once: d_1(once), + off: d_1(off), + emit: d_1(emit) + }; + + base = defineProperties({}, descriptors); + + module.exports = exports = function (o) { + return (o == null) ? create(base) : defineProperties(Object(o), descriptors); + }; + exports.methods = methods; + }); + var eventEmitter_1 = eventEmitter.methods; + + function getBoundingClientRect(element) { + if (!element) { + return; + } + let rect; + if (typeof element.getBoundingClientRect !== "undefined") { + rect = element.getBoundingClientRect(); + } else { + let range = document.createRange(); + range.selectNode(element); + rect = range.getBoundingClientRect(); + } + return rect; + } + + function getClientRects(element) { + if (!element) { + return; + } + let rect; + if (typeof element.getClientRects !== "undefined") { + rect = element.getClientRects(); + } else { + let range = document.createRange(); + range.selectNode(element); + rect = range.getClientRects(); + } + return rect; + } + + /** + * Generates a UUID + * based on: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript + * @returns {string} uuid + */ + function UUID() { + var d = new Date().getTime(); + if (typeof performance !== "undefined" && typeof performance.now === "function"){ + d += performance.now(); //use high-precision timer if available + } + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16); + }); + } + + function attr(element, attributes) { + for (var i = 0; i < attributes.length; i++) { + if(element.hasAttribute(attributes[i])) { + return element.getAttribute(attributes[i]); + } + } + } + + /* Based on by https://mths.be/cssescape v1.5.1 by @mathias | MIT license + * Allows # and . + */ + function querySelectorEscape(value) { + if (arguments.length == 0) { + throw new TypeError("`CSS.escape` requires an argument."); + } + var string = String(value); + var length = string.length; + var index = -1; + var codeUnit; + var result = ""; + var firstCodeUnit = string.charCodeAt(0); + while (++index < length) { + codeUnit = string.charCodeAt(index); + // Note: there’s no need to special-case astral symbols, surrogate + // pairs, or lone surrogates. + + // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER + // (U+FFFD). + if (codeUnit == 0x0000) { + result += "\uFFFD"; + continue; + } + + if ( + // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is + // U+007F, […] + (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F || + // If the character is the first character and is in the range [0-9] + // (U+0030 to U+0039), […] + (index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || + // If the character is the second character and is in the range [0-9] + // (U+0030 to U+0039) and the first character is a `-` (U+002D), […] + ( + index == 1 && + codeUnit >= 0x0030 && codeUnit <= 0x0039 && + firstCodeUnit == 0x002D + ) + ) { + // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point + result += "\\" + codeUnit.toString(16) + " "; + continue; + } + + if ( + // If the character is the first character and is a `-` (U+002D), and + // there is no second character, […] + index == 0 && + length == 1 && + codeUnit == 0x002D + ) { + result += "\\" + string.charAt(index); + continue; + } + + // If the character is not handled by one of the above rules and is + // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or + // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to + // U+005A), or [a-z] (U+0061 to U+007A), […] + if ( + codeUnit >= 0x0080 || + codeUnit == 0x002D || + codeUnit == 0x005F || + codeUnit == 35 || // Allow # + codeUnit == 46 || // Allow . + codeUnit >= 0x0030 && codeUnit <= 0x0039 || + codeUnit >= 0x0041 && codeUnit <= 0x005A || + codeUnit >= 0x0061 && codeUnit <= 0x007A + ) { + // the character itself + result += string.charAt(index); + continue; + } + + // Otherwise, the escaped character. + // https://drafts.csswg.org/cssom/#escape-a-character + result += "\\" + string.charAt(index); + + } + return result; + } + + /** + * Creates a new pending promise and provides methods to resolve or reject it. + * From: https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred#backwards_forwards_compatible + * @returns {object} defered + */ + function defer() { + this.resolve = null; + + this.reject = null; + + this.id = UUID(); + + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + Object.freeze(this); + } + + const requestIdleCallback = typeof window !== "undefined" && ("requestIdleCallback" in window ? window.requestIdleCallback : window.requestAnimationFrame); + + function CSSValueToString(obj) { + return obj.value + (obj.unit || ""); + } + + function isElement(node) { + return node && node.nodeType === 1; + } + + function isText(node) { + return node && node.nodeType === 3; + } + + function *walk(start, limiter) { + let node = start; + + while (node) { + + yield node; + + if (node.childNodes.length) { + node = node.firstChild; + } else if (node.nextSibling) { + if (limiter && node === limiter) { + node = undefined; + break; + } + node = node.nextSibling; + } else { + while (node) { + node = node.parentNode; + if (limiter && node === limiter) { + node = undefined; + break; + } + if (node && node.nextSibling) { + node = node.nextSibling; + break; + } + + } + } + } + } + + function nodeAfter(node, limiter) { + let after = node; + + if (after.nextSibling) { + if (limiter && node === limiter) { + return; + } + after = after.nextSibling; + } else { + while (after) { + after = after.parentNode; + if (limiter && after === limiter) { + after = undefined; + break; + } + if (after && after.nextSibling) { + after = after.nextSibling; + break; + } + } + } + + return after; + } + + function nodeBefore(node, limiter) { + let before = node; + if (before.previousSibling) { + if (limiter && node === limiter) { + return; + } + before = before.previousSibling; + } else { + while (before) { + before = before.parentNode; + if (limiter && before === limiter) { + before = undefined; + break; + } + if (before && before.previousSibling) { + before = before.previousSibling; + break; + } + } + } + + return before; + } + + function elementAfter(node, limiter) { + let after = nodeAfter(node); + + while (after && after.nodeType !== 1) { + after = nodeAfter(after); + } + + return after; + } + + function rebuildAncestors(node) { + let parent, ancestor; + let ancestors = []; + let added = []; + + let fragment = document.createDocumentFragment(); + + // Gather all ancestors + let element = node; + while(element.parentNode && element.parentNode.nodeType === 1) { + ancestors.unshift(element.parentNode); + element = element.parentNode; + } + + for (var i = 0; i < ancestors.length; i++) { + ancestor = ancestors[i]; + parent = ancestor.cloneNode(false); + + parent.setAttribute("data-split-from", parent.getAttribute("data-ref")); + // ancestor.setAttribute("data-split-to", parent.getAttribute("data-ref")); + + if (parent.hasAttribute("id")) { + let dataID = parent.getAttribute("id"); + parent.setAttribute("data-id", dataID); + parent.removeAttribute("id"); + } + + // This is handled by css :not, but also tidied up here + if (parent.hasAttribute("data-break-before")) { + parent.removeAttribute("data-break-before"); + } + + if (parent.hasAttribute("data-previous-break-after")) { + parent.removeAttribute("data-previous-break-after"); + } + + if (added.length) { + let container = added[added.length-1]; + container.appendChild(parent); + } else { + fragment.appendChild(parent); + } + added.push(parent); + } + + added = undefined; + return fragment; + } + + /* + export function split(bound, cutElement, breakAfter) { + let needsRemoval = []; + let index = indexOf(cutElement); + + if (!breakAfter && index === 0) { + return; + } + + if (breakAfter && index === (cutElement.parentNode.children.length - 1)) { + return; + } + + // Create a fragment with rebuilt ancestors + let fragment = rebuildAncestors(cutElement); + + // Clone cut + if (!breakAfter) { + let clone = cutElement.cloneNode(true); + let ref = cutElement.parentNode.getAttribute('data-ref'); + let parent = fragment.querySelector("[data-ref='" + ref + "']"); + parent.appendChild(clone); + needsRemoval.push(cutElement); + } + + // Remove all after cut + let next = nodeAfter(cutElement, bound); + while (next) { + let clone = next.cloneNode(true); + let ref = next.parentNode.getAttribute('data-ref'); + let parent = fragment.querySelector("[data-ref='" + ref + "']"); + parent.appendChild(clone); + needsRemoval.push(next); + next = nodeAfter(next, bound); + } + + // Remove originals + needsRemoval.forEach((node) => { + if (node) { + node.remove(); + } + }); + + // Insert after bounds + bound.parentNode.insertBefore(fragment, bound.nextSibling); + return [bound, bound.nextSibling]; + } + */ + + function needsBreakBefore(node) { + if( typeof node !== "undefined" && + typeof node.dataset !== "undefined" && + typeof node.dataset.breakBefore !== "undefined" && + (node.dataset.breakBefore === "always" || + node.dataset.breakBefore === "page" || + node.dataset.breakBefore === "left" || + node.dataset.breakBefore === "right" || + node.dataset.breakBefore === "recto" || + node.dataset.breakBefore === "verso") + ) { + return true; + } + + return false; + } + + function needsPreviousBreakAfter(node) { + if( typeof node !== "undefined" && + typeof node.dataset !== "undefined" && + typeof node.dataset.previousBreakAfter !== "undefined" && + (node.dataset.previousBreakAfter === "always" || + node.dataset.previousBreakAfter === "page" || + node.dataset.previousBreakAfter === "left" || + node.dataset.previousBreakAfter === "right" || + node.dataset.previousBreakAfter === "recto" || + node.dataset.previousBreakAfter === "verso") + ) { + return true; + } + + return false; + } + + function needsPageBreak(node) { + if( typeof node !== "undefined" && + typeof node.dataset !== "undefined" && + (node.dataset.page || node.dataset.afterPage) + ) { + return true; + } + + return false; + } + + function *words(node) { + let currentText = node.nodeValue; + let max = currentText.length; + let currentOffset = 0; + let currentLetter; + + let range; + + while(currentOffset < max) { + currentLetter = currentText[currentOffset]; + if (/^[\S\u202F\u00A0]$/.test(currentLetter)) { + if (!range) { + range = document.createRange(); + range.setStart(node, currentOffset); + } + } else { + if (range) { + range.setEnd(node, currentOffset); + yield range; + range = undefined; + } + } + + currentOffset += 1; + } + + if (range) { + range.setEnd(node, currentOffset); + yield range; + range = undefined; + } + } + + function *letters(wordRange) { + let currentText = wordRange.startContainer; + let max = currentText.length; + let currentOffset = wordRange.startOffset; + // let currentLetter; + + let range; + + while(currentOffset < max) { + // currentLetter = currentText[currentOffset]; + range = document.createRange(); + range.setStart(currentText, currentOffset); + range.setEnd(currentText, currentOffset+1); + + yield range; + + currentOffset += 1; + } + } + + function isContainer(node) { + let container; + + if (typeof node.tagName === "undefined") { + return true; + } + + if (node.style.display === "none") { + return false; + } + + switch (node.tagName) { + // Inline + case "A": + case "ABBR": + case "ACRONYM": + case "B": + case "BDO": + case "BIG": + case "BR": + case "BUTTON": + case "CITE": + case "CODE": + case "DFN": + case "EM": + case "I": + case "IMG": + case "INPUT": + case "KBD": + case "LABEL": + case "MAP": + case "OBJECT": + case "Q": + case "SAMP": + case "SCRIPT": + case "SELECT": + case "SMALL": + case "SPAN": + case "STRONG": + case "SUB": + case "SUP": + case "TEXTAREA": + case "TIME": + case "TT": + case "VAR": + case "P": + case "H1": + case "H2": + case "H3": + case "H4": + case "H5": + case "H6": + case "FIGCAPTION": + case "BLOCKQUOTE": + case "PRE": + case "LI": + case "TR": + case "DT": + case "DD": + case "VIDEO": + case "CANVAS": + container = false; + break; + default: + container = true; + } + + return container; + } + + function cloneNode(n, deep=false) { + return n.cloneNode(deep); + } + + function findElement(node, doc) { + const ref = node.getAttribute("data-ref"); + return findRef(ref, doc); + } + + function findRef(ref, doc) { + return doc.querySelector(`[data-ref='${ref}']`); + } + + function validNode(node) { + if (isText(node)) { + return true; + } + + if (isElement(node) && node.dataset.ref) { + return true; + } + + return false; + } + + function prevValidNode(node) { + while (!validNode(node)) { + if (node.previousSibling) { + node = node.previousSibling; + } else { + node = node.parentNode; + } + + if (!node) { + break; + } + } + + return node; + } + + + function indexOf$1(node) { + let parent = node.parentNode; + if (!parent) { + return 0; + } + return Array.prototype.indexOf.call(parent.childNodes, node); + } + + function child(node, index) { + return node.childNodes[index]; + } + + function hasContent(node) { + if (isElement(node)) { + return true; + } else if (isText(node) && + node.textContent.trim().length) { + return true; + } + return false; + } + + function indexOfTextNode(node, parent) { + if (!isText(node)) { + return -1; + } + let nodeTextContent = node.textContent; + let child; + let index = -1; + for (var i = 0; i < parent.childNodes.length; i++) { + child = parent.childNodes[i]; + if (child.nodeType === 3) { + let text = parent.childNodes[i].textContent; + if (text.includes(nodeTextContent)) { + index = i; + break; + } + } + } + + return index; + } + + /** + * Hooks allow for injecting functions that must all complete in order before finishing + * They will execute in parallel but all must finish before continuing + * Functions may return a promise if they are asycn. + * From epubjs/src/utils/hooks + * @param {any} context scope of this + * @example this.content = new Hook(this); + */ + class Hook { + constructor(context){ + this.context = context || this; + this.hooks = []; + } + + /** + * Adds a function to be run before a hook completes + * @example this.content.register(function(){...}); + * @return {undefined} void + */ + register(){ + for(var i = 0; i < arguments.length; ++i) { + if (typeof arguments[i] === "function") { + this.hooks.push(arguments[i]); + } else { + // unpack array + for(var j = 0; j < arguments[i].length; ++j) { + this.hooks.push(arguments[i][j]); + } + } + } + } + + /** + * Triggers a hook to run all functions + * @example this.content.trigger(args).then(function(){...}); + * @return {Promise} results + */ + trigger(){ + var args = arguments; + var context = this.context; + var promises = []; + + this.hooks.forEach(function(task) { + var executing = task.apply(context, args); + + if(executing && typeof executing["then"] === "function") { + // Task is a function that returns a promise + promises.push(executing); + } + // Otherwise Task resolves immediately, add resolved promise with result + promises.push(new Promise((resolve, reject) => { + resolve(executing); + })); + }); + + + return Promise.all(promises); + } + + /** + * Triggers a hook to run all functions synchronously + * @example this.content.trigger(args).then(function(){...}); + * @return {Array} results + */ + triggerSync(){ + var args = arguments; + var context = this.context; + var results = []; + + this.hooks.forEach(function(task) { + var executing = task.apply(context, args); + + results.push(executing); + }); + + + return results; + } + + // Adds a function to be run before a hook completes + list(){ + return this.hooks; + } + + clear(){ + return this.hooks = []; + } + } + + const MAX_CHARS_PER_BREAK = 1500; + + /** + * Layout + * @class + */ + class Layout { + + constructor(element, hooks, maxChars) { + this.element = element; + + this.bounds = this.element.getBoundingClientRect(); + + if (hooks) { + this.hooks = hooks; + } else { + this.hooks = {}; + this.hooks.layout = new Hook(); + this.hooks.renderNode = new Hook(); + this.hooks.layoutNode = new Hook(); + this.hooks.beforeOverflow = new Hook(); + this.hooks.onOverflow = new Hook(); + this.hooks.onBreakToken = new Hook(); + } + + this.maxChars = maxChars || MAX_CHARS_PER_BREAK; + } + + async renderTo(wrapper, source, breakToken, bounds=this.bounds) { + let start = this.getStart(source, breakToken); + let walker = walk(start, source); + + let node; + let done; + let next; + + let hasRenderedContent = false; + let newBreakToken; + + let length = 0; + + while (!done && !newBreakToken) { + next = walker.next(); + node = next.value; + done = next.done; + + if (!node) { + this.hooks && this.hooks.layout.trigger(wrapper, this); + + let imgs = wrapper.querySelectorAll("img"); + if (imgs.length) { + await this.waitForImages(imgs); + } + + newBreakToken = this.findBreakToken(wrapper, source, bounds); + return newBreakToken; + } + + this.hooks && this.hooks.layoutNode.trigger(node); + + // Check if the rendered element has a break set + if (hasRenderedContent && this.shouldBreak(node)) { + + this.hooks && this.hooks.layout.trigger(wrapper, this); + + let imgs = wrapper.querySelectorAll("img"); + if (imgs.length) { + await this.waitForImages(imgs); + } + + newBreakToken = this.findBreakToken(wrapper, source, bounds); + + if (!newBreakToken) { + newBreakToken = this.breakAt(node); + } + + length = 0; + + break; + } + + // Should the Node be a shallow or deep clone + let shallow = isContainer(node); + + let rendered = this.append(node, wrapper, breakToken, shallow); + + length += rendered.textContent.length; + + // Check if layout has content yet + if (!hasRenderedContent) { + hasRenderedContent = hasContent(node); + } + + // Skip to the next node if a deep clone was rendered + if (!shallow) { + walker = walk(nodeAfter(node, source), source); + } + + // Only check x characters + if (length >= this.maxChars) { + + this.hooks && this.hooks.layout.trigger(wrapper, this); + + let imgs = wrapper.querySelectorAll("img"); + if (imgs.length) { + await this.waitForImages(imgs); + } + + newBreakToken = this.findBreakToken(wrapper, source, bounds); + + if (newBreakToken) { + length = 0; + } + } + + } + + return newBreakToken; + } + + breakAt(node, offset=0) { + return { + node, + offset + }; + } + + shouldBreak(node) { + let previousSibling = node.previousSibling; + let parentNode = node.parentNode; + let parentBreakBefore = needsBreakBefore(node) && parentNode && !previousSibling && needsBreakBefore(parentNode); + let doubleBreakBefore; + + if (parentBreakBefore) { + doubleBreakBefore = node.dataset.breakBefore === parentNode.dataset.breakBefore; + } + + return !doubleBreakBefore && needsBreakBefore(node) || needsPreviousBreakAfter(node) || needsPageBreak(node); + } + + getStart(source, breakToken) { + let start; + let node = breakToken && breakToken.node; + + if (node) { + start = node; + } else { + start = source.firstChild; + } + + return start; + } + + append(node, dest, breakToken, shallow=true, rebuild=true) { + + let clone = cloneNode(node, !shallow); + + if (node.parentNode && isElement(node.parentNode)) { + let parent = findElement(node.parentNode, dest); + // Rebuild chain + if (parent) { + parent.appendChild(clone); + } else if (rebuild) { + let fragment = rebuildAncestors(node); + parent = findElement(node.parentNode, fragment); + if (!parent) { + dest.appendChild(clone); + } else if (breakToken && isText(breakToken.node) && breakToken.offset > 0) { + clone.textContent = clone.textContent.substring(breakToken.offset); + parent.appendChild(clone); + } else { + parent.appendChild(clone); + } + + dest.appendChild(fragment); + } else { + dest.appendChild(clone); + } + + + } else { + dest.appendChild(clone); + } + + let nodeHooks = this.hooks.renderNode.triggerSync(clone, node); + nodeHooks.forEach((newNode) => { + if (typeof newNode != "undefined") { + clone = newNode; + } + }); + + return clone; + } + + async waitForImages(imgs) { + let results = Array.from(imgs).map(async (img) => { + return this.awaitImageLoaded(img); + }); + await Promise.all(results); + } + + async awaitImageLoaded(image) { + return new Promise(resolve => { + if (image.complete !== true) { + image.onload = function() { + let { width, height } = window.getComputedStyle(image); + resolve(width, height); + }; + image.onerror = function(e) { + let { width, height } = window.getComputedStyle(image); + resolve(width, height, e); + }; + } else { + let { width, height } = window.getComputedStyle(image); + resolve(width, height); + } + }); + } + + avoidBreakInside(node, limiter) { + let breakNode; + + if (node === limiter) { + return; + } + + while (node.parentNode) { + node = node.parentNode; + + if (node === limiter) { + break; + } + + if(window.getComputedStyle(node)["break-inside"] === "avoid") { + breakNode = node; + break; + } + + } + return breakNode; + } + + createBreakToken(overflow, rendered, source) { + let container = overflow.startContainer; + let offset = overflow.startOffset; + let node, renderedNode, parent, index, temp; + + if (isElement(container)) { + temp = child(container, offset); + + if (isElement(temp)) { + renderedNode = findElement(temp, rendered); + + if (!renderedNode) { + // Find closest element with data-ref + renderedNode = findElement(prevValidNode(temp), rendered); + return; + } + + node = findElement(renderedNode, source); + offset = 0; + } else { + renderedNode = findElement(container, rendered); + + if (!renderedNode) { + renderedNode = findElement(prevValidNode(container), rendered); + } + + parent = findElement(renderedNode, source); + index = indexOfTextNode(temp, parent); + node = child(parent, index); + offset = 0; + } + } else { + renderedNode = findElement(container.parentNode, rendered); + + if (!renderedNode) { + renderedNode = findElement(prevValidNode(container.parentNode), rendered); + } + + parent = findElement(renderedNode, source); + index = indexOfTextNode(container, parent); + + if (index === -1) { + return; + } + + node = child(parent, index); + + offset += node.textContent.indexOf(container.textContent); + } + + if (!node) { + return; + } + + return { + node, + offset + }; + + } + + findBreakToken(rendered, source, bounds=this.bounds, extract=true) { + let overflow = this.findOverflow(rendered, bounds); + let breakToken; + + let overflowHooks = this.hooks.onOverflow.triggerSync(overflow, rendered, bounds, this); + overflowHooks.forEach((newOverflow) => { + if (typeof newOverflow != "undefined") { + overflow = newOverflow; + } + }); + + if (overflow) { + breakToken = this.createBreakToken(overflow, rendered, source); + + let breakHooks = this.hooks.onBreakToken.triggerSync(breakToken, overflow, rendered, this); + breakHooks.forEach((newToken) => { + if (typeof newToken != "undefined") { + breakToken = newToken; + } + }); + + + if (breakToken && breakToken.node && extract) { + this.removeOverflow(overflow); + } + + } + return breakToken; + } + + hasOverflow(element, bounds=this.bounds) { + let constrainingElement = element && element.parentNode; // this gets the element, instead of the wrapper for the width workaround + let { width } = element.getBoundingClientRect(); + let scrollWidth = constrainingElement ? constrainingElement.scrollWidth : 0; + return Math.max(Math.floor(width), scrollWidth) > Math.round(bounds.width); + } + + findOverflow(rendered, bounds=this.bounds) { + if (!this.hasOverflow(rendered, bounds)) return; + + let start = Math.round(bounds.left); + let end = Math.round(bounds.right); + let range; + + let walker = walk(rendered.firstChild, rendered); + + // Find Start + let next, done, node, offset, skip, breakAvoid, prev, br; + while (!done) { + next = walker.next(); + done = next.done; + node = next.value; + skip = false; + breakAvoid = false; + prev = undefined; + br = undefined; + + if (node) { + let pos = getBoundingClientRect(node); + let left = Math.floor(pos.left); + let right = Math.floor(pos.right); + + if (!range && left >= end) { + // Check if it is a float + let isFloat = false; + + if (isElement(node) ) { + let styles = window.getComputedStyle(node); + isFloat = styles.getPropertyValue("float") !== "none"; + skip = styles.getPropertyValue("break-inside") === "avoid"; + breakAvoid = node.dataset.breakBefore === "avoid" || node.dataset.previousBreakAfter === "avoid"; + prev = breakAvoid && nodeBefore(node, rendered); + br = node.tagName === "BR" || node.tagName === "WBR"; + } + + if (prev) { + range = document.createRange(); + range.setStartBefore(prev); + break; + } + + if (!br && !isFloat && isElement(node)) { + range = document.createRange(); + range.setStartBefore(node); + break; + } + + if (isText(node) && node.textContent.trim().length) { + range = document.createRange(); + range.setStartBefore(node); + break; + } + + } + + if (!range && isText(node) && + node.textContent.trim().length && + window.getComputedStyle(node.parentNode)["break-inside"] !== "avoid") { + + let rects = getClientRects(node); + let rect; + left = 0; + for (var i = 0; i != rects.length; i++) { + rect = rects[i]; + if (rect.width > 0 && (!left || rect.left > left)) { + left = rect.left; + } + } + + if(left >= end) { + range = document.createRange(); + offset = this.textBreak(node, start, end); + if (!offset) { + range = undefined; + } else { + range.setStart(node, offset); + } + break; + } + } + + // Skip children + if (skip || right < end) { + next = nodeAfter(node, rendered); + if (next) { + walker = walk(next, rendered); + } + + } + + } + } + + // Find End + if (range) { + range.setEndAfter(rendered.lastChild); + return range; + } + + } + + findEndToken(rendered, source, bounds=this.bounds) { + if (rendered.childNodes.length === 0) { + return; + } + + let lastChild = rendered.lastChild; + + let lastNodeIndex; + while (lastChild && lastChild.lastChild) { + if (!validNode(lastChild)) { + // Only get elements with refs + lastChild = lastChild.previousSibling; + } else if(!validNode(lastChild.lastChild)) { + // Deal with invalid dom items + lastChild = prevValidNode(lastChild.lastChild); + break; + } else { + lastChild = lastChild.lastChild; + } + } + + if (isText(lastChild)) { + + if (lastChild.parentNode.dataset.ref) { + lastNodeIndex = indexOf$1(lastChild); + lastChild = lastChild.parentNode; + } else { + lastChild = lastChild.previousSibling; + } + } + + let original = findElement(lastChild, source); + + if (lastNodeIndex) { + original = original.childNodes[lastNodeIndex]; + } + + let after = nodeAfter(original); + + return this.breakAt(after); + } + + textBreak(node, start, end) { + let wordwalker = words(node); + let left = 0; + let right = 0; + let word, next, done, pos; + let offset; + while (!done) { + next = wordwalker.next(); + word = next.value; + done = next.done; + + if (!word) { + break; + } + + pos = getBoundingClientRect(word); + + left = Math.floor(pos.left); + right = Math.floor(pos.right); + + if (left >= end) { + offset = word.startOffset; + break; + } + + if (right > end) { + let letterwalker = letters(word); + let letter, nextLetter, doneLetter; + + while (!doneLetter) { + nextLetter = letterwalker.next(); + letter = nextLetter.value; + doneLetter = nextLetter.done; + + if (!letter) { + break; + } + + pos = getBoundingClientRect(letter); + left = Math.floor(pos.left); + + if (left >= end) { + offset = letter.startOffset; + done = true; + + break; + } + } + } + + } + + return offset; + } + + removeOverflow(overflow) { + let {startContainer} = overflow; + let extracted = overflow.extractContents(); + + this.hyphenateAtBreak(startContainer); + + return extracted; + } + + hyphenateAtBreak(startContainer) { + if (isText(startContainer)) { + let startText = startContainer.textContent; + let prevLetter = startText[startText.length-1]; + + // Add a hyphen if previous character is a letter or soft hyphen + if (/^\w|\u00AD$/.test(prevLetter)) { + startContainer.parentNode.classList.add("pagedjs_hyphen"); + startContainer.textContent += "\u2011"; + } + } + } + } + + eventEmitter(Layout.prototype); + + /** + * Render a page + * @class + */ + class Page { + constructor(pagesArea, pageTemplate, blank, hooks) { + this.pagesArea = pagesArea; + this.pageTemplate = pageTemplate; + this.blank = blank; + + this.width = undefined; + this.height = undefined; + + this.hooks = hooks; + + // this.element = this.create(this.pageTemplate); + } + + create(template, after) { + //let documentFragment = document.createRange().createContextualFragment( TEMPLATE ); + //let page = documentFragment.children[0]; + let clone = document.importNode(this.pageTemplate.content, true); + + let page, index; + if (after) { + this.pagesArea.insertBefore(clone, after.nextElementSibling); + index = Array.prototype.indexOf.call(this.pagesArea.children, after.nextElementSibling); + page = this.pagesArea.children[index]; + } else { + this.pagesArea.appendChild(clone); + page = this.pagesArea.lastChild; + } + + let pagebox = page.querySelector(".pagedjs_pagebox"); + let area = page.querySelector(".pagedjs_page_content"); + + + let size = area.getBoundingClientRect(); + + + area.style.columnWidth = Math.round(size.width) + "px"; + area.style.columnGap = "calc(var(--pagedjs-margin-right) + var(--pagedjs-margin-left))"; + // area.style.overflow = "scroll"; + + this.width = Math.round(size.width); + this.height = Math.round(size.height); + + this.element = page; + this.pagebox = pagebox; + this.area = area; + + return page; + } + + createWrapper() { + let wrapper = document.createElement("div"); + + this.area.appendChild(wrapper); + + this.wrapper = wrapper; + + return wrapper; + } + + index(pgnum) { + this.position = pgnum; + + let page = this.element; + // let pagebox = this.pagebox; + + let index = pgnum+1; + + let id = `page-${index}`; + + this.id = id; + + // page.dataset.pageNumber = index; + + page.dataset.pageNumber = index; + page.setAttribute('id', id); + + if (this.name) { + page.classList.add("pagedjs_" + this.name + "_page"); + } + + if (this.blank) { + page.classList.add("pagedjs_blank_page"); + } + + if (pgnum === 0) { + page.classList.add("pagedjs_first_page"); + } + + if (pgnum % 2 !== 1) { + page.classList.remove("pagedjs_left_page"); + page.classList.add("pagedjs_right_page"); + } else { + page.classList.remove("pagedjs_right_page"); + page.classList.add("pagedjs_left_page"); + } + } + + /* + size(width, height) { + if (width === this.width && height === this.height) { + return; + } + this.width = width; + this.height = height; + + this.element.style.width = Math.round(width) + "px"; + this.element.style.height = Math.round(height) + "px"; + this.element.style.columnWidth = Math.round(width) + "px"; + } + */ + + async layout(contents, breakToken, maxChars) { + + this.clear(); + + this.startToken = breakToken; + + this.layoutMethod = new Layout(this.area, this.hooks, maxChars); + + let newBreakToken = await this.layoutMethod.renderTo(this.wrapper, contents, breakToken); + + this.addListeners(contents); + + this.endToken = newBreakToken; + + return newBreakToken; + } + + async append(contents, breakToken) { + + if (!this.layoutMethod) { + return this.layout(contents, breakToken); + } + + let newBreakToken = await this.layoutMethod.renderTo(this.wrapper, contents, breakToken); + + this.endToken = newBreakToken; + + return newBreakToken; + } + + getByParent(ref, entries) { + let e; + for (var i = 0; i < entries.length; i++) { + e = entries[i]; + if(e.dataset.ref === ref) { + return e; + } + } + } + + onOverflow(func) { + this._onOverflow = func; + } + + onUnderflow(func) { + this._onUnderflow = func; + } + + clear() { + this.removeListeners(); + this.wrapper && this.wrapper.remove(); + this.createWrapper(); + } + + addListeners(contents) { + if (typeof ResizeObserver !== "undefined") { + this.addResizeObserver(contents); + } else { + this._checkOverflowAfterResize = this.checkOverflowAfterResize.bind(this, contents); + this.element.addEventListener("overflow", this._checkOverflowAfterResize, false); + this.element.addEventListener("underflow", this._checkOverflowAfterResize, false); + } + // TODO: fall back to mutation observer? + + this._onScroll = function() { + if(this.listening) { + this.element.scrollLeft = 0; + } + }.bind(this); + + // Keep scroll left from changing + this.element.addEventListener("scroll", this._onScroll); + + this.listening = true; + + return true; + } + + removeListeners() { + this.listening = false; + + if (typeof ResizeObserver !== "undefined" && this.ro) { + this.ro.disconnect(); + } else if (this.element) { + this.element.removeEventListener("overflow", this._checkOverflowAfterResize, false); + this.element.removeEventListener("underflow", this._checkOverflowAfterResize, false); + } + + this.element &&this.element.removeEventListener("scroll", this._onScroll); + + } + + addResizeObserver(contents) { + let wrapper = this.wrapper; + let prevHeight = wrapper.getBoundingClientRect().height; + this.ro = new ResizeObserver( entries => { + + if (!this.listening) { + return; + } + + for (let entry of entries) { + const cr = entry.contentRect; + + if (cr.height > prevHeight) { + this.checkOverflowAfterResize(contents); + prevHeight = wrapper.getBoundingClientRect().height; + } else if (cr.height < prevHeight ) { // TODO: calc line height && (prevHeight - cr.height) >= 22 + this.checkUnderflowAfterResize(contents); + prevHeight = cr.height; + } + } + }); + + this.ro.observe(wrapper); + } + + checkOverflowAfterResize(contents) { + if (!this.listening || !this.layoutMethod) { + return; + } + + let newBreakToken = this.layoutMethod.findBreakToken(this.wrapper, contents); + + if (newBreakToken) { + this.endToken = newBreakToken; + this._onOverflow && this._onOverflow(newBreakToken); + } + } + + checkUnderflowAfterResize(contents) { + if (!this.listening || !this.layoutMethod) { + return; + } + + let endToken = this.layoutMethod.findEndToken(this.wrapper, contents); + + // let newBreakToken = this.layoutMethod.findBreakToken(this.wrapper, contents); + + if (endToken) { + this._onUnderflow && this._onUnderflow(endToken); + } + } + + + destroy() { + this.removeListeners(); + + this.element.remove(); + + this.element = undefined; + this.wrapper = undefined; + } + } + + eventEmitter(Page.prototype); + + /** + * Render a flow of text offscreen + * @class + */ + class ContentParser { + + constructor(content, cb) { + if (content && content.nodeType) { + // handle dom + this.dom = this.add(content); + } else if (typeof content === "string") { + this.dom = this.parse(content); + } + + return this.dom; + } + + parse(markup, mime) { + let range = document.createRange(); + let fragment = range.createContextualFragment(markup); + + this.addRefs(fragment); + this.removeEmpty(fragment); + + return fragment; + } + + add(contents) { + // let fragment = document.createDocumentFragment(); + // + // let children = [...contents.childNodes]; + // for (let child of children) { + // let clone = child.cloneNode(true); + // fragment.appendChild(clone); + // } + + this.addRefs(contents); + this.removeEmpty(contents); + + return contents; + } + + addRefs(content) { + var treeWalker = document.createTreeWalker( + content, + NodeFilter.SHOW_ELEMENT, + { acceptNode: function(node) { return NodeFilter.FILTER_ACCEPT; } }, + false + ); + + let node = treeWalker.nextNode(); + while(node) { + + if (!node.hasAttribute("data-ref")) { + let uuid = UUID(); + node.setAttribute("data-ref", uuid); + } + + if (node.id) { + node.setAttribute("data-id", node.id); + } + + // node.setAttribute("data-children", node.childNodes.length); + + // node.setAttribute("data-text", node.textContent.trim().length); + node = treeWalker.nextNode(); + } + } + + removeEmpty(content) { + var treeWalker = document.createTreeWalker( + content, + NodeFilter.SHOW_TEXT, + { acceptNode: function(node) { + // Only remove more than a single space + if (node.textContent.length > 1 && !node.textContent.trim()) { + + // Don't touch whitespace if text is preformated + let parent = node.parentNode; + let pre = isElement(parent) && parent.closest("pre"); + if (pre) { + return NodeFilter.FILTER_REJECT; + } + + return NodeFilter.FILTER_ACCEPT; + } else { + return NodeFilter.FILTER_REJECT; + } + } }, + false + ); + + let node; + let current; + node = treeWalker.nextNode(); + while(node) { + current = node; + node = treeWalker.nextNode(); + // if (!current.nextSibling || (current.nextSibling && current.nextSibling.nodeType === 1)) { + current.parentNode.removeChild(current); + // } + } + } + + find(ref) { + return this.refs[ref]; + } + + // isWrapper(element) { + // return wrappersRegex.test(element.nodeName); + // } + + isText(node) { + return node.tagName === "TAG"; + } + + isElement(node) { + return node.nodeType === 1; + } + + hasChildren(node) { + return node.childNodes && node.childNodes.length; + } + + + destroy() { + this.refs = undefined; + this.dom = undefined; + } + } + + /** + * Queue for handling tasks one at a time + * @class + * @param {scope} context what this will resolve to in the tasks + */ + class Queue { + constructor(context){ + this._q = []; + this.context = context; + this.tick = requestAnimationFrame; + this.running = false; + this.paused = false; + } + + /** + * Add an item to the queue + * @return {Promise} enqueued + */ + enqueue() { + var deferred, promise; + var queued; + var task = [].shift.call(arguments); + var args = arguments; + + // Handle single args without context + // if(args && !Array.isArray(args)) { + // args = [args]; + // } + if(!task) { + throw new Error("No Task Provided"); + } + + if(typeof task === "function"){ + + deferred = new defer(); + promise = deferred.promise; + + queued = { + "task" : task, + "args" : args, + //"context" : context, + "deferred" : deferred, + "promise" : promise + }; + + } else { + // Task is a promise + queued = { + "promise" : task + }; + + } + + this._q.push(queued); + + // Wait to start queue flush + if (this.paused == false && !this.running) { + this.run(); + } + + return queued.promise; + } + + /** + * Run one item + * @return {Promise} dequeued + */ + dequeue(){ + var inwait, task, result; + + if(this._q.length && !this.paused) { + inwait = this._q.shift(); + task = inwait.task; + if(task){ + // console.log(task) + + result = task.apply(this.context, inwait.args); + + if(result && typeof result["then"] === "function") { + // Task is a function that returns a promise + return result.then(function(){ + inwait.deferred.resolve.apply(this.context, arguments); + }.bind(this), function() { + inwait.deferred.reject.apply(this.context, arguments); + }.bind(this)); + } else { + // Task resolves immediately + inwait.deferred.resolve.apply(this.context, result); + return inwait.promise; + } + + + + } else if(inwait.promise) { + // Task is a promise + return inwait.promise; + } + + } else { + inwait = new defer(); + inwait.deferred.resolve(); + return inwait.promise; + } + + } + + // Run All Immediately + dump(){ + while(this._q.length) { + this.dequeue(); + } + } + + /** + * Run all tasks sequentially, at convince + * @return {Promise} all run + */ + run(){ + + if(!this.running){ + this.running = true; + this.defered = new defer(); + } + + this.tick.call(window, () => { + + if(this._q.length) { + + this.dequeue() + .then(function(){ + this.run(); + }.bind(this)); + + } else { + this.defered.resolve(); + this.running = undefined; + } + + }); + + // Unpause + if(this.paused == true) { + this.paused = false; + } + + return this.defered.promise; + } + + /** + * Flush all, as quickly as possible + * @return {Promise} ran + */ + flush(){ + + if(this.running){ + return this.running; + } + + if(this._q.length) { + this.running = this.dequeue() + .then(function(){ + this.running = undefined; + return this.flush(); + }.bind(this)); + + return this.running; + } + + } + + /** + * Clear all items in wait + * @return {void} + */ + clear(){ + this._q = []; + } + + /** + * Get the number of tasks in the queue + * @return {number} tasks + */ + length(){ + return this._q.length; + } + + /** + * Pause a running queue + * @return {void} + */ + pause(){ + this.paused = true; + } + + /** + * End the queue + * @return {void} + */ + stop(){ + this._q = []; + this.running = false; + this.paused = true; + } + } + + const TEMPLATE = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`; + + /** + * Chop up text into flows + * @class + */ + class Chunker { + constructor(content, renderTo) { + // this.preview = preview; + + this.hooks = {}; + this.hooks.beforeParsed = new Hook(this); + this.hooks.afterParsed = new Hook(this); + this.hooks.beforePageLayout = new Hook(this); + this.hooks.layout = new Hook(this); + this.hooks.renderNode = new Hook(this); + this.hooks.layoutNode = new Hook(this); + this.hooks.onOverflow = new Hook(this); + this.hooks.onBreakToken = new Hook(); + this.hooks.afterPageLayout = new Hook(this); + this.hooks.afterRendered = new Hook(this); + + this.pages = []; + this._total = 0; + + this.q = new Queue(this); + this.stopped = false; + this.rendered = false; + + this.content = content; + + this.charsPerBreak = []; + this.maxChars; + + if (content) { + this.flow(content, renderTo); + } + } + + setup(renderTo) { + this.pagesArea = document.createElement("div"); + this.pagesArea.classList.add("pagedjs_pages"); + + if (renderTo) { + renderTo.appendChild(this.pagesArea); + } else { + document.querySelector("body").appendChild(this.pagesArea); + } + + this.pageTemplate = document.createElement("template"); + this.pageTemplate.innerHTML = TEMPLATE; + + } + + async flow(content, renderTo) { + let parsed; + + await this.hooks.beforeParsed.trigger(content, this); + + parsed = new ContentParser(content); + + this.source = parsed; + this.breakToken = undefined; + + if (this.pagesArea && this.pageTemplate) { + this.q.clear(); + this.removePages(); + } else { + this.setup(renderTo); + } + + this.emit("rendering", content); + + await this.hooks.afterParsed.trigger(parsed, this); + + await this.loadFonts(); + + let rendered = await this.render(parsed, this.breakToken); + while (rendered.canceled) { + this.start(); + rendered = await this.render(parsed, this.breakToken); + } + + this.rendered = true; + + await this.hooks.afterRendered.trigger(this.pages, this); + + this.emit("rendered", this.pages); + + return this; + } + + // oversetPages() { + // let overset = []; + // for (let i = 0; i < this.pages.length; i++) { + // let page = this.pages[i]; + // if (page.overset) { + // overset.push(page); + // // page.overset = false; + // } + // } + // return overset; + // } + // + // async handleOverset(parsed) { + // let overset = this.oversetPages(); + // if (overset.length) { + // console.log("overset", overset); + // let index = this.pages.indexOf(overset[0]) + 1; + // console.log("INDEX", index); + // + // // Remove pages + // // this.removePages(index); + // + // // await this.render(parsed, overset[0].overset); + // + // // return this.handleOverset(parsed); + // } + // } + + async render(parsed, startAt) { + let renderer = this.layout(parsed, startAt); + + let done = false; + let result; + + while (!done) { + result = await this.q.enqueue(() => { return this.renderAsync(renderer); }); + done = result.done; + } + + return result; + } + + start() { + this.rendered = false; + this.stopped = false; + } + + stop() { + this.stopped = true; + // this.q.clear(); + } + + renderOnIdle(renderer) { + return new Promise(resolve => { + requestIdleCallback(async () => { + if (this.stopped) { + return resolve({ done: true, canceled: true }); + } + let result = await renderer.next(); + if (this.stopped) { + resolve({ done: true, canceled: true }); + } else { + resolve(result); + } + }); + }); + } + + async renderAsync(renderer) { + if (this.stopped) { + return { done: true, canceled: true }; + } + let result = await renderer.next(); + if (this.stopped) { + return { done: true, canceled: true }; + } else { + return result; + } + } + + async handleBreaks(node) { + let currentPage = this.total + 1; + let currentPosition = currentPage % 2 === 0 ? "left" : "right"; + // TODO: Recto and Verso should reverse for rtl languages + let currentSide = currentPage % 2 === 0 ? "verso" : "recto"; + let previousBreakAfter; + let breakBefore; + let page; + + if (currentPage === 1) { + return; + } + + if (node && + typeof node.dataset !== "undefined" && + typeof node.dataset.previousBreakAfter !== "undefined") { + previousBreakAfter = node.dataset.previousBreakAfter; + } + + if (node && + typeof node.dataset !== "undefined" && + typeof node.dataset.breakBefore !== "undefined") { + breakBefore = node.dataset.breakBefore; + } + + if( previousBreakAfter && + (previousBreakAfter === "left" || previousBreakAfter === "right") && + previousBreakAfter !== currentPosition) { + page = this.addPage(true); + } else if( previousBreakAfter && + (previousBreakAfter === "verso" || previousBreakAfter === "recto") && + previousBreakAfter !== currentSide) { + page = this.addPage(true); + } else if( breakBefore && + (breakBefore === "left" || breakBefore === "right") && + breakBefore !== currentPosition) { + page = this.addPage(true); + } else if( breakBefore && + (breakBefore === "verso" || breakBefore === "recto") && + breakBefore !== currentSide) { + page = this.addPage(true); + } + + if (page) { + await this.hooks.beforePageLayout.trigger(page, undefined, undefined, this); + this.emit("page", page); + // await this.hooks.layout.trigger(page.element, page, undefined, this); + await this.hooks.afterPageLayout.trigger(page.element, page, undefined, this); + this.emit("renderedPage", page); + } + } + + async *layout(content, startAt) { + let breakToken = startAt || false; + + while (breakToken !== undefined && (true)) { + + if (breakToken && breakToken.node) { + await this.handleBreaks(breakToken.node); + } else { + await this.handleBreaks(content.firstChild); + } + + let page = this.addPage(); + + await this.hooks.beforePageLayout.trigger(page, content, breakToken, this); + this.emit("page", page); + + // Layout content in the page, starting from the breakToken + breakToken = await page.layout(content, breakToken, this.maxChars); + + await this.hooks.afterPageLayout.trigger(page.element, page, breakToken, this); + this.emit("renderedPage", page); + + this.recoredCharLength(page.wrapper.textContent.length); + + yield breakToken; + + // Stop if we get undefined, showing we have reached the end of the content + } + } + + recoredCharLength(length) { + if (length === 0) { + return; + } + + this.charsPerBreak.push(length); + + // Keep the length of the last few breaks + if (this.charsPerBreak.length > 4) { + this.charsPerBreak.shift(); + } + + this.maxChars = this.charsPerBreak.reduce((a, b) => a + b, 0) / (this.charsPerBreak.length); + } + + removePages(fromIndex=0) { + + if (fromIndex >= this.pages.length) { + return; + } + + // Remove pages + for (let i = fromIndex; i < this.pages.length; i++) { + this.pages[i].destroy(); + } + + if (fromIndex > 0) { + this.pages.splice(fromIndex); + } else { + this.pages = []; + } + } + + addPage(blank) { + let lastPage = this.pages[this.pages.length - 1]; + // Create a new page from the template + let page = new Page(this.pagesArea, this.pageTemplate, blank, this.hooks); + + this.pages.push(page); + + // Create the pages + page.create(undefined, lastPage && lastPage.element); + + page.index(this.total); + + if (!blank) { + // Listen for page overflow + page.onOverflow((overflowToken) => { + console.warn("overflow on", page.id, overflowToken); + + // Only reflow while rendering + if (this.rendered) { + return; + } + + let index = this.pages.indexOf(page) + 1; + + // Stop the rendering + this.stop(); + + // Set the breakToken to resume at + this.breakToken = overflowToken; + + // Remove pages + this.removePages(index); + + if (this.rendered === true) { + this.rendered = false; + + this.q.enqueue(async () => { + + this.start(); + + await this.render(this.source, this.breakToken); + + this.rendered = true; + + }); + } + + + }); + + page.onUnderflow((overflowToken) => { + // console.log("underflow on", page.id, overflowToken); + + // page.append(this.source, overflowToken); + + }); + } + + this.total = this.pages.length; + + return page; + } + /* + insertPage(index, blank) { + let lastPage = this.pages[index]; + // Create a new page from the template + let page = new Page(this.pagesArea, this.pageTemplate, blank, this.hooks); + + let total = this.pages.splice(index, 0, page); + + // Create the pages + page.create(undefined, lastPage && lastPage.element); + + page.index(index + 1); + + for (let i = index + 2; i < this.pages.length; i++) { + this.pages[i].index(i); + } + + if (!blank) { + // Listen for page overflow + page.onOverflow((overflowToken) => { + if (total < this.pages.length) { + this.pages[total].layout(this.source, overflowToken); + } else { + let newPage = this.addPage(); + newPage.layout(this.source, overflowToken); + } + }); + + page.onUnderflow(() => { + // console.log("underflow on", page.id); + }); + } + + this.total += 1; + + return page; + } + */ + + get total() { + return this._total; + } + + set total(num) { + this.pagesArea.style.setProperty("--pagedjs-page-count", num); + this._total = num; + } + + loadFonts() { + let fontPromises = []; + document.fonts.forEach((fontFace) => { + if (fontFace.status !== "loaded") { + let fontLoaded = fontFace.load().then((r) => { + return fontFace.family; + }, (r) => { + console.warn("Failed to preload font-family:", fontFace.family); + return fontFace.family; + }); + fontPromises.push(fontLoaded); + } + }); + return Promise.all(fontPromises).catch((err) => { + console.warn(err); + }); + } + + destroy() { + this.pagesArea.remove(); + this.pageTemplate.remove(); + } + + } + + eventEmitter(Chunker.prototype); + + // + // item item item item + // /------\ /------\ /------\ /------\ + // | data | | data | | data | | data | + // null <--+-prev |<---+-prev |<---+-prev |<---+-prev | + // | next-+--->| next-+--->| next-+--->| next-+--> null + // \------/ \------/ \------/ \------/ + // ^ ^ + // | list | + // | /------\ | + // \--------------+-head | | + // | tail-+--------------/ + // \------/ + // + + function createItem(data) { + return { + prev: null, + next: null, + data: data + }; + } + + function allocateCursor(node, prev, next) { + var cursor; + + if (cursors !== null) { + cursor = cursors; + cursors = cursors.cursor; + cursor.prev = prev; + cursor.next = next; + cursor.cursor = node.cursor; + } else { + cursor = { + prev: prev, + next: next, + cursor: node.cursor + }; + } + + node.cursor = cursor; + + return cursor; + } + + function releaseCursor(node) { + var cursor = node.cursor; + + node.cursor = cursor.cursor; + cursor.prev = null; + cursor.next = null; + cursor.cursor = cursors; + cursors = cursor; + } + + var cursors = null; + var List = function() { + this.cursor = null; + this.head = null; + this.tail = null; + }; + + List.createItem = createItem; + List.prototype.createItem = createItem; + + List.prototype.updateCursors = function(prevOld, prevNew, nextOld, nextNew) { + var cursor = this.cursor; + + while (cursor !== null) { + if (cursor.prev === prevOld) { + cursor.prev = prevNew; + } + + if (cursor.next === nextOld) { + cursor.next = nextNew; + } + + cursor = cursor.cursor; + } + }; + + List.prototype.getSize = function() { + var size = 0; + var cursor = this.head; + + while (cursor) { + size++; + cursor = cursor.next; + } + + return size; + }; + + List.prototype.fromArray = function(array) { + var cursor = null; + + this.head = null; + + for (var i = 0; i < array.length; i++) { + var item = createItem(array[i]); + + if (cursor !== null) { + cursor.next = item; + } else { + this.head = item; + } + + item.prev = cursor; + cursor = item; + } + + this.tail = cursor; + + return this; + }; + + List.prototype.toArray = function() { + var cursor = this.head; + var result = []; + + while (cursor) { + result.push(cursor.data); + cursor = cursor.next; + } + + return result; + }; + + List.prototype.toJSON = List.prototype.toArray; + + List.prototype.isEmpty = function() { + return this.head === null; + }; + + List.prototype.first = function() { + return this.head && this.head.data; + }; + + List.prototype.last = function() { + return this.tail && this.tail.data; + }; + + List.prototype.each = function(fn, context) { + var item; + + if (context === undefined) { + context = this; + } + + // push cursor + var cursor = allocateCursor(this, null, this.head); + + while (cursor.next !== null) { + item = cursor.next; + cursor.next = item.next; + + fn.call(context, item.data, item, this); + } + + // pop cursor + releaseCursor(this); + }; + + List.prototype.forEach = List.prototype.each; + + List.prototype.eachRight = function(fn, context) { + var item; + + if (context === undefined) { + context = this; + } + + // push cursor + var cursor = allocateCursor(this, this.tail, null); + + while (cursor.prev !== null) { + item = cursor.prev; + cursor.prev = item.prev; + + fn.call(context, item.data, item, this); + } + + // pop cursor + releaseCursor(this); + }; + + List.prototype.forEachRight = List.prototype.eachRight; + + List.prototype.nextUntil = function(start, fn, context) { + if (start === null) { + return; + } + + var item; + + if (context === undefined) { + context = this; + } + + // push cursor + var cursor = allocateCursor(this, null, start); + + while (cursor.next !== null) { + item = cursor.next; + cursor.next = item.next; + + if (fn.call(context, item.data, item, this)) { + break; + } + } + + // pop cursor + releaseCursor(this); + }; + + List.prototype.prevUntil = function(start, fn, context) { + if (start === null) { + return; + } + + var item; + + if (context === undefined) { + context = this; + } + + // push cursor + var cursor = allocateCursor(this, start, null); + + while (cursor.prev !== null) { + item = cursor.prev; + cursor.prev = item.prev; + + if (fn.call(context, item.data, item, this)) { + break; + } + } + + // pop cursor + releaseCursor(this); + }; + + List.prototype.some = function(fn, context) { + var cursor = this.head; + + if (context === undefined) { + context = this; + } + + while (cursor !== null) { + if (fn.call(context, cursor.data, cursor, this)) { + return true; + } + + cursor = cursor.next; + } + + return false; + }; + + List.prototype.map = function(fn, context) { + var result = new List(); + var cursor = this.head; + + if (context === undefined) { + context = this; + } + + while (cursor !== null) { + result.appendData(fn.call(context, cursor.data, cursor, this)); + cursor = cursor.next; + } + + return result; + }; + + List.prototype.filter = function(fn, context) { + var result = new List(); + var cursor = this.head; + + if (context === undefined) { + context = this; + } + + while (cursor !== null) { + if (fn.call(context, cursor.data, cursor, this)) { + result.appendData(cursor.data); + } + cursor = cursor.next; + } + + return result; + }; + + List.prototype.clear = function() { + this.head = null; + this.tail = null; + }; + + List.prototype.copy = function() { + var result = new List(); + var cursor = this.head; + + while (cursor !== null) { + result.insert(createItem(cursor.data)); + cursor = cursor.next; + } + + return result; + }; + + List.prototype.prepend = function(item) { + // head + // ^ + // item + this.updateCursors(null, item, this.head, item); + + // insert to the beginning of the list + if (this.head !== null) { + // new item <- first item + this.head.prev = item; + + // new item -> first item + item.next = this.head; + } else { + // if list has no head, then it also has no tail + // in this case tail points to the new item + this.tail = item; + } + + // head always points to new item + this.head = item; + + return this; + }; + + List.prototype.prependData = function(data) { + return this.prepend(createItem(data)); + }; + + List.prototype.append = function(item) { + return this.insert(item); + }; + + List.prototype.appendData = function(data) { + return this.insert(createItem(data)); + }; + + List.prototype.insert = function(item, before) { + if (before !== undefined && before !== null) { + // prev before + // ^ + // item + this.updateCursors(before.prev, item, before, item); + + if (before.prev === null) { + // insert to the beginning of list + if (this.head !== before) { + throw new Error('before doesn\'t belong to list'); + } + + // since head points to before therefore list doesn't empty + // no need to check tail + this.head = item; + before.prev = item; + item.next = before; + + this.updateCursors(null, item); + } else { + + // insert between two items + before.prev.next = item; + item.prev = before.prev; + + before.prev = item; + item.next = before; + } + } else { + // tail + // ^ + // item + this.updateCursors(this.tail, item, null, item); + + // insert to the ending of the list + if (this.tail !== null) { + // last item -> new item + this.tail.next = item; + + // last item <- new item + item.prev = this.tail; + } else { + // if list has no tail, then it also has no head + // in this case head points to new item + this.head = item; + } + + // tail always points to new item + this.tail = item; + } + + return this; + }; + + List.prototype.insertData = function(data, before) { + return this.insert(createItem(data), before); + }; + + List.prototype.remove = function(item) { + // item + // ^ + // prev next + this.updateCursors(item, item.prev, item, item.next); + + if (item.prev !== null) { + item.prev.next = item.next; + } else { + if (this.head !== item) { + throw new Error('item doesn\'t belong to list'); + } + + this.head = item.next; + } + + if (item.next !== null) { + item.next.prev = item.prev; + } else { + if (this.tail !== item) { + throw new Error('item doesn\'t belong to list'); + } + + this.tail = item.prev; + } + + item.prev = null; + item.next = null; + + return item; + }; + + List.prototype.push = function(data) { + this.insert(createItem(data)); + }; + + List.prototype.pop = function() { + if (this.tail !== null) { + return this.remove(this.tail); + } + }; + + List.prototype.unshift = function(data) { + this.prepend(createItem(data)); + }; + + List.prototype.shift = function() { + if (this.head !== null) { + return this.remove(this.head); + } + }; + + List.prototype.prependList = function(list) { + return this.insertList(list, this.head); + }; + + List.prototype.appendList = function(list) { + return this.insertList(list); + }; + + List.prototype.insertList = function(list, before) { + // ignore empty lists + if (list.head === null) { + return this; + } + + if (before !== undefined && before !== null) { + this.updateCursors(before.prev, list.tail, before, list.head); + + // insert in the middle of dist list + if (before.prev !== null) { + // before.prev <-> list.head + before.prev.next = list.head; + list.head.prev = before.prev; + } else { + this.head = list.head; + } + + before.prev = list.tail; + list.tail.next = before; + } else { + this.updateCursors(this.tail, list.tail, null, list.head); + + // insert to end of the list + if (this.tail !== null) { + // if destination list has a tail, then it also has a head, + // but head doesn't change + + // dest tail -> source head + this.tail.next = list.head; + + // dest tail <- source head + list.head.prev = this.tail; + } else { + // if list has no a tail, then it also has no a head + // in this case points head to new item + this.head = list.head; + } + + // tail always start point to new item + this.tail = list.tail; + } + + list.head = null; + list.tail = null; + + return this; + }; + + List.prototype.replace = function(oldItem, newItemOrList) { + if ('head' in newItemOrList) { + this.insertList(newItemOrList, oldItem); + } else { + this.insert(newItemOrList, oldItem); + } + + this.remove(oldItem); + }; + + var list = List; + + var createCustomError = function createCustomError(name, message) { + // use Object.create(), because some VMs prevent setting line/column otherwise + // (iOS Safari 10 even throws an exception) + var error = Object.create(SyntaxError.prototype); + var errorStack = new Error(); + + error.name = name; + error.message = message; + + Object.defineProperty(error, 'stack', { + get: function() { + return (errorStack.stack || '').replace(/^(.+\n){1,3}/, name + ': ' + message + '\n'); + } + }); + + return error; + }; + + var MAX_LINE_LENGTH = 100; + var OFFSET_CORRECTION = 60; + var TAB_REPLACEMENT = ' '; + + function sourceFragment(error, extraLines) { + function processLines(start, end) { + return lines.slice(start, end).map(function(line, idx) { + var num = String(start + idx + 1); + + while (num.length < maxNumLength) { + num = ' ' + num; + } + + return num + ' |' + line; + }).join('\n'); + } + + var lines = error.source.split(/\r\n?|\n|\f/); + var line = error.line; + var column = error.column; + var startLine = Math.max(1, line - extraLines) - 1; + var endLine = Math.min(line + extraLines, lines.length + 1); + var maxNumLength = Math.max(4, String(endLine).length) + 1; + var cutLeft = 0; + + // column correction according to replaced tab before column + column += (TAB_REPLACEMENT.length - 1) * (lines[line - 1].substr(0, column - 1).match(/\t/g) || []).length; + + if (column > MAX_LINE_LENGTH) { + cutLeft = column - OFFSET_CORRECTION + 3; + column = OFFSET_CORRECTION - 2; + } + + for (var i = startLine; i <= endLine; i++) { + if (i >= 0 && i < lines.length) { + lines[i] = lines[i].replace(/\t/g, TAB_REPLACEMENT); + lines[i] = + (cutLeft > 0 && lines[i].length > cutLeft ? '\u2026' : '') + + lines[i].substr(cutLeft, MAX_LINE_LENGTH - 2) + + (lines[i].length > cutLeft + MAX_LINE_LENGTH - 1 ? '\u2026' : ''); + } + } + + return [ + processLines(startLine, line), + new Array(column + maxNumLength + 2).join('-') + '^', + processLines(line, endLine) + ].filter(Boolean).join('\n'); + } + + var CssSyntaxError = function(message, source, offset, line, column) { + var error = createCustomError('CssSyntaxError', message); + + error.source = source; + error.offset = offset; + error.line = line; + error.column = column; + + error.sourceFragment = function(extraLines) { + return sourceFragment(error, isNaN(extraLines) ? 0 : extraLines); + }; + Object.defineProperty(error, 'formattedMessage', { + get: function() { + return ( + 'Parse error: ' + error.message + '\n' + + sourceFragment(error, 2) + ); + } + }); + + // for backward capability + error.parseError = { + offset: offset, + line: line, + column: column + }; + + return error; + }; + + var error = CssSyntaxError; + + // token types (note: value shouldn't intersect with used char codes) + var WHITESPACE = 1; + var IDENTIFIER = 2; + var NUMBER = 3; + var STRING = 4; + var COMMENT = 5; + var PUNCTUATOR = 6; + var CDO = 7; + var CDC = 8; + var ATKEYWORD = 14; + var FUNCTION = 15; + var URL$1 = 16; + var RAW = 17; + + var TAB = 9; + var N = 10; + var F = 12; + var R = 13; + var SPACE = 32; + + var TYPE = { + WhiteSpace: WHITESPACE, + Identifier: IDENTIFIER, + Number: NUMBER, + String: STRING, + Comment: COMMENT, + Punctuator: PUNCTUATOR, + CDO: CDO, + CDC: CDC, + AtKeyword: ATKEYWORD, + Function: FUNCTION, + Url: URL$1, + Raw: RAW, + + ExclamationMark: 33, // ! + QuotationMark: 34, // " + NumberSign: 35, // # + DollarSign: 36, // $ + PercentSign: 37, // % + Ampersand: 38, // & + Apostrophe: 39, // ' + LeftParenthesis: 40, // ( + RightParenthesis: 41, // ) + Asterisk: 42, // * + PlusSign: 43, // + + Comma: 44, // , + HyphenMinus: 45, // - + FullStop: 46, // . + Solidus: 47, // / + Colon: 58, // : + Semicolon: 59, // ; + LessThanSign: 60, // < + EqualsSign: 61, // = + GreaterThanSign: 62, // > + QuestionMark: 63, // ? + CommercialAt: 64, // @ + LeftSquareBracket: 91, // [ + Backslash: 92, // \ + RightSquareBracket: 93, // ] + CircumflexAccent: 94, // ^ + LowLine: 95, // _ + GraveAccent: 96, // ` + LeftCurlyBracket: 123, // { + VerticalLine: 124, // | + RightCurlyBracket: 125, // } + Tilde: 126 // ~ + }; + + var NAME = Object.keys(TYPE).reduce(function(result, key) { + result[TYPE[key]] = key; + return result; + }, {}); + + // https://drafts.csswg.org/css-syntax/#tokenizer-definitions + // > non-ASCII code point + // > A code point with a value equal to or greater than U+0080 + // > name-start code point + // > A letter, a non-ASCII code point, or U+005F LOW LINE (_). + // > name code point + // > A name-start code point, a digit, or U+002D HYPHEN-MINUS (-) + // That means only ASCII code points has a special meaning and we a maps for 0..127 codes only + var SafeUint32Array = typeof Uint32Array !== 'undefined' ? Uint32Array : Array; // fallback on Array when TypedArray is not supported + var SYMBOL_TYPE = new SafeUint32Array(0x80); + var PUNCTUATION = new SafeUint32Array(0x80); + var STOP_URL_RAW = new SafeUint32Array(0x80); + + for (var i = 0; i < SYMBOL_TYPE.length; i++) { + SYMBOL_TYPE[i] = IDENTIFIER; + } + + // fill categories + [ + TYPE.ExclamationMark, // ! + TYPE.QuotationMark, // " + TYPE.NumberSign, // # + TYPE.DollarSign, // $ + TYPE.PercentSign, // % + TYPE.Ampersand, // & + TYPE.Apostrophe, // ' + TYPE.LeftParenthesis, // ( + TYPE.RightParenthesis, // ) + TYPE.Asterisk, // * + TYPE.PlusSign, // + + TYPE.Comma, // , + TYPE.HyphenMinus, // - + TYPE.FullStop, // . + TYPE.Solidus, // / + TYPE.Colon, // : + TYPE.Semicolon, // ; + TYPE.LessThanSign, // < + TYPE.EqualsSign, // = + TYPE.GreaterThanSign, // > + TYPE.QuestionMark, // ? + TYPE.CommercialAt, // @ + TYPE.LeftSquareBracket, // [ + // TYPE.Backslash, // \ + TYPE.RightSquareBracket, // ] + TYPE.CircumflexAccent, // ^ + // TYPE.LowLine, // _ + TYPE.GraveAccent, // ` + TYPE.LeftCurlyBracket, // { + TYPE.VerticalLine, // | + TYPE.RightCurlyBracket, // } + TYPE.Tilde // ~ + ].forEach(function(key) { + SYMBOL_TYPE[Number(key)] = PUNCTUATOR; + PUNCTUATION[Number(key)] = PUNCTUATOR; + }); + + for (var i = 48; i <= 57; i++) { + SYMBOL_TYPE[i] = NUMBER; + } + + SYMBOL_TYPE[SPACE] = WHITESPACE; + SYMBOL_TYPE[TAB] = WHITESPACE; + SYMBOL_TYPE[N] = WHITESPACE; + SYMBOL_TYPE[R] = WHITESPACE; + SYMBOL_TYPE[F] = WHITESPACE; + + SYMBOL_TYPE[TYPE.Apostrophe] = STRING; + SYMBOL_TYPE[TYPE.QuotationMark] = STRING; + + STOP_URL_RAW[SPACE] = 1; + STOP_URL_RAW[TAB] = 1; + STOP_URL_RAW[N] = 1; + STOP_URL_RAW[R] = 1; + STOP_URL_RAW[F] = 1; + STOP_URL_RAW[TYPE.Apostrophe] = 1; + STOP_URL_RAW[TYPE.QuotationMark] = 1; + STOP_URL_RAW[TYPE.LeftParenthesis] = 1; + STOP_URL_RAW[TYPE.RightParenthesis] = 1; + + // whitespace is punctuation ... + PUNCTUATION[SPACE] = PUNCTUATOR; + PUNCTUATION[TAB] = PUNCTUATOR; + PUNCTUATION[N] = PUNCTUATOR; + PUNCTUATION[R] = PUNCTUATOR; + PUNCTUATION[F] = PUNCTUATOR; + // ... hyper minus is not + PUNCTUATION[TYPE.HyphenMinus] = 0; + + var _const = { + TYPE: TYPE, + NAME: NAME, + + SYMBOL_TYPE: SYMBOL_TYPE, + PUNCTUATION: PUNCTUATION, + STOP_URL_RAW: STOP_URL_RAW + }; + + var PUNCTUATION$1 = _const.PUNCTUATION; + var STOP_URL_RAW$1 = _const.STOP_URL_RAW; + var TYPE$1 = _const.TYPE; + var FULLSTOP = TYPE$1.FullStop; + var PLUSSIGN = TYPE$1.PlusSign; + var HYPHENMINUS = TYPE$1.HyphenMinus; + var PUNCTUATOR$1 = TYPE$1.Punctuator; + var TAB$1 = 9; + var N$1 = 10; + var F$1 = 12; + var R$1 = 13; + var SPACE$1 = 32; + var BACK_SLASH = 92; + var E = 101; // 'e'.charCodeAt(0) + + function firstCharOffset(source) { + // detect BOM (https://en.wikipedia.org/wiki/Byte_order_mark) + if (source.charCodeAt(0) === 0xFEFF || // UTF-16BE + source.charCodeAt(0) === 0xFFFE) { // UTF-16LE + return 1; + } + + return 0; + } + + function isHex(code) { + return (code >= 48 && code <= 57) || // 0 .. 9 + (code >= 65 && code <= 70) || // A .. F + (code >= 97 && code <= 102); // a .. f + } + + function isNumber(code) { + return code >= 48 && code <= 57; + } + + function isWhiteSpace(code) { + return code === SPACE$1 || code === TAB$1 || isNewline(code); + } + + function isNewline(code) { + return code === R$1 || code === N$1 || code === F$1; + } + + function getNewlineLength(source, offset, code) { + if (isNewline(code)) { + if (code === R$1 && offset + 1 < source.length && source.charCodeAt(offset + 1) === N$1) { + return 2; + } + + return 1; + } + + return 0; + } + + function cmpChar(testStr, offset, referenceCode) { + var code = testStr.charCodeAt(offset); + + // code.toLowerCase() for A..Z + if (code >= 65 && code <= 90) { + code = code | 32; + } + + return code === referenceCode; + } + + function cmpStr(testStr, start, end, referenceStr) { + if (end - start !== referenceStr.length) { + return false; + } + + if (start < 0 || end > testStr.length) { + return false; + } + + for (var i = start; i < end; i++) { + var testCode = testStr.charCodeAt(i); + var refCode = referenceStr.charCodeAt(i - start); + + // testCode.toLowerCase() for A..Z + if (testCode >= 65 && testCode <= 90) { + testCode = testCode | 32; + } + + if (testCode !== refCode) { + return false; + } + } + + return true; + } + + function findWhiteSpaceStart(source, offset) { + while (offset >= 0 && isWhiteSpace(source.charCodeAt(offset))) { + offset--; + } + + return offset + 1; + } + + function findWhiteSpaceEnd(source, offset) { + while (offset < source.length && isWhiteSpace(source.charCodeAt(offset))) { + offset++; + } + + return offset; + } + + function findCommentEnd(source, offset) { + var commentEnd = source.indexOf('*/', offset); + + if (commentEnd === -1) { + return source.length; + } + + return commentEnd + 2; + } + + function findStringEnd(source, offset, quote) { + for (; offset < source.length; offset++) { + var code = source.charCodeAt(offset); + + // TODO: bad string + if (code === BACK_SLASH) { + offset++; + } else if (code === quote) { + offset++; + break; + } + } + + return offset; + } + + function findDecimalNumberEnd(source, offset) { + while (offset < source.length && isNumber(source.charCodeAt(offset))) { + offset++; + } + + return offset; + } + + function findNumberEnd(source, offset, allowFraction) { + var code; + + offset = findDecimalNumberEnd(source, offset); + + // fraction: .\d+ + if (allowFraction && offset + 1 < source.length && source.charCodeAt(offset) === FULLSTOP) { + code = source.charCodeAt(offset + 1); + + if (isNumber(code)) { + offset = findDecimalNumberEnd(source, offset + 1); + } + } + + // exponent: e[+-]\d+ + if (offset + 1 < source.length) { + if ((source.charCodeAt(offset) | 32) === E) { // case insensitive check for `e` + code = source.charCodeAt(offset + 1); + + if (code === PLUSSIGN || code === HYPHENMINUS) { + if (offset + 2 < source.length) { + code = source.charCodeAt(offset + 2); + } + } + + if (isNumber(code)) { + offset = findDecimalNumberEnd(source, offset + 2); + } + } + } + + return offset; + } + + // skip escaped unicode sequence that can ends with space + // [0-9a-f]{1,6}(\r\n|[ \n\r\t\f])? + function findEscapeEnd(source, offset) { + for (var i = 0; i < 7 && offset + i < source.length; i++) { + var code = source.charCodeAt(offset + i); + + if (i !== 6 && isHex(code)) { + continue; + } + + if (i > 0) { + offset += i - 1 + getNewlineLength(source, offset + i, code); + if (code === SPACE$1 || code === TAB$1) { + offset++; + } + } + + break; + } + + return offset; + } + + function findIdentifierEnd(source, offset) { + for (; offset < source.length; offset++) { + var code = source.charCodeAt(offset); + + if (code === BACK_SLASH) { + offset = findEscapeEnd(source, offset + 1); + } else if (code < 0x80 && PUNCTUATION$1[code] === PUNCTUATOR$1) { + break; + } + } + + return offset; + } + + function findUrlRawEnd(source, offset) { + for (; offset < source.length; offset++) { + var code = source.charCodeAt(offset); + + if (code === BACK_SLASH) { + offset = findEscapeEnd(source, offset + 1); + } else if (code < 0x80 && STOP_URL_RAW$1[code] === 1) { + break; + } + } + + return offset; + } + + var utils = { + firstCharOffset: firstCharOffset, + + isHex: isHex, + isNumber: isNumber, + isWhiteSpace: isWhiteSpace, + isNewline: isNewline, + getNewlineLength: getNewlineLength, + + cmpChar: cmpChar, + cmpStr: cmpStr, + + findWhiteSpaceStart: findWhiteSpaceStart, + findWhiteSpaceEnd: findWhiteSpaceEnd, + findCommentEnd: findCommentEnd, + findStringEnd: findStringEnd, + findDecimalNumberEnd: findDecimalNumberEnd, + findNumberEnd: findNumberEnd, + findEscapeEnd: findEscapeEnd, + findIdentifierEnd: findIdentifierEnd, + findUrlRawEnd: findUrlRawEnd + }; + + var TYPE$2 = _const.TYPE; + var NAME$1 = _const.NAME; + var SYMBOL_TYPE$1 = _const.SYMBOL_TYPE; + + + var firstCharOffset$1 = utils.firstCharOffset; + var cmpStr$1 = utils.cmpStr; + var isNumber$1 = utils.isNumber; + var findWhiteSpaceStart$1 = utils.findWhiteSpaceStart; + var findWhiteSpaceEnd$1 = utils.findWhiteSpaceEnd; + var findCommentEnd$1 = utils.findCommentEnd; + var findStringEnd$1 = utils.findStringEnd; + var findNumberEnd$1 = utils.findNumberEnd; + var findIdentifierEnd$1 = utils.findIdentifierEnd; + var findUrlRawEnd$1 = utils.findUrlRawEnd; + + var NULL = 0; + var WHITESPACE$1 = TYPE$2.WhiteSpace; + var IDENTIFIER$1 = TYPE$2.Identifier; + var NUMBER$1 = TYPE$2.Number; + var STRING$1 = TYPE$2.String; + var COMMENT$1 = TYPE$2.Comment; + var PUNCTUATOR$2 = TYPE$2.Punctuator; + var CDO$1 = TYPE$2.CDO; + var CDC$1 = TYPE$2.CDC; + var ATKEYWORD$1 = TYPE$2.AtKeyword; + var FUNCTION$1 = TYPE$2.Function; + var URL$2 = TYPE$2.Url; + var RAW$1 = TYPE$2.Raw; + + var N$2 = 10; + var F$2 = 12; + var R$2 = 13; + var STAR = TYPE$2.Asterisk; + var SLASH = TYPE$2.Solidus; + var FULLSTOP$1 = TYPE$2.FullStop; + var PLUSSIGN$1 = TYPE$2.PlusSign; + var HYPHENMINUS$1 = TYPE$2.HyphenMinus; + var GREATERTHANSIGN = TYPE$2.GreaterThanSign; + var LESSTHANSIGN = TYPE$2.LessThanSign; + var EXCLAMATIONMARK = TYPE$2.ExclamationMark; + var COMMERCIALAT = TYPE$2.CommercialAt; + var QUOTATIONMARK = TYPE$2.QuotationMark; + var APOSTROPHE = TYPE$2.Apostrophe; + var LEFTPARENTHESIS = TYPE$2.LeftParenthesis; + var RIGHTPARENTHESIS = TYPE$2.RightParenthesis; + var LEFTCURLYBRACKET = TYPE$2.LeftCurlyBracket; + var RIGHTCURLYBRACKET = TYPE$2.RightCurlyBracket; + var LEFTSQUAREBRACKET = TYPE$2.LeftSquareBracket; + var RIGHTSQUAREBRACKET = TYPE$2.RightSquareBracket; + + var MIN_BUFFER_SIZE = 16 * 1024; + var OFFSET_MASK = 0x00FFFFFF; + var TYPE_SHIFT = 24; + var SafeUint32Array$1 = typeof Uint32Array !== 'undefined' ? Uint32Array : Array; // fallback on Array when TypedArray is not supported + + function computeLinesAndColumns(tokenizer, source) { + var sourceLength = source.length; + var start = firstCharOffset$1(source); + var lines = tokenizer.lines; + var line = tokenizer.startLine; + var columns = tokenizer.columns; + var column = tokenizer.startColumn; + + if (lines === null || lines.length < sourceLength + 1) { + lines = new SafeUint32Array$1(Math.max(sourceLength + 1024, MIN_BUFFER_SIZE)); + columns = new SafeUint32Array$1(lines.length); + } + + for (var i = start; i < sourceLength; i++) { + var code = source.charCodeAt(i); + + lines[i] = line; + columns[i] = column++; + + if (code === N$2 || code === R$2 || code === F$2) { + if (code === R$2 && i + 1 < sourceLength && source.charCodeAt(i + 1) === N$2) { + i++; + lines[i] = line; + columns[i] = column; + } + + line++; + column = 1; + } + } + + lines[i] = line; + columns[i] = column; + + tokenizer.linesAnsColumnsComputed = true; + tokenizer.lines = lines; + tokenizer.columns = columns; + } + + function tokenLayout(tokenizer, source, startPos) { + var sourceLength = source.length; + var offsetAndType = tokenizer.offsetAndType; + var balance = tokenizer.balance; + var tokenCount = 0; + var prevType = 0; + var offset = startPos; + var anchor = 0; + var balanceCloseCode = 0; + var balanceStart = 0; + var balancePrev = 0; + + if (offsetAndType === null || offsetAndType.length < sourceLength + 1) { + offsetAndType = new SafeUint32Array$1(sourceLength + 1024); + balance = new SafeUint32Array$1(sourceLength + 1024); + } + + while (offset < sourceLength) { + var code = source.charCodeAt(offset); + var type = code < 0x80 ? SYMBOL_TYPE$1[code] : IDENTIFIER$1; + + balance[tokenCount] = sourceLength; + + switch (type) { + case WHITESPACE$1: + offset = findWhiteSpaceEnd$1(source, offset + 1); + break; + + case PUNCTUATOR$2: + switch (code) { + case balanceCloseCode: + balancePrev = balanceStart & OFFSET_MASK; + balanceStart = balance[balancePrev]; + balanceCloseCode = balanceStart >> TYPE_SHIFT; + balance[tokenCount] = balancePrev; + balance[balancePrev++] = tokenCount; + for (; balancePrev < tokenCount; balancePrev++) { + if (balance[balancePrev] === sourceLength) { + balance[balancePrev] = tokenCount; + } + } + break; + + case LEFTSQUAREBRACKET: + balance[tokenCount] = balanceStart; + balanceCloseCode = RIGHTSQUAREBRACKET; + balanceStart = (balanceCloseCode << TYPE_SHIFT) | tokenCount; + break; + + case LEFTCURLYBRACKET: + balance[tokenCount] = balanceStart; + balanceCloseCode = RIGHTCURLYBRACKET; + balanceStart = (balanceCloseCode << TYPE_SHIFT) | tokenCount; + break; + + case LEFTPARENTHESIS: + balance[tokenCount] = balanceStart; + balanceCloseCode = RIGHTPARENTHESIS; + balanceStart = (balanceCloseCode << TYPE_SHIFT) | tokenCount; + break; + } + + // /* + if (code === STAR && prevType === SLASH) { + type = COMMENT$1; + offset = findCommentEnd$1(source, offset + 1); + tokenCount--; // rewrite prev token + break; + } + + // edge case for -.123 and +.123 + if (code === FULLSTOP$1 && (prevType === PLUSSIGN$1 || prevType === HYPHENMINUS$1)) { + if (offset + 1 < sourceLength && isNumber$1(source.charCodeAt(offset + 1))) { + type = NUMBER$1; + offset = findNumberEnd$1(source, offset + 2, false); + tokenCount--; // rewrite prev token + break; + } + } + + // + if (code === HYPHENMINUS$1 && prevType === HYPHENMINUS$1) { + if (offset + 1 < sourceLength && source.charCodeAt(offset + 1) === GREATERTHANSIGN) { + type = CDC$1; + offset = offset + 2; + tokenCount--; // rewrite prev token + break; + } + } + + // ident( + if (code === LEFTPARENTHESIS && prevType === IDENTIFIER$1) { + offset = offset + 1; + tokenCount--; // rewrite prev token + balance[tokenCount] = balance[tokenCount + 1]; + balanceStart--; + + // 4 char length identifier and equal to `url(` (case insensitive) + if (offset - anchor === 4 && cmpStr$1(source, anchor, offset, 'url(')) { + // special case for url() because it can contain any symbols sequence with few exceptions + anchor = findWhiteSpaceEnd$1(source, offset); + code = source.charCodeAt(anchor); + if (code !== LEFTPARENTHESIS && + code !== RIGHTPARENTHESIS && + code !== QUOTATIONMARK && + code !== APOSTROPHE) { + // url( + offsetAndType[tokenCount++] = (URL$2 << TYPE_SHIFT) | offset; + balance[tokenCount] = sourceLength; + + // ws* + if (anchor !== offset) { + offsetAndType[tokenCount++] = (WHITESPACE$1 << TYPE_SHIFT) | anchor; + balance[tokenCount] = sourceLength; + } + + // raw + type = RAW$1; + offset = findUrlRawEnd$1(source, anchor); + } else { + type = URL$2; + } + } else { + type = FUNCTION$1; + } + break; + } + + type = code; + offset = offset + 1; + break; + + case NUMBER$1: + offset = findNumberEnd$1(source, offset + 1, prevType !== FULLSTOP$1); + + // merge number with a preceding dot, dash or plus + if (prevType === FULLSTOP$1 || + prevType === HYPHENMINUS$1 || + prevType === PLUSSIGN$1) { + tokenCount--; // rewrite prev token + } + + break; + + case STRING$1: + offset = findStringEnd$1(source, offset + 1, code); + break; + + default: + anchor = offset; + offset = findIdentifierEnd$1(source, offset); + + // merge identifier with a preceding dash + if (prevType === HYPHENMINUS$1) { + // rewrite prev token + tokenCount--; + // restore prev prev token type + // for case @-prefix-ident + prevType = tokenCount === 0 ? 0 : offsetAndType[tokenCount - 1] >> TYPE_SHIFT; + } + + if (prevType === COMMERCIALAT) { + // rewrite prev token and change type to + tokenCount--; + type = ATKEYWORD$1; + } + } + + offsetAndType[tokenCount++] = (type << TYPE_SHIFT) | offset; + prevType = type; + } + + // finalize arrays + offsetAndType[tokenCount] = offset; + balance[tokenCount] = sourceLength; + balance[sourceLength] = sourceLength; // prevents false positive balance match with any token + while (balanceStart !== 0) { + balancePrev = balanceStart & OFFSET_MASK; + balanceStart = balance[balancePrev]; + balance[balancePrev] = sourceLength; + } + + tokenizer.offsetAndType = offsetAndType; + tokenizer.tokenCount = tokenCount; + tokenizer.balance = balance; + } + + // + // tokenizer + // + + var Tokenizer = function(source, startOffset, startLine, startColumn) { + this.offsetAndType = null; + this.balance = null; + this.lines = null; + this.columns = null; + + this.setSource(source, startOffset, startLine, startColumn); + }; + + Tokenizer.prototype = { + setSource: function(source, startOffset, startLine, startColumn) { + var safeSource = String(source || ''); + var start = firstCharOffset$1(safeSource); + + this.source = safeSource; + this.firstCharOffset = start; + this.startOffset = typeof startOffset === 'undefined' ? 0 : startOffset; + this.startLine = typeof startLine === 'undefined' ? 1 : startLine; + this.startColumn = typeof startColumn === 'undefined' ? 1 : startColumn; + this.linesAnsColumnsComputed = false; + + this.eof = false; + this.currentToken = -1; + this.tokenType = 0; + this.tokenStart = start; + this.tokenEnd = start; + + tokenLayout(this, safeSource, start); + this.next(); + }, + + lookupType: function(offset) { + offset += this.currentToken; + + if (offset < this.tokenCount) { + return this.offsetAndType[offset] >> TYPE_SHIFT; + } + + return NULL; + }, + lookupNonWSType: function(offset) { + offset += this.currentToken; + + for (var type; offset < this.tokenCount; offset++) { + type = this.offsetAndType[offset] >> TYPE_SHIFT; + + if (type !== WHITESPACE$1) { + return type; + } + } + + return NULL; + }, + lookupValue: function(offset, referenceStr) { + offset += this.currentToken; + + if (offset < this.tokenCount) { + return cmpStr$1( + this.source, + this.offsetAndType[offset - 1] & OFFSET_MASK, + this.offsetAndType[offset] & OFFSET_MASK, + referenceStr + ); + } + + return false; + }, + getTokenStart: function(tokenNum) { + if (tokenNum === this.currentToken) { + return this.tokenStart; + } + + if (tokenNum > 0) { + return tokenNum < this.tokenCount + ? this.offsetAndType[tokenNum - 1] & OFFSET_MASK + : this.offsetAndType[this.tokenCount] & OFFSET_MASK; + } + + return this.firstCharOffset; + }, + getOffsetExcludeWS: function() { + if (this.currentToken > 0) { + if ((this.offsetAndType[this.currentToken - 1] >> TYPE_SHIFT) === WHITESPACE$1) { + return this.currentToken > 1 + ? this.offsetAndType[this.currentToken - 2] & OFFSET_MASK + : this.firstCharOffset; + } + } + return this.tokenStart; + }, + getRawLength: function(startToken, endTokenType1, endTokenType2, includeTokenType2) { + var cursor = startToken; + var balanceEnd; + + loop: + for (; cursor < this.tokenCount; cursor++) { + balanceEnd = this.balance[cursor]; + + // belance end points to offset before start + if (balanceEnd < startToken) { + break loop; + } + + // check token is stop type + switch (this.offsetAndType[cursor] >> TYPE_SHIFT) { + case endTokenType1: + break loop; + + case endTokenType2: + if (includeTokenType2) { + cursor++; + } + break loop; + + default: + // fast forward to the end of balanced block + if (this.balance[balanceEnd] === cursor) { + cursor = balanceEnd; + } + } + + } + + return cursor - this.currentToken; + }, + isBalanceEdge: function(pos) { + var balanceStart = this.balance[this.currentToken]; + return balanceStart < pos; + }, + + getTokenValue: function() { + return this.source.substring(this.tokenStart, this.tokenEnd); + }, + substrToCursor: function(start) { + return this.source.substring(start, this.tokenStart); + }, + + skipWS: function() { + for (var i = this.currentToken, skipTokenCount = 0; i < this.tokenCount; i++, skipTokenCount++) { + if ((this.offsetAndType[i] >> TYPE_SHIFT) !== WHITESPACE$1) { + break; + } + } + + if (skipTokenCount > 0) { + this.skip(skipTokenCount); + } + }, + skipSC: function() { + while (this.tokenType === WHITESPACE$1 || this.tokenType === COMMENT$1) { + this.next(); + } + }, + skip: function(tokenCount) { + var next = this.currentToken + tokenCount; + + if (next < this.tokenCount) { + this.currentToken = next; + this.tokenStart = this.offsetAndType[next - 1] & OFFSET_MASK; + next = this.offsetAndType[next]; + this.tokenType = next >> TYPE_SHIFT; + this.tokenEnd = next & OFFSET_MASK; + } else { + this.currentToken = this.tokenCount; + this.next(); + } + }, + next: function() { + var next = this.currentToken + 1; + + if (next < this.tokenCount) { + this.currentToken = next; + this.tokenStart = this.tokenEnd; + next = this.offsetAndType[next]; + this.tokenType = next >> TYPE_SHIFT; + this.tokenEnd = next & OFFSET_MASK; + } else { + this.currentToken = this.tokenCount; + this.eof = true; + this.tokenType = NULL; + this.tokenStart = this.tokenEnd = this.source.length; + } + }, + + eat: function(tokenType) { + if (this.tokenType !== tokenType) { + var offset = this.tokenStart; + var message = NAME$1[tokenType] + ' is expected'; + + // tweak message and offset + if (tokenType === IDENTIFIER$1) { + // when identifier is expected but there is a function or url + if (this.tokenType === FUNCTION$1 || this.tokenType === URL$2) { + offset = this.tokenEnd - 1; + message += ' but function found'; + } + } else { + // when test type is part of another token show error for current position + 1 + // e.g. eat(HYPHENMINUS) will fail on "-foo", but pointing on "-" is odd + if (this.source.charCodeAt(this.tokenStart) === tokenType) { + offset = offset + 1; + } + } + + this.error(message, offset); + } + + this.next(); + }, + eatNonWS: function(tokenType) { + this.skipWS(); + this.eat(tokenType); + }, + + consume: function(tokenType) { + var value = this.getTokenValue(); + + this.eat(tokenType); + + return value; + }, + consumeFunctionName: function() { + var name = this.source.substring(this.tokenStart, this.tokenEnd - 1); + + this.eat(FUNCTION$1); + + return name; + }, + consumeNonWS: function(tokenType) { + this.skipWS(); + + return this.consume(tokenType); + }, + + expectIdentifier: function(name) { + if (this.tokenType !== IDENTIFIER$1 || cmpStr$1(this.source, this.tokenStart, this.tokenEnd, name) === false) { + this.error('Identifier `' + name + '` is expected'); + } + + this.next(); + }, + + getLocation: function(offset, filename) { + if (!this.linesAnsColumnsComputed) { + computeLinesAndColumns(this, this.source); + } + + return { + source: filename, + offset: this.startOffset + offset, + line: this.lines[offset], + column: this.columns[offset] + }; + }, + + getLocationRange: function(start, end, filename) { + if (!this.linesAnsColumnsComputed) { + computeLinesAndColumns(this, this.source); + } + + return { + source: filename, + start: { + offset: this.startOffset + start, + line: this.lines[start], + column: this.columns[start] + }, + end: { + offset: this.startOffset + end, + line: this.lines[end], + column: this.columns[end] + } + }; + }, + + error: function(message, offset) { + var location = typeof offset !== 'undefined' && offset < this.source.length + ? this.getLocation(offset) + : this.eof + ? this.getLocation(findWhiteSpaceStart$1(this.source, this.source.length - 1)) + : this.getLocation(this.tokenStart); + + throw new error( + message || 'Unexpected input', + this.source, + location.offset, + location.line, + location.column + ); + }, + + dump: function() { + var offset = 0; + + return Array.prototype.slice.call(this.offsetAndType, 0, this.tokenCount).map(function(item, idx) { + var start = offset; + var end = item & OFFSET_MASK; + + offset = end; + + return { + idx: idx, + type: NAME$1[item >> TYPE_SHIFT], + chunk: this.source.substring(start, end), + balance: this.balance[idx] + }; + }, this); + } + }; + + // extend with error class + Tokenizer.CssSyntaxError = error; + + // extend tokenizer with constants + Object.keys(_const).forEach(function(key) { + Tokenizer[key] = _const[key]; + }); + + // extend tokenizer with static methods from utils + Object.keys(utils).forEach(function(key) { + Tokenizer[key] = utils[key]; + }); + + // warm up tokenizer to elimitate code branches that never execute + // fix soft deoptimizations (insufficient type feedback) + new Tokenizer('\n\r\r\n\f//""\'\'/*\r\n\f*/1a;.\\31\t\+2{url(a);func();+1.2e3 -.4e-5 .6e+7}').getLocation(); + + var Tokenizer_1 = Tokenizer; + + var tokenizer = Tokenizer_1; + + function noop$1(value) { + return value; + } + + function generateMultiplier(multiplier) { + if (multiplier.min === 0 && multiplier.max === 0) { + return '*'; + } + + if (multiplier.min === 0 && multiplier.max === 1) { + return '?'; + } + + if (multiplier.min === 1 && multiplier.max === 0) { + return multiplier.comma ? '#' : '+'; + } + + if (multiplier.min === 1 && multiplier.max === 1) { + return ''; + } + + return ( + (multiplier.comma ? '#' : '') + + (multiplier.min === multiplier.max + ? '{' + multiplier.min + '}' + : '{' + multiplier.min + ',' + (multiplier.max !== 0 ? multiplier.max : '') + '}' + ) + ); + } + + function generateSequence(node, forceBraces, decorate) { + var result = node.terms.map(function(term) { + return generate(term, forceBraces, decorate); + }).join(node.combinator === ' ' ? ' ' : ' ' + node.combinator + ' '); + + if (node.explicit || forceBraces) { + result = (result[0] !== ',' ? '[ ' : '[') + result + ' ]'; + } + + return result; + } + + function generate(node, forceBraces, decorate) { + var result; + + switch (node.type) { + case 'Group': + result = + generateSequence(node, forceBraces, decorate) + + (node.disallowEmpty ? '!' : ''); + break; + + case 'Multiplier': + // return since node is a composition + return ( + generate(node.term, forceBraces, decorate) + + decorate(generateMultiplier(node), node) + ); + + case 'Type': + result = '<' + node.name + '>'; + break; + + case 'Property': + result = '<\'' + node.name + '\'>'; + break; + + case 'Keyword': + result = node.name; + break; + + case 'AtKeyword': + result = '@' + node.name; + break; + + case 'Function': + result = node.name + '('; + break; + + case 'String': + case 'Token': + result = node.value; + break; + + case 'Comma': + result = ','; + break; + + default: + throw new Error('Unknown node type `' + node.type + '`'); + } + + return decorate(result, node); + } + + var generate_1 = function(node, options) { + var decorate = noop$1; + var forceBraces = false; + + if (typeof options === 'function') { + decorate = options; + } else if (options) { + forceBraces = Boolean(options.forceBraces); + if (typeof options.decorate === 'function') { + decorate = options.decorate; + } + } + + return generate(node, forceBraces, decorate); + }; + + function fromMatchResult(matchResult) { + var tokens = matchResult.tokens; + var longestMatch = matchResult.longestMatch; + var node = longestMatch < tokens.length ? tokens[longestMatch].node : null; + var mismatchOffset = 0; + var entries = 0; + var css = ''; + + for (var i = 0; i < tokens.length; i++) { + if (i === longestMatch) { + mismatchOffset = css.length; + } + + if (node !== null && tokens[i].node === node) { + if (i <= longestMatch) { + entries++; + } else { + entries = 0; + } + } + + css += tokens[i].value; + } + + if (node === null) { + mismatchOffset = css.length; + } + + return { + node: node, + css: css, + mismatchOffset: mismatchOffset, + last: node === null || entries > 1 + }; + } + + function getLocation(node, point) { + var loc = node && node.loc && node.loc[point]; + + if (loc) { + return { + offset: loc.offset, + line: loc.line, + column: loc.column + }; + } + + return null; + } + + var SyntaxReferenceError = function(type, referenceName) { + var error = createCustomError( + 'SyntaxReferenceError', + type + (referenceName ? ' `' + referenceName + '`' : '') + ); + + error.reference = referenceName; + + return error; + }; + + var MatchError = function(message, lexer, syntax, node, matchResult) { + var error = createCustomError('SyntaxMatchError', message); + var details = fromMatchResult(matchResult); + var mismatchOffset = details.mismatchOffset || 0; + var badNode = details.node || node; + var end = getLocation(badNode, 'end'); + var start = details.last ? end : getLocation(badNode, 'start'); + var css = details.css; + + error.rawMessage = message; + error.syntax = syntax ? generate_1(syntax) : ''; + error.css = css; + error.mismatchOffset = mismatchOffset; + error.loc = { + source: (badNode && badNode.loc && badNode.loc.source) || '', + start: start, + end: end + }; + error.line = start ? start.line : undefined; + error.column = start ? start.column : undefined; + error.offset = start ? start.offset : undefined; + error.message = message + '\n' + + ' syntax: ' + error.syntax + '\n' + + ' value: ' + (error.css || '') + '\n' + + ' --------' + new Array(error.mismatchOffset + 1).join('-') + '^'; + + return error; + }; + + var error$1 = { + SyntaxReferenceError: SyntaxReferenceError, + MatchError: MatchError + }; + + var hasOwnProperty = Object.prototype.hasOwnProperty; + var keywords = Object.create(null); + var properties = Object.create(null); + var HYPHENMINUS$2 = 45; // '-'.charCodeAt() + + function isCustomProperty(str, offset) { + offset = offset || 0; + + return str.length - offset >= 2 && + str.charCodeAt(offset) === HYPHENMINUS$2 && + str.charCodeAt(offset + 1) === HYPHENMINUS$2; + } + + function getVendorPrefix(str, offset) { + offset = offset || 0; + + // verdor prefix should be at least 3 chars length + if (str.length - offset >= 3) { + // vendor prefix starts with hyper minus following non-hyper minus + if (str.charCodeAt(offset) === HYPHENMINUS$2 && + str.charCodeAt(offset + 1) !== HYPHENMINUS$2) { + // vendor prefix should contain a hyper minus at the ending + var secondDashIndex = str.indexOf('-', offset + 2); + + if (secondDashIndex !== -1) { + return str.substring(offset, secondDashIndex + 1); + } + } + } + + return ''; + } + + function getKeywordDescriptor(keyword) { + if (hasOwnProperty.call(keywords, keyword)) { + return keywords[keyword]; + } + + var name = keyword.toLowerCase(); + + if (hasOwnProperty.call(keywords, name)) { + return keywords[keyword] = keywords[name]; + } + + var custom = isCustomProperty(name, 0); + var vendor = !custom ? getVendorPrefix(name, 0) : ''; + + return keywords[keyword] = Object.freeze({ + basename: name.substr(vendor.length), + name: name, + vendor: vendor, + prefix: vendor, + custom: custom + }); + } + + function getPropertyDescriptor(property) { + if (hasOwnProperty.call(properties, property)) { + return properties[property]; + } + + var name = property; + var hack = property[0]; + + if (hack === '/') { + hack = property[1] === '/' ? '//' : '/'; + } else if (hack !== '_' && + hack !== '*' && + hack !== '$' && + hack !== '#' && + hack !== '+') { + hack = ''; + } + + var custom = isCustomProperty(name, hack.length); + + // re-use result when possible (the same as for lower case) + if (!custom) { + name = name.toLowerCase(); + if (hasOwnProperty.call(properties, name)) { + return properties[property] = properties[name]; + } + } + + var vendor = !custom ? getVendorPrefix(name, hack.length) : ''; + var prefix = name.substr(0, hack.length + vendor.length); + + return properties[property] = Object.freeze({ + basename: name.substr(prefix.length), + name: name.substr(hack.length), + hack: hack, + vendor: vendor, + prefix: prefix, + custom: custom + }); + } + + var names = { + keyword: getKeywordDescriptor, + property: getPropertyDescriptor, + isCustomProperty: isCustomProperty, + vendorPrefix: getVendorPrefix + }; + + var findIdentifierEnd$2 = utils.findIdentifierEnd; + var findNumberEnd$2 = utils.findNumberEnd; + var findDecimalNumberEnd$1 = utils.findDecimalNumberEnd; + var isHex$1 = utils.isHex; + + var SYMBOL_TYPE$2 = _const.SYMBOL_TYPE; + var IDENTIFIER$2 = _const.TYPE.Identifier; + var PLUSSIGN$2 = _const.TYPE.PlusSign; + var HYPHENMINUS$3 = _const.TYPE.HyphenMinus; + var NUMBERSIGN = _const.TYPE.NumberSign; + + var PERCENTAGE = { + '%': true + }; + + // https://www.w3.org/TR/css-values-3/#lengths + var LENGTH = { + // absolute length units + 'px': true, + 'mm': true, + 'cm': true, + 'in': true, + 'pt': true, + 'pc': true, + 'q': true, + + // relative length units + 'em': true, + 'ex': true, + 'ch': true, + 'rem': true, + + // viewport-percentage lengths + 'vh': true, + 'vw': true, + 'vmin': true, + 'vmax': true, + 'vm': true + }; + + var ANGLE = { + 'deg': true, + 'grad': true, + 'rad': true, + 'turn': true + }; + + var TIME = { + 's': true, + 'ms': true + }; + + var FREQUENCY = { + 'hz': true, + 'khz': true + }; + + // https://www.w3.org/TR/css-values-3/#resolution (https://drafts.csswg.org/css-values/#resolution) + var RESOLUTION = { + 'dpi': true, + 'dpcm': true, + 'dppx': true, + 'x': true // https://github.com/w3c/csswg-drafts/issues/461 + }; + + // https://drafts.csswg.org/css-grid/#fr-unit + var FLEX = { + 'fr': true + }; + + // https://www.w3.org/TR/css3-speech/#mixing-props-voice-volume + var DECIBEL = { + 'db': true + }; + + // https://www.w3.org/TR/css3-speech/#voice-props-voice-pitch + var SEMITONES = { + 'st': true + }; + + function consumeFunction(token, addTokenToMatch, getNextToken) { + var length = 1; + var cursor; + + do { + cursor = getNextToken(length++); + } while (cursor !== null && cursor.node !== token.node); + + if (cursor === null) { + return false; + } + + while (true) { + // consume tokens until cursor + if (addTokenToMatch() === cursor) { + break; + } + } + + return true; + } + + // TODO: implement + // can be used wherever , , ,