This commit is contained in:
Q
2023-09-27 19:51:55 +03:00
parent 4de2f56b29
commit 07d6d5242c
9 changed files with 2134 additions and 0 deletions

45
README.md Normal file
View File

@@ -0,0 +1,45 @@
# MDSnippets #
MDSnippets is a static HTML page with JS markdown parser to create a
generic snippet storage
Snippets are syntax highlighted with [highlight.js](https://highlightjs.org).
See their documentation for language names.
## UI
* At the top there is a search bar. Enter values to filter snippet names.
* Pressing key 's' or / focuses the search bar.
## Configuration
* config.js
* File is read to replace values in "config.xx" object.
* config.source='path_to_markdown.txt';
* config.style='path_to_style.css';
* config.title='Title of page';
* config.search=true; // Display link search bar
* config.favicon='path_to_favicon.ico';
## Markdown
Only 1st level heads, names and code blocks are parsed:
```
# header
name
\`\`\`python
print("Hello World!")
# Dot escape the ticks in real config.
\`\`\`
```
## Static HTML builder
Sometimes the reloading of styles and links is hard due to caching.
You can create a static single HTML page with:
```
bash build_static.sh -l example_snippets.txt -c config.js -s style.css > templated.html
```

107
build_static.sh Executable file
View File

@@ -0,0 +1,107 @@
#!/bin/bash
_help() {
echo "Pass arguments to create a static version of the index, printed on stdout
Templating will replace config values config.style and config.source to null
-l [snippets.txt] Path to snippets.txt. must be passed
-c [config.js] *Path to configuration js replacing config values
-s [styles.css] *Path to extra styles
* can be left out
"
exit
}
_checkenv() {
if [[ ! -e "$SNIPPETS" ]]; then
echo "Snippets file '$SNIPPETS' not found."
_help
exit 1
fi
if [[ -n "$CONFIG" ]]; then
if [[ ! -e "$CONFIG" ]]; then
echo "Config file '$CONFIG' not found."
exit 1
fi
fi
if [[ -n "$STYLE" ]]; then
if [[ ! -e "$STYLE" ]]; then
echo "Styles file '$STYLE' not found."
exit 1
fi
fi
echo "
Snippets file: $SNIPPETS
Config file: $CONFIG
Style file: $STYLE
" >&2
}
_replace_snippets() {
if [[ -e "$SNIPPETS" ]]; then
sniptmp=$( mktemp )
sed 's/`/\\`/g' "$SNIPPETS" > "$sniptmp"
sed \
-e '/TEMPLATED:SNIPPETS/a config.source=null;' \
-e '/TEMPLATED:SNIPPETS/a config.snippets=`' \
-e "/TEMPLATED:SNIPPETS/r $sniptmp" \
-e '/TEMPLATED:SNIPPETS/a `;'
#-e "/TEMPLATED:SNIPPETS/d"
rm -f "$sniptmp"
else
cat -
fi
}
_replace_config() {
if [[ -e "$CONFIG" ]]; then
sed \
-e "/TEMPLATED:CONFIG/r $CONFIG" \
-e "/^.script language.*config.js.*script.*/d"
#-e "/TEMPLATED:CONFIG/d"
else
cat -
fi
}
_replace_style() {
if [[ -e "$STYLE" ]]; then
sed \
-e "/TEMPLATED:STYLE/r $STYLE" \
-e '/TEMPLATED:CONFIG/a config.style=null;' \
#-e "/TEMPLATED:STYLE/d"
else
cat -
fi
}
_inject_dependencies() {
sed \
-e "/TEMPLATED:HIGHLIGHT_STYLE/r $SELFDIR/default.min.css" \
-e "/TEMPLATED:HIGHLIGHT_STYLE/r $SELFDIR/dark.min.css" \
-e "/TEMPLATED:HIGHLIGHT_CODE/r $SELFDIR/highlight.min.js" \
-e "/stylesheet.*default.min.css/d" \
-e "/stylesheet.*dark.min.css/d" \
-e "/script.*highlight.min.js/d"
}
_replace() {
cat "$SELFDIR"/index.html | _replace_snippets | _replace_style | _replace_config | _inject_dependencies
}
SELFDIR=$( dirname $( readlink -f "$0") )
CONFIG=
STYLE=
SNIPPETS=
for (( i=1; i<=$#; i++ )); do
j=$(( i + 1 ))
[[ "${!i}" = "-h" ]] && _help
[[ "${!i}" = "--help" ]] && _help
[[ "${!i}" = "-l" ]] && { SNIPPETS="${!j}"; shift 1; continue; }
[[ "${!i}" = "-c" ]] && { CONFIG="${!j}"; shift 1; continue; }
[[ "${!i}" = "-s" ]] && { STYLE="${!j}"; shift 1; continue; }
done
_checkenv
_replace

1
dark.min.css vendored Normal file
View File

@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#ddd;background:#303030}.hljs-keyword,.hljs-link,.hljs-literal,.hljs-section,.hljs-selector-tag{color:#fff}.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-name,.hljs-string,.hljs-symbol,.hljs-template-tag,.hljs-template-variable,.hljs-title,.hljs-type,.hljs-variable{color:#d88}.hljs-comment,.hljs-deletion,.hljs-meta,.hljs-quote{color:#979797}.hljs-doctag,.hljs-keyword,.hljs-literal,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-strong,.hljs-title,.hljs-type{font-weight:700}.hljs-emphasis{font-style:italic}

9
default.min.css vendored Normal file
View File

@@ -0,0 +1,9 @@
/*!
Theme: Default
Description: Original highlight.js style
Author: (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
Maintainer: @highlightjs/core-team
Website: https://highlightjs.org/
License: see project LICENSE
Touched: 2021
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}

5
example_config.js Normal file
View File

@@ -0,0 +1,5 @@
config.title = "MDSnippets";
config.search = true;
config.source = "example_snippets.txt";
config.favicon = null; // link to favicon
config.style = null; // link to style file

49
example_snippets.txt Normal file
View File

@@ -0,0 +1,49 @@
# Help
Usage
```plaintext
Search at top^
Use Tab or click item to show code.
Code is selected automatically, just ctrl-c or copy.
Code is stored in a markdown file: example_snippets.txt
snippets syntax:
======
# header
item1
` ``Language
Language code
` ``
^ note no space in between in real life
item2
` ``bash
for e in *; do loop $e;done
` ``
======
```
# Python
Hello
```python
print("HELLO")
```
sql
```SQL
SELECT
*
FROM {WORK.TABLE.r()}
```
# Bash
for files
```bash
for e in *; do echo "$e"; done
```

1207
highlight.min.js vendored Normal file

File diff suppressed because one or more lines are too long

708
index.html Normal file
View File

@@ -0,0 +1,708 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="default.min.css">
<link rel="stylesheet" href="dark.min.css">
<script src="highlight.min.js"></script>
<STYLE type="text/css">
HTML {
--txt-col: #ddd;
--high-col: rgb(255, 255, 255);
--head-col: #AFC2FF;
--fg-col: #eee;
--bg-col: #282828;
--filt-col: #AFC2FF;
}
BODY {
color: var(--txt-col);
background-color: var(--bg-col);
}
#main {
display: flex;
width: 100%;
min-height: 100%;
}
.tableWrapper {
flex: 1;
min-width: 20%;
max-width: 50%;
}
.snippetWrapper {
flex: 2;
min-width: 50%;
max-width: 80%;
}
.code {
margin-left: 0.4em;
margin-right: 0.4em;
border: 1px solid black;
height: calc(100vh - 5em);
max-height: calc(100vh - 5em);
}
#hiddenCode {
display: inline-block;
max-width: 15em;
height: 1.2em;
overflow: hidden;
}
#snippet {
display: block;
padding: 0.4em;
white-space: pre-wrap;
overflow-x: hidden;
overflow-y: auto;
height: calc(100vh - 8em);
max-height: calc(100vh - 8em);
}
TABLE {}
HR {
height: 5px;
border: 0px;
}
TD {
vertical-align: top;
width: 10%;
}
LI {
text-indent: -1.3em;
padding-left: 1.4em;
list-style-type: none;
overflow: hidden;
margin-bottom: 0.2em;
}
LI:before {
content: "-";
padding-right: 0.5em;
}
TD.narrow LI {
margin-bottom: 1em;
}
LI.nolink:before {
content: " ";
padding-right: 1.0em;
}
A.link {
text-decoration: none;
color: var(--txt-col);
cursor: pointer;
}
A.link:hover {
text-decoration: underline;
var(--txt-col);
}
A.link:focus {
text-decoration: underline;
}
A.selectedName {
color: var(--high-col);
}
#filter {
width: 100%;
background-color: var(--filt-col);
color: var(--bg-col);
}
#search {
width: 100%;
display: table;
}
.search_span {
display: table-cell;
text-align: center;
vertical-align: middle;
padding-left: 0.1em;
padding-right: 0.1em;
}
.head {
text-align: left;
padding-top: 0.2em;
margin-bottom: -0.5em;
color: var(--head-col);
}
#menu {
width: 3em;
background: var(--bg-col);
border-color: var(--fg-col);
border-width: 2px;
color: var(--txt-col);
}
.tickbox {}
#reload_span {
float: right;
}
hr {
background-color: var(--head-col);
/*box-shadow: 0px 3px var(--high-col), 0px -3px var(--txt-col);
border-radius: 0px;*/
}
.hidden {
display: none;
}
.fold_plus::after {
content: " (+)";
vertical-align: super;
font-size: 50%;
}
.blink {
-webkit-animation: blinker 0.5s infinite;
-moz-animation: blinker 0.5s infinite;
animation: blinker 0.5s infinite;
}
@keyframes blinker {
0% {
opacity: 0.00;
}
100% {
opacity: 1.00;
}
}
/* TEMPLATED:STYLE */
/* TEMPLATED:HIGHLIGHT_STYLE */
</STYLE>
<script language="javascript">
function init() {
var head = document.head;
if (config.style != null) {
var link = document.createElement('link');
link.type = 'text/css';
link.rel = 'stylesheet';
link.href = config.style;
head.appendChild(link);
}
if (config.favicon != null) {
var link = document.createElement('link');
link.type = 'image/x-icon';
link.rel = 'icon';
link.href = config.favicon;
head.appendChild(link);
}
var title = document.createElement('title');
title.innerHTML = config.title;
head.appendChild(title);
if (!config.search) {
document.getElementById('search').style.display = 'none';
}
document.getElementById("filter").focus();
filter_load();
hitList = search_hits(document.getElementById("filter").value);
print_results(hitList);
fold_initial();
scroll_load();
parse_request();
}
String.prototype.capitalizeFirstLetter = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
}
function show_code(element, c) {
var positionY = window.scrollY;
var links = document.getElementsByTagName('a');
for (i = 0; i < links.length; i++) {
links[i].classList.remove("selectedName");
}
element.classList.add("selectedName");
var snippet = document.getElementById('snippet');
snippet.className = snippet.className.split(" ").filter(function(c) {
return c.lastIndexOf("language-", 0) !== 0;
}).join(" ");
snippet.classList.add(`language-${snippetList[c].language}`);
snippet.innerHTML = snippetList[c].value;
hljs.highlightElement(snippet);
window.scrollTo(0, positionY);
var hidden = document.getElementById('hiddenCode');
hidden.innerHTML = snippetList[c].value;
select_text(hidden);
}
function make_link(c) {
var l = document.createElement('a');
l.innerHTML = snippetList[c].name;
l.classList.add("link");
l.href = "#";
l.id = `link_${c}`;
l.onclick = function(event) {
show_code(this, c);
event.preventDefault();
}
l.onfocus = function() {
show_code(this, c);
}
var li = document.createElement('li');
li.appendChild(l);
return li
}
function open_snippet(hit) {
document.getElementById(`link_${hit}`).click();
}
function select_text(id) {
window.getSelection()
.selectAllChildren(
id
);
}
function make_head(c, index = "") {
var name = c.name.substring(1).trim();
var head = c.name.replace(/ /g, "&nbsp;");
var id = name.replace(/ /g, "") + "_" + index;
return '<a name="' + name + '" id="anchor_' + id + '" class="nolink" onclick="fold_toggle(\'' + id + '\')">' +
'<div class=head align=center id="head_' + id + '">' + head + '</div></a>' +
'<hr WIDTH="100%" SIZE=3 NOSHADE>';
}
function filter(ev) {
var needle = document.getElementById('filter').value;
set_cookie("filter", needle);
hitList = search_hits(needle);
var c = hitList.length;
var lastHit = hitList[c - 1];
print_results(hitList);
// At parsing:
if (ev == null) {
if (c == 1) {
open_snippet(lastHit);
};
return
}
// At manual entry:
var key = ev.keyCode;
if (key == 13) {
if (c == 0) {
document.getElementById('filter').focus();
}
if (c == 1) {
open_snippet(lastHit);
}
if (c > 1) {
document.getElementById('filter').focus();
}
}
}
function no_filter() {}
function search_hits(needle) {
needle = needle.trim().toLowerCase();
if (needle == "") {
return Array.apply(null, {
length: snippetList.length
}).map(Number.call, Number);
}
hitList = [];
var headName = "";
for (l = 0; l < snippetList.length; l++) {
if (snippetList[l].type == "head") {
headName = snippetList[l].name.replace(/ /g, "").toLowerCase();
}
if (needle.substring(0, 1) == "#") {
/* search for header */
if (headName.indexOf(needle.substring(1).trim()) > -1) {
/* current header matches */
hitList.push(l);
continue
}
}
if (snippetList[l].name.toLowerCase().indexOf(needle) > -1) {
hitList.push(l);
continue
}
}
return hitList
}
var cats = [];
function print_results(hitList) {
var str = "";
var cat = 0;
cats = [];
var td = document.createElement("td"); // If there are no heads to startwith
var td_content = document.createElement("div");
td.appendChild(td_content);
cats.push({
name: 'N/A',
td: td,
count: 0
});
cat += 1;
for (c = 0; c < snippetList.length; c++) {
if (snippetList[c].type == "head") {
var name = snippetList[c].name.substring(1).trim().replace(/ /g, "");
var td = document.createElement("td");
td.id = "td_" + name + "_" + cat;
var td_content = document.createElement("div");
td_content.id = "content_" + name + "_" + cat;
td.innerHTML += make_head(snippetList[c], cat);
td.appendChild(td_content);
cats.push({
name: name,
td: td,
count: 0
});
cat += 1;
} else {
if (hitList.indexOf(c) == -1) {
continue
}
td_content.appendChild(make_link(c));
cats[cat - 1].count++;
}
}
var doc = document.getElementById("main");
doc.innerHTML = "";
var snippetWrapper = document.createElement("div");
snippetWrapper.classList.add('snippetWrapper');
var tableWrapper = document.createElement("div");
tableWrapper.classList.add('tableWrapper');
var tbl = document.createElement("table");
var tbody = document.createElement("tbody");
var tr = document.createElement("tr");
var print_cat = 0;
for (cat = 0; cat < cats.length; cat++) {
if (cats[cat].count > 0) {
var tr = document.createElement("tr");
tbody.appendChild(tr);
tr.appendChild(cats[cat].td);
print_cat++;
}
}
var pre = document.createElement("pre")
var hidden = document.createElement("span")
var snippet = document.createElement("code")
var copy_doc = document.createElement("span")
snippet.id = "snippet";
hidden.id = "hiddenCode";
pre.id = "pre";
pre.classList.add("code");
copy_doc.id = "copyDoc";
copy_doc.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" role="presentation"><g fill="currentColor"><path d="M10 19h8V8h-8v11zM8 7.992C8 6.892 8.902 6 10.009 6h7.982C19.101 6 20 6.893 20 7.992v11.016c0 1.1-.902 1.992-2.009 1.992H10.01A2.001 2.001 0 018 19.008V7.992z"></path><path d="M5 16V4.992C5 3.892 5.902 3 7.009 3H15v13H5zm2 0h8V5H7v11z"></path></g></svg>';
snippet.onfocus = function() {
select_text(this);
}
snippet.onclick = function() {
select_text(this);
}
hidden.onfocus = function() {
select_text(this);
}
hidden.onclick = function() {
select_text(this);
}
pre.appendChild(snippet);
pre.appendChild(copy_doc);
pre.appendChild(hidden);
snippetWrapper.appendChild(pre);
tbody.appendChild(tr);
tbl.appendChild(tbody);
tableWrapper.appendChild(tbl);
doc.appendChild(tableWrapper);
doc.appendChild(snippetWrapper);
}
function reload_source() {
location.href = config.source
}
function get_URL(s) {
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
} else {
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
parse_snippets(xmlhttp.responseText);
}
}
xmlhttp.open("GET", s, true);
xmlhttp.send();
}
function read_snippets() {
if (config.source === null && config.snippets) {
parse_snippets(config.snippets);
} else {
get_URL(config.source);
}
}
function parse_snippets(s) {
var rows = s.split("\n");
var snippets = [];
var codeOpen = false;
var code = '';
var title = '';
var lang = '';
for (r = 0; r < rows.length; r++) {
if (!codeOpen) {
if (rows[r].substring(0, 3) == '```') {
codeOpen = true;
code = "";
lang = rows[r].substring(3);
lang = (lang.length > 0) ? lang : 'plaintext';
} else {
if (rows[r].length > 1) {
if (rows[r].substring(0, 2) == "# ") {
snippets.push({
'name': rows[r],
'type': 'head',
})
} else {
title = rows[r];
}
}
}
} else {
if (rows[r] == '```') {
codeOpen = false;
snippets.push({
'name': title,
'type': 'code',
'value': code,
'language': lang
});
title = 'Unnamed';
} else {
code += rows[r] + "\n"
}
}
}
snippetList = snippets.slice(0);
init();
}
function parse_request() {
var request = window.location.search.substring(1);
var vars = request.split('&');
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
if (decodeURIComponent(pair[0]) == "filter") {
var value = decodeURIComponent(pair[1]);
document.getElementById("filter").value = value;
filter(null);
}
}
}
function keyboard_entry(ev) {
var kC = ev.keyCode;
var k = String.fromCharCode(ev.keyCode);
var ctrlDown = ev.ctrlKey || ev.metaKey;
if (document.activeElement === document.getElementById("filter")) {
return
}
if (/S/.test(k) || kC == 191) { // slash or S
document.getElementById("filter").focus();
}
}
function scroll_page() {
if (document.getElementById('pre')) {
document.getElementById('pre').style.marginTop = `${window.scrollY}px`;
}
scroll_save()
}
function scroll_save() {
set_cookie("position", window.scrollY);
}
function scroll_load() {
var position = parseInt(get_cookie("position"));
window.scrollTo(0, position);
}
function filter_load() {
var needle = get_cookie("filter");
if (needle === null) {
return;
}
if (needle.startsWith("/")) {
needle = "";
}
document.getElementById("filter").value = needle;
}
function get_cookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(";");
var cookies = Array();
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == " ") c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) {
cookies.push(c.substring(nameEQ.length, c.length));
}
}
return filter_cookies(cookies, name)
}
function filter_cookies(cookies, name) {
// return only cookies that have the pathname
for (var i = 0; i < cookies.length; i++) {
var c = cookies[i];
var split = c.split("&");
if (split.length == 2) {
if (split[1] == window.location.pathname) {
return split[0];
}
}
}
return null;
}
function fold_toggle(name, save_state = true) {
if (!config.foldCategories) {
return
}
var title = document.getElementById("head_" + name);
var el = document.getElementById("content_" + name);
el.hidden = !el.hidden;
title.classList.toggle("fold_plus");
if (save_state) {
set_cookie("fold", fold_get_state());
}
}
function fold_set(name, value) {
if (!config.foldCategories) {
return
}
var title = document.getElementById("head_" + name);
var el = document.getElementById("content_" + name);
el.hidden = value;
if (value) {
title.classList.add("fold_plus");
} else {
title.classList.remove("fold_plus");
}
}
function fold_initial() {
if (!config.foldCategories) {
return
}
var state = get_cookie("fold");
if (state !== null) {
fold_set_state(state);
return;
}
var slides = document.getElementsByClassName("head");
for (var i = 0; i < slides.length; i++) {
var el = slides.item(i);
var rect = el.getBoundingClientRect();
if (window.innerHeight * 1.5 < rect.top) {
var name = el.id.replace("head_", "");
fold_toggle(name);
}
}
}
function fold_get_state() {
var slides = document.getElementsByClassName("head");
var state = "";
for (var i = 0; i < slides.length; i++) {
var title = slides.item(i);
var name = title.id.replace("head_", "");
var el = document.getElementById("content_" + name);
if (el.hidden) {
state += '-';
} else {
state += '+';
}
}
return state;
}
function fold_set_state(state) {
var slides = document.getElementsByClassName("head");
for (var i = 0; i < slides.length; i++) {
var title = slides.item(i);
var name = title.id.replace("head_", "");
if (state[i] == '-') {
fold_set(name, true);
}
if (state[i] == '+') {
fold_set(name, false);
}
}
return;
}
function set_cookie(name, value) {
// Save cookies for 30 minutes
document.cookie = name + "=" + value.toString() + "&" + window.location.pathname +
";path=" + window.location.pathname + ";max-age=" + (60 * 30).toString() + ";";
}
function clear() {
document.getElementById("filter").value = '';
filter(true);
}
document.onkeyup = keyboard_entry;
document.onscroll = scroll_page;
var snippetList = [];
var hitList = [];
var config = {
title: 'MDSnippets', // title of snippets
source: 'snippets.txt', // file for snippets
search: true, // include search bar
style: null, // file to load css from
favicon: null, // link to favicon file
};
// TEMPLATED:HIGHLIGHT_CODE
// TEMPLATED:CONFIG
// TEMPLATED:SNIPPETS
</script>
<script language="javascript" src="config.js"></script>
</head>
<body onload="read_snippets()">
<div id="search">
<span class="search_span"><input type="text" oninput="filter(event)" onkeypress="filter(event)" onblur="no_filter()" id="filter" title="[s] Filter snippets by name"></span>
</div>
<div id="main"></div>
<span class="search_span" id="reload_span"><a href="#" onclick="reload_source()" title="Reload snippets source">&#8635;</a></span>
</body>
</html>

3
instant_webserver.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
exec python3 -m http.server 8123