docs: Implement client-side search

Search is now performed with a pre-computed index file. This commit
removes the old PHP-based search.
master
Lukas Werling 2018-03-23 21:15:37 +01:00
parent 6aa72c1bdd
commit f4c854b651
17 changed files with 1587 additions and 972 deletions

18
.gitignore vendored
View File

@ -18,7 +18,7 @@ C4Version.h
Makefile
C4Include.h.gch
intermediate
build*
/build*
CMakeScripts
CMakeFiles
CMakeCache.txt
@ -35,13 +35,15 @@ tests/CTestTestfile.cmake
# Documentation
*.pyc
docs/online
docs/chm
docs/sdk-de
docs/sdk/content.xml
docs/*.mo
docs/doku.pot
docs/Functions.txt
node_modules
/docs/online
/docs/chm
/docs/sdk-de
/docs/sdk/content.xml
/docs/*.mo
/docs/doku.pot
/docs/Functions.txt
/docs/lunr.js
# Visual studio files
openclonk.opensdf

View File

@ -19,6 +19,7 @@ HHC = hhc.exe
MKDIR_P = mkdir -p
CP = cp
CP_R = cp -r
NODE = node
stylesheet = clonk.xsl
@ -29,9 +30,10 @@ sdk-dirs := $(shell find sdk -name '.*' -prune -o -type d -print)
# find all *.xml files recursively in sdk/
xmlfiles := $(sort $(shell find sdk -name '.*' -prune -o -name 'content.xml' -prune -o -name \*.xml -print))
xmlfiles-de := $(subst sdk, sdk-de, $(xmlfiles))
# misc
extra-files := $(sort $(wildcard *.css *.php *.js images/*.*))
extra-files := $(sort $(wildcard *.css *.js images/*.*) lunr.js)
extra-files-chm := $(sort $(wildcard *.css *.js images/*.*))
# Targets:
@ -46,15 +48,16 @@ sdk-de-dirs := $(subst sdk, sdk-de, $(sdk-dirs))
online-dirs := $(foreach lang, en de, $(addprefix online/$(lang)/, $(sdk-dirs) images))
online-sdk-files := $(foreach lang, en de, $(addprefix online/$(lang)/, $(htmlfiles) sdk/content.html))
online-extra-files := $(foreach lang, en de, $(addprefix online/$(lang)/, $(extra-files)))
online-index-files := $(foreach lang, en de, $(addprefix online/$(lang)/, index.json))
# For Entwickler.chm
chm-dirs := $(foreach lang, en de, $(addprefix chm/$(lang)/, . $(sdk-dirs) images))
.PHONY: all online-en chm install check clean
all: $(sdk-de-dirs) $(online-dirs) $(online-sdk-files) $(online-extra-files)
all: $(sdk-de-dirs) $(online-dirs) $(online-sdk-files) $(online-extra-files) $(online-index-files)
online-en: $(addprefix online/en/, $(sdk-dirs) images $(htmlfiles) sdk/content.html $(extra-files))
online-en: $(addprefix online/en/, $(sdk-dirs) images $(htmlfiles) sdk/content.html $(extra-files) index.json)
chm: $(sdk-de-dirs) $(chm-dirs) chm/en/Developer.chm chm/de/Entwickler.chm
@ -69,23 +72,36 @@ clean:
rm -f *.mo Entwickler.chm Developer.chm doku.pot sdk/content.xml
rm -rf online sdk-de chm
sdk/content.xml: sdk/content.xml.in $(xmlfiles) build_contents.py experimental.py
sdk/content.xml: sdk/content.xml.in $(xmlfiles) tools/build_contents.py tools/experimental.py
@echo generate $@
@python2 build_contents.py $(xmlfiles)
@python2 tools/build_contents.py $(xmlfiles)
chm/en/Output.hhp: $(xmlfiles) chm/en/. build_hhp.py Template.hhp
# Node dependencies for index.
node_modules/.make: package.json
npm install
@touch $@
lunr.js: node_modules/.make
$(CP) node_modules/lunr/lunr.js $@
online/en/index.json: node_modules/.make $(xmlfiles) tools/build_index.js
@$(NODE) tools/build_index.js $@ $(xmlfiles)
online/de/index.json: node_modules/.make $(xmlfiles-de) tools/build_index.js
@$(NODE) tools/build_index.js $@ $(xmlfiles-de)
chm/en/Output.hhp: $(xmlfiles) chm/en/. tools/build_hhp.py Template.hhp
@echo generate $@
@python2 build_hhp.py $@ Template.hhp $(xmlfiles)
chm/de/Output.hhp: $(xmlfiles) chm/de/. build_hhp.py Template.de.hhp
@python2 tools/build_hhp.py $@ Template.hhp $(xmlfiles)
chm/de/Output.hhp: $(xmlfiles) chm/de/. tools/build_hhp.py Template.de.hhp
@echo generate $@
@python2 build_hhp.py $@ Template.de.hhp $(xmlfiles)
@python2 tools/build_hhp.py $@ Template.de.hhp $(xmlfiles)
$(sdk-de-dirs) $(online-dirs) $(chm-dirs):
mkdir -p $@
doku.pot: $(xmlfiles) extra-strings.xml sdk/content.xml.in xml2po.py clonk.py
doku.pot: $(xmlfiles) extra-strings.xml sdk/content.xml.in tools/xml2po.py tools/clonk.py
@echo extract strings to $@
@python2 xml2po.py -e -m clonk -o $@ $(xmlfiles) extra-strings.xml sdk/content.xml.in
@python2 tools/xml2po.py -e -m clonk -o $@ $(xmlfiles) extra-strings.xml sdk/content.xml.in
%.po: doku.pot
@echo update $@
@ -96,9 +112,9 @@ doku.pot: $(xmlfiles) extra-strings.xml sdk/content.xml.in xml2po.py clonk.py
@echo compile $@
@msgfmt --statistics -o $@ $<
sdk-de/%.xml: sdk/%.xml de.mo xml2po.py clonk.py
sdk-de/%.xml: sdk/%.xml de.mo tools/xml2po.py tools/clonk.py
@echo generate $@
@python2 xml2po.py -e -m clonk -t de.mo -o $@ $<
@python2 tools/xml2po.py -e -m clonk -t de.mo -o $@ $<
define run-xslt
@echo generate $@

View File

@ -35,7 +35,12 @@
</title>
</xsl:template>
<xsl:template match="script">
<xsl:copy><xsl:apply-templates select="@*|node()" /></xsl:copy>
<xsl:copy>
<xsl:for-each select="@*">
<xsl:copy />
</xsl:for-each>
<xsl:apply-templates />
</xsl:copy>
</xsl:template>
<xsl:template match="func|const" mode="head">
<xsl:apply-templates mode="head" />
@ -349,16 +354,6 @@
<caption><xsl:apply-templates select="@id|node()" /></caption>
</xsl:template>
<xsl:template match="search">
<xsl:if test="not($chm)">
<form action="../search.php" method="get">
<input name="search" type="text"></input>
<input type="submit" name="func" value="Search"></input>
<input type="submit" name="fulltext" value="Fulltext"></input>
</form>
</xsl:if>
</xsl:template>
<xsl:template match="table/bitmask">
<xsl:value-of select="." />:
<input id="input" onKeyUp="Calc();" name="input" type="text">

2131
docs/de.po

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
@font-face{
font-family: Endeavour;
src: url('/Endeavour.eot');
src: local('Endeavour'),url('/Endeavour.ttf') format("truetype");
/*src: url('/Endeavour.eot');
src: local('Endeavour'),url('/Endeavour.ttf') format("truetype");*/
}
body {
@ -243,10 +243,6 @@ ul.nav a {
text-decoration: none;
}
ul.nav a:hover, ul.nav a:hover {
background: #333 url(images/stripe-wide.gif) left repeat-x;
}
ul.nav li.switchlang img {
margin: 0;
}

32
docs/package-lock.json generated 100644
View File

@ -0,0 +1,32 @@
{
"name": "openclonk-docs",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"lunr": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.1.6.tgz",
"integrity": "sha512-ydJpB8CX8cZ/VE+KMaYaFcZ6+o2LruM6NG76VXdflYTgluvVemz1lW4anE+pyBbLvxJHZdvD1Jy/fOqdzAEJog=="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": "1.2.4",
"xmlbuilder": "9.0.7"
}
},
"xmlbuilder": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
}
}
}

22
docs/package.json 100644
View File

@ -0,0 +1,22 @@
{
"name": "openclonk-docs",
"version": "1.0.0",
"description": "Developer documentation for OpenClonk",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/openclonk/openclonk.git"
},
"author": "The OpenClonk Team",
"license": "ISC",
"bugs": {
"url": "https://github.com/openclonk/openclonk/issues"
},
"homepage": "https://github.com/openclonk/openclonk#readme",
"dependencies": {
"lunr": "^2.1.6",
"xml2js": "^0.4.19"
}
}

View File

@ -211,6 +211,10 @@
window.scrollTo(0, y0);
}
})();
// Collapse the index.
document.querySelector('.index > img').onclick();
]]>
</script>
<script src="../lunr.js"></script>
<script src="../search.js"></script>
</toc>

View File

@ -13,8 +13,6 @@
<h>Get started</h>
<text><emlink href="files.html">Game data</emlink> - Get an overview over the game data needed for the creation of custom game content.</text>
<text><emlink href="script/index.html">C4Script</emlink> - View the introduction to the script language C4Script.</text>
<h>Search the documentation</h>
<search/>
</part>
<author>Newton</author><date>2011-08</date>
</doc>

52
docs/search.js 100644
View File

@ -0,0 +1,52 @@
(function() {
'use strict'
fetch('../index.json').then(r => r.json()).then(data => {
var index = lunr.Index.load(data.index)
var titles = data.titles
var toc = document.getElementById('toc')
var searchWrapper = document.createElement('div')
searchWrapper.innerHTML = `
Search: <input type="search">
<ul class="results">
</ul>
`
toc.insertBefore(searchWrapper, toc.querySelector('.contents'))
var searchInput = searchWrapper.querySelector('input[type=search]')
var resultsList = searchWrapper.querySelector('.results')
searchInput.addEventListener('change', doSearch)
// Allow specifying search query via query parameter.
var m = window.top.location.search.match(/[?&]q=([^&]+)/)
if (m) {
searchInput.value = m[1]
doSearch()
}
function doSearch() {
var searchTerm = searchInput.value.trim()
var html = ''
if (searchTerm) {
html = search(index, searchInput.value).map(result => `<li><a href='${result.ref}'>${titles[result.ref]}</a></li>`).join('\n')
if (!html) html = `<li><em>No results</em></li>`
}
resultsList.innerHTML = html
}
})
function search(index, searchTerm) {
return index.query(query => {
searchTerm.split(/\s+/).forEach(term => {
query.term(term, {
fields: ["title"],
boost: 10,
wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING,
})
query.term(term, {
fields: ["body"],
editDistance: 1,
})
})
})
}
})()

View File

@ -1,174 +0,0 @@
<?php
//parameters: $_GET('func')
//search?
if(isset($_GET['search']))
{
if(strlen($_GET['search']) < 3) {
$less = true;
}
else {
if(isset($_GET['func'])) {
$path = "sdk/script/fn/";
$search = strtolower($_GET['search']);
$result = array();
$dir = opendir($path);
//search
while (($item = readdir($dir)) !== FALSE)
{
$name = substr($item,0,strpos($item,'.'));
if ("." != $item && ".." != $item
&& (strpos(strtolower($name), $search) !== FALSE)
&& !is_dir($path.$item))
{
// exact match -> redirect
if ($search == strtolower($name))
{
header("Location: $path$item");
exit;
}
array_push($result,array($path,$item));
}
}
$showresults = 1;
}
elseif(isset($_GET['fulltext'])) {
$result = SearchDir('sdk/');
$showresults = 2;
}
}
}
function SearchDir($path) {
if(!$dir = opendir($path))
return;
$result = array();
$search = strtolower($_GET['search']);
while (false !== ($file = readdir($dir))) {
if ($file != "." && $file != "..") {
if(is_dir($path.$file))
$result = array_merge($result, SearchDir($path.$file.'/'));
else {
// HTML-Dokument auslesen
$doc = new DOMDocument();
@$doc->loadHTMLFile($path.$file);
$divs = $doc->getElementsByTagName('div');
foreach($divs as $div) {
if(strpos($div->getAttribute('class'), 'text') !== false) {
if(strpos(strtolower(strip_tags($div->nodeValue)),htmlspecialchars($search)) !== false) {
$dirname = basename(rtrim($path, '/'));
if(!isset($result[$dirname]))
$result[$dirname] = array();
$name = $doc->getElementsByTagName('h1')->item(0)->nodeValue;
array_push($result[$dirname], array($path.$file,$name));
break;
}
}
}
}
}
}
closedir($dir);
return $result;
}
?>
<?php
$lang = basename(dirname(__FILE__));
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<link rel="stylesheet" type="text/css" href="doku.css">
<link rel="stylesheet" type="text/css" href="https://www.openclonk.org/header/header.css">
<title>OpenClonk <?php echo $lang == 'de' ? 'Referenz' : 'Reference' ?></title>
<style>
ul {
list-style-position: inside;
list-style-image: url(images/bullet_sheet.png);
}
ul a {
color: navy;
text-decoration: none;
}
ul a.visited {
color: navy;
text-decoration: none;
}
</style>
</head>
<body>
<?php
readfile("https://www.openclonk.org/header/header.html");
?>
<div id="iframe"><iframe src="sdk/content.html"></iframe></div>
<div id="content">
<h1><?php print ($lang == 'de' ? 'Suche' : 'Search'); ?></h1>
<div class="text">
<form action="search.php" method="get">
<?php
echo '&nbsp;<input type="text" name="search"';
if (isset($_GET['search'])) echo ' value="' . htmlspecialchars($_GET['search']) . '"';
echo '> ';
echo '<input type="submit" name="func" value="' . ($lang == 'de' ? 'Suche' : 'Search') . '"> ';
echo '<input type="submit" name="fulltext" value="' . ($lang == 'de' ? 'Volltext' : 'Fulltext') . '">';
?>
</form>
<?php
if($less) {
echo $lang == 'de' ? 'Mindestens 3 Zeichen.' : '3 characters minimum.';
}
$dirtrans = array('de' => array('sdk' => 'Dokumentation', 'script' => 'Script', 'fn' => 'Funktionen', 'scenario' => 'Szenario', 'particle' => 'Partikel', 'material' => 'Material', 'folder' => 'Rundenordner', 'definition' => 'Objektdefinition'),
'en' => array('sdk' => 'Documentation', 'script' => 'Script', 'fn' => 'Functions', 'scenario' => 'Scenario', 'particle' => 'Particle', 'material' => 'Material', 'folder' => 'Folder', 'definition' => 'Definition'));
//nothing found
if($showresults == 1) {
if (count($result) == 0)
{
echo $lang == 'de' ? 'Es wurde keine Funktion gefunden.' : 'No function found.';
}
else {
echo "<ul>\n";
for($i = 0; $i < count($result); ++$i)
{
$item = $result[$i][1];
if(!$name = $result[$i][2])
$name = substr($item,0,strpos($item,'.'));
$path = $result[$i][0];
echo "<li><a href=\"$path$item\">$name</a></li>\n";
}
echo "</ul>\n";
}
}
elseif($showresults == 2) {
if (count($result) == 0)
{
echo $lang == 'de' ? 'Nichts gefunden.' : 'Nothing found.';
}
else {
foreach($result as $dirname => $values) {
$dirname = $dirtrans[$lang][$dirname];
echo "<b>$dirname</b>\n";
echo "<ul>\n";
foreach($values as $val)
{
$item = $val[0];
$name = $val[1];
echo "<li><a href=\"$item\">$name</a></li>\n";
}
echo "</ul>\n";
}
}
}
?>
</div>
</div>
</body></html>

View File

@ -0,0 +1,57 @@
#!/usr/bin/env node
const lunr = require('lunr')
const xml2js = require('xml2js')
const fs = require('fs')
if (process.argv.length < 4) {
console.log(`Usage: ${__filename} <output> <files...>`)
process.exit(1)
}
const outfile = process.argv[2]
const files = process.argv.slice(3)
let builder = new lunr.Builder()
builder.pipeline.add(
lunr.trimmer,
lunr.stopWordFilter,
lunr.stemmer
)
builder.ref('path')
builder.field('title')
builder.field('body')
function extractText(obj) {
if (typeof obj == 'string') return obj
let result = ''
for (let o of Array.isArray(obj) ? obj : Object.values(obj)) {
result += extractText(o) + '\n'
}
return result
}
let titles = {}
for (let file of files) {
let contents = fs.readFileSync(file)
let xml;
xml2js.parseString(contents, {async: false}, (err, result) => {
if (err) { console.error(file, err); process.exit(1) }
xml = result
})
// sdk/script/fn/Explode.xml => script/fn/Explode.html
let doc = {path: file.replace(/^.*sdk(-de)?\//, '').slice(0, -3) + 'html'}
if ('doc' in xml) {
doc.title = xml.doc.title.toString()
doc.body = extractText(xml.doc)
} else if ('funcs' in xml) {
let fn = 'func' in xml.funcs ? xml.funcs.func[0] : xml.funcs.const[0]
doc.title = fn.title.toString()
doc.body = fn.desc.toString()
}
builder.add(doc)
titles[doc.path] = doc.title
}
let index = builder.build()
fs.writeFileSync(outfile, JSON.stringify({index, titles}))