Full Code of jordwalke/paradoc for AI

master 739e329c8c80 cached
23 files
1.1 MB
568.8k tokens
195 symbols
1 requests
Download .txt
Showing preview only (1,184K chars total). Download the full file or copy to clipboard to get everything.
Repository: jordwalke/paradoc
Branch: master
Commit: 739e329c8c80
Files: 23
Total size: 1.1 MB

Directory structure:
gitextract_wtgaopnk/

├── .gitignore
├── API.html
├── MIT-LICENSE
├── README.html
├── VERSIONS.html
├── configuring-pages.html
├── integration.html
├── site/
│   ├── ORIGINS.md
│   ├── Paradoc.js
│   ├── fonts/
│   │   ├── CodingFont.css
│   │   ├── LICENSE-Fira
│   │   ├── LICENSE-Roboto
│   │   └── WordFont.css
│   ├── package.json
│   ├── theme.styl.html
│   └── vendor/
│       ├── highlight-styles/
│       │   ├── atom-one-light.css
│       │   └── mono-blue.css
│       ├── highlight.pack.js
│       ├── jquery.js
│       ├── medium-zoom.js
│       └── reason-highlightjs.js
├── siteTemplate.html
└── styles.html

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.merlin
node_modules/
_build
_esy
_release
*.byte
*.native
*.install
site/index.html
site/index.rendered.html
site/fonts/PrivateCodingFont.css
site/fonts/PrivateWordFont.css
site/node_modules
Design
.DS_Store
*/.DS_Store


================================================
FILE: API.html
================================================
[ vim: set filetype=Markdown: ]: # (<style type="text/css">body {visibility: hidden} </style>)
[ vim: set filetype=Markdown: ]: # (<meta charset="utf-8">)
[ vim: set filetype=Markdown: ]: # (<script charset="utf-8" src="site/Bookmark.js"> </script>)
[//]: # (---)
[//]: # (title: API)
[//]: # (subtitle: API documentation)
[//]: # (description: Bookmark - Easy Markdown Pages API documentation)
[//]: # (siteTemplate: siteTemplate.html)
[//]: # (---)


## Configuration Options

The following describe the properties accepted on the `Bookmark` constructor
which should be initialized in your `siteTemplate.html` file.

```js
Bookmark.go({
  stylus: stylusConfig,
  doc: docConfig,
  highlight: highlightConfig,
  slugify: slugifyConfig,
  slugContributions: slugifyConfig,
  sideNavify: sideNavifyConfig,
  searchFormId: searchFormId,
  searchHitsId: searchHitsId,
});
```

###### `stylus:`
The path(s) to stylus configuration(s). May either be a string or an array of
strings. Each string must be the path to a valid Bookmark compatible [stylus
configuration](#styles) (Stylus files with the special script include on the
first line).

```javascript
Bookmark.go({
  stylus: './theme-white/theme.styl.html',
  ...
});
```

###### `doc:`
The path(s) to Bookmark doc(s). May either be a string or an array of strings.
Each string must be the path to a valid Bookmark compatible [doc ](#doc) files
(markdown files with the special script include on the first line).

```javascript
Bookmark.go({
  stylus: './theme-white/theme.styl.html',
  ...
});
```


###### `slugify:`

Describes which header elements should become slugified (linkable with a
readable url). The default is shown below.
```javascript
{
  h1: true,
  h2: true,
  h3: true,
  h4: false,
  h5: false,
  h6: false,
}

```

<continueRight/>

If a header level is not slugified, its content can still partake in the slug
content of other header levels. See [`slugContributions:`](#api-slugcontributions).

###### `slugContributions:`

Describes which header elements should partake in _other_, header slugs.
For example if `h1` is marked `true`, then `<h2/>`s will have the previous
`h1` text in their slugs. Consequently, if everything is configured `false`,
only *that* header's text will be used to create the slug.
Slugs are always deduped regardless of which headers are slug contributors.
The first slug of value `name` becomes `#name` and the second becomes `#name-1`
and so on. Enabling more slug contributions doesn't change the deduping
behavior, it just makes deduping less necessary.
The defaults are as follows:

```javascript
{
  h1: true,
  h2: true,
  h3: true,
  h4: false,
  h5: false,
  h6: false,
}
```

<continueRight/>

`slugContributions` can help you avoid breaking existing links to pages.  If
you never had an `h1` heading, and your existing slug urls did not have any
component for the `h1` heading, you can later add an `h1` heading without
breaking existing links by configuring the `slugContributions`'s `h1` field to
be `false.`

The more headings you allow to partake in slugs, the more fragile your links
will be. But the fewer headings you allow to partake in slugs, the more
non-unique slugs you will run into (with a `-1` appended after them).

###### `sidenavify:`

Describes which headings will be added to the side nav. This will also cause
the `slugify` to be activated for this heading level even if it was set to
`false` (only slugified headings can be linked to from the side nav).
The defaults are as follows:

```javascript
{
  h1: true,
  h2: true,
  h3: true,
  h4: false,
  h5: false,
  h6: false,
}
```


## Customizing

### Add Files

You can add more markdown docs (using the `.html`) or [Stylus](stylus) files
(using the `.styl.html` extension). All new styles and html markdown pages should have the
appropriate `<script>`Then just add it to the list of `pages:`
in your `siteTemplate.html`. The `doc` and `stylus` options accept
an array of paths.

```html
<script>
Bookmark.go({
  stylus: ["./path/to/YourStyle.styl.html"],
  pages: {
    "MYNEWFILE": { }
  }
});
</script>
```
<continueRight/>

- Stylus files should have the `.styl.html` extension, and be included in the
  `stylusFetcher:`.
- Markdown files shoud have the `.md.html` extension and be included in the
  `fetcher:`


### Highlighting

Bookmark includes a vendored [hljs](hljs), and it is enabled by default in the main
script tag in `index.dev.html` that runs `Bookmark`. `hljs` is by default configured
to be the highlighter, and you can customize this.


```html
<script>
Bookmark.go({
  stylus: ...,
  pages: ...,
  highlight: (txt, lan) =>
    hljs.highlight(lan, txt).lan;
});
</script>
```




================================================
FILE: MIT-LICENSE
================================================
Copyright 2016-2018 Various Authors

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.html
================================================
[ vim: set filetype=Markdown: ]: # (<style type="text/css">body {visibility: hidden} </style>)
[ vim: set filetype=Markdown: ]: # (<meta charset="utf-8">)
[ vim: set filetype=Markdown: ]: # (<script src="site/Paradoc.js"> </script>)
[//]: # (---)
[//]: # (title: Paradoc)
[//]: # (subtitle: Instantly write, deploy, and enjoy docs)
[//]: # (description: Paradoc - Easy Markdown Pages)
[//]: # (nextPage: configuring-pages)
[//]: # (siteTemplate: siteTemplate.html)
[//]: # (---)


- **No servers, No build**

- **Deployable Sites Right [From Chrome](#easy-optimizing-with-chrome)**

- **Markdown [features](#markdown-features) for developers. [Offline Search](#search-features).**

### Workflow

- Write regular markdown in `.html` files with a single script tag at the top.
- Everything after the script tag is regular markdown.


```markdown
<script src="site/Paradoc.js"></script>
Everything _after_ the first line is
plain **markdown**.
```

<continueRight/>

- Double click the `.html` file to turn your markdown into a beautiful webpage.

- To make changes, just edit the contents of the file and reload the browser.


### Try It Out

[The Paradoc landing page](https://jordwalke.github.io/paradoc/README.html) was
created from the Paradoc repo's main `README.html` markdown file. Clone this
repo and double click `README.html` to turn it into this webpage.

1. `git clone git@github.com:jordwalke/paradoc.git`
2. Double click `README.html`



> Note: The script include in _this_ `README.html` looks complicated because it
> uses [Advanced Features](#integration-github-readme) that allows a single
> `README.html` to act as a website, _and_ a Github README, always kept in
> sync.  Your files will probably use the [very simple script
> include](#workflow?txt=41316212813224330877011010h0) already discussed.


### Looks


#### Split View

Paradoc comes with a UI theme that supports a left/right responsive split
layout.  As you increase the window size, code samples and block quotes are
split into a right hand column.

> Note: The style for this split view comes from Paradoc's use of
> [flatdoc](https://ricostacruz.com/flatdoc/), but has been heavily modified.
> The split view can be disabled in your
> [`siteTemplate`](configuring-pages.html#the-site-template).

Some of the additional [markdown extension features](#markdown-extensions)
control the behavior of this split view.


### Markdown Extensions

Paradoc supports extended markdown features specified in [Github Flavored
markdown](https://github.github.com/gfm/), and also supports some additional
Paradoc specific features.


#### Code highlighting

With [GitHub Flavored Markdown][fences] you can use Markdown code fences to
make syntax-highlighted text. In Paradoc, codeblocks will be rendered in the right column
of the [split view](#looks-split-view) if the window is sufficiently wide.

~~~markdown
```javascript
console.log("This", "is a log");
```
~~~


#### Blockquotes

Blockquotes also show up in the right hand column when the window is
sufficiently large. This is useful for providing extra information or non-code
examples that move out of the way of the main document.

> Blockquotes are blocks that begin with `>`.

#### Smart quotes

Single quotes, double quotes, and double-hyphens are replaced to their
"typographically-accurate" equivalent. This does not apply to `<code>` and
`<pre>` blocks.

> "Check out this quote here. Look how how correct the quotes are"
> --me


#### `<continueRight/>`

Paradoc adds an additional feature that allows a right column element to continue
flowing.

> This blockquote comes immediately after the text
> _"Paradoc adds an additional feature that allows a right column element to
> continue flowing"_
> but notice how this blockquote also continues to "flow" into the list that
> comes after it? This is important for creating a better balance of left and
> right content. Doing so requires the author to opt into having particular
> blockquote/code blocks flow into subsequent left content when it makes sense.

<continueRight/>

- After a blockquote or code fence region, include a `<continueRight/>` tag.
- It will cause that blockquote/fence region to continue flowing into whatever
  comes after it in the left column.
- Until another blockquote or code fence region begins.

#### Images In Right Column

Images may also be placed into the right column of the document by placing
them in blockquotes.

```markdown
> ![Another Beach](site/images/beach2.jpg)
```

Like all other elements, you may place a `<continueRight/>` after blockquote
containing the image to get subsequent content to flow alongside the image on
its left side:

> ![Another Beach](site/images/beach2.jpg)

<continueRight/>


> - Such as this list here
> - **And this bold line here**

#### Code Tabs

You can create [Docusaurus](https://docusaurus.io/docs/en/doc-markdown) style
code toggle switches for viewing multiple different code samples. In this
example, clicking on the headers (reason, javascript, ocaml) will toggle
between different syntaxes for a particular print command.

<!--CODE_TABS-->
```reason
Console.log(["This", "Logs", "Reason", "Lists"]);
Library.callSomeFunction(10, 200);
```

```javascript
Console.log(["This", "Logs", "Reason", "Arrays"]);
library.callSomeFunction(10, 200);
```

```ocaml
Console.log ["This", "Logs", "Ocaml", "Lists"]
Library.callSomeFunction 10 200;
```
<!--END_CODE_TABS-->



To create code tabs, place multiple code blocks between special `CODE_TABS`
HTML comments as follows.

````md
 <!--CODE_TABS-->
 ... multiple code blocks go here...
 <!--END_CODE_TABS-->

````

#### Custom Tab Titles

By default, code tab titles are inferred from the code block syntaxes, but you
may give custom names to the tabs by including an HTML comment before each code
example.


````md
 <!--CODE_TABS-->
 <!--My JS Code Block-->
 ...js code block...
 <!--My Python Code Block-->
 ...python code block...
 <!--END_CODE_TABS-->
````

Note: Specifying the title from this code block syntax is a Paradoc feature,
not supported in Docusaurus.


The previous example produces the following result:

<!--CODE_TABS-->

<!--My JS Code Block-->
```js
console.log('Hello, world!');
```
<!--My Python Code Block-->
```py
print('Hello, world!')
```
<!--END_CODE_TABS-->


### UI Enhancements

#### Medium-Zoom Images

Images are specified using standard markdown syntax, but they are enhanced with
a plugin called [Medium Zoom](https://medium-zoom.francoischalifour.com/).

```markdown
![Beach](site/logo.svg)
```

![Beach](site/images/beach.jpg)

> Click on the image to view a full view. Click, or scroll a small amount to
> cause the image to animate back into place.

<continueRight/>



#### Buttons

Include a `>` at the end of your link text (for instance: `Continue >`), to
turn them into buttons. This is a feature from flatdoc.

> [Go To Github >][github]


### YAML Headers

Paradoc parses valid key/value items from "YAML headers". These headers are
where you specify information that applies to that entire document.  YAML
headers consist of an unlimited number of `key:value` lines sandwiched between
two `---` at the start of the document, immediately after the initial Paradoc
`<script>`.

You can specify any key/value pairs you like - but Paradoc will look for
certain keys to configure your website and rendering. See [Configuring
Pages](configuring-pages.html).


```html
<script src="site/Paradoc.js"></script>
---
title: me
description: "Hi there here is an escaped quote \" inside of quotes"
anythingYouWant: hey
---
```


### Multi-Doc Pages

To add another doc page, have an existing page specify the new page as its
`nextPage:` in the [Page Header](#page-headers) . Then make sure that new page
actually exists, and has the Paradoc script include as usual.

> Note: All pages should also supply a `rootPage:` header property that
> specifies the "first page" in the list.

The following header specifies that the next markdown page should be
`my-next-page.html`, and that the starting "root page" should be `README.html`.


```html
<script src="site/Paradoc.js"></script>
---
title: me
nextPage: my-next-page
rootPage: readme
---
```

<continueRight/>

See how to [Add More Pages](configuring-pages.html#adding-pages)


## Search Features

Paradoc supports _offline_ search across all of the documents that are
[added](configuring-pages.html#adding-pages). No build steps or servers are
required to search, and no subscriptions to search services are required.
Content is searched interactively while authoring docs locally, when users
consume your deployed site, and when users save local copies of your deployed
site to disk.


#### Offline Search
- Press the <kbd>/</kbd> key and start typing.
- Searches across multiple pages.
- No server, no build steps.


#### Keyboard Interactions

| Keyboard                                                             | Action                                       |
|--------------------------------------------------------------------- |----------------------------------------------|
| <kbd>/</kbd>                                                         | Focus the Search input                       |
| <kbd>Esc</kbd>                                                       | Close search results and blur search input   |
| <kbd>Ctrl+c</kbd> or <kbd>Ctrl+\[</kbd>                              | Toggle search results open when focused      |
| <kbd>Down</kbd> or <kbd>Ctrl+n</kbd>                                 | When results open, move up / down in results |
| <kbd>Up</kbd> or <kbd>Ctrl+p</kbd>                                   | When results open, move up / down in results |
| <kbd>Enter</kbd> or Click                                            | Go to currently selected result              |


## Change Resistant Deep Linking

All content in Paradoc pages can be "deep linked" to. This means that you can
create a url link to a specific paragraph, code sample, or table row (not just
the headers).

These links are "change resistant", meaning that the content can be moved to another location
in the document, and all the links that have proliferated on the internet will still work correctly.

It also means that the contents that are linked can be changed (fixing typos,
refactoring sentence structure), and all proliferated links to that specific
content will still usually work (up to a certain amount of changes).

This works by creating a text fingerprint of the content and when loading the
page, finding the content that most closely matches that fingerprint in the
url. Even if the text has changed changes Paradoc will find the best match
possible.

###### Try It Out

Hit <kbd>/</kbd> to search for anything, and hit enter on a search result. Then
copy/paste the url into a new browser window. The search results encode the
change resistant deep link in the url, and you can share that specific search
result with anyone or link to it from a blog while feeling confident your links
won't break.


## Deploying

### Easy Optimizing With Chrome

Just "Save As" in Chrome, select "Complete Webpage" to generated an optimized,
pre-rendered version of the site with all docs served as a single page application
with working online/offline search.

### Packing Into A Single `.html` file.
"Save As" in Chrome generates an optimized rendered build of your website as a
single page application, but it will generate a folder with assets/styles and
images for your docs to be distributed/deployed along with your main html page.
You can take it even further also optimize your docs page into a single,
minified `.html` file which bundles all of its resources *including* fonts and
images! There are many benefits to the way Paradoc compresses your docs site
into a single, shareable `.html` file.

```
cd site
npm install
node ./Paradoc.js ../readme.html
```

Now you can deploy `../readme.bookmark-inlined.html` as a single file to any
web host, and it will operate as a single page application.

With this mode:
- Paradoc prerenders at build time instead of page load time (faster loading).
- The document is served as a single web request.
- Easily send the docs as an attachment in Discord/Messenger chat thread.
- Save your online docs using the browser's '"Save As"
- Paradoc makes sure your page looks exactly the same on anyone's computer,
  including the fonts.


## More

#### Issues:
You must only load markdown html files that you authored and trust. Currently,
the way that the `marked` library is being used does not sanitize the output
before injecting it into the DOM.


### Acknowledgements
See [ORIGINS.md](./ORIGINS.md) for links and licenses of various
components that are embedded in this project.



[github]: https://github.com
[fences]:https://help.github.com/articles/github-flavored-markdown#syntax-highlighting
[hljs]: https://highlightjs.org/
[stylus]: https://stylus-lang.com


================================================
FILE: VERSIONS.html
================================================
[ vim: set filetype=Markdown: ]: # (<style type="text/css">body {visibility: hidden} </style>)
[ vim: set filetype=Markdown: ]: # (<meta charset="utf-8">)
[ vim: set filetype=Markdown: ]: # (<script charset="utf-8" src="site/Bookmark.js"> </script>)
[//]: # (---)
[//]: # (title: Versions)
[//]: # (subtitle: Bookmark - Version History)
[//]: # (description: Bookmark - Version History)
[//]: # (siteTemplate: siteTemplate.html)
[//]: # (---)


### Version 1.2
- Introduced new feature abc.
- Bug fix in ancient feature blah.


### Version 1.3
- Introduced new feature abc.
- Bug fix in ancient feature blah.



### Version 1.3
- Introduced new feature abc.
- Bug fix in ancient feature blah.






### Version 1.4
- Introduced new feature abc.
- Bug fix in ancient feature blah.






### Version 1.5
- Introduced new feature abc.
- Bug fix in ancient feature blah.



### Version 1.6
- Introduced new feature abc.
- Bug fix in ancient feature blah.














================================================
FILE: configuring-pages.html
================================================
[//]: # ( vim: set filetype=Markdown: )
[//]: # (<style type="text/css">body {visibility: hidden} </style>)
[//]: # (<meta charset="utf-8">)
[//]: # (<script src="site/Paradoc.js"> </script>)
---
title: Configuring Paradoc Pages
subtitle: Configuring Your Pages
description: Configuring Your Pages
nextPage: styles
rootPage: readme
siteTemplate: siteTemplate.html
---

### Header Properties

Page properties are configured in [YAML Headers](readme.html#yaml-headers).
The following table lists header properties that Paradoc pays attention to and
uses to render your page.

#### YAML Header Properties

| Property                          | Purpose                                                                                                |
|---------------------------------- |--------------------------------------------------------------------------------------------------------|
| `title:`                          | Title rendered at top of page as `<h0>`                                                                |
| `subtitle:`                       | Subtle rendered below the title in the page.                                                           |
| `description:`                    | Description of page.                                                                                   |
| `hideInNav:`                      | Whether or not to hide in the sticky page navigation.                                                  |
| `hideInSearch:`                   | Hide page and contents in search results.                                                              |
| `siteTemplate:`                   | HTML page template to use to control overall layout of page  (optional defaults to `siteTemplate.html`)|
| `linkText:`                       | Text for links to the page (in the sticky navigation header)                                           |
| `nextPage:`                       | Page that should come after this page in navigation header and search results ordering.                |
| `rootPage:`                       | Root "main" page that (usually `readme.html`/`index.html` etc)                                         |


### Adding Pages

To Add a new page _after_ your current page:

1. Set the `nextPage:` property of your current page's YAML header to be the root name of the new page (`myNextPage`).
2. Make sure `myNextPage.html` exists and has the proper Paradoc script include.
2. As always make sure your current page and next page both include a
   `rootPage:` header property pointing back to the "first" page they are child
   pages of (usually `readme` or `index`).


##### The current page would be configured similar to:

```markdown
<script src="site/Paradoc.js"></script>
---
title: My Current Page
nextPage: myNextPage
rootPage: readme
---
```

##### `myNextPage.html` would be configured as:


```markdown
<script src="site/Paradoc.js"></script>
---
title: My Next Page
rootPage: readme
---
```


### The Site Template

TODO: Document this.


================================================
FILE: integration.html
================================================
[//]: # ( vim: set filetype=Markdown: )
[//]: # (<style type="text/css">body {visibility: hidden} </style>)
[//]: # (<meta charset="utf-8">)
[//]: # (<script src="site/Paradoc.js"> </script>)
---
title: Integration
subtitle: Playing Well With Github and Editors
description: Playing Well With Github and Editors
rootPage: readme
nextPage: readme
siteTemplate: siteTemplate.html
---

This page tells you how to:

- Use a single `.html` as both a Github markdown preview and your website.
- Highlight as markdown in Vim/Emacs.
- Work well with Safari in development mode.

If none of that is interesting to you then this page is not for you. You just
keep writing `.html` markdown files with a single script tag at the top, and
(optional) YAML headers:

```markdown
<script src="site/Paradoc.js"> </script>
---
title: You don't even need to include this --- YAML header
---
Nothing fancy  for you!
- Just regular markdown files
- With a single <script></script> tag at the top.
```

<continueRight/>

For the rest of you, read on.



### All The Integrations

The rest of this page explains each of the Github/editor/browser integration
options in detail. Here's a quick example of using all the integrations at once
at the start of an html markdown file. This example:

- Tells Github to render a markdown preview of your page in your repo and tells
  Vim to highlight it as markdown.
- Include a style that hides the flash of content when loading the page in
  local development mode.
- Makes Safari play nicer with UTF-8 text in development mode.
- Includes the `Paradoc.js` script.

```markdown
[//]: # ( vim: set filetype=Markdown: )
[//]: # (<style type="text/css">body {visibility: hidden} </style>)
[//]: # (<meta charset="utf-8">)
[//]: # (<script src="site/Paradoc.js"> </script>)
---
title: My Page Title
rootPage: readme
---
```


#### Hiding The YAML headers in Github preview

To take it a step further, you can hide the YAML headers in Github's preview
rendering, while still providing that header information to Paradoc for
configuring your page.  Just include the YAML yeader lines in comments as well.
The previous example becomes:

```markdown
[//]: # ( vim: set filetype=Markdown: )
[//]: # (<style type="text/css">body {visibility: hidden} </style>)
[//]: # (<meta charset="utf-8">)
[//]: # (<script src="site/Paradoc.js"> </script>)
[//]: # (---)
[//]: # (title: My Page Title)
[//]: # (rootPage: bookmark)
[//]: # (---)
```



### Background

Github by default will not render a `README.html` (or any `.html` file) as
markdown, unless it knows it's a markdown file.
Similarly, vim/emacs will not highlight an html file as markdown unless you
instruct it to.

The way that you instruct Github and Vim to treat the html file as a markdown
file is the same. You must include the text `vim: set filetype=Markdown:`
somewhere in the first line. You _could_ stick the text `vim: set
filetype=Markdown:` at the top of your file, and it would work but Github would
_also_ render that literal text which is not what you want.

### Comments To The Rescue

Markdown supports comments of the following form:

```markdown
[//]: # (you can put anything you want here)
```

<continueRight/>

The text between the `( )` will not be rendered by Github's preview, and
Paradoc will also ignore then when rendering the page.

#### Make Github Render Markdown Preview

While it won't be rendered, fortunately Vim/Github will still search for
`vim: set filetype=Markdown:` inside the parenthesis, so we can include a
markdown comment at the top of the document that tells Github/editors to treat
this file as markdown even though it has an html extension.

```markdown
[//]: # ( vim: set filetype=Markdown: )
```


#### Hide The Script Tag In Github Preview

Github will also render the initial `<script>` include at the top of files as
plain text which is not what you want. You wanted to use a single `.html`
source of truth as your main Paradoc page, and have it render nicely on
Github!  We can use the same markdown comment trick that we used with the vim
filetype line to hide the script tag in Github's preview. Let's add this to
the previous example:

```markdown
[//]: # ( vim: set filetype=Markdown: )
[//]: # (<script src="site/Paradoc.js"> </script>)
Your regular markdown _here_.
```


Now Github will render your Paradoc `.html` page as a markdown preview in your
repo. Pretty cool! There's one small problem. When in development mode (locally
authoring/reloading) there _might_ be a small flash of unstyled text when
reloading depending on how fast you reload (the contents up until the script
tag).
We can fix that by including a page hider in yet another markdown comment
(inserted before the script include).
```markdown
[//]: # ( vim: set filetype=Markdown: )
[//]: # (<style type="text/css">body {visibility: hidden} </style>)
[//]: # (<script src="site/Paradoc.js"> </script>)
Your regular markdown _here_.
```

So now we have a single source of truth `.html` page that can function as both
a Paradoc website, and render nicely in your Github repo's markdown preview,
and it feels great when reloading it locally in development mode.

#### Hide The YAML header In Github Preview


```markdown
[//]: # ( vim: set filetype=Markdown: )
[//]: # (<style type="text/css">body {visibility: hidden} </style>)
[//]: # (<script src="site/Paradoc.js"> </script>)
Your regular markdown _here_.
```

#### Silent Github YAML Headers

If you are using a single source of truth for both Github markdown preview and
your Paradoc website, you might want to hide the YAML headers in the Github
preview (Github renders them as a nice looking table, but maybe you want to
omit them entirely).

Paradoc provides a solution using - you guessed it - markdown comments!
Paradoc supports embedding ("silent") YAML headersl in markdown comments.
This is a bookmark specific feature. Github and all other markdown tooling will
ignore these headers, but Paradoc will still pay attention to them to
configure your page. We'll continue building from the previous example, and
addd silent YAML headers.


```markdown
[//]: # ( vim: set filetype=Markdown: )
[//]: # (<style type="text/css">body {visibility: hidden} </style>)
[//]: # (<script src="site/Paradoc.js"> </script>)
[//]: # (---)
[//]: # (title: My Page Title)
[//]: # (rootPage: readme)
[//]: # (---)
Your regular markdown _here_.
```


This prevents the header metadata from being rendered as a table in Github's viewer,
but Paradoc will still extract them just as if they were YAML headers.
The result is a single Paradoc doc that serves as a Github viewable markdown page,
as well as powering a website.

#### Developing In Safari:

Safari does not interpret the encoding of files as utf-8 by default unless the
html document has `<meta charset="utf-8">` at the top of the file.  That means
any `.html` page, or style `.html` page must have `<meta charset="utf-8">` at
the top, if you want to be able to load it in Safari.  This only effects
development mode because normally you would perform a build of the site before
deploying it, where the encoding issue doesn't arise.

Putting everything together gives us the following file:

```markdown
[//]: # ( vim: set filetype=Markdown: )
[//]: # (<style type="text/css">body {visibility: hidden} </style>)
[//]: # (<meta charset="utf-8">)
[//]: # (<script src="site/Paradoc.js"> </script>)
[//]: # (---)
[//]: # (title: My Page Title)
[//]: # (rootPage: readme)
[//]: # (---)
Your regular markdown _here_.
```

## More Options:

#### Emacs File Detection

Instead of using the Vim file detection line to trick Github into rendering yor
preview, you could use the following line using the emacs file detection form.
This is the same as the Vim form except it will tell Emacs to highlight as Markdown
instead of telling Vim to do so.

```markdown
[//]: # (-*-mode:markdown-*-)
```



### How Paradoc Works

`README.md.html` is both a valid html page and a valid markdown page. Because
browsers allow loading of html pages in iframes across origins, no web server
is needed to develop and reload docs entirely in the browser without a build
step/web server.

`README.md.html` looks like:

```markdown
<script src="site/Paradoc.js"></script>
Everything _after_ the first line is
plain **markdown**.
1. Nothing needs to be escaped.
2. Not even if your markdown contains
  a `<script>` tag.
```
<continueRight/>

Everything after the first line is plain markdown. There is nothing special you
need to do to your markdown even though it is in an `.html` file. You can
include literally any markdown after that first script tag line, and you don't
have to escape any of it. The browser won't even think you're starting a script
region if you include a `<script>` tag somewhere after the first line. How?
The inclusion of the first `Paradoc.js` script forces the rest of the document
to be interpreted as plain text that needn't be escaped.

When you have your script include be specified in a markdown comment like this:


```markdown
[//]: # (<script src="site/Paradoc.js"> </script>)
Everything _after_ the first line is
plain **markdown**.
- Even this `<script>` text.
```

<continueRight/>

Then the browser will still run the script tag as usual, and Paradoc will
delete all the text content that was included before the first line's
'`<script`.  The fact that the script include is inside of a markdown comment
tells Github rendering to not show the `<script>` tag.



#### `.gitattributes` Approach:
There is an approach to getting Github to render your `.html` page as markdown
that uses `.gitattributes`.  This is left as an exercize to the reader.


================================================
FILE: site/ORIGINS.md
================================================
Bookmark:
(A simple tool for editing and rendering Reason documentation)

Here's a list of all of the technologies that are used and included in Bookmark.

Each of these projects should include a LICENSE file from their original
project, but even if they do not, they still retain the license/copyright of
their original project (even though they are copied/"vendored" in 

## Flatdoc:
This project is just a fork of flatdoc, so of course
[https://github.com/rstacruz/flatdoc](flatdoc) should be mentioned.  Flatdoc is
excellent.

# jquery:
jquery is licensed under the MIT license, and is vendored in vendor/jquery.js
See its license [here](https://jquery.org/license/)

#medium-zoom
[Medium-zoom](https://github.com/francoischalifour/medium-zoom) is vendored as
well.

#Fira Mono Font
Is also vendored as the default source code font. See its LICENSE included in the vendor directory.

#Roboto Font
Is also vendored and is the default font for text. See its LICENSE included in the vendor directory.

# Stylus
The vendored stylus in `support/vendor/stylus.min.js` is gotten from
https://github.com/stylus/stylus/tree/client and is under MIT license (see the
repo).

# hljs
HLJS is vendored in support/vendor/ along with all of its out of the box css
styles.  hljs is licensed under BSD 3-Clause License and the license can be
found in its repo [here](https://github.com/highlightjs/highlight.js).

#reasonml-highlightjs

site/vendor/reason-highlightjs.js is from (MIT)
https://raw.githubusercontent.com/reasonml-editor/reason-highlightjs/master/index.js

`site/vendor/highlight-styles/atom-one-light.css`
`site/vendor/highlight-styles/xcode.min.css`
From highlightjs repo. (See its original license)



# marked
Flatdoc vendors marked (markdown parsing library) inside of flatdoc.js
https://github.com/markedjs/marked
It is licensed under MIT (see the repo for license).

# Beach Images:
Credit Lance Aspser:
https://unsplash.com/photos/W785zpEXZZo
https://unsplash.com/photos/woDxDNvpmdk


================================================
FILE: site/Paradoc.js
================================================
/*!
 * Flatdoc - (c) 2013, 2014 Rico Sta. Cruz
 * http://ricostacruz.com/flatdoc
 * @license MIT
 */

// Keep this in sync with $header-height in style.
var headerHeight = 52;

/**
 * Pass the window.location.href
 */
var urlBasename = function (s) {
  return s.split("/").pop().split("#")[0].split("?")[0];
};
var urlDir = function (s) {
  var lst =  s.split("/");
  var withoutLast = lst.pop();
};
var indexify = function(path) {
  var splits = path.split('/');
  if(splits.length > 0) {
    var last = splits[splits.length - 1];
    if (path.lastIndexOf(".js") !== path.length - 3) {
      return path + '/index.js';
    } else {
      return path;
    }
  }
};


var kebabToWords = function(s) {
  var ss = s.replace(
    /-./g,
    function(x) {
      return ' ' + x.toUpperCase()[1];
    }
  );
  return ss.length > 0 ? s[0].toUpperCase() + ss.substr(1) : ss;
};

/**
 * Must supply href as written in dom node, not a.href which is fully resolved.
 * TODOSecurityAudit:
 */
var isHrefAttributeLocal = function (relativeToPageUrl, href) {
  return href.indexOf('file://') !== 0 &&
    href.indexOf('https://') !== 0 &&
    href.indexOf('http://') !== 0;
};

var urlExtensionlessBasename = function (s) {
  return s.split("/").pop().split("#")[0].split("?")[0].replace(".html", "");
};

var removeSiblingsBefore = function(n, node) {
  while(n > 0 && node.previousElementSibling) {
    n--;
    node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);
  }
};

var urlForPageKey = function (s) {
  return s + ".html";
};

var slugPrefix = function (hash) {
  if (hash === "" || hash[0] !== "#") {
    return "";
  } else {
    hash = hash.substr(1);
    return (hash.split("-").length ? hash.split("-")[0] : "").toLowerCase();
  }
};

function dictToSearchParams(dict) {
  var segs = [];
  for (var p in dict) {
    segs.push(encodeURIComponent(p) + "=" + encodeURIComponent(dict[p]));
  }
  return "?" + segs.join("&");
}

var Bookmark = {};
var mapKeys = function (dict, onPage) {
  var result = {};
  for (var pageKey in dict) {
    result[pageKey] = onPage(dict[pageKey], pageKey);
  }
  return result;
};
var forEachKey = function (dict, onPage) {
  var _throwAway = mapKeys(dict, (pageData, pageKey) => (onPage(pageData, pageKey), pageData));
};
/**
 * n squared but so what.
 */
var nextKeyWithNonEmptyArrayOrNullIfNone = function (curKey, resultsByPageKey) {
  var nextPageKey = nextKeyOrNull(curKey, resultsByPageKey);
  if (nextPageKey === null) {
    return null;
  } else if (resultsByPageKey[nextPageKey].length === 0) {
    return nextKeyWithNonEmptyArrayOrNullIfNone(nextPageKey, resultsByPageKey);
  } else {
    return nextPageKey;
  }
};

/**
 * n squared but so what.
 */
var prevKeyWithNonEmptyArrayOrNullIfNone = function (curKey) {
  var prevPageKey = prevKeyOrNull(curKey, resultsByPageKey);
  if (prevPageKey === null) {
    return null;
  } else if (resultsByPageKey[prevPageKey].length === 0) {
    return prevKeyWithNonEmptyArrayOrNullIfNone(prevPageKey);
  } else {
    return prevPageKey;
  }
};

var numKeysWhere = function (dict, f) {
  var num = 0;
  for (var k in dict) {
    if (f(k, dict)) {
      num++;
    }
  }
  return num;
};
var numKeys = function (dict) {
  return numKeysWhere(dict, function() {return true;});
};
/**
 * Returns nextKey or null if there is no next key after this one.  Returns
 * the first key if in the dictionary if `key` provided was null.  Undefined
 * behavior if the key was not null but not in the dict.
 */
var nextKeyOrNull = function (key, dict) {
  var seen = key === null;
  for (var k in dict) {
    if (seen) {
      return k;
    } else if (k === key) {
      seen = true;
    }
  }
  return null;
};
/**
 * Returns previousKey or null if there is no previous key after this one.
 * Returns the last key if in the dictionary if `key` provided was null.
 * Undefined behavior if the key was not null but not in the dict.
 */
var prevKeyOrNull = function (key, dict) {
  var prev = null;
  for (var k in dict) {
    if (k === key) {
      return prev;
    }
    prev = k;
  }
  return prev;
};

var keyIndexOrNegativeOne = function (key, dict) {
  var i = 0;
  for (var k in dict) {
    if (k === key) {
      return i;
    }
    i++;
  }
  return -1;
};

var keepOnlyKeys = function (dict, f) {
  var result = {};
  for (var pageKey in dict) {
    if (f(dict[pageKey], pageKey)) {
      result[pageKey] = dict[pageKey];
    }
  }
  return result;
};

/**
 * Inserts/moves a key/val after `afterKey`.
 * Errors if `afterKey` is not present.
 * Does nothing if `beforeKey` is the first key and `afterKey` is the last.
 *
 * The "currently viewed" page is the first in the list regardless of if it is
 * supplied to initial page config items.  Then explicitly specified pages
 * passed to initial config options tend to be the next in the key order.
 * Then later discovered pages (via `nextPage` are added to the end of the
 * dictionary).
 * We will try to build off of the "first" key assuming that's the most
 * important one to have first. So we pin the first key in place and try to
 * build off of that if possible (in a circular manner if necessary).
 */
var ensureKeyValOrderCircular = function (dict, preKey, postKey) {
  if(!(postKey in dict) || !(preKey in dict)) {
    throw new Error(
      'Key ' +
      postKey +
      ' and ' +
      preKey +
      ' are not both in pages. This is a problem with Bookmark implementation.'
    );
  }
  var result = {};
  var first = null;
  var last = null;
  for (var k in dict) {
    if(first === null) {
      first = k;
    } else {
      last = k;
    }
  }
  if(first === postKey && last === preKey) {
    return dict;
  }
  var inserted = false;
  for (var k in dict) {
    if(k === postKey && postKey !== first) {
      result[preKey] = dict[preKey];
      result[postKey] = dict[postKey];
    } else if(k === preKey) {
      result[k] = dict[k];
      result[postKey] = dict[postKey];
    } else {
      result[k] = dict[k];
    }
  }
  return result;
};


var moveKeyToFront = function(dict, key) {
  var result = {};
  if(key in dict) {
    result[key] = dict[key];
  }
  for(var k in dict) {
    result[k] = dict[k];
  }
  return result;
};

var indexOfKey = function(dict, key) {
  var i = -1;
  for(var k in dict) {
    i++;
    if(k === key) {
      return i;
    }
  }
  return -1;
};

var resultsByPageKeyLen = function (resultsByPageKey) {
  var totalLen = 0;
  for (var pageKey in resultsByPageKey) {
    totalLen += resultsByPageKey[pageKey].length;
  }
  return totalLen;
};

/**
 * Extracts the ? query param string from a url. It will always be after the
 * hash tag.
 */
var splitHrefHashAndQueryString = function(s) {
  var querySearchLoc = href.indexOf("?");
  var queryParamString = null;
  if (querySearchLoc !== -1) {
    queryParamString = s.substr(querySearchLoc + 1);
    s = s.substr(0, querySearchLoc);
  }
  var hashSearchLoc = href.indexOf("#");
}

var areEqualPathArrs = function(a1, a2) {
  if(a1.length !== a2.length) {
    return false;
  }
  for(var i = 0; i < a1.length; i++) {
    if(a1[i] !== a2[i]) {
      return false;
    }
  }
  return true;
};


/**
 *
 * Urls like blah.html#foo/bar#another-hash
 * Are interpreted as being another way to reference the page
 * foo/bar.html#another-hash (Currently everything relative from the
 * timeTemplate). This function analyzes any link that is not yet in the
 * standard form `siblingPage.html#hash?queryParams` and returns the abstract
 * data about that link (which sibling page it refers to, which hash/query
 * params etc) so that the abstract link information can be reasoned about
 * and/or transformed into a "single page bundle" form
 * rootPage.html#sibingPage#hash?queryParams if necessary.
 *
 * It also normalizes links that might not have been converted properly when
 * ported to `.html` files (from `.md` files).
 * You may have forgotten to change a link from `siblingDoc.md` to
 * `siblingDoc.html`. This function also fixes that.
 *
 * - relativeToPageUrl: Some hrefs will be totally expanded and in terms of the
 *   page that was *originally* rendered at the time the markup was generated
 *   (this is the Save As case in Chrome).  This function will make sure to
 *   normalize hrefs if they are in terms of the originally expanded href the
 *   page was rendered at when saved. Docs must all reside in the same
 *   directory, and only docs (and docs assets) may reside in the same
 *   directory.
 *
 * Returns one of two types of links:
 *
 * External: A link to an external page (not within the documentation).
 *
 *     {
 *       type: 'external',
 *       href: full href
 *     }
 *
 * Internal: A link to a page within the documentation.
 *
 *     {
 *       type: 'internal',
 *       asAnEmbeddedSubpageOfEntrypointPageKey: urlBasenameRootLowerCase,
 *       pageKey: urlBasenameRootLowerCase,
 *       pageExtension: hrefExtension,
 *       hashContents: hash.substr(1),
 *       queryParams: queryParams,
 *     }
 *
 * For local URLs, will expect hashes to appear *before* the query params
 * (which is not standard but looks better for this use case).
 *
 * Normalizes links across the various doc workflows:
 *
 * - When using Chrome "Save As": Links to internal doc pages will become
 *   hardcoded to the absolute file path on disk, *including* links to hashes
 *   within *the same page*!
 *   - #foo becomes file://Path/To/yourPage.html#foo
 *   - ./siblingPage.html becomes file://Path/To/siblingPage.html
 *
 * These urls are expanded into the "written" attribute itself, not even after
 * accessing the fully resolved href.
 *
 * When running in pre-rendered, and or compressed mode, *not* from "Save As",
 * you will still have an originallyRenderedAtUrl, but the hrefAsWritten will
 * not be expanded out to it.
 *
 * If there is an `originallyRenderedAtUrl` and we see a url that has the same
 * dir as the `originallyRenderedAtUrl` then it's a Chrome "Save As" link local
 * to the docs.
 * If there is an `originallyRenderedAtUrl` and we see a url that does *not*
 * have the same dir as `originallyRenderedAtUrl`, but has the same dir as
 * `currentPageUrl`, it's a link local to the docs (but not Chrome Save As).
 *
 */
var getLink = function (originallyRenderedPageKey, originallyRenderedAtUrl, currentPageUrl, fullyResolvedLinkHref) {

  var currentPageOrigin = currentPageUrl.origin;
  var currentPagePathArr = currentPageUrl.pathname.split('/');
  var currentPageDirPathArr = currentPagePathArr.slice(0, currentPagePathArr.length - 1);

  var fullyResolvedLinkUrl = new URL(fullyResolvedLinkHref);
  var fullyResolvedLinkOrigin = fullyResolvedLinkUrl.origin;
  var fullyResolvedLinkPathArr = fullyResolvedLinkUrl.pathname.split('/');
  var fullyResolvedLinkDirPathArr = fullyResolvedLinkPathArr.slice(0, fullyResolvedLinkPathArr.length - 1);

  var currentPageHasSameOrigin = currentPageOrigin === fullyResolvedLinkOrigin;
  var currentPageHasSameDir = areEqualPathArrs(currentPageDirPathArr, fullyResolvedLinkDirPathArr);
  var isLocalLink = false;
  if(currentPageHasSameOrigin && currentPageHasSameDir) {
    isLocalLink = true;
  } else if(originallyRenderedAtUrl){
    var originallyRenderedAtOrigin = originallyRenderedAtUrl.origin;
    var originallyRenderedAtPathArr = originallyRenderedAtUrl.pathname.split('/');
    var originallyRenderedAtDirPathArr = originallyRenderedAtPathArr.slice(0, originallyRenderedAtPathArr.length - 1);

    var originallyRenderedAtHasSameOrigin = originallyRenderedAtOrigin === fullyResolvedLinkOrigin;
    var originallyRenderedAtHasSameDir = areEqualPathArrs(originallyRenderedAtDirPathArr, fullyResolvedLinkDirPathArr);
    if(originallyRenderedAtHasSameOrigin && originallyRenderedAtHasSameDir) {
      isLocalLink = true;
    }
  }
  if(!isLocalLink) {
    return {
      type: 'external',
      href: fullyResolvedLinkHref
    };
  }
  var hashAndQueryString = fullyResolvedLinkUrl.hash;  // Includes hash sign
  var hashStr =
    hashAndQueryString === '' || hashAndQueryString[0] !== '#' ? '' :
    hashAndQueryString.indexOf("?") !== -1 ? hashAndQueryString.substr(1, hashAndQueryString.indexOf("?") - 1) :
    hashAndQueryString.substr(1);
  var queryStr =
    hashAndQueryString.indexOf("?") == -1 ? '' :
    hashAndQueryString.substr(hashAndQueryString.indexOf("?") + 1);
  var queryParams = null;
  if (queryStr !== '') {
    var queryParams = {};
    var params = queryStr.split("&");
    for (var i = 0; i < params.length; i++) {
      var param = params[i].split("=");
      queryParams[decodeURIComponent(param[0])] = decodeURIComponent(param[1] || "");
    }
  }

  var asEmbeddedSubpageOf;
  if (!!originallyRenderedPageKey) {
    asEmbeddedSubpageOf = originallyRenderedPageKey;
  } else {
    var hrefBasename = urlBasename(fullyResolvedLinkHref);
    var hrefBasenameExtensionIndex = hrefBasename.lastIndexOf('.');
    var hrefExtension = hrefBasenameExtensionIndex !== -1 ?
      hrefBasename.substr(hrefBasenameExtensionIndex + 1) :
      null;
    var hrefExtensionlessBasename = hrefBasenameExtensionIndex !== -1 ?
      hrefBasename.substr(0, hrefBasenameExtensionIndex) :
      hrefBasename;
    // This is just the file portion.
    var urlBasenameRootLowerCase = hrefExtensionlessBasename.toLowerCase();
    urlBasenameRootLowerCase = urlBasenameRootLowerCase.replace(".paradoc-rendered", "");
    urlBasenameRootLowerCase = urlBasenameRootLowerCase.replace(".paradoc-inlined", "");
    asEmbeddedSubpageOf = urlBasenameRootLowerCase !== '' ? urlBasenameRootLowerCase : 'index';
  }

  // If there's no hash string or there is a hash string but it doesn't have
  // another hash inside of it (then it's not an embedded single doc page
  // link). It's either a link inside the current page (regular hash), or not
  // even a hash link at all.
  if (hashStr === '' || hashStr.indexOf("#") === -1) {
    return {
      type: 'internal',
      // TODO: This should be null in this case.
      asAnEmbeddedSubpageOfEntrypointPageKey: asEmbeddedSubpageOf,
      pageKey: asEmbeddedSubpageOf,
      pageExtension: hrefExtension,
      hashContents: hashStr,
      queryParams: queryParams,
    };
  } else {
    // It's an embedded hash link for single doc mode.
    var effectivePageKey =
      (hashStr.indexOf("#") === -1 ? hashStr : hashStr.substr(0, hashStr.indexOf("#")))
      .replace(".html", "")
      .replace(".htm", "")
      .toLowerCase();
    return {
      type: 'internal',
      asAnEmbeddedSubpageOfEntrypointPageKey: asEmbeddedSubpageOf,
      pageKey: effectivePageKey,
      pageExtension: hrefExtension,
      hashContents: hashStr.substr(hashStr.lastIndexOf("#") + 1),
      queryParams: queryParams,
    };
  }
};


var isNodeSearchHit = function (node) {
  return (
    node.tagName === "TR" ||
    node.tagName === "tr" ||
    node.tagName === "H0" ||
    node.tagName === "h0" ||
    node.tagName === "H1" ||
    node.tagName === "h1" ||
    node.tagName === "H2" ||
    node.tagName === "h2" ||
    node.tagName === "H3" ||
    node.tagName === "h3" ||
    node.tagName === "H4" ||
    node.tagName === "h4" ||
    node.tagName === "H5" ||
    node.tagName === "h5" ||
    node.tagName === "H6" ||
    node.tagName === "h6" ||
    node.tagName === "codetabbutton" ||
    node.tagName === "CODETABBUTTON" ||
    node.tagName === "P" ||
    node.tagName === "p" ||
    node.tagName === "LI" ||
    node.tagName === "li" ||
    node.tagName === "UL" ||
    node.tagName === "ul" ||
    node.tagName === "CODE" ||
    node.tagName === "code" ||
    node.tagName === "PRE" ||
    node.tagName === "pre" ||
    node.nodeType === Node.TEXT_NODE
  );
};

var SUPPORTS_SEARCH_TABBING = false;
// Number of headers in search results per page.
var NUM_HEADERS = 1;

/**
 * We can't have the ids of elements be the exact same as the hashes in the URL
 * because that will cause the browser to scroll. But we want to have full
 * control over scroll for things like better back button support and deep
 * linking / custom animation.
 * So the element to scroll to would have id="--bookmark-linkified--foo", but
 * the anchor links that jump to it would have href="#foo".
 *
 * This allows deep linking to page#section-header?text=this%20text Which will
 * animate a scroll to a specific text portion of that section with an
 * animation.  If we don't have full control over the animation, then our own
 * animation might fight the browser's.
 */
var BOOKMARK_LINK_ID_PREFIX = "--bookmark-linkified--";

/**
 * Prepends the linkified prefix.
 */

function pageifiedIdForHash(slug, pageKey) {
  return pageKey + "#" + slug;
}

function fullyQualifiedHeaderId(slug, pageKey) {
  return BOOKMARK_LINK_ID_PREFIX + pageifiedIdForHash(slug, pageKey);
}

/**
 * Strips the linkified prefix and page prefix.
 */
function hashForFullFullyQualifiedHeaderId(s) {
  var withoutLinkifiedPrefix =
    s.indexOf(BOOKMARK_LINK_ID_PREFIX) === 0 ? s.substring(BOOKMARK_LINK_ID_PREFIX.length) : s;
  var splitOnHash = withoutLinkifiedPrefix.split("#");
  if (splitOnHash.length > 1) {
    return splitOnHash[splitOnHash.length - 1];
  } else {
    return withoutLinkifiedPrefix;
  }
}

var queryContentsViaIframe = function (url, onDoneCell, onFailCell) {
  var timeout = window.setTimeout(function () {
    onFailCell.contents &&
      onFailCell.contents(
        "Timed out loading " +
          url +
          ". Maybe it doesn't exist? Alternatively, perhaps you were paused " +
          "in the debugger so it timed out?"
      );
  }, 900);
  var listenerID = window.addEventListener("message", function (e) {
    if (e.data.messageType === "docPageContent" && e.data.iframeName === url) {
      window.removeEventListener("message", listenerID);
      if (onDoneCell.contents) {
        window.clearTimeout(timeout);
        var start = Date.now();
        onDoneCell.contents(e.data.content);
        var end = Date.now();
        // console.log(end-start,'spent loading ' + url);
      }
    }
  });
  var iframe = document.createElement("iframe");
  iframe.name = url;
  // Themes may opt to handle offline/pre rendering, and this is convenient
  // to mark these iframes as not-essential once rendered so they may be
  // removed from the DOM after rendering, and won't take up space in the
  // bundle.
  // TODO: Consider this for merging many html pages into one book https://github.com/fidian/metalsmith-bookify-html
  iframe.className = "removeFromRenderedPage";
  iframe.src = url + "?bookmarkContentQuery=true";
  iframe.style = "display:none !important";
  iframe.type = "text/plain";
  iframe.onerror = function (e) {
    if (onFailCell.contents) {
      onFailCell.contents(e);
    }
  };
  // iframe.onload = function(e) {
  // };
  document.body.appendChild(iframe);
};

function scrollIntoViewAndHighlightNode(node) {
  var highlightNode = function (node) {
    $(".bookmark-in-doc-highlight").each(function () {
      var $el = $(this);
      $el.removeClass("bookmark-in-doc-highlight");
    });
    $(node).addClass("bookmark-in-doc-highlight");
  };
  if (!node) {
    return;
  }
  customScrollIntoView({
    smooth: true,
    container: "page",
    element: node,
    mode: "top",
    topMargin: headerHeight,
    bottomMargin: 0,
  });
  highlightNode(node);
}

function scrollIntoViewAndHighlightNodeById(id) {
  if (id != "") {
    var header = document.getElementById(id);
    scrollIntoViewAndHighlightNode(header);
  }
}

// https://stackoverflow.com/a/8342709
var customScrollIntoView = function (props) {
  var smooth = props.smooth || false;
  var container = props.container;
  var containerElement = props.container === "page" ? document.documentElement : props.container;
  var scrollerElement = props.container === "page" ? window : containerElement;
  var element = props.element;
  // closest-if-needed | top | bottom
  var mode = props.mode || "closest-if-needed";
  var topMargin = props.topMargin || 0;
  var bottomMargin = props.bottomMargin || 0;
  var containerRect = containerElement.getBoundingClientRect();
  var elementRect = element.getBoundingClientRect();
  var containerOffset = $(containerElement).offset();
  var elementOffset = $(element).offset();
  // TODO: For "whole document" scrolling,
  // use Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop)
  // When loading the page from entrypoint mode, the document.documentElement scrollTop is zero!!
  // But not when loading form an index.dev.html. Something about the way loading from entrypoint
  // rewrites the entire document with document.write screws up the scroll measurement.
  if (mode !== "top" && mode !== "closest-if-needed" && mode !== "bottom") {
    console.error("Invalid mode to scrollIntoView", mode);
  }
  var containerScrollTop =
    container === "page"
      ? Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop)
      : containerElement.scrollTop;
  var elementOffsetInContainer =
    elementOffset.top -
    containerOffset.top +
    // Relative to the document element does not need to account for document scrollTop
    (container === "page" ? 0 : containerScrollTop);
  if (
    mode === "bottom" ||
    (mode === "closest-if-needed" &&
      elementOffsetInContainer + elementRect.height >
        containerScrollTop + containerRect.height - bottomMargin)
  ) {
    var newTop =
      elementOffsetInContainer - containerRect.height + elementRect.height + bottomMargin;
    scrollerElement.scrollTo({ left: 0, top: newTop, behavior: smooth ? "smooth" : "auto" });
  } else if (
    mode === "top" ||
    (mode === "closest-if-needed" && elementOffsetInContainer < containerScrollTop)
  ) {
    var newTop = elementOffsetInContainer - topMargin;
    scrollerElement.scrollTo({ left: 0, top: newTop, behavior: smooth ? "smooth" : "auto" });
  }
};

var defaultSidenavifyConfig = {
  h1: true,
  h2: true,
  h3: true,
  h4: false,
  h5: false,
  h6: false,
};

var defaultSlugContributions = {
  h1: true,
  h2: true,
  h3: true,
  h4: true,
  h5: true,
  h6: true,
};

// Thank you David Walsh:
// https://davidwalsh.name/query-string-javascript
function queryParam(name) {
  var res = new RegExp(
    "[\\?&]" + name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]") + "=([^&#]*)"
  ).exec(location.search);
  return res === null ? "" : decodeURIComponent(res[1].replace(/\+/g, " "));
}

function parseYamlHeader(markdown, locationPathname) {
  markdown = markdown.trim();
  var yamlBoundaries = allMatchingIndicesWillMutateYourRegex(/\-\-\-\n/g, markdown);
  if (yamlBoundaries.length > 1 && yamlBoundaries[0].atIndex === 0) {
    var firstBoundary = yamlBoundaries[0];
    var secondBoundary = yamlBoundaries[1];
    var yamlContent = markdown.substring(firstBoundary.atIndex + firstBoundary.matchingString.length, secondBoundary.atIndex - 1);
    var lines = yamlContent.split("\n");
    var props = {};
    for (var i = 0; i < lines.length; i++) {
      if(lines[i].trim() === '') {
        continue;
      }
      var colonIndex = lines[i].indexOf(":");
      if (colonIndex === -1) {
        return { markdown: markdown, headerProps: {} };
      } else {
        var field = lines[i].substr(0, colonIndex);
        // Todo: escape strings
        var content = lines[i].substr(colonIndex + 1).trim();
        if (content[0] === '"' && content[content.length - 1] === '"') {
          var strContent = content.substr(1, content.length - 2);
          content = content.replace(new RegExp('\\\\"', "g"), '"');
        }
        props[field] = content;
      }
    }
    if (!props.id) {
      var filename = locationPathname.substring(locationPathname.lastIndexOf("/") + 1);
      props.id =
        filename.indexOf(".") !== -1
          ? filename.substring(0, filename.lastIndexOf("."))
          : filename;
    }
    return {
      markdown: markdown.substr(secondBoundary.atIndex + secondBoundary.matchingString.length),
      headerProps: props
    };
  } else {
    return { markdown: markdown, headerProps: {} };
  }
}

/**
 * Regexes are stateful in JS. Named appropriately.
 */
function allMatchingIndicesWillMutateYourRegex(regex, haystack) {
  var match;
  var matches = [];
  while (match = regex.exec(haystack)) {
    matches.push({matchingString: match[0], atIndex: match.index});
  }
  return matches;
};

/**
 * Strips out a special case of markdown "comments" which is supported in all
 * markdown parsers, will not be rendered in Github previews, but can be used
 * to convey yaml header information.
 *
 * Include this in your doc to have Bookmark interpret the yaml headers without
 * it appearing in the Github preview. This allows using one source of truth
 * markdown file for Github READMEs as well as using to generate your site
 * (when you don't want metadata showing up in your Github previews).
 *
 *     [//]: # (---)
 *     [//]: # (something: hey)
 *     [//]: # (title: me)
 *     [//]: # (description: "Hi there here is an escaped quote \" inside of quotes")
 *     [//]: # (---)
 */
function normalizeYamlMarkdownComments(markdown) {
  markdown = markdown.trim();
  var silentYamlBoundaries = allMatchingIndicesWillMutateYourRegex(
    new RegExp(escapeRegExpSearchString("[//]: # (") + "---" + escapeRegExpSearchString(")\n"), "g"),
    markdown
  );
  // Since white space trimmed, should be at index zero if first thing after script include
  if (silentYamlBoundaries.length > 1 && silentYamlBoundaries[0].atIndex === 0) {
    var firstBoundary = silentYamlBoundaries[0];
    var secondBoundary = silentYamlBoundaries[1];
    var yamlContent = markdown.substring(firstBoundary.atIndex + firstBoundary.matchingString.length, secondBoundary.atIndex - 1);
    var yamlContentWithoutComment =
      yamlContent.replaceAll(
        new RegExp(escapeRegExpSearchString("[//]: # (") + "(.*)" + escapeRegExpSearchString(")"), "g"),
        function(_s, content) { return content }
      );
    return "---\n" + yamlContentWithoutComment + "\n---\n" + markdown.substr(secondBoundary.atIndex + secondBoundary.matchingString.length);
  } else {
    return markdown;
  }
}

/**
 * The user can put this in their html file to:
 * 1. Get vim syntax highlighting to work.
 * 2. Get github to treat their html/htm file as a markdown file for rendering.
 * 3. Load the script tag only when rendered with ReFresh.
 *
 * [ vim:syntax=Markdown ]: # (<script src="flatdoc.js"></script>)
 *
 * Only downside is that it leaves a dangling ) in the text returned to
 * us which we can easily normalize.
 */
function normalizeMarkdownResponse(markdown) {
  if (markdown[0] === ")" && markdown[1] === "\n") {
    markdown = markdown.substring(2);
  }
  return markdown;
}

/**
 * [^] means don't match "no" characters - which is all characters including
 * newlines. The ? makes it not greddy.
 */
var docusaurusTabsRegionRegex = new RegExp(
  "^" +
    escapeRegExpSearchString("<!--DOCUSAURUS_CODE_TABS-->") +
    "$([^]*?)" +
    escapeRegExpSearchString("<!--END_DOCUSAURUS_CODE_TABS-->"),
  "gm"
);
var nonDocusaurusTabsRegionRegex = new RegExp(
  "^" +
    escapeRegExpSearchString("<!--CODE_TABS-->") +
    "$([^]*?)" +
    escapeRegExpSearchString("<!--END_CODE_TABS-->"),
  "gm"
);
var anyHtmlCommentRegex = new RegExp(
  "(^(" +
    escapeRegExpSearchString("<!--") +
    "([^]*?)" +
    escapeRegExpSearchString("-->") +
    ")[\n\r])?^```(.+)[\n\r]([^]*?)[\n\r]```",
  "gm"
);
function normalizeDocusaurusCodeTabs(markdown) {
  // Used to look it up later in the DOM and move things around to a more
  // convenient structure targetable by css.
  var onReplace = function (matchedStr, matchedCommentContents) {
    var tabs = [];
    var maxLengthOfCode = 0;
    var getMaxLengthOfCode = function (matchedStr, _, _, commentContent, syntax, codeContents) {
      var split = codeContents.split("\n");
      maxLengthOfCode =
        codeContents && split.length > maxLengthOfCode ? split.length : maxLengthOfCode;
      return matchedStr;
    };
    var onIndividualReplace = function (_, _, _, commentContent, syntax, codeContents) {
      var className = tabs.length === 0 ? "active" : "";
      var split = codeContents.split("\n");
      var splitLen = split.length;
      // For some reason - 1 is needed when adding empty strings, instead of
      // non-empty spacers.
      while (splitLen - 1 < maxLengthOfCode) {
        split.push(" ");
        splitLen++;
      }
      tabs.push({
        syntax: syntax,
        codeContents: split.join("\n"),
        tabMarkup:
          "<codetabbutton class='" +
          className +
          "'" +
          " data-index=" +
          (tabs.length + 1) +
          ">" +
          escapeHtml(commentContent || syntax) +
          "</codetabbutton>",
      });
      return "\n```" + syntax + "\n" + split.join("\n") + "\n```";
    };
    tabs = [];
    maxLengthOfCode = 0;
    matchedCommentContents.replace(anyHtmlCommentRegex, getMaxLengthOfCode);
    var ret = matchedCommentContents.replace(anyHtmlCommentRegex, onIndividualReplace);
    return (
      "<codetabscontainer data-num-codes=" +
      tabs.length +
      " class='bookmark-codetabs-active1 bookmark-codetabs-length" +
      tabs.length +
      "'>" +
      tabs
        .map(function (t) {
          return t.tabMarkup;
        })
        .join("") +
      "</codetabscontainer>" +
      ret
    );
  };
  return markdown.replace(docusaurusTabsRegionRegex, onReplace)
        .replace(nonDocusaurusTabsRegionRegex, onReplace);
  return ret;
}

var emptyHTML = "";

/**
 * Scrolling into view:
 * https://www.bram.us/2020/03/01/prevent-content-from-being-hidden-underneath-a-fixed-header-by-using-scroll-margin-top/
 */

function escapePlatformStringLoop(html, lastIndex, index, s, len) {
  var html__0 = html;
  var lastIndex__0 = lastIndex;
  var index__0 = index;
  for (;;) {
    if (index__0 === len) {
      var match = 0 === lastIndex__0 ? 1 : 0;
      if (0 === match) {
        var match__0 = lastIndex__0 !== index__0 ? 1 : 0;
        return 0 === match__0 ? html__0 : html__0 + s.substring(lastIndex__0, len);
      }
      return s;
    }
    var code = s.charCodeAt(index__0);
    if (40 <= code) {
      var switcher = (code + -60) | 0;
      if (!(2 < switcher >>> 0)) {
        switch (switcher) {
          case 0:
            var html__1 = html__0 + s.substring(lastIndex__0, index__0);
            var lastIndex__1 = (index__0 + 1) | 0;
            var html__2 = html__1 + "&lt;";
            var index__2 = (index__0 + 1) | 0;
            var html__0 = html__2;
            var lastIndex__0 = lastIndex__1;
            var index__0 = index__2;
            continue;
          case 1:
            break;
          default:
            var html__3 = html__0 + s.substring(lastIndex__0, index__0);
            var lastIndex__2 = (index__0 + 1) | 0;
            var html__4 = html__3 + "&gt;";
            var index__3 = (index__0 + 1) | 0;
            var html__0 = html__4;
            var lastIndex__0 = lastIndex__2;
            var index__0 = index__3;
            continue;
        }
      }
    } else if (34 <= code) {
      var switcher__0 = (code + -34) | 0;
      switch (switcher__0) {
        case 0:
          var su = s.substring(lastIndex__0, index__0);
          var html__5 = html__0 + su;
          var lastIndex__3 = (index__0 + 1) | 0;
          var html__6 = html__5 + "&quot;";
          var index__4 = (index__0 + 1) | 0;
          var html__0 = html__6;
          var lastIndex__0 = lastIndex__3;
          var index__0 = index__4;
          continue;
        case 4:
          var su__0 = s.substring(lastIndex__0, index__0);
          var html__7 = html__0 + su__0;
          var lastIndex__4 = (index__0 + 1) | 0;
          var html__8 = html__7 + "&amp;";
          var index__5 = (index__0 + 1) | 0;
          var html__0 = html__8;
          var lastIndex__0 = lastIndex__4;
          var index__0 = index__5;
          continue;
        case 5:
          var su__1 = s.substring(lastIndex__0, index__0);
          var html__9 = html__0 + su__1;
          var lastIndex__5 = (index__0 + 1) | 0;
          var html__10 = html__9 + "&#x27;";
          var index__6 = (index__0 + 1) | 0;
          var html__0 = html__10;
          var lastIndex__0 = lastIndex__5;
          var index__0 = index__6;
          continue;
      }
    }
    var index__1 = (index__0 + 1) | 0;
    var index__0 = index__1;
    continue;
  }
}

function escapeHtml(s) {
  return escapePlatformStringLoop(emptyHTML, 0, 0, s, s.length);
}

var updateContextFromTreeNode = function (context, treeNode) {
  if (treeNode.level === 0) {
    return { ...context, h0: treeNode, h1: null, h2: null, h3: null, h4: null, h5: null, h6: null };
  }
  if (treeNode.level === 1) {
    return { ...context, h1: treeNode, h2: null, h3: null, h4: null, h5: null, h6: null };
  }
  if (treeNode.level === 2) {
    return { ...context, h2: treeNode, h3: null, h4: null, h5: null, h6: null };
  }
  if (treeNode.level === 3) {
    return { ...context, h3: treeNode, h4: null, h5: null, h6: null };
  }
  if (treeNode.level === 4) {
    return { ...context, h4: treeNode, h5: null, h6: null };
  }
  if (treeNode.level === 5) {
    return { ...context, h5: treeNode, h6: null };
  }
  if (treeNode.level === 6) {
    return { ...context, h6: treeNode };
  }
  // LEAF_LEVEL
  return context;
};

/**
 * Turn a search string into a regex portion.
 * https://stackoverflow.com/a/1144788
 */
function escapeRegExpSearchString(string) {
  return string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
}

function replaceAllStringsCaseInsensitive(str, find, replace) {
  return str.replace(new RegExp(escapeRegExp(find), "gi"), replace);
}

function escapeRegExpSplitString(string) {
  return string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
}

function splitStringCaseInsensitiveImpl(regexes, str, find) {
  return str.split(regexes.caseInsensitive.anywhere);
}
function splitStringCaseInsensitive(str, find) {
  return str.split(new RegExp("(" + escapeRegExpSplitString(find) + ")", "gi"));
}

/**
 * Makes code blocks wrap better.
 */
function splitIndentable(s) {
   var lines = s.split('\n');
   return lines.map(function(ln) {
     var numLeading = ln.search(/\S|$/);
     return '<div ' +
       (numLeading > 0 ? 'class="paradoc-indented-' + numLeading  + '"': '') + ">" +
       ln +
       '</div>';
   }).join('');
}

/**
 * Makes code blocks wrap better (if already highlighted by hljs).  Assumes the
 * first line is not indented (pretty safe assumption).  Will only cause better
 * wrapping for line breaks that are contained within *non* highlighted
 * portions of the code, which is most of them.
 */
function splitIndentableHighlighted(root) {
  var newLineGroups = [[]];
  var newLineGroupsObservedIndent = [0];
  var nodes = [];
  // backup copy
  for(var t = 0; t < root.childNodes.length; t++) {
    nodes.push(root.childNodes[t]);
  }
  for(var i = 0; i < nodes.length; i++) {
    var node = nodes[i];
    if(node.nodeType !== Node.TEXT_NODE) {
      var lastLineGroupIndex = newLineGroups.length - 1;
      var lastLineGroup = newLineGroups[lastLineGroupIndex];
      lastLineGroup.push(node);
    } else {
      var lines = node.nodeValue.split('\n');
      // TODO: The first should not have any indent no matter what.
      // Even if it has a leading space
      for(var j = 0; j < lines.length; j++) {
        var ln = lines[j];
        var txtElem = document.createTextNode(ln);
        if(j === 0) {
          // Stick leading text before a newline onto the previous line group.
          var lastLineGroupIndex = newLineGroups.length - 1;
          var lastLineGroup = newLineGroups[lastLineGroupIndex];
          newLineGroups[newLineGroups.length - 1].push(txtElem);
        } else {
          var numLeading = ln.search(/\S|$/);
          var lastLineGroupIndex = newLineGroups.length - 1;
          newLineGroupsObservedIndent[lastLineGroupIndex + 1] = numLeading;
          newLineGroups[lastLineGroupIndex + 1] = [txtElem];
        }
      }
    }
  }
  root.innerHTML = '';
  for(var z = 0; z < newLineGroups.length; z++) {
    var lineDiv = document.createElement('div');
    lineDiv.classList.add('paradoc-indented-' + newLineGroupsObservedIndent[z]);
    root.appendChild(lineDiv);
    for(var zz = 0; zz < newLineGroups[z].length; zz++) {
      lineDiv.appendChild(newLineGroups[z][zz]);
    }
  }
}


/**
 * Only trust for markdown that came from trusted source (your own page).
 * I do not know exactly what portions are unsafe - perhaps none.
 */
var trustedTraverseAndHighlightImpl = function traverseAndHighlightImpl(regex, text, node) {
  var tagName = node.nodeType === Node.TEXT_NODE ? "p" : node.tagName.toLowerCase();
  var className = node.nodeType === Node.TEXT_NODE ? "" : node.getAttributeNode("class");
  var childNodes = node.nodeType === Node.TEXT_NODE ? [node] : node.childNodes;
  var childNode = childNodes.length > 0 ? childNodes[0] : null;
  var i = 0;
  var newInnerHtml = "";
  while (childNode && i < 2000) {
    if (childNode.nodeType === Node.TEXT_NODE) {
      if (regex) {
        var splitOnMatch = splitStringCaseInsensitiveImpl(regex, childNode.textContent, text);
        splitOnMatch.forEach(function (seg) {
          if (seg !== "") {
            if (seg.toLowerCase() === text.toLowerCase()) {
              newInnerHtml += "<search-highlight>" + escapeHtml(seg) + "</search-highlight>";
            } else {
              newInnerHtml += escapeHtml(seg);
            }
          }
        });
      } else {
        newInnerHtml += escapeHtml(childNode.textContent);
      }
    } else {
      newInnerHtml += trustedTraverseAndHighlightImpl(regex, text, childNode);
    }
    i++;
    childNode = childNodes[i];
  }
  var openTag = "";
  var closeTag = "";
  classAttr = className
    ? ' class="' + escapeHtml(className.value.replace("bookmark-in-doc-highlight", "")) + '"'
    : "";
  switch (tagName) {
    case "a":
      var href = node.getAttributeNode("href");
      openTag = href ? '<a onclick="false" tabindex=-1 ' + classAttr + ">" : "<a>";
      closeTag = "</a>";
      break;
    case "code":
      var className = node.getAttributeNode("class");
      if(node.classList.contains('odoc-bookmark-code-in-spec')) {
        // TODO: Should this be splitIndentableHighlighted() ?
        newInnerHtml = splitIndentable(newInnerHtml);
      }
      openTag = className
        ? '<code class="' + escapeHtml(className.value) + '"' + classAttr + ">"
        : "<code>";
      closeTag = "</code>";
      break;
    default:
      openTag = "<" + tagName + classAttr + ">";
      closeTag = "</" + tagName + ">";
  }
  return openTag + newInnerHtml + closeTag;
};

var trustedTraverseAndHighlight = function (searchRegex, text, node) {
  return trustedTraverseAndHighlightImpl(searchRegex, text, node);
};

/**
 * Leaf nodes will be considered level 999 (something absurdly high).
 */
var LEAF_LEVEL = 999;
var PAGE_LEVEL = -1;
var getDomNodeStructureLevel = function getStructureLevel(node) {
  if (node.tagName === "h0" || node.tagName === "H0") {
    return 0;
  }
  if (node.tagName === "h1" || node.tagName === "H1") {
    return 1;
  }
  if (node.tagName === "h2" || node.tagName === "H2") {
    return 2;
  }
  if (node.tagName === "h3" || node.tagName === "H3") {
    return 3;
  }
  if (node.tagName === "h4" || node.tagName === "H4") {
    return 4;
  }
  if (node.tagName === "h5" || node.tagName === "H5") {
    return 5;
  }
  if (node.tagName === "h6" || node.tagName === "H6") {
    return 6;
  }
  return LEAF_LEVEL;
};
var deepensContext = function (treeNode) {
  return treeNode.level >= 0 && treeNode.level < 7;
};

/**
 * Searches up in the context for the correct place for this level to be
 * inserted.
 */
function recontext(context, nextTreeNode) {
  // Root document level is level zero.
  while (context.length > 1 && context[context.length - 1].level >= nextTreeNode.level) {
    context.pop();
  }
}

function lazyHierarchicalIndexForSearch(pageState) {
  for (var pageKey in pageState) {
    if (!pageState[pageKey].hierarchicalIndex) {
      var containerNode = pageState[pageKey].contentContainerNode;
      pageState[pageKey].hierarchicalIndex = hierarchicalIndexFromHierarchicalDoc(
        pageState[pageKey].hierarchicalDoc
      );
    }
  }
}

function forEachHierarchyOne(f, context, treeNode) {
  var newContext = updateContextFromTreeNode(context, treeNode);
  f(treeNode, newContext);
  forEachHierarchyImpl(f, newContext, treeNode.subtreeNodes);
}
function forEachHierarchyImpl(f, context, treeNodes) {
  return treeNodes.forEach(forEachHierarchyOne.bind(null, f, context));
}
function forEachHierarchy(f, treeNodes) {
  var context = startContext;
  return treeNodes.forEach(forEachHierarchyOne.bind(null, f, context));
}

function mapHierarchyOne(f, context, treeNode) {
  var newContext = updateContextFromTreeNode(context, treeNode);
  return f(
    {
      levelContent: treeNode.levelContent,
      level: treeNode.level,
      slug: treeNode.slug,
      subtreeNodes: mapHierarchyImpl(f, newContext, treeNode.subtreeNodes),
    },
    newContext
  );
}
function mapHierarchyImpl(f, context, treeNodes) {
  return treeNodes.map(mapHierarchyOne.bind(null, f, context));
}
function mapHierarchy(f, treeNodes) {
  var context = startContext;
  return treeNodes.map(mapHierarchyOne.bind(null, f, context));
}

/**
 * Returns a hierarchy tree where level contents are the individual items that
 * may be searchable. The original structured hierarchy tree has the
 * levelContent of each subtreeNode being the root node of every element that
 * appears directly under that heading. The hierarchicalIndex expands a single
 * tree node (such as one for a ul element) into several tree nodes (one for
 * each li in the ul for example). So it's a pretty simple mapping of the tree,
 * where each levelContent is expanded out into an array of content.  Retains
 * the original context because when filtering the index at a later point, the
 * context would be in terms of filtered nodes, when you often also want the
 * original context as well.
 */
function hierarchicalIndexFromHierarchicalDoc(treeNodes) {
  function expandTreeNodeContentToSearchables(domNode, inclusiveContext) {
    if (isNodeSearchHit(domNode)) {
      // Filter out empty searchables.
      if (domNode.textContent.trim() !== "") {
        return [
          {
            indexable: domNode,
            lazyCharacterCounts: null,
            originalInclusiveContext: inclusiveContext,
          },
        ];
      } else {
        return [];
      }
    } else {
      var more = [];
      var childDomNode = domNode.firstChild;
      while (childDomNode) {
        more = more.concat(expandTreeNodeContentToSearchables(childDomNode, inclusiveContext));
        childDomNode = childDomNode.nextSibling;
      }
      return more;
    }
  }
  function mapper(treeNode, inclusiveContext) {
    if (treeNode.level !== LEAF_LEVEL) {
      return {
        ...treeNode,
        levelContent: [
          {
            indexable: treeNode.levelContent,
            lazyCharacterCounts: null,
            originalInclusiveContext: inclusiveContext,
          },
        ],
      };
    } else {
      var domNode = treeNode.levelContent;
      return {
        ...treeNode,
        levelContent: expandTreeNodeContentToSearchables(domNode, inclusiveContext),
      };
    }
  }
  return mapHierarchy(mapper, treeNodes);
}

/**
 * Forms a hierarchy of content from structure forming nodes (such as headers)
 * from what would otherwise be a flat document.
 * The subtreeNodes are not dom Subtree nodes but the hierarchy subtree (level
 * heading content etc).
 *
 * The subtreeNodes are either the list of Dom nodes immediately under that
 * level, else another "tree" node. (Type check it at runtime by looking for
 * .tagName property).
 *
 *  page:
 *
 *  H1Text
 *  text
 *  H2Text
 *  textB
 *  textC
 *
 *  Would be of the shape:
 *  {
 *    level: 0,                                                                // page
 *    levelContent: null,
 *    subtreeNodes: [
 *      {
 *        level: 1,
 *        levelContent: <h1>H1Text</h1>,                                       // h1 dom node
 *        subtreeNodes: [
 *          {level: LEAF_LEVEL, levelContent: <p>text</p>},                    // p DOM node
 *          {
 *            level: 2,
 *            levelContent: <h2>H2Text</h2>,
 *            subtreeNodes: [
 *              {level: LEAF_LEVEL, levelContent: <p>textB</p>},
 *              {level: LEAF_LEVEL, levelContent: <p>textC</p>}
 *            ]
 *          }
 *        ]
 *
 *      }
 *
 *    ]
 *  }
 */
function hierarchize(containerNode) {
  // Mutable reference.
  var dummyNode = {
    // Such as the h2 node that forms the new level etc.
    levelContent: null,
    level: PAGE_LEVEL,
    subtreeNodes: [],
    // Lazily or deferred
    slug: null,
  };
  var context = [dummyNode];
  function hierarchicalIndexChildrenImpl(domNode) {
    var childDomNode = domNode.firstChild;
    while (childDomNode) {
      hierarchicalIndexImpl(childDomNode);
      childDomNode = childDomNode.nextSibling;
    }
  }
  function hierarchicalIndexImpl(domNode) {
    var domNodeLevel = getDomNodeStructureLevel(domNode);
    var treeNode = {
      levelContent: domNode,
      level: domNodeLevel,
      subtreeNodes: [],
      // Lazily or deferred
      slug: null,
    };
    recontext(context, treeNode);
    context[context.length - 1].subtreeNodes.push(treeNode);
    if (deepensContext(treeNode)) {
      context.push(treeNode);
    }
  }
  hierarchicalIndexChildrenImpl(containerNode);
  return dummyNode.subtreeNodes;
}

/**
 * Renders text filtered hierarchical index. The caps on rendered list size
 * happens at the renddering stage so that you can refer to "the n'th item" in
 * a permalink even if you change the configuration for capping the rendered
 * list size (for perf).
 */
var hierarchicalRenderFilteredSearchables = function (
  query,
  filteredHierarchicalIndexByPage,
  renderTopRow
) {
  var txt = query.trim();
  var searchRegex = regexesFor(query);
  new RegExp("(" + escapeRegExpSplitString(txt) + ")", "gi");
  // On the first keystroke, it will return far too many results, almost all of
  // them useless since it matches anything with that character. In that case, limit to
  // 20 results. Then on the next keystroke allow more.
  var maxResultsLen = txt.length < 3 ? 15 :
    txt.length === 3 ? 20 : 999;
  return mapKeys(filteredHierarchicalIndexByPage, (filteredHierarchicalIndex, pageKey) => {
    var results = [];
    forEachHierarchy(function (treeNode, inclusiveContext) {
      var filteredSearchables = treeNode.levelContent;
      for (var i = 0; filteredSearchables !== null && i < filteredSearchables.length; i++) {
        if (results.length < maxResultsLen) {
          var searchable = filteredSearchables[i];
          results.push({
            searchable: searchable,
            highlightedInnerText: trustedTraverseAndHighlight(
              searchRegex,
              txt,
              searchable.indexable
            ),
            topRowMarkup: renderTopRow(
              treeNode.level,
              searchable.originalInclusiveContext,
              searchable.indexable
            ),
          });
        }
      }
    }, filteredHierarchicalIndex);
    return results;
  });
};

/**
 * We need to use textContent to return the content of nodes that are not
 * visible/hidden (also avoiding reflows)
 * From Mozilla docs:
 * https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#differences_from_innertext
 * Don't get confused by the differences between Node.textContent and
 * HTMLElement.innerText. Although the names seem similar, there are important
 * differences:
 *  - textContent gets the content of all elements, including <script> and
 *  <style> elements. In contrast, innerText only shows “human-readable”
 *  elements.
 *  - textContent returns every element in the node. In contrast, innerText is
 *  aware of styling and won’t return the text of “hidden” elements.
 *  - Moreover, since innerText takes CSS styles into account, reading the
 *  value of innerText triggers a reflow to ensure up-to-date computed styles.
 *  (Reflows can be computationally expensive, and thus should be avoided when
 *  possible.)
 *  - Unlike textContent, altering innerText in Internet Explorer (version 11
 *  and below) removes child nodes from the element and permanently destroys
 *  all descendant text nodes. It is impossible to insert the nodes again into
 *  any other element or into the same element after doing so.
 *  https://stackoverflow.com/a/35213639
 */
var getDomThingInnerText = function (domThing) {
  return domThing.nodeType === Node.TEXT_NODE ? domThing.textContent : domThing.textContent;
};

/**
 * Table rows return textContent that is a concat of their tds, but there's no
 * space between them - yet users see a space, so we want to index as if there
 * were a space there.
 */
var getDomThingSearchableText = function (domThing) {
  if(domThing.tagName === 'TR' || domThing.tagName === 'tr') {
    return Array.prototype.map.call(domThing.children, function(child) {
      return getDomThingInnerText(child);
    }).join(' ');
  } else {
    return getDomThingInnerText(domThing);
  }
  return domThing.nodeType === Node.TEXT_NODE ? domThing.textContent : domThing.textContent;
};

var filterHierarchicalSearchables = function (query, pageState) {
  var txt = query.trim();
  var searchRegex = regexesFor(txt);
  new RegExp("(" + escapeRegExpSplitString(txt) + ")", "gi");
  // On the first keystroke, it will return far too many results, almost all of
  // them useless since it matches anything with that character. In that case, limit to
  // 20 results. Then on the next keystroke allow more.
  var maxResultsLen = txt.length === 1 ? 20 : 999;
  return mapKeys(pageState, (pageData, pageKey) => {
    return mapHierarchy(function (treeNode, inclusiveContext) {
      // TODO: this is unfortunate. We should be able to *also* filter the
      // header content, while preserving the original context so we can use
      // the original unfiltered context to render top row, but also determine
      // which headers themselves match the filtering so they can be rendered
      // as individual results themselves.
      // if(treeNode.level !== LEAF_LEVEL) {
      //   return treeNode;
      // }
      var levelContent = treeNode.levelContent;
      var searchables = levelContent;
      var smartCaseWordBoundaryResults = [];
      var smartCaseAnywhereNotWordBoundaryResults = [];
      var caseInsensitiveWordBoundaryResults = [];
      var caseInsensitiveAnywhereNotWordBoundaryResults = [];
      searchables.forEach(function (searchable) {
        var indexable = searchable.indexable;
        var nodeText = getDomThingSearchableText(indexable);
        var test = findBestMatch(nodeText, searchRegex);
        var resultsToPush =
          test === -1
            ? null
            : test & (SMARTCASE | WORDBOUNDARY)
            ? smartCaseWordBoundaryResults
            : test & SMARTCASE
            ? smartCaseAnywhereNotWordBoundaryResults
            : test & WORDBOUNDARY
            ? caseInsensitiveAnywhereNotWordBoundaryResults
            : caseInsensitiveAnywhereNotWordBoundaryResults;
        if (resultsToPush !== null) {
          resultsToPush.push(searchable);
        }
      });

      var noResults =
        !smartCaseWordBoundaryResults.length &&
        !smartCaseAnywhereNotWordBoundaryResults.length &&
        !caseInsensitiveWordBoundaryResults.length &&
        !caseInsensitiveAnywhereNotWordBoundaryResults.length;

      return {
        ...treeNode,
        levelContent: noResults
          ? []
          : smartCaseWordBoundaryResults
              .concat(smartCaseAnywhereNotWordBoundaryResults)
              .concat(smartCaseAnywhereNotWordBoundaryResults)
              .concat(caseInsensitiveWordBoundaryResults)
              .concat(caseInsensitiveAnywhereNotWordBoundaryResults),
      };
    }, pageData.hierarchicalIndex);
  });
};

/**
 * For a context, finds the deepest header, and uses that slug if it exists.
 */
var bestSlugForContext = function (context) {
  if (context.h6 && context.h6.slug) {
    return context.h6.slug;
  } else if (context.h5 && context.h5.slug) {
    return context.h5.slug;
  } else if (context.h4 && context.h4.slug) {
    return context.h4.slug;
  } else if (context.h3 && context.h3.slug) {
    return context.h3.slug;
  } else if (context.h2 && context.h2.slug) {
    return context.h2.slug;
  } else if (context.h1 && context.h1.slug) {
    return context.h1.slug;
  } else {
    return null;
  }
};

var SMARTCASE = 0b10;
var WORDBOUNDARY = 0b01;

var regexesFor = function (str) {
  var hasUpper = str.toLowerCase() !== str;
  return {
    // TODO: Add checks that remove symbols like hyphen, dot, parens
    smartCase: {
      // Priority 1
      wordBoundary: !hasUpper
        ? null
        : new RegExp("\\b(" + escapeRegExpSplitString(str) + ")", "g" + (hasUpper ? "" : "i")),
      // Priority 2
      anywhere: !hasUpper
        ? null
        : new RegExp("(" + escapeRegExpSplitString(str) + ")", "g" + (hasUpper ? "" : "i")),
    },
    caseInsensitive: {
      // Priority 3
      wordBoundary: new RegExp("\\b(" + escapeRegExpSplitString(str) + ")", "gi"),
      // Priority 4
      anywhere: new RegExp("(" + escapeRegExpSplitString(str) + ")", "gi"),
    },
  };
};

/**
 * Resets the regexes lastIndex to 0 as a side effect.
 * Regexes are stateful.
 */
var findBestMatch = function (stringToTest, regexes) {
  var ret = -1;

  if (regexes.smartCase.wordBoundary && regexes.smartCase.wordBoundary.test(stringToTest)) {
    ret = SMARTCASE | WORDBOUNDARY;
  } else if (regexes.smartCase.anywhere && regexes.smartCase.anywhere.test(stringToTest)) {
    ret = SMARTCASE;
  } else if (regexes.caseInsensitive.wordBoundary.test(stringToTest)) {
    ret = WORDBOUNDARY;
  } else if (regexes.caseInsensitive.anywhere.test(stringToTest)) {
    ret = 0;
  }
  if(regexes.smartCase.wordBoundary) regexes.smartCase.wordBoundary.lastIndex = 0;
  if(regexes.smartCase.anywhere) regexes.smartCase.anywhere.lastIndex = 0;
  regexes.caseInsensitive.wordBoundary.lastIndex = 0;
  regexes.caseInsensitive.anywhere.lastIndex = 0;
  return ret;
};

/* Matches found in the header itself will be considered in that context */
var startContext = {
  h0: null,
  h1: null,
  h2: null,
  h3: null,
  h4: null,
  h5: null,
  h6: null,
};

/**
 * If the encoding has an underscore anywhere it means the numbers were
 * doubled. If it has no underscore, the numbers might have been doubled - but
 * there might just not have been any odd number of character counts.
 */
var computeCharacterCounts = function (s) {
  var sNoWhite = s.replace(/\s/g, "");
  var baseRangeStart = "a".charCodeAt(0); // 97, a
  var baseRangeEnd = "z".charCodeAt(0); // 122, z
  // Squash all other characters into two slots.
  var baseRangeLowerThanStart = baseRangeEnd + 1;
  var baseRangeHigherThanEnd = baseRangeEnd + 2;
  var counts = [];
  for (var j = baseRangeStart; j <= baseRangeHigherThanEnd; j++) {
    counts[j - baseRangeStart] = 0;
  }
  var lower = sNoWhite.toLowerCase();
  for (var i = 0; i < lower.length; i++) {
    var charCode = lower.charCodeAt(i);
    var effectiveCharCode;
    if (charCode < baseRangeStart) {
      effectiveCharCode = baseRangeEnd + 1;
    } else if (charCode > baseRangeEnd) {
      effectiveCharCode = baseRangeEnd + 2;
    } else {
      effectiveCharCode = charCode;
    }
    counts[effectiveCharCode - baseRangeStart] = counts[effectiveCharCode - baseRangeStart] + 1;
  }
  return counts;
};

function computeCharacterCountDistance(a, b) {
  var dist = 0;
  for (var i = 0; i < a.length; i++) {
    dist += Math.abs(a[i] - b[i]);
  }
  return dist;
}

/**
 * Weighted character counts mapped to numeric representation in the range of
 * of a-ZA-Z0-9$_ (64 points).
 */
var ENCODED_HASH_BASE = 64;
var numberToEncodedLarge = function (n) {
  var remainder = n % ENCODED_HASH_BASE;
  var flooredDivision = Math.floor(n / ENCODED_HASH_BASE);
  return (
    (flooredDivision >= ENCODED_HASH_BASE
      ? numberToEncodedLarge(flooredDivision)
      : numberToEncodedImpl(flooredDivision)) + numberToEncodedImpl(remainder)
  );
};
var numberToEncodedImpl = function (n) {
  return n >= ENCODED_HASH_BASE
    ? numberToEncodedLarge(n)
    : n < 10
    ? String.fromCharCode(48 /*"0"*/ + n)
    : n < 36
    ? String.fromCharCode(97 /*a*/ + n - 10)
    : n < 62
    ? String.fromCharCode(65 /*A*/ + n - 36)
    : n === 62
    ? "$"
    : "_";
};
/**
 * Special encoding of a number in url friendly base64 where the numer of
 * leading dashes tell you how many following characters to interpret.
 * One leading dash means two, two leading dashes means three and so on.
 */
var numberToEncoded = function (n) {
  var encoded = numberToEncodedImpl(n);
  var len = encoded.length;
  var prefix = "";
  for (var i = 1; i < len; i++) {
    prefix = prefix + "-";
  }
  return prefix + encoded;
};

/**
 * We can change the hash encoding by changing the name of the query param from
 * txt= to something like s=
 */
/**
 * Encodes character counts into the range
 */
var characterCountsToEncoded = function (arr) {
  var str = "";
  return arr
    .map(function (itm) {
      return numberToEncoded(itm);
    })
    .join("");
};

var encodedCountToNumber = function (s) {
  var base = 1;
  var sum = 0;
  for (var i = s.length - 1; i >= 0; i--) {
    var digitCharCode = s[i].charCodeAt(0);
    var digitEquivalent =
      digitCharCode >= 48 && digitCharCode < 58
        ? digitCharCode - 48
        : digitCharCode >= 97 && digitCharCode < 97 + 26
        ? 10 + digitCharCode - 97
        : digitCharCode >= 65 && digitCharCode < 65 + 26
        ? 10 + 26 + digitCharCode - 65
        : s === "$"
        ? 62
        : 63;
    sum += digitEquivalent * base;
    base = base * ENCODED_HASH_BASE;
  }
  return sum;
};

var encodedToCharacterCountsImpl = function (s, numCharsToParse) {
  if (s.length === 0) {
    return [];
  } else if (s[0] === "-") {
    return encodedToCharacterCountsImpl(s.substr(1), numCharsToParse + 1);
  } else {
    return [encodedCountToNumber(s.substr(0, numCharsToParse))].concat(
      encodedToCharacterCountsImpl(s.substr(numCharsToParse), 1)
    );
  }
};
var encodedToCharacterCounts = function (s) {
  return encodedToCharacterCountsImpl(s, 1);
};
var encodedToNumbers = function (s) {
  var encoded = numberToEncoded(s);
};

var testEncodingOfString = function (s) {
  var characterCounts = computeCharacterCounts(s);
  var encodedString = characterCountsToEncoded(characterCounts);
  var reCharacterCounts = encodedToCharacterCounts(encodedString);

  if (JSON.stringify(reCharacterCounts) !== JSON.stringify(characterCounts)) {
    console.error(
      "Re Encoded not same as encoded \n" +
        JSON.stringify(reCharacterCounts) +
        " \n" +
        JSON.stringify(characterCounts)
    );
  } else {
    console.log(
      "Re Encoded " +
        encodedString +
        " IS same when reencoding \n" +
        JSON.stringify(reCharacterCounts)
    );
  }
};

// testEncodingOfString("testing testing another thing testing");
// testEncodingOfString("aaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaazzzzzzaaaaaaaaaaaaaaaatesting testing another thing testing");

/**
 * Problem:
 * You don't want 1.23 to become 123 because then 12.3 becomes 123 as well, and
 * therefore the slug will have a "deduping" 2 appended resulting in nonsense
 * version numbers in header text such as 1232.
 * This doesn't solve that but we can easily replace dots with spaces then
 * slugify. However, this will break github markdown slugs so the solution is
 * to support alternative slugs in links in the markdown
 * [here](./DOC.html#github-lame-slug?bookmark-better-slug=awesome-slug).
 */
var contextToSlug = function (context, slugContributions) {
  var slug = "";
  // h0 shouldn't contribute to the slug. The page URL (or embedded single page
  // #pageKey) accomplishes that.
  // if(context.h0 && slugContributions.h0) {
  //   slug +=  Flatdoc.slugify(context.h0.levelContent.textContent);
  // }
  if (context.h1 && slugContributions.h1) {
    slug += " " + Flatdoc.slugify(context.h1.levelContent.textContent);
  }
  if (context.h2 && slugContributions.h2) {
    slug += " " + Flatdoc.slugify(context.h2.levelContent.textContent);
  }
  if (context.h3 && slugContributions.h3) {
    slug += " " + Flatdoc.slugify(context.h3.levelContent.textContent);
  }
  if (context.h4 && slugContributions.h4) {
    slug += " " + Flatdoc.slugify(context.h4.levelContent.textContent);
  }
  if (context.h5 && slugContributions.h5) {
    slug += " " + Flatdoc.slugify(context.h5.levelContent.textContent);
  }
  if (context.h6 && slugContributions.h6) {
    slug += " " + Flatdoc.slugify(context.h6.levelContent.textContent);
  }
  return Flatdoc.slugify(slug.length > 0 ? slug.substring(1) : "");
};

/**
 * H0s never partake in slugs.
 */
function annotateSlugsOnTreeNodes(hierarchicalDoc, slugContributions) {
  var seenSlugs = {};
  // Requesting side-nav requires linkifying
  var headers = "h1 h2 h3 h4 h5 h6 H1 H2 H3 H4 H5 H6";
  var appendSlug = function (treeNode, inclusiveContext) {
    var levelContent = treeNode.levelContent;
    var level = treeNode.level;
    var subtreeNodes = treeNode.subtreeNodes;
    if (headers.indexOf(levelContent.tagName) !== -1) {
      var slugCandidate = contextToSlug(inclusiveContext, slugContributions);
      var slug = seenSlugs[slugCandidate]
        ? slugCandidate + "--" + (seenSlugs[slugCandidate] + 1)
        : slugCandidate;
      seenSlugs[slugCandidate] = seenSlugs[slugCandidate] ? seenSlugs[slugCandidate] + 1 : 1;
      treeNode.slug = slug;
    }
  };
  forEachHierarchy(appendSlug, hierarchicalDoc);
}

/**
 * 1. Fixes links that pointed to /page.md or /page to point to page.html when
 * not in single docs mode.  This makes it easier to port files over to
 * Bookmark by just renaming them to .html and adding the script header.
 * However, you should fix up these links too so that your markdown rendered
 * docs render correctly on github.
 * 2. Changes links from something/ to something.html.
 * TODO: Should probably not do any anchor link rewriting to links to
 * documents. (if you have an image.png anchor link and a page key named
 * image).
 *
 * When rendering in development mode, we need to turn links like:
 * foo.html into ./foo.html
 */
function fixAllAnchorLinksUnderRoot(runner, rootNode) {
  $(rootNode).find('a').each(function() {
    var node = this;
    fixupHrefOnAnchor(runner, node);
  });
}

var fixupHrefOnAnchor = function(runner, node) {
  var hrefAsWritten = node.getAttribute('href');
  var fullHref = node.href;
  var linkInfo = getLink(runner.discoveredToBePrerenderedPageKey, runner.discoveredToBePrerenderedAtUrl, window.location, fullHref);
  if (linkInfo.type !== 'external' && node.href) {
    var pageKey = linkInfo.pageKey;
    if(runner.pageState[pageKey]) {
      var hash = linkInfo.hashContents;
      var queryParams = linkInfo.queryParams;
      var queryParamsString = queryParams ? dictToSearchParams(queryParams) : "";
      var slugAndQueryParams = hash + queryParamsString;
      var fullyResolvedNodeHrefBefore = node.href;
      // TODO: Don't think I need to escape this if setting the attribute
      // and not injecting into html (double escaped).
      node.href = runner.constructEscapedBaseUrlFromRoot(pageKey, slugAndQueryParams);
      // if(fullyResolvedNodeHrefBefore !== node.href) {
        // console.log("FIXED UP ANCHOR:", fullyResolvedNodeHrefBefore, node.href);
      // }
    }
  }
};

var substituteSiteTemplateContentsWithHeaderPropsOnFetch = function (
  siteTemplate,
  normalizedPageKeyForBasename,
  headerProps
) {
  siteTemplate = siteTemplate.replace(
    new RegExp(
      "(" +
        escapeRegExpSearchString("<template>") +
        "|" +
        escapeRegExpSearchString("</template>") +
        "|" +
        escapeRegExpSearchString("<plaintext>") +
        ")",
      "g"
    ),
    function (_) {
      return "";
    }
  );
  siteTemplate = siteTemplate.replace(
    new RegExp("\\$\\{Bookmark\\.Header\\.([^:\\}\\|]*)(\\|[^}]*)?}", "g"),
    function (matchString, field, defaultVal) {
      if (field !== "siteTemplate" && field in headerProps) {
        return escapeHtml(headerProps[field]);
      } else if(defaultVal && defaultVal[0] === '|') {
        return escapeHtml(defaultVal.substr(1));
      }
    }
  );
  var effectiveId = headerProps.id || normalizedPageKeyForBasename;
  siteTemplate = siteTemplate.replace(
    new RegExp("\\$\\{Bookmark\\.Active\\.([^\\}]*)}", "g"),
    function (matchString, field) {
      return effectiveId.toLowerCase() === field.toLowerCase() ? "active" : "inactive";
    }
  );
  return siteTemplate;
};

/**
 * Bookmark is just a paired down version of Flatdoc with some additional
 * features, and many features removed.
 *
 * This version of flatdoc can run in three modes:
 *
 * Main entrypoint script include (when included from an index.html or
 * foo.html).
 *
 *     <script start src="pathto/Paradoc.js"></script>
 *
 * Included in a name.md.html markdown document or name.styl.html Stylus
 * document at the start of file
 *
 *     <script src="pathto/Paradoc.js"></script>
 *     # Rest of markdown here
 *     - regular markdown
 *     - regular markdown
 *
 * or:
 *
 *     <script src="pathto/Paradoc.js"></script>
 *     Rest of .styl document here:
 *
 * As a node script which will bundle your page into a single file assuming you've run npm install.
 */

/**
 * Since we use one simple script for everything, we need to detect how it's
 * being used. If not a node script, it could be included form the main html
 * page, or from a docs/stylus page. The main script tag in the main page will
 * be run at a point where there's no body in the document. For doc pages
 * (markdown/stylus) it will have a script tag at the top which implicitly
 * defines a body.
 */
function isRunningScriptFromExecutedSiteTemplate() {
  return document.currentScript.hasAttribute("fromSiteTemplate");
}

/**
 * Assuming you are a doc or a style file (in an html extension), is this
 * trying to be loaded as an async doc/style content fetch from another HTML
 * page, or is this file attempting to be loaded as the main entrypoint (wihout
 * going through an index.html or something?) All requests for doc content go
 * through the Bookmark loader, and will ensure there is a query param
 * indicating this.
 */
function detectMode() {
  if (typeof process !== "undefined") {
    return "bookmarkNodeMode";
  }
  if (isRunningScriptFromExecutedSiteTemplate()) {
    return "runningScriptFromExecutedSiteTemplate";
  } else {
    var isHostPageQueryingContent = queryParam("bookmarkContentQuery");
    if (isHostPageQueryingContent) {
      // Querying the content from some other page (including executed site template)
      return "bookmarkContentQuery";
    } else {
      // The user double clicked on an .html markdown file that has the
      // Paradoc.js bootstrap <script> tag in it.
      return "bookmarkEntrypoint";
    }
  }
}

var MODE = detectMode();

/**
 * Here's the order of events that occur when using the local file system at least:
 * 1. body DOMContentLoaded
 * 2. body onload event
 * 3. settimeout 0 handler.
 */
if (MODE === "bookmarkNodeMode") {
  if (process.argv && process.argv.length > 2) {
    var relFilePath = process.argv[2];
    var path = require("path");
    var absFilePath = path.resolve(process.cwd(), relFilePath);
    var absDirPath = path.dirname(absFilePath);
    var fs = require("fs");
    var path = require("path");
    var Inliner = require("inliner");
    if(!fs.existsSync(absFilePath)) {
      console.error('File ' + absFilePath + ' does not exist');
      process.exit(1);
    }

    var siteDir = __dirname;

    var pathToChrome =
      process.platform === "win32"
        ? path.join(
            require("process").env["LOCALAPPDATA"],
            "Google",
            "Chrome",
            "Application",
            "chrome.exe"
          )
        : "/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome";

    var crashDumps =
      process.platform !== "win32" ?
      "--crash-dumps-dir=/tmp" : "";
    var cmd =
      pathToChrome +
      " --headless --window-size=1920,1080 " + crashDumps + " --no-sandbox --disable-gpu --dump-dom --virtual-time-budget=1900 " +
      absFilePath;
    console.log('Running command ', cmd);
    var rendered = require("child_process").execSync(cmd).toString();

    var renderedHtmlPath = absFilePath.replace('.html', '') + ".paradoc-rendered.html";
    var indexHtmlPath = absFilePath.replace('.html', '') + ".paradoc-inlined.html";
    fs.writeFileSync(renderedHtmlPath, rendered);

    console.log("INLINING PAGE: ", indexHtmlPath);

    /* Make sure you have images set to true to avoid flickering jumps */
    /* collapseWhitespace fals otherwise messes up hljs */
    var options = {
      images: true,
      compressCSS: false,
      compressJS: false,
      collapseWhitespace: false,
      nosvg: true,
      skipAbsoluteUrls: false,
      preserveComments: true,
      iesafe: false,
    };

    new Inliner(renderedHtmlPath, options, function (error, html) {
      if (error) {
        console.error(e);
        process.exit(1);
      }
      fs.writeFileSync(indexHtmlPath, html);
      process.exit(0);
    });


    // One liner:
    // Run this command on the result of a Chrome Save As.
    // npx inliner \
    //   --skip-absolute-urls \
    //  --nocompress \
    //  --preserve-comments \
    //  --videos \
    //  ~/Desktop/input.html > ~/Desktop/output.html
  } else {
    console.error('Make sure you supply a path to the file you want to use as the entrypoint of the bundle. You have omitted it.');
  }
} else if (MODE === "bookmarkContentQuery") {

/**
 * How Github decides to render previews as markdown:
 * https://github.com/github/markup/issues/1069#issuecomment-306084234
 *
 * This comment says you can force your file to be rendered as markdown in
 * github a couple of ways: A vim region, or a gitattributes but gitattributes
 * isn't working.
 * Abusing the Vim mode:
 * <!-- vim: syntax=Markdown -->
 * Abusing the Emacs mode:
 * <!--*- mode: markdown -*-->
 * (note that emacs mode is determined by the `-*- mode: markdown; -*-` It just fits nicely
 * with the html comment *)
 * https://github.com/github/markup/issues/1069#issuecomment-460056003
 *
 * Supposedly, Chrome is supposed to detect html files on local disk by
 * sniffing some tags.  but it doesn't appear to work.
 * Chromium mime type sniffing:
 *  https://source.chromium.org/chromium/chromium/src/+/master:net/base/mime_sniffer.cc;drc=faa13f8c8516dd027f5fc5a6ba984099ff330d05;l=781?originalUrl=https:%2F%2Fcs.chromium.org%2Fchromium%2Fsrc%2Fnet%2Fbase%2Fmime_sniffer.cc
 *  https://source.chromium.org/chromium/chromium/src/+/master:net/base/mime_sniffer.cc;l=795?originalUrl=https:%2F%2Fcs.chromium.org%2Fchromium%2Fsrc%2Fnet%2Fbase%2Fmime_sniffer.cc
 *  SniffForHTML:
 *  https://source.chromium.org/chromium/chromium/src/+/master:net/base/mime_sniffer.cc;drc=c12f7a008d7096c48d0c4db36c6d6edbc71700fb;l=381?originalUrl=https:%2F%2Fcs.chromium.org%2Fchromium%2Fsrc%2Fnet%2Fbase%2Fmime_sniffer.cc
 *
 * A trick to create markdown comments that don't render in Github:
 * http://alvinalexander.com/technology/markdown-comments-syntax-not-in-generated-output/
 * (Suggestion, use the # form and make sure there's a line before it)
 * https://stackoverflow.com/questions/4823468/comments-in-markdown
 *
 * <!--*- mode: markdown -*-->
 * This doesn't work because it needs spaces around the [] for ft detection to
 * kick in on Github:
 * [vim: syntax=Markdown]: # (<script src="./flatdoc.js"></script>)
 * This works!
 * [ vim:syntax=Markdown ]: # (<script src="./flatdoc.js"></script>)
 * But this does!
 * [-*-mode:markdown-*-]: # (<script src="./flatdoc.js"> </script>)
 *
 * Another supposed way to write comments in markdown is:
 * [this is a comment]::
 * So this works for injecting the script tag and getting Github to render it
 * as a markdown file:
 * [<script src="./flatdoc.js"> </script>]:-*-mode:markdown-*-:
 * However with that last approach, there's much more to clean up in the output
 * on Flatdoc's side.
 *
 * This approach is the cleanest and only has us searching for / cleaning up a
 * single `)` closing paren before Flatdoc renders the markup.
 *
 *     [ vim:syntax=Markdown ]: # (<script src="flatdoc.js"></script>)
 *
 */
  // We are being asked about the document content from some host page (like an index.html that
  // manually calls out to docs).
  document.write('<plaintext style="display:none">');
  document.addEventListener("DOMContentLoaded", function () {
    var plaintexts = document.querySelectorAll("plaintext");
    if (plaintexts.length === 1) {
      window.parent.postMessage(
        {
          messageType: "docPageContent",
          iframeName: window.name,
          // innerHtml escapes markup in plaintext in Safari, but not Chrome.
          // innerText behaves correctly for both.
          // TODO: investigate using textContent (which is usually more
          // performant) but in cases where perf doesn't matter like this it
          // may allow returning more interesting data from documents.
          content: plaintexts[0].innerText,
        },
        "*"
      );
    } else {
      window.parent.postMessage(
        {
          messageType: "docPageError",
          iframeName: window.name,
          error:
            "There isn't exactly one plaintext tag inside of " +
            window.name +
            ". Something went wrong and we didn't inject the plaintext tag.",
        },
        "*"
      );
    }
  });
} else if (MODE === "bookmarkEntrypoint") {
  // This is the a workflow where the md html page itself wants to be loadable without
  // needing to be included via some index.html. In this mode it can specify a page template
  // in its markdown header.

  // Remove the typical leading content before the script: This just helps
  // minimize the flash of that text. To completely eliminate it during
  // development mode, you can put this at the top of your md.
  // [ vim: set filetype=Markdown: ]: # (<style type="text/css">body {display: none} </style>)
  // while(document.body.hasChildNodes) {
  while (document.body && document.body.childNodes[0].nodeType === document.TEXT_NODE) {
    document.body.removeChild(document.body.childNodes[0]);
  }
  // Try to hide the plain text that comes before the important script include.
  // Minimize flash.
  document.write('<plaintext style="display:none">');
  // I find page reloads much less reliable if you document.close()
  // document.close();
  // However, I think this caused html contents inside of the markdown to be executed as html?
  window.onbeforeunload = function () {};
  document.addEventListener("DOMContentLoaded", function () {
    var plaintexts = document.querySelectorAll("plaintext");
    if (plaintexts.length === 1) {
      // innerHtml escapes markup in plaintext in Safari, but not Chrome.
      // innerText behaves correctly for both.
      // Parse out the yaml header just so we can get the siteTemplate, then
      // forward along the original markdown. Might as well leave the yaml
      // lines normalized.
      var markdown = normalizeMarkdownResponse(plaintexts[0].innerText);
      var markdownNormalizedYaml = normalizeYamlMarkdownComments(markdown);
      // In this entrypoint mode, we still parse the header even though it will be done again.
      // The reason is so that we can extract out the site template.
      var markdownAndHeader = parseYamlHeader(markdownNormalizedYaml, window.location.pathname);
      if (typeof window.BookmarkTemplate === "undefined") {
        window.BookmarkTemplate = {};
      }

      window.BookmarkTemplate.prefetchedCurrentPageBasename = urlBasename(window.location.href);

      var normalizedPageKeyForBasename = urlExtensionlessBasename(
        BookmarkTemplate.prefetchedCurrentPageBasename
      ).toLowerCase();
      window.BookmarkTemplate.prefetchedCurrentPageMarkdownAndHeader = markdownNormalizedYaml;
      // Set the variables for templates to read from.
      // https://www.geeksforgeeks.org/how-to-replace-the-entire-html-node-using-javascript/
      var siteTemplate = markdownAndHeader.headerProps.siteTemplate || 'siteTemplate.html';
      console.log("Using default site template - assumed to be at ./siteTemplate.html. You can customize this in your markdown 'yaml' header siteTemplate: field.");
      var templateFetchStart = Date.now();
      /**
       * The iframe's onDone will fire before the document's readystatechange 'complete' event.
       */
      var onDone = function (siteTemplate) {
        var templateFetchEnd = Date.now();
        console.log("fetching SITE TEMPLATE took", templateFetchEnd - templateFetchStart);
        var yetAnotherHtml = document.open("text/html", "replace");
        // If you want to listen for another readystatechange 'complete'
        // after images have loaded you have to create yetAnotherHtml This
        // isn't really needed since we don't listen to this.  Make sure to
        // hide the content while it is loading, since .write replaces.
        // `handleReady` will reveal it after images load.
        siteTemplate = substituteSiteTemplateContentsWithHeaderPropsOnFetch(
          siteTemplate,
          normalizedPageKeyForBasename,
          markdownAndHeader.headerProps
        );
        // The site template should also have
        //  <script>document.body.style="display:none" </script>
        //  So that when pre-rendered it is also correctly hidden
        yetAnotherHtml.write(siteTemplate);
        yetAnotherHtml.close();
      };
      var onDoneCell = { contents: onDone };
      var onFailCell = {
        contents: (err) => {
          console.error(err);
        },
      };
      queryContentsViaIframe(siteTemplate, onDoneCell, onFailCell);
    } else {
      console.error(
        "There isn't exactly one plaintext tag inside of " +
          window.name +
          ". Something went wrong and we didn't inject the plaintext tag."
      );
    }
  });
} else {
  // Must be 'runningScriptFromExecutedSiteTemplate' mode. At least populate this empty
  // dictionary so that when
  // BookmarkTemplate.prefetchedCurrentPageMarkdownAndHeader is accessed in
  // the rehydration workflow it doesn't fail (it will bail out) when it
  // realizes the page is already rendered though.
  if (typeof window.BookmarkTemplate === "undefined") {
    window.BookmarkTemplate = {};
  }

  (function () {
    var exports = this;

    var marked;

    /**
     * Basic Flatdoc module.
     *
     * The main entry point is Flatdoc.run(), which invokes the [Runner].
     *
     *     Flatdoc.run({
     *       fetcher: Flatdoc.github('rstacruz/backbone-patterns');
     *     });
     *
     * These fetcher functions are available:
     *
     *     Flatdoc.github('owner/repo')
     *     Flatdoc.github('owner/repo', 'API.md')
     *     Flatdoc.github('owner/repo', 'API.md', 'branch')
     *     Flatdoc.bitbucket('owner/repo')
     *     Flatdoc.bitbucket('owner/repo', 'API.md')
     *     Flatdoc.bitbucket('owner/repo', 'API.md', 'branch')
     *     Flatdoc.file('http://path/to/url')
     *     Flatdoc.file([ 'http://path/to/url', ... ])
     */

    var Flatdoc = (exports.Flatdoc = {});
    exports.Bookmark = exports.Flatdoc;

    /**
     * Creates a runner.
     * See [Flatdoc].
     */
    Flatdoc.run = function (options) {
      var runner = new Flatdoc.runner(options);
      runner.run();
      return runner;
    };

    /**
     * Explicit page config will override the same config from header props.
     */
    Flatdoc.getPageConfig = function(configKey, pageData, defaultVal) {
      if(pageData.explicitlySpecifiedPageConfig && pageData.explicitlySpecifiedPageConfig[configKey]) {
        return pageData.explicitlySpecifiedPageConfig[configKey];
      } else if(pageData.markdownAndHeader.headerProps && pageData.markdownAndHeader.headerProps[configKey]) {
        return pageData.markdownAndHeader.headerProps[configKey];
      } else {
        return defaultVal;
      }
    };
    /**
     * Gets the page config from explicit config or header prop, and normalizes
     * it to a boolean value or throws.
     */
    Flatdoc.getPageConfigBool = function(configKey, pageData, defaultVal) {
      if(pageData.explicitlySpecifiedPageConfig && pageData.explicitlySpecifiedPageConfig[configKey]) {
        return !!pageData.explicitlySpecifiedPageConfig[configKey];
      } else if(pageData.markdownAndHeader.headerProps && pageData.markdownAndHeader.headerProps[configKey]) {
        var val = pageData.markdownAndHeader.headerProps[configKey];
        if(val.toLowerCase() === 'true') {
          return true;
        } else if(val.toLowerCase() === 'false') {
          return false;
        } else {
          console.warn(
            'Header for page ' + pageData.___pageKeyForErrorMessages +
            ' has invalid value for property ' + configKey +
            '. It should be either true or false, but was specified as ' + val
          );
        }
      } else {
        return defaultVal;
      }
    };

    Flatdoc.emptyPageData = function(pageKey) {
      return {
        ___pageKeyForErrorMessages: pageKey,
        explicitlySpecifiedPageConfig: null,
        fetcher: null,
        markdownAndHeader: null,
        contentContainerNode: null,
        menuContainerNode: null,
        hierarchicalDoc: null,
        hierarchicalIndex: null
      }
    }

    /**
     * Simplified easy to use API that calls the underlying API.
     */
    Flatdoc.go = function (options) {
      var pageState = {};
      var actualOptions = {
        pageState: pageState,
        // Could flip to true
        discoveredToBePrerenderedAtUrl: null,
        discoveredToBePrerenderedPageKey: null,
        pageTemplateOptions: {
          runPrerenderedInSingleDocsMode: options.runPrerenderedInSingleDocsMode,
          runDevelopmentInSingleDocsMode: options.runDevelopmentInSingleDocsMode,
          sidenavify: options.sidenavify || defaultSidenavifyConfig,
          slugContributions: options.slugContributions || defaultSlugContributions,
          searchFormId: options.searchFormId,
          searchHitsId: options.searchHitsId,
          versionButtonId: options.versionButtonId,
          versionPageIs: options.versionPageIs ? options.versionPageIs.toLowerCase() : null,
        }
      };
      if (options.stylus) {
        actualOptions.stylusFetcher = Flatdoc.docPage(options.stylus);
      }
      var pages = options.pages || {};
      for (var pageKey in pages) {
        var pageKeyLowerCase = pageKey.toLowerCase();
        var page = pages[pageKey];
        pageState[pageKeyLowerCase] = {
          ...Flatdoc.emptyPageData(pageKeyLowerCase),
          explicitlySpecifiedPageConfig: pages[pageKey]
        };
        Flatdoc.setFetcher(pageKeyLowerCase, pageState[pageKeyLowerCase]);
      }
      if (options.highlight) {
        actualOptions.highlight = options.highlight;
      }
      var runner = Flatdoc.run(actualOptions);
    };

    /**
     * File fetcher function.
     *
     * Fetches a given url via AJAX.
     * See [Runner#run()] for a description of fetcher functions.
     */

    Flatdoc.file = function (url) {
      function loadData(locations, response, callback) {
        if (locations.length === 0) callback(null, response);
        else
          $.get(locations.shift())
            .fail(function (e) {
              callback(e, null);
            })
            .done(function (data) {
              if (response.length > 0) response += "\n\n";
              response += data;
              loadData(locations, response, callback);
            });
      }

      return function (callback) {
        loadData(url instanceof Array ? url : [url], "", callback);
      };
    };

    Flatdoc.setFetcher = function (keyLowerCase, obj) {
      if (
        BookmarkTemplate.prefetchedCurrentPageBasename &&
        urlExtensionlessBasename(BookmarkTemplate.prefetchedCurrentPageBasename).toLowerCase() ===
          keyLowerCase
      ) {
        obj.fetcher = Flatdoc.prefetchedDocPageContent(
          keyLowerCase,
          BookmarkTemplate.prefetchedCurrentPageMarkdownAndHeader
        );
      } else {
        obj.fetcher = Flatdoc.docPage(keyLowerCase + ".html");
      }
    };

    /**
     * Runs with the already loaded string contents representing a doc.
     * This is used for "entrypoint mode".
     * TODO: Instead just maintain a cache, warm it up and use the regular
     * fetcher. This also allows reuse as a "style pre-fetch" property in the
     * yaml header.
     */
    Flatdoc.prefetchedDocPageContent = function (pageKey, url) {
      if (!Flatdoc.errorHandler) {
        var listenerID = window.addEventListener("message", function (e) {
          if (e.data.messageType === "docPageError") {
            console.error(e.data.error);
          }
        });
        Flatdoc.docPageErrorHandler = listenerID;
      }
      var fetchdocPage = function (content) {
        var onDone = null;
        var onFail = null;
        var returns = {
          fail: function (cb) {onFail = cb; return returns;},
          done: function (cb) {
            onDone = cb;
            onDone(content);
            return returns;
          },
        };
        return returns;
      };
      function loadData(locations, response, callback) {
        if (locations.length === 0) {
          callback(null, response);
        } else {
          fetchdocPage(locations.shift())
            .fail(function (e) {callback(e, null); })
            .done(function (data) {
              if (response.length > 0) response += "\n\n";
              response += data;
              loadData(locations, response, callback);
            });
        }
      }

      var url = url instanceof Array ? url : [url];
      var ret = function (callback) {
        loadData(url, "", callback);
      };
      // Tag the fetcher with the url in case you want it.
      ret.url = url;
      return ret;
    };

    /**
     * Local docPage doc fetcher function.
     *
     * Fetches a given url via iframe inclusion, expecting the file to be of
     * the "docPage" form of markdown which can be loaded offline.
     * See [Runner#run()] for a description of fetcher functions.
     *
     * Tags the url argument on the fetcher itself so it can be used for other
     * debugging/relativization.
     */

    Flatdoc.docPageErrorHandler = null;

    Flatdoc.docPage = function (url) {
      if (!Flatdoc.errorHandler) {
        var listenerID = window.addEventListener("message", function (e) {
          if (e.data.messageType === "docPageError") {
            console.error(e.data.error);
          }
        });
        Flatdoc.docPageErrorHandler = listenerID;
      }
      var fetchdocPage = function (url) {
        var onDoneCell = { contents: null };
        var onFailCell = { contents: null };
        var returns = {
          fail: function (cb) {
            onFailCell.contents = cb;
            return returns;
          },
          done: function (cb) {
            onDoneCell.contents = cb;
            return returns;
          },
        };
        queryContentsViaIframe(url, onDoneCell, onFailCell);
        // Even if using the local file system, this will immediately resume
        // after appending without waiting or blocking.  There is no way to tell
        // that an iframe has loaded successfully without some kind of a timeout.
        // Even bad src locations will fire the onload event. An onerror event is
        // a solid signal that the page failed, but abscense of an onerror on the
        // iframe is not a confirmation of success or that it hasn't failed.
        return returns;
      };
      function loadData(locations, response, callback) {
        if (locations.length === 0) callback(null, response);
        else
          fetchdocPage(locations.shift())
            .fail(function (e) {
              callback(e, null);
            })
            .done(function (data) {
              if (response.length > 0) response += "\n\n";
              response += data;
              loadData(locations, response, callback);
            });
      }

      var url = url instanceof Array ? url : [url];
      var ret = function (callback) {
        loadData(url, "", callback);
      };
      // Tag the fetcher with the url in case you want it.
      ret.url = url;
      return ret;
    };

    /**
     * Github fetcher.
     * Fetches from repo repo (in format 'user/repo').
     *
     * If the parameter filepath` is supplied, it fetches the contents of that
     * given file in the repo's default branch. To fetch the contents of
     * `filepath` from a different branch, the parameter `ref` should be
     * supplied with the target branch name.
     *
     * See [Runner#run()] for a description of fetcher functions.
     *
     * See: http://developer.github.com/v3/repos/contents/
     */
    Flatdoc.github = function (opts) {
      if (typeof opts === "string") {
        opts = {
          repo: arguments[0],
          filepath: arguments[1],
        };
      }
      var url;
      if (opts.filepath) {
        url = "https://api.github.com/repos/" + opts.repo + "/contents/" + opts.filepath;
      } else {
        url = "https://api.github.com/repos/" + opts.repo + "/readme";
      }
      var data = {};
      if (opts.token) {
        data.access_token = opts.token;
      }
      if (opts.ref) {
        data.ref = opts.ref;
      }
      return function (callback) {
        $.get(url, data)
          .fail(function (e) {
            callback(e, null);
          })
          .done(function (data) {
            var markdown = exports.Base64.decode(data.content);
            callback(null, markdown);
          });
      };
    };

    /**
     * Bitbucket fetcher.
     * Fetches from repo `repo` (in format 'user/repo').
     *
     * If the parameter `filepath` is supplied, it fetches the contents of that
     * given file in the repo.
     *
     * See [Runner#run()] for a description of fetcher functions.
     *
     * See: https://confluence.atlassian.com/display/BITBUCKET/src+Resources#srcResources-GETrawcontentofanindividualfile
     * See: http://ben.onfabrik.com/posts/embed-bitbucket-source-code-on-your-website
     * Bitbucket appears to have stricter restrictions on
     * Access-Control-Allow-Origin, and so the method here is a bit
     * more complicated than for Github
     *
     * If you don't pass a branch name, then 'default' for Hg repos is assumed
     * For git, you should pass 'master'. In both cases, you should also be able
     * to pass in a revision number here -- in Mercurial, this also includes
     * things like 'tip' or the repo-local integer revision number
     * Default to Mercurial because Git users historically tend to use GitHub
     */
    Flatdoc.bitbucket = function (opts) {
      if (typeof opts === "string") {
        opts = {
          repo: arguments[0],
          filepath: arguments[1],
          branch: arguments[2],
        };
      }
      if (!opts.filepath) opts.filepath = "readme.md";
      if (!opts.branch) opts.branch = "default";

      var url =
        "https://bitbucket.org/api/1.0/repositories/" +
        opts.repo +
        "/src/" +
        opts.branch +
        "/" +
        opts.filepath;

      return function (callback) {
        $.ajax({
          url: url,
          dataType: "jsonp",
          error: function (xhr, status, error) {
            alert(error);
          },
          success: function (response) {
            var markdown = response.data;
            callback(null, markdown);
          },
        });
      };
    };

    var Parser = {};


    Parser.setMarkedOptions = function (highlight) {
      marked.setOptions({
        highlight: function (code, lang) {
          if (lang) {
            return highlight(code, lang);
          }
          return code;
        },
      });
      marked.Renderer.prototype.paragraph = (text) => {
        if (text.startsWith("<codetabscontainer")) {
          return text + "\n";
        }
        return "<p>" + text + "</p>";
      };

      /**
       * This actually doesn't work because it escapes an extra time for some reason.
       * I think the problem is in highligh.js
      marked.Renderer.prototype.codespan = (text) => {
        return marked.Renderer.prototype.code(text, 'reason', false);
        return '<bookmark-inline-codeblock>' + highlight(text, 'reason') + '</bookmark-inline-codeblock>';
        return text;
      };
      */
    };

    var Transformer = (Flatdoc.transformer = {});

    /**
     * Adds IDs to headings. What's nice about this approach is that it is
     * agnostic to how the markup is rendered.
     * TODO: These (better) links won't always work in markdown on github because
     * github doesn't encode subsections into the links. To address this, we can allow
     * Github links in the markdown and then transform them into the better ones
     * on the rendered page.  This produces more stable linked slugs.
     */
    Transformer.addIDsToHierarchicalDoc = function (runner, hierarchicalDoc, pageKey) {
      forEachHierarchy(function (treeNode, inclusiveContext) {
        if (treeNode.slug) {
          var levelContent = treeNode.levelContent;
          levelContent.id = fullyQualifiedHeaderId(treeNode.slug, pageKey);
        }
      }, hierarchicalDoc);
    };

    /**
     * Returns menu data for a given HTML.
     *
     *     menu = Flatdoc.transformer.getMenu($content);
     *     menu == {
     *       level: 0,
     *       items: [{
     *         sectionHtml: "Getting started",
     *         level: 1,
     *         items: [...]}, ...]}
     */

    Transformer.getMenu = function (runner, $content) {
      var root = { items: [], linkifiedId: "", level: 0 };
      var cache = [root];

      function mkdir_p(level) {
        cache.length = level + 1;
        var obj = cache[level];
        if (!obj) {
          var parent = level > 1 ? mkdir_p(level - 1) : root;
          obj = { items: [], level: level };
          cache = cache.concat([obj, obj]);
          parent.items.push(obj);
        }
        return obj;
      }

      var query = [];
      var sidenavify = runner.pageTemplateOptions.sidenavify;
      if (sidenavify.h0) {
        query.push("h0");
      }
      if (sidenavify.h1) {
        query.push("h1");
      }
      if (sidenavify.h2) {
        query.push("h2");
      }
      if (sidenavify.h3) {
        query.push("h3");
      }
      if (sidenavify.h4) {
        query.push("h4");
      }
      if (sidenavify.h5) {
        query.push("h5");
      }
      if (sidenavify.h6) {
        query.push("h6");
      }
      $content.find(query.join(",")).each(function () {
        var $el = $(this);
        var level = +this.nodeName.substr(1);

        var parent = mkdir_p(level - 1);
        var text = $el.text();
        var el = $el[0];
        if (
          (el.childNodes.length === 1 && el.childNodes[0].tagName === "code") ||
          el.childNodes[0].tagName === "CODE"
        ) {
          text = "<code>" + escapeHtml(text) + "</code>";
        }
        var obj = { sectionHtml: text, items: [], level: level, linkifiedId: $el.attr("id") };
        parent.items.push(obj);
        cache[level] = obj;
      });

      return root;
    };

    /**
     * Changes "button >" text to buttons.
     */

    Transformer.buttonize = function (content) {
      $(content)
        .find("a")
        .each(function () {
          var $a = $(this);

          var m = $a.text().match(/^(.*) >$/);
          if (m) $a.text(m[1]).addClass("button");
        });
    };

    /**
     * Applies smart quotes to a given element.
     * It leaves `code` and `pre` blocks alone.
     */

    Transformer.smartquotes = function (content) {
      var nodes = getTextNodesIn($(content)),
        len = nodes.length;
      for (var i = 0; i < len; i++) {
        var node = nodes[i];
        node.nodeValue = quotify(node.nodeValue);
      }
    };

    /**
     * Syntax highlighters.
     *
     * You may add or change more highlighters via the `Flatdoc.highlighters`
     * object.
     *
     *     Flatdoc.highlighters.js = function(code) {
     *     };
     *
     * Each of these functions
     */

    var Highlighters = (Flatdoc.highlighters = {});

    /**
     * JavaScript syntax highlighter.
     *
     * Thanks @visionmedia!
     */

    Highlighters.js = Highlighters.javascript = function (code) {
      return code
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/("[^\"]*?")/g, '<span class="string">$1</span>')
        .replace(/('[^\']*?')/g, '<span class="string">$1</span>')
        .replace(/\/\/(.*)/gm, '<span class="comment">//$1</span>')
        .replace(/\/\*(.*)\*\//gm, '<span class="comment">/*$1*/</span>')
        .replace(/(\d+\.\d+)/gm, '<span class="number">$1</span>')
        .replace(/(\d+)/gm, '<span class="number">$1</span>')
        .replace(/\bnew *(\w+)/gm, '<span class="keyword">new</span> <span class="init">$1</span>')
        .replace(
          /\b(function|new|throw|return|var|if|else)\b/gm,
          '<span class="keyword">$1</span>'
        );
    };

    Highlighters.html = function (code) {
      return code
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/("[^\"]*?")/g, '<span class="string">$1</span>')
        .replace(/('[^\']*?')/g, '<span class="string">$1</span>')
        .replace(/&lt;!--(.*)--&gt;/g, '<span class="comment">&lt;!--$1--&gt;</span>')
        .replace(/&lt;([^!][^\s&]*)/g, '&lt;<span class="keyword">$1</span>');
    };

    Highlighters.generic = function (code) {
      return code
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/("[^\"]*?")/g, '<span class="string">$1</span>')
        .replace(/('[^\']*?')/g, '<span class="string">$1</span>')
        .replace(/(\/\/|#)(.*)/gm, '<span class="comment">$1$2</span>')
        .replace(/(\d+\.\d+)/gm, '<span class="number">$1</span>')
        .replace(/(\d+)/gm, '<span class="number">$1</span>');
    };

    /**
     * Menu view. Renders menus
     */

    var MenuView = (Flatdoc.menuView = function (menu, pageKey) {
      var $el = $("<ul>");

      function process(node, $parent) {
        var id = node.linkifiedId || "root";
        var nodeHashToChangeTo = node.linkifiedId ? hashForFullFullyQualifiedHeaderId(id) : '';
        var $li = $("<li>")
          .attr("id", id + "-item")
          .addClass("level-" + node.level)
          .appendTo($parent);

        if (node.sectionHtml) {
          var $a = $("<a>")
            .html(node.sectionHtml)
            .attr("id", id + "-link")
            .attr("href", './' + pageKey + ".html#" + nodeHashToChangeTo)
            .addClass("level-" + node.level)
            .appendTo($li);
        }

        if (node.items.length > 0) {
          var $ul = $("<ul>")
            .addClass("level-" + (node.level + 1))
            .attr("id", id + "-list")
            .appendTo($li);

          node.items.forEach(function (item) {
            process(item, $ul);
          });
        }
      }

      process(menu, $el);
      return $el;
    });

    /**
     * A runner module that fetches via a `fetcher` function.
     *
     *     var runner = new Flatdoc.runner({
     *       fetcher: Flatdoc.url('readme.txt')
     *     });
     *     runner.run();
     *
     * The following options are available:
     *
     *  - `fetcher` - a function that takes a callback as an argument and
     *    executes that callback when data is returned.
     *
     * See: [Flatdoc.run()]
     */

    var Runner = (Flatdoc.runner = function (options) {
      this.initialize(options);
    });

    Runner.prototype.pageRootSelector = "body";

    /**
     * Really, is used to model internal *component* state based on entered
     * control value.  Like if a text input is empty, the text input component
     * sets the search component to QueryStates.NONE_AND_HIDE.
     * If the user hits enter on a dropdown selector, it toggles it between NONE
     * and ALL.
     *
     * There's three bits of information per control that determine visibility:
     *
     * 1. Which component is "active" (like focused). This is currently modeled
     * by activeSearchComponent (but that is almost redundant with document focus). It's not
     * exactly the same as focused DOM element because we also want a component
     * to be able to keep the popup open even if the user tabs to other parts of
     * the document. That doesn't always make sense for every kind of component,
     * but it's a feature. So activeSearchComponent recreates _another_ notion of active
     * element apart from the document's.
     * 2. Whether or not the internal state of the component warrants showing any
     * popup. (QueryStates). Like a search input could have empty text which
     * warrants showing no results. Or a dropdown component (which always has
     * "empty text"), could be focused but it's not supposed to show any results
     * until you click or press enter/space. That internal component state helps
     * determine whether or not a popup should be shown. In the case of text
     * input this is redundant or derivable from its input text (but not the case
     * for other component types).
     * 3. Whether or not the user requested that a popup for the currently active
     * component be supressed. Even if 1 and 2 would otherwise result in showing
     * a popup, the user could press escape.
     * An autocomplete text input with non-empty input, that is currently focused
     * (or "active") could press ctrl-c closing the popup window.
     * A dropdown component could be "active", could have been clicked on, but
     * the user could click a second time closing it (or pressing escape).
     */
    var QueryStates = {
      NONE: "NONE",
      ALL: "ALL",
      FILTER: "FILTER",
    };

    function SearchComponentBase(root) {
      this.root = root;
      this.queryState = QueryStates.ALL;
      this.resultsByPageKey = {};
      /**
       *
       * The user's intent. Could be:
       * No intent: null
       * Intent to not highlight anything: -1
       * Intent to highlight specific row and page key: {pageCursor,  pageCursorIndex}
       *
       * This state is managed both internally and externally. Internally,
       * components know when they need to reset the user requested cursor. But
       * externally search lists know when to reach out and mutate this.
       * The "page cursor" is the page key that the current item is under.
       * The "page cursor index" is the index number inside of that page cursor.
       */
      this.userRequested = null;
    }

    function TextDocSearch(props) {
      SearchComponentBase.call(this, props.root);
      this.queryState = QueryStates.FILTER;
      var placeholder = this.getPlaceholder();
      if (this.root.tagName.toUpperCase() !== "FORM") {
        console.error("You provided a searchFormId that does not exist");
        return;
      }
      var theSearchInput;
      var theSearchClear;
      if (this.root.className.indexOf("bookmark-search-form-already-setup") !== -1) {
        theSearchInput = this.root.childNodes[0];
        theSearchClear = this.root.childNodes[1];
      } else {
        this.root.className += " bookmark-search-form  bookmark-search-form-already-setup";
        this.root.onsubmit = "";
        var theSearchInputContainer = document.createElement("div");
        theSearchInputContainer.innerHTML = "<input style='border: 1px solid transparent' />";
        var theSearchInput = theSearchInputContainer.childNodes[0];
        theSearchInput.autocomplete = "off";
        theSearchInput.name = "focus";
        theSearchInput.className = "bookmark-search-input";
        theSearchInput.placeholder = placeholder;
        theSearchInput.required = true;
        theSearchClear = document.createElement("button");
        theSearchClear.tabindex = 1;
        theSearchClear.className = "bookmark-search-input-right-reset-icon";
        theSearchClear.type = "reset";
        theSearchClear.tabIndex = -1;
        this.root.prepend(theSearchClear);
        this.root.prepend(theSearchInput);
      }
      this.theSearchInput = theSearchInput;
      theSearchInput.addEventListener(
        "focus",
        function (e) {
          var focusedPlaceholder = this.getFocusedPlaceholder(this.root);
          theSearchInput.placeholder = focusedPlaceholder;
          if (this.userRequested === -1) {
            this.userRequested = null;
          }
          if (this.valueWarrantsHiding()) {
            props.onDoesntWantActiveStatus && props.onDoesntWantActiveStatus(this);
          } else {
            props.onWantsToHaveActiveStatus && props.onWantsToHaveActiveStatus(this);
          }
          props.onFocus && props.onFocus(e);
        }.bind(this)
      );
      theSearchInput.addEventListener(
        "keydown",
        function (e) {
          props.onKeydown && props.onKeydown(e);
        }.bind(this)
      );
      theSearchInput.addEventListener(
        "input",
        function (e) {
          this.userRequested = null;
          if (this.valueWarrantsHiding()) {
            props.onDoesntWantActiveStatus && props.onDoesntWantActiveStatus(this);
          } else {
            props.onWantsToHaveActiveStatus && props.onWantsToHaveActiveStatus(this);
          }
          props.onInput && props.onInput(e);
        }.bind(this)
      );
      theSearchInput.addEventListener(
        "blur",
        function (e) {
          var focusedPlaceholder = this.getPlaceholder();
          theSearchInput.placeholder = focusedPlaceholder;
          props.onBlur && props.onBlur(e);
        }.bind(this)
      );

      // This one goes on the form itself
      this.root.addEventListener(
        "reset",
        function () {
          this.setValue("");
          this.focus();
          this.userRequested = null;
          props.onDoesntWantActiveStatus && props.onDoesntWantActiveStatus(this);
          if (props.onReset) {
            props.onReset;
          }
        }.bind(this)
      );
      this.root.addEventListener(
        "submit",
        function (e) {
          e.preventDefault();
        }.bind(this)
      );
    }
    TextDocSearch.prototype.getQuery = function () {
      return this.getValue().trim();
    };
    TextDocSearch.prototype.valueWarrantsHiding = function () {
      return this.getValue().trim() === "";
    };

    TextDocSearch.prototype.getFocusedPlaceholder = function () {
      var defaultTxt = "Search (Esc close)";
      return this.root ? this.root.dataset.focusedPlaceholder || defaultTxt : defaultTxt;
    };
    TextDocSearch.prototype.getPlaceholder = function (root) {
      var defaultTxt = "Press '/' to focus";
      return this.root ? this.root.dataset.placeholder || defaultTxt : defaultTxt;
    };
    TextDocSearch.prototype.focus = function () {
      return this.theSearchInput.focus();
    };
    TextDocSearch.prototype.selectAll = function () {
      return this.theSearchInput.select();
    };
    TextDocSearch.prototype.isFocused = function () {
      return document.activeElement === this.theSearchInput;
    };
    TextDocSearch.prototype.blur = function () {
      return this.theSearchInput.blur();
    };
    TextDocSearch.prototype.getValue = function () {
      return this.theSearchInput.value;
    };
    TextDocSearch.prototype.setValue = function (v) {
      this.theSearchInput.value = v;
    };
    TextDocSearch.prototype.setPlaceholder = function (ph) {
      this.theSearchInput.placeholder = ph;
    };
    TextDocSearch.prototype.onLostActiveSearchComponent = function () {
      // this.queryState = QueryStates.NONE_AND_HIDE;
    };
    TextDocSearch.prototype.onGainedActiveSearchComponent = function () {
      // this.queryState = QueryStates.ALL;
    };

    /**
     * An "input selector" style component that uses the navigation autocomplete window.
     */
    function TextDocSelector(props) {
      SearchComponentBase.call(this, props.root);
      this.queryState = QueryStates.ALL;

      this.root.addEventListener("focus", function (e) {
        if (this.userRequested !== null && this.userRequested === -1) {
          this.userRequested = null;
        }
        props.onFocus && props.onFocus(e);
      });
      this.root.addEventListener("keydown", function (e) {
        props.onKeydown && props.onKeydown(e);
      });
      this.root.addEventListener(
        "click",
        function (e) {
          if (props.isActiveComponent()) {
            props.onDoesntWantActiveStatus && props.onDoesntWantActiveStatus(this);
          } else {
            props.onWantsToHaveActiveStatus && props.onWantsToHaveActiveStatus(this);
          }
        }.bind(this)
      );
      this.root.addEventListener(
        "blur",
        function () {
          props.onDoesntWantActiveStatus && props.onDoesntWantActiveStatus(this);
        }.bind(this)
      );
    }
    TextDocSelector.prototype.getQuery = function () {
      return "";
    };
    TextDocSelector.prototype.onLostActiveSearchComponent = function () {};
    TextDocSelector.prototype.onGainedActiveSearchComponent = function () {};

    /**
     * Custom methods (extends base API for search components).
     */

    Runner.prototype.initialize = function (options) {
      this.pageState = {};
      this.searchState = {
        /**
         * "global" state - across all searches.
         */
        activeSearchComponent: null,
        /**
         * Typically until the next event that switches the active component.
         */
        userRequestedCloseEvenIfActive: true,
        VERSIONS: null,
        CONTENT: null,
      };
      this.nodes = {
        theSearchHits: null,
        theHitsScrollContainer: null,
        versionMenuButton: null,
        versionsContainer: null,
      };
      for (var k in options) {
        this[k] = options[k];
      }
    };

    /**
     * Syntax highlighting.
     *
     * You may define a custom highlight function such as `highlight` from
     * the highlight.js library.
     *
     *     Flatdoc.run({
     *       highlight: function (code, value) {
     *         return hljs.highlight(lang, code).value;
     *       },
     *       ...
     *     });
     *
     */

    /**
     * There is only one active search component. It is the one that will be
     * responsible for providing search results. The moment a different component
     * becomes the new active component, the new active component determines
     * which results will be shown, and helps decide whether or not to show the
     * popup menu at all.
     */
    Runner.prototype.setActiveSearchComponent = function (newComp) {
      if (newComp !== this.searchState.activeSearchComponent) {
        if (this.searchState.activeSearchComponent) {
          this.searchState.activeSearchComponent.onLostActiveSearchComponent();
        }
        this.searchState.activeSearchComponent = newComp;
        this.searchState.userRequestedCloseEvenIfActive = false;
        if (this.searchState.activeSearchComponent) {
          this.searchState.activeSearchComponent.onGainedActiveSearchComponent();
        }
      }
    };
    Runner.prototype.highlight = function (code, lang) {
      var fn = Flatdoc.highlighters[lang] || Flatdoc.highlighters.generic;
      return fn(code);
    };

    Runner.prototype.noResultsNode = function (query) {
      var d = document.createElement("div");
      d.className = "bookmark-hits-noresults-list";
      d.innerText = 'No results for "' + query + '"';
      return d;
    };
    Runner.prototype.getHitsScrollContainer = function () {
      return this.nodes.theHitsScrollContainer;
    };
    /**
     * Operates on an effective cursor (which is never null, but could be -1).
     * Stops at the last cursor - doesn't "roll over" into -1.
     * TODO: But what if there are no entries? It should probably be allowd to
     * return null in that case.
     */
    Runner.prototype.nextCursorFromEffectiveCursor = function (cursor, resultsByPageKey) {
      if (cursor === -1) {
        var nextPageKey = nextKeyWithNonEmptyArrayOrNullIfNone(null, resultsByPageKey);
        return nextPageKey === null ? -1 : { pageCursor: nextPageKey, pageCursorIndex: 0 };
      } else {
        var resultsForPageKey = resultsByPageKey[cursor.pageCursor];
        var maxIndex = resultsForPageKey.length - 1;
        if (maxIndex >= cursor.pageCursorIndex + 1) {
          return { pageCursor: cursor.pageCursor, pageCursorIndex: cursor.pageCursorIndex + 1 };
        } else {
          var nextPageKey = nextKeyWithNonEmptyArrayOrNullIfNone(
            cursor.pageCursor,
            resultsByPageKey
          );
          // Stays at the last cursor if there's no more.
          if (nextPageKey === null) {
            return cursor;
          } else {
            return { pageCursor: nextPageKey, pageCursorIndex: 0 };
          }
        }
      }
    };
    /**
     * Operates on an effective cursor (which is never null, but could be -1).
     * "rolls under" to -1 if going to the previous cursor before the first page
     * key and page index. Stays at -1 if already at -1.
     */
    Runner.prototype.prevCursorFromEffectiveCursor = function (cursor, resultsByPageKey) {
      if (cursor === -1) {
        return -1;
      } else {
        if (cursor.pageCursorIndex > 0) {
          return { pageCursor: cursor.pageCursor, pageCursorIndex: cursor.pageCursorIndex - 1 };
        } else {
          var prevPageKey = prevKeyWithNonEmptyArrayOrNullIfNone(cursor.pageCursor);
          // Stays at the last cursor if there's no more.
          if (prevPageKey === null) {
            return -1;
          } else {
            return {
              pageCursor: prevPageKey,
              pageCursorIndex: resultsByPageKey[prevPageKey].length - 1,
            };
          }
        }
      }
    };
    /**
     * Returns a normalied user requested cursor - which will never be null.
     * Might be negative one though.
     */
    Runner.prototype.effectiveCursor = function (searchComponent, resultsByPageKey) {
      return searchComponent.userRequested !== null
        ? searchComponent.userRequested
        : this.nextCursorFromEffectiveCursor(-1, resultsByPageKey);
    };
    Runner.prototype.updateSearchResultsList = function (
      searchComponent,
      query,
      prevResultsByPageKey,
      resultsByPageKey,
      clickHandler
    ) {
      var runner = this;
      var emptyFunction = function() {};
      var hitsScrollContainer = this.getHitsScrollContainer();
      var firstItem = null;
      var lastItem = null;
      var effectiveCursor = runner.effectiveCursor(searchComponent, resultsByPageKey);
      var moreThanJustCursorUpdate = prevResultsByPageKey !== resultsByPageKey;

      window.prevResultsByPageKey = prevResultsByPageKey;
      window.resultsByPageKey = resultsByPageKey;
      if (!resultsByPageKeyLen(resultsByPageKey)) {
        var len = hitsScrollContainer.childNodes.length;
        for (var i = 0; i < len; i++) {
          hitsScrollContainer.removeChild(hitsScrollContainer.childNodes[i]);
        }
        hitsScrollContainer.appendChild(this.noResultsNode(query));
      } else {
        var existingHitsList;
        var hitsList;
        if (
          moreThanJustCursorUpdate &&
          hitsScrollContainer.childNodes[0] &&
          hitsScrollContainer.childNodes[0].className === "bookmark-hits-noresults-list"
        ) {
          existingHitsList = null;
          hitsScrollContainer.removeChild(hitsScrollContainer.childNodes[0]);
        } else {
          existingHitsList = hitsScrollContainer.childNodes[0];
        }
        if (!existingHitsList) {
          hitsList = document.createElement("div");
          hitsList.className = "bookmark-hits-list";
          hitsScrollContainer.appendChild(hitsList);
        } else {
          hitsList = existingHitsList;
        }
        var numExistingHitsListPageGroups = hitsList ? hitsList.childNodes.length : 0;
        var numNonEmptyPageGroups = numKeysWhere(resultsByPageKey, function (k, v) {
          return v.length !== 0;
        });
        // Remove extra page groups
        if (moreThanJustCursorUpdate) {
          for (var i = numNonEmptyPageGroups; i < numExistingHitsListPageGroups; i++) {
            hitsList.removeChild(hitsList.childNodes[hitsList.childNodes.length - 1]);
          }
          for (var i = numExistingHitsListPageGroups; i < numNonEmptyPageGroups; i++) {
            var containerForHitsItemsForPage = document.createElement("div");
            containerForHitsItemsForPage.className = "bookmark-hits-list-page";
            hitsList.appendChild(containerForHitsItemsForPage);
          }
        }
        var pageNum = 0;
        var cursorItem = null;
        forEachKey(resultsByPageKey, function (resultsForPage, pageKey) {
          var iInPageKey = 0;
          var existingPageGroup = hitsList.childNodes[pageNum];
          var numExistingPageGroupItems = existingPageGroup.childNodes.length - NUM_HEADERS;
          if (moreThanJustCursorUpdate) {
            for (var i = resultsForPage.length; i < numExistingPageGroupItems; i++) {
              existingPageGroup.removeChild(
                existingPageGroup.childNodes[existingPageGroup.childNodes.length - 1]
              );
            }
            // Add or remove the header.
            if (resultsForPage.length > 0 && existingPageGroup.childNodes.length === 0) {
              var hitsItemsHeaderForPage = document.createElement("div");
              hitsItemsHeaderForPage.className = "bookmark-hits-page-header";
              runner.pageState;
              hitsItemsHeaderForPage.innerText =
                runner.pageState[pageKey].markdownAndHeader.headerProps.title;
              existingPageGroup.appendChild(hitsItemsHeaderForPage);
            } else if (resultsForPage.length === 0 && existingPageGroup.childNodes.length !== 0) {
              existingPageGroup.removeChild(existingPageGroup.childNodes[0]);
            }
          }

          // Batch the markup parsing for performance Reduces update time (for
          // not mere cursor movements) from 45ms to 32ms (for example)
          var hitsItemsToReplaceContentsIn = [];
          var allMarkupForAllButtonContents = "";
          for (var i = 0; i < resultsForPage.length; i++) {
            var searchable = resultsForPage[i].searchable;
            // innerText causes layout/style computation
            var textContent = searchable.indexable.textContent;
            var _highlightResultContentValue = resultsForPage[i].highlightedInnerText;
            var topRowMarkup = resultsForPage[i].topRowMarkup;
            var hitsItem;
            // Reuse dom nodes to avoid flickering of css classes/animation.
            if (moreThanJustCursorUpdate) {
              if (existingPageGroup && existingPageGroup.childNodes[NUM_HEADERS + iInPageKey]) {
                hitsItem = existingPageGroup.childNodes[NUM_HEADERS + iInPageKey];
                hitsItem.onclick = null;
              } else {
                hitsItem = document.createElement("a");
                hitsItem.tabIndex = -1;
                hitsItem.className = "bookmark-hits-item";
                existingPageGroup.appendChild(hitsItem);
              }
            } else {
              hitsItem = existingPageGroup.childNodes[NUM_HEADERS + iInPageKey];
            }
            hitsItemsToReplaceContentsIn.push(hitsItem);
            if (
              effectiveCursor !== -1 &&
              effectiveCursor.pageCursor === pageKey &&
              effectiveCursor.pageCursorIndex === iInPageKey
            ) {
              cursorItem = hitsItem;
              hitsItem.classList.add("cursor");
            } else {
              hitsItem.classList.remove("cursor");
            }
            if (moreThanJustCursorUpdate) {
              hitsItem.onclick = function (pageKey, iInPageKey, searchable, e) {
                clickHandler(
                  searchComponent,
                  query,
                  resultsByPageKey,
                  pageKey,
                  iInPageKey,
                  searchable,
                  e
                );
              }.bind(null, pageKey, iInPageKey, searchable);
              hitsItem.href = runner.getUrlFromRootIncludingHashAndQuery(
                runner.lazySearchableCharacterCounts(searchable),
                searchable.originalInclusiveContext,
                pageKey
              );
              hitsItem.ontouchstart = emptyFunction;
              allMarkupForAllButtonContents +=
                '<div class="bookmark-hits-item-button-contents">' +
                topRowMarkup +
                _highlightResultContentValue +
                "</div>";
            }
            var justCursorUpdate = !moreThanJustCursorUpdate;
            iInPageKey++;
          }
          if (moreThanJustCursorUpdate) {
            var dummyForButtonContents = document.createElement("div");
            dummyForButtonContents.innerHTML = allMarkupForAllButtonContents;
            var numButtons = hitsItemsToReplaceContentsIn.length;
            for (var btnI = 0; btnI < numButtons; btnI++) {
              var hitsItemToUpdate = hitsItemsToReplaceContentsIn[btnI];
              while (hitsItemToUpdate.firstChild) {
                hitsItemToUpdate.firstChild.remove();
              }
              hitsItemToUpdate.appendChild(dummyForButtonContents.childNodes[0]);
            }
          }
          pageNum++;
        });
        if (cursorItem) {
          window.setTimeout(function () {
            customScrollIntoView({
              smooth: !moreThanJustCursorUpdate, // Instant scroll if result of typing.
              container: hitsScrollContainer,
              element: cursorItem,
              mode: "closest-if-needed",
              topMargin: 100,
              bottomMargin: 100,
            });
          }, 16);
        }
      }
    };
    var ROW_START_MARKUP =
      '<div class="bookmark-hits-item-button-contents-top-row"><div class="bookmark-hits-item-contents-top-row-crumb">';
    var ROW_START_MARKUP_LEN = ROW_START_MARKUP.length;
    var CHEVRON_MARKUP = '<span class="bookmark-hits-item-button-contents-crumb-sep">›</span>';
    Runner.prototype._appendContextCrumb = function (row, currentLevel, originalContext, level) {
      if (!row) {
        row = ROW_START_MARKUP;
      }
      if (originalContext[level]) {
        if (row.length !== ROW_START_MARKUP_LEN) {
          var chevron = CHEVRON_MARKUP;
          row += chevron;
        }
        var seg =
          '<span class="bookmark-hits-item-button-contents-crumb-row-first">' +
          escapeHtml(originalContext[level].levelContent.textContent) +
          "</span>";
        row += seg;
      }
      return row;
    };
    /**
     * @param originalContext - the originally captured context before filtering
     * or transforming into "searchables". It is a dictionary of h1,h2.. to the
     * original tree node before being mapped into an "indexed" form.
     */
    Runner.prototype.topRowForDocSearch = function (currentLevel, originalContext) {
      var row = null;
      if (currentLevel !== 1) {
        row = this._appendContextCrumb(row, currentLevel, originalContext, "h1");
      }
      if (currentLevel !== 2) {
        row = this._appendContextCrumb(row, currentLevel, originalContext, "h2");
      }
      if (currentLevel !== 3) {
        row = this._appendContextCrumb(row, currentLevel, originalContext, "h3");
      }
      if (currentLevel !== 4) {
        row = this._appendContextCrumb(row, currentLevel, originalContext, "h4");
      }
      if (currentLevel !== 5) {
        row = this._appendContextCrumb(row, currentLevel, originalContext, "h5");
      }
      if (currentLevel !== 6) {
        row = this._appendContextCrumb(row, currentLevel, originalContext, "h6");
      }
      return row + "</div></div>";
    };
    Runner.prototype.setupHitsScrollContainer = function () {
      var theSearchHitsId = this.pageTemplateOptions.searchHitsId;
      var theSearchHits = document.getElementById(theSearchHitsId);
      var hitsScrollContainer = theSearchHits.childNodes[0];
      var hitsScrollContainerAppearsSetup =
        hitsScrollContainer && hitsScrollContainer.className.indexOf("bookmark-hits-scroll") !== -1;
      // After this then this.getHitsScrollContainer() will work:

      // We are probably reviving a prerendered page
      if (theSearchHits && hitsScrollContainerAppearsSetup) {
        this.nodes.theSearchHits = theSearchHits;
        this.nodes.theHitsScrollContainer = hitsScrollContainer;
      } else if (theSearchHits && !hitsScrollContainer) {
        hitsScrollContainer = document.createElement("div");
        var hiddenClass = "bookmark-hits-scroll bookmark-hits-scroll-hidden";
        hitsScrollContainer.className = hiddenClass;
        theSearchHits.appendChild(hitsScrollContainer);
        this.nodes.theSearchHits = theSearchHits;
        this.nodes.theHitsScrollContainer = hitsScrollContainer;
      } else if (theSearchHitsId) {
        console.error(
          "You supplied options searchHitsId but we could not find one of the elements " +
            theSearchHitsId +
            ". Either that or something is wrong with the pre-rendering of the page"
        );
      }

      /**
       * Prevent blur from any existing controls that already have focus by
       * preventDefault on mouseDown event.
       * You can still style the mouse down state by using the css :active
       * pseudo-class.
       */
      hitsScrollContainer.addEventListener("mousedown", function (e) {
        e.preventDefault();
      });
      /**
       * Fix the age old safari iOS problem of ":active" css state persisting even
       * after a touch turned into a scroll.
       */
      var numActiveTouches = 0;
      hitsScrollContainer.addEventListener("scroll", function (e) {
        if(numActiveTouches > 0) {
          hitsScrollContainer.classList.add('scrolled-since-active-touches');
        }
      });
      hitsScrollContainer.addEventListener("touchstart", function (e) {
        numActiveTouches = e.touches.length;
        if(e.touches.length > 1) {
          e.preventDefault();
        }
      });
      hitsScrollContainer.addEventListener("touchend", function (e) {
        if(e.touches.length > 0) {
          e.preventDefault();
        }
        numActiveTouches = e.touches.length;
        if(numActiveTouches === 0) {
          hitsScrollContainer.classList.remove('scrolled-since-active-touches');
        }
      });
      hitsScrollContainer.addEventListener("touchcancel", function (e) {
        numActiveTouches = e.touches.length;
      });
    };

    Runner.prototype.getItemForCursor = function (effectiveCursor, resultsByPageKey) {
      if (effectiveCursor !== -1 && effectiveCursor !== null) {
        var keyIndex = keyIndexOrNegativeOne(effectiveCursor.pageCursor, resultsByPageKey);
        if (keyIndex !== -1) {
          var hitsScrollContainer = this.getHitsScrollContainer();
          var maybeHitsList = hitsScrollContainer.childNodes[0];
          if (maybeHitsList.className.indexOf("bookmark-hits-list") === -1) {
            return null;
          } else {
            var pageGroupNode = maybeHitsList.childNodes[keyIndex];
            return pageGroupNode.childNodes[NUM_HEADERS + effectiveCursor.pageCursorIndex];
          }
        }
      }
    };
    // alert('TODO: When clicking on document, set the active mode to null - let compeonts decide what they want to do when they are no longer the active mode. Dropdowns can reset their querystate to NONE. Autocompletes would not. Then make it so that all components get a notification for any active state transition away from them (or maybe even to them).');
    Runner.prototype.shouldSearchBeVisible = function (activeSearchComponent) {
      if (!activeSearchComponent) {
        return false;
      }
      if (this.searchState.userRequestedCloseEvenIfActive) {
        return false;
      } else {
        return true;
        // return activeSearchComponent.queryState !== QueryStates.NONE_AND_HIDE;
      }
    };
    Runner.prototype.setupSearchInput = function () {
      var runner = this;
      var theSearchFormId = runner.pageTemplateOptions.searchFormId;
      if (theSearchFormId) {
        var theSearchForm = document.getElementById(theSearchFormId);
        runner.searchState.CONTENT = new TextDocSearch({
          root: theSearchForm,
          /**
           * When input is blurred we do not set the active component to null.
           */
          onBlur: function inputBlur(e) {
            // console.log('blur input');
          },
          // TODO: Rembember last focused element so that escape can jump back to it.
          // Ctrl-c can toggle open, and Esc can toggle open + focus.
          // When hitting enter it can reset the "last focused" memory.
          onFocus: function doInputFocus(e) {
            setTimeout(function() {
            if (window["bookmark-header"]) {
              console.log('custom scroll into view');
              customScrollIntoView({
                smooth: true,
                container: "page",
                element: window["bookmark-header"],
                mode: "top",
                topMargin: 0,
                bottomMargin: 0,
              });
            }
            }, 25);
            // document.body.scrollTop = 400;
            // Can't really prevent default to prevent scroll. Software keyboard triggers it.
            // e.preventDefault();
          },
          onDoesntWantActiveStatus: function (comp) {
            // console.log('search input doesnt want');
            if (runner.searchState.activeSearchComponent === comp) {
              runner.setActiveSearchComponent(null);
              runner.updateSearchHitsVisibility(runner.searchState.activeSearchComponent);
            }
          },
          /**
           * When the component wants the popup menu to be shown for it, and it
           * has a useful (or new) .getQuery() that can be polled.
           */
          onWantsToHaveActiveStatus: function (comp) {
            runner.setActiveSearchComponent(comp);
            // Upon focus, reselect the first result cursor, otherwise keep old one
            // console.log("text input wants to have active status");

            var startDate = Date.now();
            runner.runSearchWithInputValue();
            var endDate = Date.now();
            // console.warn('onWantsToHaveActiveStatus duration', endDate - startDate);
          },
          onKeydown: function (e) {
            var startDate = Date.now();
            var ret = runner.handleSearchComponentKeydown(runner.searchState.CONTENT, e);
            var endDate = Date.now();
            // console.warn('key down time', endDate - startDate);
            return ret;
          },
          /**
           * Allow components to test if they are the active component.
           */
          isActiveComponent: function () {
            return runner.searchState.activeSearchComponent === runner.searchState.CONTENT;
          },
        });
      }
    };

    Runner.prototype.searchDocsWithActiveSearchComponent = function (query, renderTopRow) {
      var runner = this;
      var searchComponent = runner.searchState.activeSearchComponent;
      lazyHierarchicalIndexForSearch(runner.pageState);
      var hits = [];
      // move the current page to the front of the set.
      var linkInfo = getLink(null, null, window.location, window.location.href);
      var pageStateWithCurrentPageAtFront = moveKeyToFront(runner.pageState, linkInfo.pageKey);
      var subsetOfPages =
        runner.searchState.activeSearchComponent === runner.searchState.CONTENT
          ? keepOnlyKeys(pageStateWithCurrentPageAtFront, function(pageData, pageKey) {
            return !Flatdoc.getPageConfigBool('hideInSearch', pageData, false);
          })
          : keepOnlyKeys(pageStateWithCurrentPageAtFront, function (pageData, pageKey) {
            return runner.pageTemplateOptions.versionPageIs.toLowerCase() === pageKey;
          });
      if (searchComponent.queryState === QueryStates.ALL) {
        return hierarchicalRenderFilteredSearchables(
          query,
          mapKeys(subsetOfPages, (pageData, _) => {
            return pageData.hierarchicalIndex;
          }),
          renderTopRow
        );
      } else if (searchComponent.queryState === QueryStates.FILTER) {
        return hierarchicalRenderFilteredSearchables(
          query,
          filterHierarchicalSearchables(query, subsetOfPages),
          renderTopRow
        );
      } else {
        console.error(
          "Unknown query state",
          searchComponent.queryState,
          "for component",
          searchComponent
        );
      }
    };

    Runner.prototype.runSearchWithInputValue = function () {
      var runner = this;
      var theTextDocSearch = runner.searchState.CONTENT;
      if (runner.searchState.activeSearchComponent === theTextDocSearch) {
        var start = Date.now();
        var query = theTextDocSearch.getQuery();
        var resultsByPageKey = runner.searchDocsWithActiveSearchComponent(
          query,
          runner.topRowForDocSearch.bind(runner)
        );
        runner.updateSearchResultsList(
          runner.searchState.activeSearchComponent,
          query,
          runner.searchState.activeSearchComponent.resultsByPageKey,
          resultsByPageKey,
          runner.standardResultsClickHandler.bind(runner)
        );
        runner.searchState.activeSearchComponent.resultsByPageKey = resultsByPageKey;
        runner.updateSearchHitsVisibility(runner.searchState.activeSearchComponent);
        var end = Date.now();
        // console.log('Search updated on text change in ms:', end-start);
      }
    };

    Runner.prototype.setupVersionButton = function () {
      var runner = this;
      if (this.pageTemplateOptions.versionButtonId && this.pageTemplateOptions.versionPageIs) {
        var versionMenuButton = document.getElementById(this.pageTemplateOptions.versionButtonId);
        if (!versionMenuButton) {
          console.error(
            "Version menu selector/content with id ",
            this.pageTemplateOptions.versionButtonId,
            " doesnt exist"
          );
        }
        this.searchState.VERSIONS = new TextDocSelector({
          root: versionMenuButton,
          onKeydown: function (e) {
            return this.handleSearchComponentKeydown(runner.searchState.VERSIONS, e);
          }.bind(this),
          onWantsToHaveActiveStatus: function (comp) {
            runner.setActiveSearchComponent(comp);
            // Upon focus, reselect the first result cursor, otherwise keep old one
            console.log("version selector wants to have active status");
            runner.runVersionsSearch();
          },
          /**
           * Allow components to test if they are the active component.
           */
          isActiveComponent: function () {
            return runner.searchState.activeSearchComponent === runner.searchState.VERSIONS;
          },
          onDoesntWantActiveStatus: function (comp) {
            if (runner.searchState.activeSearchComponent === comp) {
              runner.setActiveSearchComponent(null);
              runner.updateSearchHitsVisibility(runner.searchState.activeSearchComponent);
            }
          },
          onBlur: function inputBlur(e) {
            console.log("blur input");
          },
          // TODO: Rembember last focused element so that escape can jump back to it.
          // Ctrl-c can toggle open, and Esc can toggle open + focus.
          // When hitting enter it can reset the "last focused" memory.
          onFocus: function doInputFocus(e) {
            if (window["bookmark-header"]) {
              window["bookmark-header"].scrollIntoView({ behavior: "smooth" });
            }
          },
        });
      }
    };
    Runner.prototype.updateSearchHitsVisibility = function (searchComponent) {
      // console.log('updateSearchHitsVisibility');
      var hitsScrollContainer = this.nodes.theHitsScrollContainer;
      if (!this.shouldSearchBeVisible(searchComponent)) {
        hitsScrollContainer.className = "bookmark-hits-scroll bookmark-hits-scroll-hidden";
        return false;
      } else {
        hitsScrollContainer.className = "bookmark-hits-scroll";
        return true;
      }
    };
    Runner.prototype.handleSearchComponentKeydown = function (searchComponent, evt) {
      var runner = this;
      // alert('need to make sure the active component is set here');
      var effectiveCursor = runner.effectiveCursor(
        searchComponent,
        searchComponent.resultsByPageKey
      );
      var isVisible = runner.shouldSearchBeVisible(searchComponent);
      var nextUserRequested;
      var down = (evt.keyCode === 78 && evt.ctrlKey) || evt.keyCode === 40; /* down */
      var up = (evt.keyCode === 80 && evt.ctrlKey) || evt.keyCode === 38; /* up */
      // 219 is [ and 67 is c
      var controlClose =
        (evt.keyCode === 219 && evt.ctrlKey) || (evt.keyCode === 67 && evt.ctrlKey);
      // Control n (on mac) or down arrow.
      if (down) {
        if (!isVisible && searchComponent.getQuery() !== "") {
          // Promote to zero on first down if neg one
          runner.searchState.userRequestedCloseEvenIfActive = false;
        }
        nextUserRequested = runner.nextCursorFromEffectiveCursor(
          effectiveCursor,
          searchComponent.resultsByPageKey
        );
      }
      // Control p (on mac) or up arrow.
      if (up) {
        nextUserRequested = runner.prevCursorFromEffectiveCursor(
          effectiveCursor,
          searchComponent.resultsByPageKey
        );
      }
      if (down || up) {
        searchComponent.userRequested = nextUserRequested;
      }
      if (isVisible && evt.keyCode === 13) {
        // enter
        var itemForCursor = runner.getItemForCursor(
          effectiveCursor,
          searchComponent.resultsByPageKey
        );
        $(itemForCursor)[0].click();
      } else if (!isVisible && evt.keyCode === 13) {
        runner.searchState.userRequestedCloseEvenIfActive = false;
      } else if (evt.keyCode === 27) {
        // console.log('local escape');
        // // Let's make escape close and blur
        // $(theSearchInput).blur();
        // runner.searchState.userRequestedCloseEvenIfActive = !runner.searchState.userRequestedCloseEvenIfActive;
        // runner.updateSearchHitsVisibility(searchComponent);
      } else if (controlClose) {
        // esc or ctrl-c
        // But ctrl-c can toggle without losing focus
        // runner.searchState.userRequestedCloseEvenIfActive = !runner.searchState.userRequestedCloseEvenIfActive;
        // runner.updateSearchHitsVisibility(searchComponent);
      }

      // Either way, visible or not - if enter is pressed, prevent default.
      // Because a "required" form field that is empty will submit on enter and
      // then make an ugly Chrome popup saying "this is required".
      if (down || up || evt.keyCode === 13) {
        var start = Date.now();
        evt.preventDefault();
        runner.updateSearchResultsList(
          searchComponent,
          searchComponent.getQuery(),
          searchComponent.resultsByPageKey,
          searchComponent.resultsByPageKey,
          runner.standardResultsClickHandler.bind(runner)
        );
        var end = Date.now();
        // console.log("updated cursor in ms:", end-start);
      }
    };

    Runner.prototype.deepestContextWithSlug = function (context) {
      return context.h6 && context.h6.id
        ? context.h6
        : context.h5 && context.h5.id
        ? context.h5
        : context.h4 && context.h4.id
        ? context.h4
        : context.h3 && context.h3.id
        ? context.h3
        : context.h2 && context.h2.id
        ? context.h2
        : context.h1 && context.h1.id
        ? context.h1
        : null;
    };

    /**
     * Local developer mode renders in browser, not prerendered.
     */
    Runner.prototype.isDeveloperMode = function () {
      var runner = this;
      var isDeveloperMode = !runner.discoveredToBePrerenderedAtUrl;
    };
    Runner.prototype.isSingleDocsMode = function () {
      var runner = this;
      var runPrerenderedInSingleDocsMode = runner.pageTemplateOptions.runPrerenderedInSingleDocsMode;
      var discoveredToBePrerenderedAtUrl = runner.discoveredToBePrerenderedAtUrl;
      return runner.isDeveloperMode() ? runner.pageTemplateOptions.runDevelopmentInSingleDocsMode : runPrerenderedInSingleDocsMode;
    };
    /**
     * Accepts a page key as supplied.
     */
    Runner.prototype.constructEscapedBaseUrlFromRoot = function (nonNormalizedPageKey, slugAndQueryParams) {
      var runner = this;
      var escapedNonNormalizedPageKey = nonNormalizedPageKey == null ? null : escapeHtml(nonNormalizedPageKey);
      var escapedSlugAndQueryParams = (slugAndQueryParams == null || slugAndQueryParams == '') ? null : escapeHtml(slugAndQueryParams);
      var linkInfo = getLink(runner.discoveredToBePrerenderedPageKey, runner.discoveredToBePrerenderedAtUrl, window.location, window.location.href);
      var asAnEmbeddedSubpageOfEntrypointPageKey = linkInfo.asAnEmbeddedSubpageOfEntrypointPageKey;
      var isSingleDocMode = runner.isSingleDocsMode();
      if (isSingleDocMode) {
        // For single page mode, you must have the following for referring to
        // an embedded page even if you have no other query params.  It must
        // end with a hash.
        // myPage.html#embeddedpage#
        var escapedHashSlugAndQueryParams = (escapedSlugAndQueryParams == null ? '#' : "#" + escapedSlugAndQueryParams);
        return "#" + escapedNonNormalizedPageKey + escapedHashSlugAndQueryParams;
      } else {
        var escapedHashSlugAndQueryParams = (escapedSlugAndQueryParams == null ? '' : "#" + escapedSlugAndQueryParams);
        if (asAnEmbeddedSubpageOfEntrypointPageKey.toLowerCase() === escapedNonNormalizedPageKey.toLowerCase()) {
          return escapedSlugAndQueryParams == null ?
            (escapedNonNormalizedPageKey + ".html") :
            escapedHashSlugAndQueryParams;
        } else {
          return escapedNonNormalizedPageKey + ".html" + escapedHashSlugAndQueryParams;
        }
      }
    };
    Runner.prototype.getUrlFromRootIncludingHashAndQuery = function (
      characterCounts,
      itemContext,
      itemPageKey
    ) {
      var runner = this;
      var bestSlug = bestSlugForContext(itemContext);
      var slugAndQueryParams =
        (bestSlug === null ? "" : bestSlug) +
        dictToSearchParams({txt: characterCountsToEncoded(characterCounts) });
      return runner.constructEscapedBaseUrlFromRoot(itemPageKey, slugAndQueryParams);
    };

    /**
     * TODO: Just put the href on anchor links.
     */
    Runner.prototype.standardResultsClickHandler = function (
      searchComponent,
      query,
      resultsByPageKey,
      pageKey,
      iInPageKey,
      searchable,
      e
    ) {
      var runner = this;
      searchComponent.userRequested = {
        pageCursor: pageKey,
        pageCursorIndex: iInPageKey,
      };
      runner.updateSearchResultsList(
        searchComponent,
        query,
        resultsByPageKey,
        resultsByPageKey,
        runner.standardResultsClickHandler.bind(runner)
      );
    };

    Runner.prototype.setupSearch = function () {
      var runner = this;
      runner.setupSearchInput();
      runner.setupVersionButton();
      runner.setupHitsScrollContainer();
      var theTextDocSearch = runner.searchState.CONTENT;
      var theSearchHits = runner.nodes.theSearchHits;
      if (!theTextDocSearch || !theSearchHits) {
        return;
      }
      var hitsScrollContainer = runner.nodes.theHitsScrollContainer;
      runner.nodes.theSearchHits.style.cssText +=
        "position: sticky; top: " + (headerHeight - 1) + "px; z-index: 100;";
      function setupGlobalKeybindings() {
        window.document.body.addEventListener("keypress", (e) => {
          if (!theTextDocSearch.isFocused() && e.key === "/") {
            theTextDocSearch.focus();
            theTextDocSearch.selectAll();
            e.preventDefault();
          }
        });
      }
      document.addEventListener("keydown", function (evt) {
        var controlClose =
          (evt.keyCode === 219 && evt.ctrlKey) || (evt.keyCode === 67 && evt.ctrlKey);
        if (evt.keyCode === 27) {
          // Let's make escape close and blur
          if (theTextDocSearch.isFocused()) {
            theTextDocSearch.blur();
          }
          if (!runner.searchState.userRequestedCloseEvenIfActive) {
            runner.searchState.userRequestedCloseEvenIfActive = true;
          }
          runner.setActiveSearchComponent(null);
          // Maybe updateSearchHitsVisibility should happen in setActiveSearchComponent.
          runner.updateSearchHitsVisibility(runner.searchState.activeSearchComponent);
        } else if (controlClose) {
          // esc or ctrl-c
          // But ctrl-c can toggle without losing focus
          runner.searchState.userRequestedCloseEvenIfActive = !runner.searchState
            .userRequestedCloseEvenIfActive;
          runner.updateSearchHitsVisibility(runner.searchState.activeSearchComponent);
        }
        // alert('todo have ctrl-c keep the current active component, but tell that component to go to QueryMode.NONE_AND_HIDE');
      });
      setupGlobalKeybindings();

      function onGlobalClickOff(e) {
        runner.setActiveSearchComponent(null);
        // We'll consider all other search modes to be "ephemeral".
        runner.updateSearchHitsVisibility(null);
        // e.stopPropagation();
      }
      document
        .querySelectorAll(".bookmark-content-root")[0]
        .addEventListener("click", onGlobalClickOff);
    };

    /**
     * TODO: Customize this.
     */
    Runner.prototype.topRowForVersionSearch = Runner.prototype.topRowForDocSearch;
    Runner.prototype.runVersionsSearch = function runVersionsSearch() {
      var runner = this;
      var searchComponent = runner.searchState.VERSIONS;
      if (window["bookmark-header"]) {
        window["bookmark-header"].scrollIntoView({ behavior: "smooth" });
      }
      runner.setActiveSearchComponent(searchComponent);
      console.log("running version search");
      // TODO: Reset this to NONE on blur/selection etc.
      runner.updateSearchHitsVisibility(runner.searchState.VERSIONS);
      var resultsByPageKey = runner.searchDocsWithActiveSearchComponent(
        searchComponent.getQuery(),
        runner.topRowForVersionSearch.bind(runner)
      );
      runner.updateSearchResultsList(
        searchComponent,
        searchComponent.getQuery(),
        searchComponent.resultsByPageKey,
        resultsByPageKey,
        runner.standardResultsClickHandler.bind(runner)
      );
      searchComponent.resultsByPageKey = resultsByPageKey;
    };

    Runner.prototype.makeCodeTabsInteractive = function () {
      $("codetabbutton").each(function (i, e) {
        var forTabContainerId = e.dataset.forContainerId;
        var index = e.dataset.index;
        $(e).on("click", function (evt) {
          var tabContainer = e.parentNode;
          console.log(
            'searching this query what: $("' + "#" + forTabContainerId + ' codetabbutton")'
          );
          $(e).addClass("bookmark-codetabs-active");
          $(tabContainer).removeClass("bookmark-codetabs-active1");
          $(tabContainer).removeClass("bookmark-codetabs-active2");
          $(tabContainer).removeClass("bookmark-codetabs-active3");
          $(tabContainer).removeClass("bookmark-codetabs-active4");
          $(tabContainer).removeClass("bookmark-codetabs-active5");
          $(tabContainer).addClass("bookmark-codetabs-active" + index);
        });
      });
    };

    /**
     * Remove any nodes that are not needed once rendered. This way when
     * generating a pre-rendered `.rendered.html`, they won't become part of the
     * bundle, when that rendered page is turned into a `.html` bundle. They have
     * served their purpose. Add `class='removeFromRenderedPage'` to anything you
     * want removed once used to render the page. (Don't use for script tags that
     * are needed for interactivity).
     */
    Runner.prototype.removeFromRenderedPage = function () {
      $(".removeFromRenderedPage").each(function (i, e) {
        e.parentNode.removeChild(e);
      });
    };

    /**
     * See documentation for `continueRight` css class in style.styl.
     * These are all the right split dockable items (including ul.at-tags).
     */
    Runner.prototype.fixupAlignmentClasses = function () {
      document
        .querySelectorAll(
          // TODO: Add the tabs container here too.
            ".bookmark-content > div + pre," +
            ".bookmark-content > div + blockquote," +
            ".bookmark-content > div + ul.at-tags," +
            ".bookmark-content > img + pre," +
            ".bookmark-content > img + blockquote," +
            ".bookmark-content > img + ul.at-tags," +
            ".bookmark-content > p + pre," +
            ".bookmark-content > p + blockquote," +
            ".bookmark-content > p + ul.at-tags," +
            ".bookmark-content > ul:not(.at-tags) + pre," +
            ".bookmark-content > ul:not(.at-tags) + blockquote," +
            ".bookmark-content > ul:not(.at-tags) + ul.at-tags," +
            ".bookmark-content > ol + pre," +
            ".bookmark-content > ol + blockquote," +
            ".bookmark-content > ol + ul.at-tags," +
            ".bookmark-content > h0 + pre," +
            ".bookmark-content > h0 + blockquote," +
            ".bookmark-content > h0 + ul.at-tags," +
            ".bookmark-content > h1 + pre," +
            ".bookmark-content > h1 + blockquote," +
            ".bookmark-content > h1 + ul.at-tags," +
            ".bookmark-content > h2 + pre," +
            ".bookmark-content > h2 + blockquote," +
            ".bookmark-content > h2 + ul.at-tags," +
            ".bookmark-content > h3 + pre," +
            ".bookmark-content > h3 + blockquote," +
            ".bookmark-content > h3 + ul.at-tags," +
            ".bookmark-content > h4 + pre," +
            ".bookmark-content > h4 + blockquote," +
            ".bookmark-content > h4 + ul.at-tags," +
            ".bookmark-content > h5 + pre," +
            ".bookmark-content > h5 + blockquote," +
            ".bookmark-content > h5 + ul.at-tags," +
            ".bookmark-content > h6 + pre," +
            ".bookmark-content > h6 + blockquote," +
            ".bookmark-content > h6 + ul.at-tags," +
            ".bookmark-content > table + pre," +
            ".bookmark-content > table + blockquote" +
            ".bookmark-content > table + ul.at-tags"
        )
        .forEach(function (e) {
          // Annotate classes for the left and right items that are "resynced".
          // This allows styling them differently. Maybe more top margins.  The
          // only reason to have bookmark-synced-up-left is so that in this
          // case doesn't happen. We need to tell D to stick to it's right
          // column even if everything else after A should "continueRight".
          // Normally you'd create a css rule that says (anything *before* a
          // right attachment should clear:both but css doesn't let you target
          // items that come *before* a next sibling. So we tag it with
          // bookmark-synced-up-left using JS so we can do that.  There isn't
          // currently a use for the bookmark-synced-up-right class but might
          // as well tag it since we find them.
          //
          //
          // A                   |   A
          // <continueRight/>    |   A
          // B                   |   A
          // C                   |   A
          // D                   |   A
          //                     |   D
          //
          // We also apply a css class bookmark-continued-left to B and C so
          // that CSS rules may taget them. This is used in the next use case
          // mentioned below:
          //
          // Another use case for these classes, is when we have two rows of
          // left/right pairs. We might want to provide extra spacing but
          // *only* in those cases.
          // A                   |   A
          // B                   |   B
          //
          // (Note, to get this to work with continueRight, we must apply
          // bookmark-continued-left classes to continued elements that are
          // docked left)
          //
          //
          // TODO: Move all css rules to simply target synced-up classes instead of
          // rules that specify a <blockquote> etc. The rules get far too hard
          // to target and synced-up classes make them very manageable. It
          // requires running the JS in order to render (but so does
          // everything, right? and prerendered pages make that not matter.)
          e.className += " bookmark-synced-up-right";
          if (e.previousSibling) {
            e.previousSibling.className += " bookmark-synced-up-left";
          }
        });
    };

    Runner.prototype.setupLeftNavScrollHighlighting = function () {
      var majorHeaders = $("h2, h3");
      majorHeaders.length &&
        majorHeaders.scrollagent(function (cid, pid, currentElement, previousElement) {
          if (pid) {
            var anchorForHeader = document.getElementById(pid + '-link');
            $(anchorForHeader).removeClass('active');
          }
          if (cid) {
            var anchorForHeader = document.getElementById(cid + '-link');
            $(anchorForHeader).addClass('active');
          }
        });
    };

    Runner.prototype.lazySearchableCharacterCounts = function (searchable) {
      if (!searchable.characterCounts) {
        searchable.characterCounts = computeCharacterCounts(
          getDomThingInnerText(searchable.indexable)
        );
      }
      return searchable.characterCounts;
    };
    Runner.prototype.lazyComputeTreeNodeCharacterCounts = function (treeNode) {
      for (var i = 0; i < treeNode.levelContent.length; i++) {
        var oneSearchable = treeNode.levelContent[i];
        this.lazySearchableCharacterCounts(oneSearchable);
      }
    };
    Runner.prototype.findBestTextMatchInHierarchicalIndex = function (
      hashContents,
      encodedCharacterCountsFind,
      hierarchicalDoc
    ) {
      var runner = this;
      var bestCandidate = null;
      var bestCandidateDistance = 9999999;
      var bestCandidateSlugMatches = false;
      var characterCounts = encodedToCharacterCounts(encodedCharacterCountsFind);
      var checkCharacterCounts = function (treeNode, inclusiveContext) {
        if (!bestCandidateSlugMatches || bestCandidateDistance !== 0) {
          var bestSlug = bestSlugForContext(inclusiveContext);
          runner.lazyComputeTreeNodeCharacterCounts(treeNode);
          for (var i = 0; i < treeNode.levelContent.length; i++) {
            var oneSearchable = treeNode.levelContent[i];
            var distance = computeCharacterCountDistance(
              oneSearchable.characterCounts,
              characterCounts
            );
            var slugMatches = hashContents === bestSlug;
            if (
              distance < bestCandidateDistance ||
              (distance === bestCandidateDistance && slugMatches)
            ) {
              // You can watch it refine search results down.
              // console.log("Improving distance", distance, getDomThingInnerText(oneSearchable.indexable).substr(0, 400));
              bestCandidate = oneSearchable.indexable;
              bestCandidateDistance = distance;
              bestCandidateSlugMatches = slugMatches;
            }
          }
        }
      };
      forEachHierarchy(checkCharacterCounts, hierarchicalDoc);
      return bestCandidate;
    };

    Runner.prototype.handleWindowHashChange = function () {
      var runner = this;
      runner.activatePageForCurrentUrl();

      var linkInfo = getLink(
        runner.discoveredToBePrerenderedPageKey,
        runner.discoveredToBePrerenderedAtUrl,
        window.location,
        window.location.href
      );
      var pageData = runner.pageState[linkInfo.pageKey];
      if(!pageData) {
        console.error('Page does not exist in pages: config (usually in your siteTemplate)');
        var linkInfo = getLink(
          runner.discoveredToBePrerenderedPageKey,
          runner.discoveredToBePrerenderedAtUrl,
          window.location,
          window.location.href
        );
        return;
      }

      var hashContents = linkInfo.hashContents;
      var queryParams = linkInfo.queryParams;

      var goToHeaderAsFallbackIfNotFound = function () {
        scrollIntoViewAndHighlightNodeById(
          fullyQualifiedHeaderId(linkInfo.hashContents, linkInfo.pageKey)
        );
      };
      if(!linkInfo) {
        console.error('You are trying to load a page that is not registered in the site template');
        return;
      }
      if (queryParams && queryParams.txt != null) {
        var pageKey = linkInfo.pageKey;
        lazyHierarchicalIndexForSearch(runner.pageState);
        // TODO: redirect to moved text blocks.
        var found = runner.findBestTextMatchInHierarchicalIndex(
          hashContents,
          queryParams.txt,
          runner.pageState[pageKey].hierarchicalIndex
        );
        // Can't get client bounding rect of text nodes, so don't try to scroll
        // to them and markdown parsers won't generate floating text anyways.
        if (
          found &&
          getDomThingInnerText(found).trim() !== "" &&
          found.nodeType !== Node.TEXT_NODE
        ) {
          scrollIntoViewAndHighlightNode(found);
        } else {
          goToHeaderAsFallbackIfNotFound();
        }
      } else if (hashContents !== "") {
        goToHeaderAsFallbackIfNotFound();
      }
    };
    Runner.prototype.waitForImages = function () {
      var runner = this;
      var onAllImagesLoaded = function () {
        // Has to be done after images are loaded for correct detection of position.
        runner.setupLeftNavScrollHighlighting();
        window.addEventListener("hashchange", runner.handleWindowHashChange.bind(runner));
        // Rejump after images have loaded
        runner.handleWindowHashChange();
        /**
         * If you add a style="display:none" to your document body, we will clear
         * the style after the styles have been injected. This avoids a flash of
         * unstyled content.
         * Only after scrolling and loading a stable page with all styles, do we
         * reenable visibility.
         * TODO: This is only needed if there is a hash in the URL. Otherwise,
         * we can show the page immediately, non-blocking since we don't need to scroll
         * to the current anchor. (We don't need to wait for images to load which are
         * likely below the fold). This assumes we can implement a header that is scalable
         * entirely in css. As soon as styles are loaded, the visibility can be shown.
         */
        console.log("all images loaded at", Date.now());
        document.body.style = "visibility: revert";
      };

      var imageCount = $("img").length;
      var nImagesLoaded = 0;
      // Wait for all images to be loaded by cloning and checking:
      // https://cobwwweb.com/wait-until-all-images-loaded
      // Thankfully browsers cache images.
      function onOneImageLoaded(loadedEl) {
        nImagesLoaded++;
        if (nImagesLoaded == imageCount) {
          onAllImagesLoaded();
        }
      }
      if (imageCount === 0) {
        onAllImagesLoaded();
      } else {
        $("img").each(function (_i, imgEl) {
          $("<img>").on("load", onOneImageLoaded).attr("src", $(imgEl).attr("src"));
          $("<img>").on("error", onOneImageLoaded).attr("src", $(imgEl).attr("src"));
        });
      }
    };

    Runner.prototype.run = function (
      onCurrentRenderPageDone,
      onAllRenderPagesDone,
      onNextIndexPageDone,
      onAllIndexPagesDone
    ) {};

    /**
     * We create placeholder nodes inside the template to hold the old template
     * data. It can't remain in comment form otherwise page crushing tools
     * might strip the comments.
     */
    Runner.prototype.getTemplateStringsFromContainer = function(templateContainer) {
      var isAlreadyExpanded = templateContainer.className.indexOf('expanded') !== -1;
      if(isAlreadyExpanded) {
        var childElements = templateContainer.children;
        // They have been expanded into divs that have text content with the
        // original comment contents.
        return Array.prototype.map.call(childElements, function(ce) {
          return ce.textContent;
        });
      } else {
        var commentTexts = [];
        for (var j = 0; j < templateContainer.childNodes.length; j++) {
          var maybeComment = templateContainer.childNodes[j];
          if (maybeComment.nodeType === Node.COMMENT_NODE) {
            commentTexts.push(maybeComment.data);
          }
        }
        return commentTexts;
      };
    };

    Runner.prototype.substituteAndInjectTemplatesBefore = function(templateContainerNode, templateStrings, f) {
      var dummyDiv = document.createElement("div");
      var replacedTemplates = templateStrings.map(f);
      var newHTML = replacedTemplates.join("");
      dummyDiv.innerHTML = newHTML;
      Array.prototype.forEach.call(dummyDiv.children, function(newNode) {
        templateContainerNode.parentNode.insertBefore(newNode, templateContainerNode);
      });
    };

    Runner.prototype.templatePlaceholderNodes = function(templateStrings) {
      return templateStrings.map(function(template) {
        var newPlaceholderNode = document.createElement('div');
        newPlaceholderNode.textContent = template;
        return newPlaceholderNode;
      });
    };
    Runner.prototype.substituteInDomSiteTemplateAfterSiteTemplateLoadedSingle = function() {
      var runner = this;
      console.log(Object.keys(runner.pageState));
      var templateContainers = $("div.bookmarkTemplate");
      for (var i = 0; i < templateContainers.length; i++) {
        var templateContainer = templateContainers[i];
        var templateStrings = runner.getTemplateStringsFromContainer(templateContainer);
        var isAlreadyExpanded = templateContainer.className === 'expanded bookmarkTemplate';
        var replacer = function(template) {
          return template.replaceAll(
            /\$\{Bookmark\.Template\.Pages\.([a-zA-Z\-\/]+)\.number\}/g,
            function(s, key) {
              var pageKey = key.toLowerCase();
              return runner.pageState[pageKey] ? ("" + +(indexOfKey(runner.pageState, pageKey))) : 'ERROR';
            }
          ).replaceAll(
            /\$\{Bookmark\.Template\.DomResourcesById\.([a-zA-Z\/]+)\.([a-zA-Z\/]+)\}/g,
            function(s, id, attrName) {
              var element = document.getElementById(id);
              if(element) {
                var attr = element.getAttribute(attrName);
                return escapeHtml(attr);
              } else {
                return 'element-with-id-not-found';
              }
            }
          );
        };
        if(isAlreadyExpanded) {
          removeSiblingsBefore(templateStrings.
Download .txt
gitextract_wtgaopnk/

├── .gitignore
├── API.html
├── MIT-LICENSE
├── README.html
├── VERSIONS.html
├── configuring-pages.html
├── integration.html
├── site/
│   ├── ORIGINS.md
│   ├── Paradoc.js
│   ├── fonts/
│   │   ├── CodingFont.css
│   │   ├── LICENSE-Fira
│   │   ├── LICENSE-Roboto
│   │   └── WordFont.css
│   ├── package.json
│   ├── theme.styl.html
│   └── vendor/
│       ├── highlight-styles/
│       │   ├── atom-one-light.css
│       │   └── mono-blue.css
│       ├── highlight.pack.js
│       ├── jquery.js
│       ├── medium-zoom.js
│       └── reason-highlightjs.js
├── siteTemplate.html
└── styles.html
Download .txt
SYMBOL INDEX (195 symbols across 5 files)

FILE: site/Paradoc.js
  function dictToSearchParams (line 77) | function dictToSearchParams(dict) {
  function pageifiedIdForHash (line 530) | function pageifiedIdForHash(slug, pageKey) {
  function fullyQualifiedHeaderId (line 534) | function fullyQualifiedHeaderId(slug, pageKey) {
  function hashForFullFullyQualifiedHeaderId (line 541) | function hashForFullFullyQualifiedHeaderId(s) {
  function scrollIntoViewAndHighlightNode (line 595) | function scrollIntoViewAndHighlightNode(node) {
  function scrollIntoViewAndHighlightNodeById (line 617) | function scrollIntoViewAndHighlightNodeById(id) {
  function queryParam (line 694) | function queryParam(name) {
  function parseYamlHeader (line 701) | function parseYamlHeader(markdown, locationPathname) {
  function allMatchingIndicesWillMutateYourRegex (line 747) | function allMatchingIndicesWillMutateYourRegex(regex, haystack) {
  function normalizeYamlMarkdownComments (line 772) | function normalizeYamlMarkdownComments(markdown) {
  function normalizeMarkdownResponse (line 805) | function normalizeMarkdownResponse(markdown) {
  function normalizeDocusaurusCodeTabs (line 838) | function normalizeDocusaurusCodeTabs(markdown) {
  function escapePlatformStringLoop (line 906) | function escapePlatformStringLoop(html, lastIndex, index, s, len) {
  function escapeHtml (line 987) | function escapeHtml(s) {
  function escapeRegExpSearchString (line 1021) | function escapeRegExpSearchString(string) {
  function replaceAllStringsCaseInsensitive (line 1025) | function replaceAllStringsCaseInsensitive(str, find, replace) {
  function escapeRegExpSplitString (line 1029) | function escapeRegExpSplitString(string) {
  function splitStringCaseInsensitiveImpl (line 1033) | function splitStringCaseInsensitiveImpl(regexes, str, find) {
  function splitStringCaseInsensitive (line 1036) | function splitStringCaseInsensitive(str, find) {
  function splitIndentable (line 1043) | function splitIndentable(s) {
  function splitIndentableHighlighted (line 1060) | function splitIndentableHighlighted(root) {
  function recontext (line 1210) | function recontext(context, nextTreeNode) {
  function lazyHierarchicalIndexForSearch (line 1217) | function lazyHierarchicalIndexForSearch(pageState) {
  function forEachHierarchyOne (line 1228) | function forEachHierarchyOne(f, context, treeNode) {
  function forEachHierarchyImpl (line 1233) | function forEachHierarchyImpl(f, context, treeNodes) {
  function forEachHierarchy (line 1236) | function forEachHierarchy(f, treeNodes) {
  function mapHierarchyOne (line 1241) | function mapHierarchyOne(f, context, treeNode) {
  function mapHierarchyImpl (line 1253) | function mapHierarchyImpl(f, context, treeNodes) {
  function mapHierarchy (line 1256) | function mapHierarchy(f, treeNodes) {
  function hierarchicalIndexFromHierarchicalDoc (line 1273) | function hierarchicalIndexFromHierarchicalDoc(treeNodes) {
  function hierarchize (line 1364) | function hierarchize(containerNode) {
  function computeCharacterCountDistance (line 1666) | function computeCharacterCountDistance(a, b) {
  function annotateSlugsOnTreeNodes (line 1837) | function annotateSlugsOnTreeNodes(hierarchicalDoc, slugContributions) {
  function fixAllAnchorLinksUnderRoot (line 1871) | function fixAllAnchorLinksUnderRoot(runner, rootNode) {
  function isRunningScriptFromExecutedSiteTemplate (line 1975) | function isRunningScriptFromExecutedSiteTemplate() {
  function detectMode (line 1987) | function detectMode() {
  function loadData (line 2410) | function loadData(locations, response, callback) {
  function loadData (line 2473) | function loadData(locations, response, callback) {
  function loadData (line 2540) | function loadData(locations, response, callback) {
  function mkdir_p (line 2728) | function mkdir_p(level) {
  function process (line 2878) | function process(node, $parent) {
  function SearchComponentBase (line 2971) | function SearchComponentBase(root) {
  function TextDocSearch (line 2991) | function TextDocSearch(props) {
  function TextDocSelector (line 3133) | function TextDocSelector(props) {
  function setupGlobalKeybindings (line 4003) | function setupGlobalKeybindings() {
  function onGlobalClickOff (line 4037) | function onGlobalClickOff(e) {
  function onOneImageLoaded (line 4360) | function onOneImageLoaded(loadedEl) {
  function addTouchClass (line 4542) | function addTouchClass(e) {
  function removeTouchClass (line 4555) | function removeTouchClass(e) {
  function handleDones (line 4639) | function handleDones() {
  function findAndSlugifyExperience (line 4744) | function findAndSlugifyExperience(pageKey, pageData) {
  function appendExperience (line 4788) | function appendExperience(pageKey, pageData) {
  function getTextNodesIn (line 4957) | function getTextNodesIn(el) {
  function quotify (line 4969) | function quotify(a) {
  function s (line 5091) | function s(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.en...
  function i (line 5091) | function i(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Ar...
  function g (line 5091) | function g(e,t){var n;if("undefined"!=typeof Symbol&&null!=e[Symbol.iter...
  function n (line 5091) | function n(e){return c[e]}
  function e (line 5091) | function e(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPre...
  function p (line 5091) | function p(e){return e.replace(u,function(e,t){return"colon"===(t=t.toLo...
  function v (line 5091) | function v(e,t){k[" "+e]||(b.test(e)?k[" "+e]=e+"/":k[" "+e]=w(e,"/",!0)...
  function w (line 5091) | function w(e,t,n){var r=e.length;if(0===r)return"";for(var i=0;i<r;){var...
  function j (line 5091) | function j(e,t,n){var r=t.href,i=t.title?C(t.title):null,s=e[1].replace(...
  function e (line 5091) | function e(e){this.options=e||Z}
  function G (line 5091) | function G(e){return e.replace(/---/g,"—").replace(/--/g,"–").replace(/(...
  function V (line 5091) | function V(e){for(var t,n="",r=e.length,i=0;i<r;i++)t=e.charCodeAt(i),.5...
  function n (line 5091) | function n(e){this.tokens=[],this.tokens.links=Object.create(null),this....
  function e (line 5091) | function e(e){this.options=e||J}
  function e (line 5091) | function e(){}
  function e (line 5091) | function e(){this.seen={}}
  function n (line 5091) | function n(e){this.options=e||ne,this.options.renderer=this.options.rend...
  function pe (line 5091) | function pe(e,n,r){if(null==e)throw new Error("marked(): input parameter...

FILE: site/vendor/highlight.pack.js
  function e (line 6) | function e(n){Object.freeze(n);var t="function"==typeof n;return Object....
  class n (line 6) | class n{constructor(e){void 0===e.data&&(e.data={}),this.data=e.data}ign...
    method constructor (line 6) | constructor(e){void 0===e.data&&(e.data={}),this.data=e.data}
    method ignoreMatch (line 6) | ignoreMatch(){this.ignore=!0}
  function t (line 6) | function t(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replac...
  function r (line 6) | function r(e,...n){var t={};for(const n in e)t[n]=e[n];return n.forEach(...
  function a (line 6) | function a(e){return e.nodeName.toLowerCase()}
  function l (line 6) | function l(){return e.length&&n.length?e[0].offset!==n[0].offset?e[0].of...
    method constructor (line 6) | constructor(e,n){this.buffer="",this.classPrefix=n.classPrefix,e.walk(...
    method addText (line 6) | addText(e){this.buffer+=t(e)}
    method openNode (line 6) | openNode(e){if(!o(e))return;let n=e.kind;e.sublanguage||(n=`${this.cla...
    method closeNode (line 6) | closeNode(e){o(e)&&(this.buffer+=s)}
    method value (line 6) | value(){return this.buffer}
    method span (line 6) | span(e){this.buffer+=`<span class="${e}">`}
  function c (line 6) | function c(e){s+="<"+a(e)+[].map.call(e.attributes,(function(e){return" ...
    method constructor (line 6) | constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}
    method top (line 6) | get top(){return this.stack[this.stack.length-1]}
    method root (line 6) | get root(){return this.rootNode}
    method add (line 6) | add(e){this.top.children.push(e)}
    method openNode (line 6) | openNode(e){const n={kind:e,children:[]};this.add(n),this.stack.push(n)}
    method closeNode (line 6) | closeNode(){if(this.stack.length>1)return this.stack.pop()}
    method closeAllNodes (line 6) | closeAllNodes(){for(;this.closeNode(););}
    method toJSON (line 6) | toJSON(){return JSON.stringify(this.rootNode,null,4)}
    method walk (line 6) | walk(e){return this.constructor._walk(e,this.rootNode)}
    method _walk (line 6) | static _walk(e,n){return"string"==typeof n?e.addText(n):n.children&&(e...
    method _collapse (line 6) | static _collapse(e){"string"!=typeof e&&e.children&&(e.children.every(...
  function u (line 6) | function u(e){s+="</"+a(e)+">"}
    method constructor (line 6) | constructor(e){super(),this.options=e}
    method addKeyword (line 6) | addKeyword(e,n){""!==e&&(this.openNode(n),this.addText(e),this.closeNo...
    method addText (line 6) | addText(e){""!==e&&this.add(e)}
    method addSublanguage (line 6) | addSublanguage(e,n){const t=e.root;t.kind=n,t.sublanguage=!0,this.add(t)}
    method toHTML (line 6) | toHTML(){return new l(this,this.options).value()}
    method finalize (line 6) | finalize(){return!0}
  function d (line 6) | function d(e){("start"===e.event?c:u)(e.node)}
  class l (line 6) | class l{constructor(e,n){this.buffer="",this.classPrefix=n.classPrefix,e...
    method constructor (line 6) | constructor(e,n){this.buffer="",this.classPrefix=n.classPrefix,e.walk(...
    method addText (line 6) | addText(e){this.buffer+=t(e)}
    method openNode (line 6) | openNode(e){if(!o(e))return;let n=e.kind;e.sublanguage||(n=`${this.cla...
    method closeNode (line 6) | closeNode(e){o(e)&&(this.buffer+=s)}
    method value (line 6) | value(){return this.buffer}
    method span (line 6) | span(e){this.buffer+=`<span class="${e}">`}
  class c (line 6) | class c{constructor(){this.rootNode={children:[]},this.stack=[this.rootN...
    method constructor (line 6) | constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}
    method top (line 6) | get top(){return this.stack[this.stack.length-1]}
    method root (line 6) | get root(){return this.rootNode}
    method add (line 6) | add(e){this.top.children.push(e)}
    method openNode (line 6) | openNode(e){const n={kind:e,children:[]};this.add(n),this.stack.push(n)}
    method closeNode (line 6) | closeNode(){if(this.stack.length>1)return this.stack.pop()}
    method closeAllNodes (line 6) | closeAllNodes(){for(;this.closeNode(););}
    method toJSON (line 6) | toJSON(){return JSON.stringify(this.rootNode,null,4)}
    method walk (line 6) | walk(e){return this.constructor._walk(e,this.rootNode)}
    method _walk (line 6) | static _walk(e,n){return"string"==typeof n?e.addText(n):n.children&&(e...
    method _collapse (line 6) | static _collapse(e){"string"!=typeof e&&e.children&&(e.children.every(...
  class u (line 6) | class u extends c{constructor(e){super(),this.options=e}addKeyword(e,n){...
    method constructor (line 6) | constructor(e){super(),this.options=e}
    method addKeyword (line 6) | addKeyword(e,n){""!==e&&(this.openNode(n),this.addText(e),this.closeNo...
    method addText (line 6) | addText(e){""!==e&&this.add(e)}
    method addSublanguage (line 6) | addSublanguage(e,n){const t=e.root;t.kind=n,t.sublanguage=!0,this.add(t)}
    method toHTML (line 6) | toHTML(){return new l(this,this.options).value()}
    method finalize (line 6) | finalize(){return!0}
  function d (line 6) | function d(e){return e?"string"==typeof e?e:e.source:null}
  function w (line 6) | function w(e,n){return n?+n:function(e){return N.includes(e.toLowerCase(...
  function p (line 6) | function p(e){return f.noHighlightRe.test(e)}
  function b (line 6) | function b(e,n,t,r){var a={code:n,language:e};S("before:highlight",a);va...
  function m (line 6) | function m(e,t,a,s){var o=t;function c(e,n){var t=E.case_insensitive?n[0...
  function v (line 6) | function v(e,n){n=n||f.languages||Object.keys(i);var t=function(e){const...
  function x (line 6) | function x(e){return f.tabReplace||f.useBR?e.replace(c,e=>"\n"===e?f.use...
  function E (line 6) | function E(e){let n=null;const t=function(e){var n=e.className+" ";n+=e....
  function T (line 6) | function T(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]}
  function A (line 6) | function A(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach(e=>...
  function I (line 6) | function I(e){var n=T(e);return n&&!n.disableAutodetect}
  function S (line 6) | function S(e,n){var t=e;o.forEach((function(e){e[t]&&e[t](n)}))}
  function e (line 6) | function e(e){return e?"string"==typeof e?e:e.source:null}
  function n (line 6) | function n(e){return a("(",e,")?")}
    method constructor (line 6) | constructor(e){void 0===e.data&&(e.data={}),this.data=e.data}
    method ignoreMatch (line 6) | ignoreMatch(){this.ignore=!0}
  function a (line 6) | function a(...n){return n.map(n=>e(n)).join("")}
  function s (line 6) | function s(...n){return"("+n.map(n=>e(n)).join("|")+")"}
  function t (line 6) | function t(e){return"(?:"+e+")?"}
  function s (line 6) | function s(e){return r("(?=",e,")")}
  function r (line 6) | function r(...e){return e.map(e=>(function(e){return e?"string"==typeof ...

FILE: site/vendor/jquery.js
  function M (line 3) | function M(e){var t=e.length,n=b.type(e);return b.isWindow(e)?!1:1===e.n...
  function F (line 3) | function F(e){var t=_[e]={};return b.each(e.match(w)||[],function(e,n){t...
  function P (line 3) | function P(e,n,r,i){if(b.acceptData(e)){var o,a,s=b.expando,u="string"==...
  function R (line 3) | function R(e,t,n){if(b.acceptData(e)){var r,i,o,a=e.nodeType,s=a?b.cache...
  function W (line 3) | function W(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(B,"-...
  function $ (line 3) | function $(e){var t;for(t in e)if(("data"!==t||!b.isEmptyObject(e[t]))&&...
  function it (line 3) | function it(){return!0}
  function ot (line 3) | function ot(){return!1}
  function rt (line 4) | function rt(e){return Y.test(e+"")}
  function it (line 4) | function it(){var e,t=[];return e=function(n,r){return t.push(n+=" ")>i....
  function ot (line 4) | function ot(e){return e[x]=!0,e}
  function at (line 4) | function at(e){var t=p.createElement("div");try{return e(t)}catch(n){ret...
  function st (line 4) | function st(e,t,n,r){var i,o,a,s,u,l,f,g,m,v;if((t?t.ownerDocument||t:w)...
  function ut (line 4) | function ut(e,t){var n=t&&e,r=n&&(~t.sourceIndex||j)-(~e.sourceIndex||j)...
  function lt (line 4) | function lt(e){return function(t){var n=t.nodeName.toLowerCase();return"...
  function ct (line 4) | function ct(e){return function(t){var n=t.nodeName.toLowerCase();return(...
  function pt (line 4) | function pt(e){return ot(function(t){return t=+t,ot(function(n,r){var i,...
  function ft (line 4) | function ft(e,t){var n,r,o,a,s,u,l,c=E[e+" "];if(c)return t?0:c.slice(0)...
  function dt (line 4) | function dt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}
  function ht (line 4) | function ht(e,t,n){var i=t.dir,o=n&&"parentNode"===i,a=C++;return t.firs...
  function gt (line 4) | function gt(e){return e.length>1?function(t,n,r){var i=e.length;while(i-...
  function mt (line 4) | function mt(e,t,n,r,i){var o,a=[],s=0,u=e.length,l=null!=t;for(;u>s;s++)...
  function yt (line 4) | function yt(e,t,n,r,i,o){return r&&!r[x]&&(r=yt(r)),i&&!i[x]&&(i=yt(i,o)...
  function vt (line 4) | function vt(e){var t,n,r,o=e.length,a=i.relative[e[0].type],s=a||i.relat...
  function bt (line 4) | function bt(e,t){var n=0,o=t.length>0,a=e.length>0,s=function(s,u,c,f,d)...
  function xt (line 4) | function xt(e,t,n){var r=0,i=t.length;for(;i>r;r++)st(e,t[r],n);return n}
  function wt (line 4) | function wt(e,t,n,r){var o,a,u,l,c,p=ft(e);if(!r&&1===p.length){if(a=p[0...
  function Tt (line 4) | function Tt(){}
  function pt (line 4) | function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}
  function ft (line 4) | function ft(e,t,n){if(t=t||0,b.isFunction(t))return b.grep(e,function(e,...
  function dt (line 4) | function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.cre...
  function Lt (line 4) | function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ow...
  function Ht (line 4) | function Ht(e){var t=e.getAttributeNode("type");return e.type=(t&&t.spec...
  function qt (line 4) | function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttrib...
  function Mt (line 4) | function Mt(e,t){var n,r=0;for(;null!=(n=e[r]);r++)b._data(n,"globalEval...
  function _t (line 4) | function _t(e,t){if(1===t.nodeType&&b.hasData(e)){var n,r,i,o=b._data(e)...
  function Ft (line 4) | function Ft(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCas...
  function Ot (line 4) | function Ot(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getEl...
  function Bt (line 4) | function Bt(e){Nt.test(e.type)&&(e.defaultChecked=e.checked)}
  function tn (line 5) | function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.sl...
  function nn (line 5) | function nn(e,t){return e=t||e,"none"===b.css(e,"display")||!b.contains(...
  function rn (line 5) | function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.sty...
  function on (line 5) | function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[...
  function an (line 5) | function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:...
  function sn (line 5) | function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o...
  function un (line 5) | function un(e){var t=o,n=Gt[e];return n||(n=ln(e,t),"none"!==n&&n||(Pt=(...
  function ln (line 5) | function ln(e,t){var n=b(t.createElement(e)).appendTo(t.body),r=b.css(n[...
  function gn (line 5) | function gn(e,t,n,r){var i;if(b.isArray(t))b.each(t,function(t,i){n||pn....
  function Hn (line 5) | function Hn(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var ...
  function qn (line 5) | function qn(e,n,r,i){var o={},a=e===jn;function s(u){var l;return o[u]=!...
  function Mn (line 5) | function Mn(e,n){var r,i,o=b.ajaxSettings.flatOptions||{};for(i in n)n[i...
  function k (line 5) | function k(e,n,r,i){var c,y,v,w,T,C=n;2!==x&&(x=2,s&&clearTimeout(s),l=t...
  function _n (line 5) | function _n(e,n,r){var i,o,a,s,u=e.contents,l=e.dataTypes,c=e.responseFi...
  function Fn (line 5) | function Fn(e,t){var n,r,i,o,a={},s=0,u=e.dataTypes.slice(),l=u[0];if(e....
  function In (line 5) | function In(){try{return new e.XMLHttpRequest}catch(t){}}
  function zn (line 5) | function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(...
  function Kn (line 5) | function Kn(){return setTimeout(function(){Xn=t}),Xn=b.now()}
  function Zn (line 5) | function Zn(e,t){b.each(t,function(t,n){var r=(Qn[t]||[]).concat(Qn["*"]...
  function er (line 5) | function er(e,t,n){var r,i,o=0,a=Gn.length,s=b.Deferred().always(functio...
  function tr (line 5) | function tr(e,t){var n,r,i,o,a;for(i in e)if(r=b.camelCase(i),o=t[r],n=e...
  function nr (line 5) | function nr(e,t,n){var r,i,o,a,s,u,l,c,p,f=this,d=e.style,h={},g=[],m=e....
  function rr (line 5) | function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}
  function ir (line 5) | function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r...
  function or (line 5) | function or(e){return b.isWindow(e)?e:9===e.nodeType?e.defaultView||e.pa...

FILE: site/vendor/medium-zoom.js
  function noop (line 86) | function noop() {}
  function styleInject (line 453) | function styleInject(css, ref) {

FILE: site/vendor/reason-highlightjs.js
  function reasonmlHighlightJs (line 6) | function reasonmlHighlightJs(hljs) {
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,204K chars).
[
  {
    "path": ".gitignore",
    "chars": 221,
    "preview": ".merlin\nnode_modules/\n_build\n_esy\n_release\n*.byte\n*.native\n*.install\nsite/index.html\nsite/index.rendered.html\nsite/fonts"
  },
  {
    "path": "API.html",
    "chars": 4681,
    "preview": "[ vim: set filetype=Markdown: ]: # (<style type=\"text/css\">body {visibility: hidden} </style>)\n[ vim: set filetype=Markd"
  },
  {
    "path": "MIT-LICENSE",
    "chars": 1060,
    "preview": "Copyright 2016-2018 Various Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of thi"
  },
  {
    "path": "README.html",
    "chars": 12919,
    "preview": "[ vim: set filetype=Markdown: ]: # (<style type=\"text/css\">body {visibility: hidden} </style>)\n[ vim: set filetype=Markd"
  },
  {
    "path": "VERSIONS.html",
    "chars": 963,
    "preview": "[ vim: set filetype=Markdown: ]: # (<style type=\"text/css\">body {visibility: hidden} </style>)\n[ vim: set filetype=Markd"
  },
  {
    "path": "configuring-pages.html",
    "chars": 3016,
    "preview": "[//]: # ( vim: set filetype=Markdown: )\n[//]: # (<style type=\"text/css\">body {visibility: hidden} </style>)\n[//]: # (<me"
  },
  {
    "path": "integration.html",
    "chars": 9650,
    "preview": "[//]: # ( vim: set filetype=Markdown: )\n[//]: # (<style type=\"text/css\">body {visibility: hidden} </style>)\n[//]: # (<me"
  },
  {
    "path": "site/ORIGINS.md",
    "chars": 1991,
    "preview": "Bookmark:\n(A simple tool for editing and rendering Reason documentation)\n\nHere's a list of all of the technologies that "
  },
  {
    "path": "site/Paradoc.js",
    "chars": 238046,
    "preview": "/*!\n * Flatdoc - (c) 2013, 2014 Rico Sta. Cruz\n * http://ricostacruz.com/flatdoc\n * @license MIT\n */\n\n// Keep this in sy"
  },
  {
    "path": "site/fonts/CodingFont.css",
    "chars": 113062,
    "preview": "/*\n * This is actually Fira Mono\n */\n@font-face {\n    font-family: 'CodingFont';\n    src: url(data:application/font-woff"
  },
  {
    "path": "site/fonts/LICENSE-Fira",
    "chars": 4373,
    "preview": "Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A.\n\nThis Font Software is licensed under the SIL Open "
  },
  {
    "path": "site/fonts/LICENSE-Roboto",
    "chars": 11359,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "site/fonts/WordFont.css",
    "chars": 469084,
    "preview": "/*\n * This is actually Roboto.\n */\n@font-face {\n    font-family: 'WordFont';\n    src: url(data:application/font-woff;cha"
  },
  {
    "path": "site/package.json",
    "chars": 612,
    "preview": "{\n  \"name\": \"paradoc-bundler\",\n  \"description\": \"Write Deploy Enjoy beautiful, searchable docs\",\n  \"keywords\": [\n    \"do"
  },
  {
    "path": "site/theme.styl.html",
    "chars": 111285,
    "preview": "<meta charset=\"utf-8\" vim: set filetype=Stylus: >\n<script src=\"./Paradoc.js\"> </script>\nsupport-for-ie = true\n\n\n\n/**\n * "
  },
  {
    "path": "site/vendor/highlight-styles/atom-one-light.css",
    "chars": 1320,
    "preview": "/*\n\nAtom One Light by Daniel Gamage\nOriginal One Light Syntax theme from https://github.com/atom/one-light-syntax\n\nbase:"
  },
  {
    "path": "site/vendor/highlight-styles/mono-blue.css",
    "chars": 738,
    "preview": "/*\n  Five-color theme from a single blue hue.\n*/\n.hljs {\n  display: block;\n  overflow-x: auto;\n  padding: 0.5em;\n  backg"
  },
  {
    "path": "site/vendor/highlight.pack.js",
    "chars": 62450,
    "preview": "/*\n  Highlight.js 10.1.1 (93fd0d73)\n  License: BSD-3-Clause\n  Copyright (c) 2006-2020, Ivan Sagalaev\n*/\nvar hljs=functio"
  },
  {
    "path": "site/vendor/jquery.js",
    "chars": 92629,
    "preview": "/*! jQuery v1.9.1 | (c) 2005, 2012 jQuery Foundation, Inc. | jquery.org/license\n//@ sourceMappingURL=jquery.min.map\n*/(f"
  },
  {
    "path": "site/vendor/medium-zoom.js",
    "chars": 19255,
    "preview": "/*! medium-zoom 1.0.6 | MIT License | https://github.com/francoischalifour/medium-zoom */\n(function(global, factory) {\n "
  },
  {
    "path": "site/vendor/reason-highlightjs.js",
    "chars": 8148,
    "preview": "/*\nLanguage: ReasonML\nAuthor: Gidi Meir Morris <oss@gidi.io>, Cheng Lou\nCategory: functional\n*/\nfunction reasonmlHighlig"
  },
  {
    "path": "siteTemplate.html",
    "chars": 8826,
    "preview": "<meta charset='utf-8'>\n<script charset=\"UTF-8\" src=\"site/Paradoc.js\"> </script>\n<template>\n<html>\n<head>\n  <meta charset"
  },
  {
    "path": "styles.html",
    "chars": 3973,
    "preview": "[//]: # ( vim: set filetype=Markdown: )\n[//]: # (<style type=\"text/css\">body {visibility: hidden} </style>)\n[//]: # (<me"
  }
]

About this extraction

This page contains the full source code of the jordwalke/paradoc GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (1.1 MB), approximately 568.8k tokens, and a symbol index with 195 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!