A => app.js +269 -0
@@ 1,269 @@
+(function() {
+
+let data = [];
+let cells = [];
+let initialized = false;
+let matched = [];
+let selected = 0;
+
+let pendingUpdate = null;
+
+const IDX_NAME = 0;
+const IDX_NAMESPACE = 1;
+const IDX_PACKAGE = 2;
+const IDX_TARGET = 3;
+
+const IDX_NAME_LOWER = 4;
+const IDX_FULLNAME_LOWER = 5;
+
+function renderEntry(entry, idx) {
+ const mkEl = (s) => document.createElement(s);
+ const li = mkEl('li');
+ li.className = 'indexentry';
+ const a = mkEl('a');
+ a.setAttribute('href', 'data/' + entry[IDX_TARGET]);
+ li.appendChild(a);
+ li.setAttribute('data-entryidx', idx);
+
+ const name = mkEl('span');
+ name.className = 'name';
+ name.innerHTML = entry[IDX_NAME];
+ a.appendChild(name);
+
+ const pkg = mkEl('span');
+ pkg.className = 'package dimmed';
+ pkg.innerHTML = '[' + entry[IDX_PACKAGE] + ']';
+ a.appendChild(pkg);
+
+ a.appendChild(mkEl('br'));
+
+ const ns = mkEl('span');
+ ns.className = 'namespace dimmed';
+ ns.innerHTML = entry[IDX_NAMESPACE];
+ a.appendChild(ns);
+
+ a.addEventListener('click', clickResult);
+
+ return li;
+}
+
+function isSubsequence(s, query) {
+ let pos = 0;
+ let queryPos = 0;
+
+ while (queryPos < query.length) {
+ const c = query[queryPos];
+ const idx = s.indexOf(c, pos);
+
+ if (idx === -1) {
+ return false;
+ }
+ pos = idx + 1;
+ ++queryPos;
+ }
+
+ return true;
+}
+
+function matchSubsequence(item, query) {
+ return isSubsequence(item[IDX_FULLNAME_LOWER], query);
+}
+
+function matchNamespace(item, query) {
+ const quoted = query.replaceAll(/[.+*\\()\[\]<>$^]/g, '\\$&');
+ const reQuery = new RegExp(quoted.replaceAll('\\.', '.*\\.'));
+ return reQuery.test(item[IDX_FULLNAME_LOWER]);
+}
+
+function matchSimple(item, query) {
+ if (item[IDX_FULLNAME_LOWER].indexOf(query) != -1) {
+ return true;
+ }
+ return false;
+}
+
+function openResult(linkTarget) {
+ const iframe = document.querySelector('#content iframe');
+ iframe.src = linkTarget;
+}
+
+function clickResult(evt) {
+ const linkTarget = evt.currentTarget.getAttribute('href');
+
+ openResult(linkTarget);
+
+ const entryIdx = Number(evt.currentTarget.parentElement.getAttribute('data-entryidx'));
+ selected = matched.indexOf(entryIdx);
+
+ updateSelected();
+
+ evt.preventDefault();
+ return false;
+}
+
+const matches = matchNamespace;
+
+function searchKeyDown(evt) {
+ if (evt.key === 'ArrowDown') {
+ ++selected;
+ updateSelected();
+ evt.preventDefault();
+ return false;
+ }
+ if (evt.key === 'ArrowUp') {
+ --selected;
+ updateSelected();
+ evt.preventDefault();
+ return false;
+ }
+ if (evt.key === 'Enter') {
+ const chosen = matched[selected];
+ const chosenCell = cells[chosen];
+ chosenCell.firstElementChild.click();
+ evt.preventDefault();
+ return false;
+ }
+}
+
+function updateCellVisibility(startIndex) {
+ // Keep track of how many cells where "switched" from display "block" <->
+ // "none" to limit time for frame update
+ let cellsChanged = 0;
+ const UPDATE_BATCH_SIZE = 500;
+ let i;
+ for (i = startIndex; i < cells.length; ++i) {
+ const entryMatches = matched.indexOf(i) !== -1;
+
+ if (cellsChanged < UPDATE_BATCH_SIZE) {
+ const oldStatus = cells[i].style.display;
+ const newStatus = entryMatches ? 'block' : 'none';
+ if (oldStatus !== newStatus) {
+ cells[i].style.display = newStatus;
+ ++cellsChanged;
+ }
+ } else {
+ break;
+ }
+ }
+
+ if (i < cells.length) {
+ pendingUpdate = window.requestAnimationFrame(() => {
+ // continue update in next frame
+ updateCellVisibility(i);
+ });
+ }
+}
+
+function updateSearchResults(evt) {
+ const query = evt.target.value.toLocaleLowerCase('en-US');
+ matched = [];
+
+ console.log(`search (${cells.length} / ${data.length}): `, query);
+ const startTime = Number(new Date());
+
+ for (let i = 0; i < data.length; ++i) {
+ const entryMatches = matches(data[i], query);
+ if (entryMatches) {
+ matched.push(i);
+ }
+ }
+ const finishTime = Number(new Date());
+ console.log(`search took ${finishTime - startTime} ms`);
+
+ if (pendingUpdate !== null) {
+ window.cancelAnimationFrame(pendingUpdate);
+ pendingUpdate = null;
+ }
+ updateCellVisibility(0);
+
+ updateSelected();
+}
+
+function updateSelected() {
+ selected = Math.max(0, Math.min(selected, matched.length - 1));
+ let selectedCell = undefined;
+ for (let i = 0; i < cells.length; ++i) {
+ if (i === matched[selected]) {
+ cells[i].classList.add('result-selected');
+ selectedCell = cells[i];
+ } else {
+ cells[i].classList.remove('result-selected');
+ }
+ }
+
+ if (selectedCell === undefined || !selectedCell.offsetParent) {
+ return;
+ }
+
+ const parent = selectedCell.offsetParent;
+ if (parent.scrollTop > selectedCell.offsetTop) {
+ selectedCell.scrollIntoView(true);
+ } else if ((parent.scrollTop + parent.offsetHeight) < (selectedCell.offsetTop + selectedCell.offsetHeight)) {
+ selectedCell.scrollIntoView(false);
+ }
+}
+
+function init() {
+ if (initialized) {
+ // do not init twice
+ return;
+ }
+ initialized = true;
+
+ const search = document.getElementById('i2d_search_results');
+ const searchBox = document.getElementById('i2d_searchbox')
+
+ fetch('data/index.json').then((res) => {
+ if (res.ok) {
+ return res.json();
+ } else {
+ throw new Error("error loading index: " + e.status);
+ }
+ }).then((index) => {
+ data = index;
+ cells = [];
+ matched = [];
+ data.forEach((entry) => {
+ entry[IDX_NAME_LOWER] = entry[IDX_NAME].toLocaleLowerCase('en-US');
+ entry[IDX_FULLNAME_LOWER] = entry[IDX_NAMESPACE].toLocaleLowerCase('en-US') + '.' + entry[IDX_NAME].toLocaleLowerCase('en-US');
+
+ const idx = cells.length;
+
+ const newNode = renderEntry(entry, idx);
+ search.appendChild(newNode);
+ cells.push(newNode);
+ matched.push(idx);
+ });
+
+ updateSelected();
+ searchBox.focus();
+ });
+
+ searchBox.addEventListener('input', (evt) => {
+ updateSearchResults(evt);
+ });
+
+ searchBox.addEventListener('keydown', searchKeyDown);
+
+ document.addEventListener('keydown', (evt) => {
+ if (evt.key === 'Tab' && document.activeElement !== searchBox) {
+ searchBox.focus();
+ searchBox.select();
+ evt.preventDefault();
+ return false;
+ }
+ });
+
+ const iframe = document.querySelector('#content iframe');
+ iframe.addEventListener('load', (evt) => {
+ console.log('iframe loaded', evt);
+ document.title = iframe.contentWindow.document.title;
+ });
+}
+
+document.addEventListener('DOMContentLoaded', init, {once: true});
+if (document.readyState === 'interactive') {
+ init();
+}
+
+})();
A => index.html +24 -0
@@ 1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Idris2 Docs</title>
+ <link rel="stylesheet" href="style.css">
+ <script src="app.js" async></script>
+ </head>
+ <body class="nodebug">
+ <div class="flex-container">
+ <div id="sidebar">
+ <div class="searchbox-container">
+ <input type=text class="searchbox" id="i2d_searchbox" placeholder="Type to begin searching">
+ </div>
+ <ul id="i2d_search_results">
+ </ul>
+ </div>
+ <div id="content">
+ <iframe style="width: 100%; height: 100%;" src="data/home.html">
+ </iframe>
+ </div>
+ </div>
+ </body>
+</html>
A => mkindex.py +78 -0
@@ 1,78 @@
+#!/usr/bin/env python3
+
+import json
+from pathlib import Path
+import re
+import traceback
+from typing import NamedTuple
+
+from bs4 import BeautifulSoup
+
+class IndexEntry(NamedTuple):
+ name: str
+ namespace: str
+ package: str
+ target: str
+
+RE_NAMESPACE = re.compile(r"""(?P<ns>([^.]+\.)+)(?P<name>.+)""")
+def split_ns(full_name):
+ match = RE_NAMESPACE.match(full_name)
+ return match.group('ns')[:-1], match.group('name')
+
+def sortkey(x):
+ return (x.name.lower(),
+ x.namespace.lower(),
+ x.package.lower(),
+ x.target
+ )
+
+class IndexBuilder:
+ def __init__(self, root: str):
+ self.root = Path(root)
+ self.entries = []
+
+ def run(self):
+ sources = self.root.glob('**/*.html')
+
+ for source in sources:
+ try:
+ self.scan(source)
+ except Exception as e:
+ print(f"error processing file {source}")
+ traceback.print_exc()
+
+ self.entries.sort(key=sortkey)
+ with open(self.root / 'index.json', 'w') as f:
+ json.dump(self.entries, f)
+
+ def scan(self, source):
+ entries = []
+ relpath = source.relative_to(self.root)
+ package = relpath.parts[0]
+ print(f"scanning {source}")
+
+ with open(source) as f:
+ soup = BeautifulSoup(f, 'html.parser')
+
+ docclass = soup.find('body')['class']
+ if 'namespace' not in docclass:
+ return
+
+ for span in soup.select('dl.decls > dt > span.name'):
+ id = span.parent['id']
+ full_name = span.get_text(strip=True)
+ namespace, name = split_ns(full_name)
+ # print(f"{full_name} -> {namespace} , {name}")
+ # namespace, name = full_name.rsplit('.', 1)
+ entries.append(IndexEntry(
+ name,
+ namespace,
+ package,
+ relpath.as_posix() + '#' + id
+ ))
+ self.entries += entries
+
+if __name__ == '__main__':
+ import sys
+ builder = IndexBuilder(sys.argv[1])
+ builder.run()
A => poetry.lock +40 -0
@@ 1,40 @@
+[[package]]
+category = "main"
+description = "Screen-scraping library"
+name = "beautifulsoup4"
+optional = false
+python-versions = "*"
+version = "4.9.3"
+
+[package.dependencies]
+[package.dependencies.soupsieve]
+python = ">=3.0"
+version = ">1.2"
+
+[package.extras]
+html5lib = ["html5lib"]
+lxml = ["lxml"]
+
+[[package]]
+category = "main"
+description = "A modern CSS selector implementation for Beautiful Soup."
+marker = "python_version >= \"3.0\""
+name = "soupsieve"
+optional = false
+python-versions = ">=3.5"
+version = "2.1"
+
+[metadata]
+content-hash = "0aed234b5ffb3d308de24294c6b9d687f1f61ff50711eea9cb63a07a57074644"
+python-versions = "^3.8"
+
+[metadata.files]
+beautifulsoup4 = [
+ {file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"},
+ {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"},
+ {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"},
+]
+soupsieve = [
+ {file = "soupsieve-2.1-py3-none-any.whl", hash = "sha256:4bb21a6ee4707bf43b61230e80740e71bfe56e55d1f1f50924b087bb2975c851"},
+ {file = "soupsieve-2.1.tar.gz", hash = "sha256:6dc52924dc0bc710a5d16794e6b3480b2c7c08b07729505feab2b2c16661ff6e"},
+]
A => pyproject.toml +15 -0
@@ 1,15 @@
+[tool.poetry]
+name = "idris2-quickdocs"
+version = "0.1.0"
+description = ""
+authors = ["Johann Rudloff <johann@sinyax.net>"]
+
+[tool.poetry.dependencies]
+python = "^3.8"
+beautifulsoup4 = "^4.9.3"
+
+[tool.poetry.dev-dependencies]
+
+[build-system]
+requires = ["poetry>=0.12"]
+build-backend = "poetry.masonry.api"
A => style.css +107 -0
@@ 1,107 @@
+.debug div {
+ border: 1px solid red;
+}
+
+#sidebar {
+ width: 280px;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ border-right: 1px solid #ccc;
+}
+
+.searchbox-container {
+ width: 100%;
+ height: 31px;
+ flex-shrink: 0;
+ background-color: rgb(37, 37, 37);
+ /*border-bottom: 3px solid rgb(101, 159, 219);*/
+ display: flex;
+ flex-direction: row;
+}
+
+.searchbox {
+ width: 100%;
+}
+
+#i2d_search_results {
+ position: relative;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ flex-grow: 100;
+ margin: 0;
+ padding: 0;
+}
+
+#content {
+ flex-grow: 100;
+ overflow: hidden;
+}
+
+#content > iframe {
+ border: 0;
+}
+
+.result-hidden {
+ display: none;
+}
+
+.flex-container {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+}
+
+.indexentry {
+ position: relative;
+}
+
+.indexentry .package {
+ float: right;
+ text-align: right;
+ font-size: 85%;
+}
+
+.indexentry .name {
+ color: #000;
+ font-family: Mensch, Menlo, "DejaVu Sans Mono", monospace;
+ background-color: inherit;
+}
+
+.indexentry a {
+ display: block;
+ padding: 4px;
+ background-color: inherit;
+
+ color: #000;
+ text-decoration: none;
+}
+
+.indexentry .dimmed {
+ color: #999;
+}
+
+.result-selected .dimmed {
+ color: rgb(50, 80, 109);
+}
+
+.indexentry:hover {
+ background-color: #eee;
+}
+
+.result-selected, .result-selected:hover {
+ background-color: rgb(101, 159, 219);
+}
+
+html, body {
+ margin: 0;
+ height: 100%;
+ box-sizing: border-box;
+
+ font-family: "Trebuchet MS", Helvetica, sans-serif;
+ font-size: 10pt;
+}
+
+* {
+ box-sizing: inherit;
+}