Compare commits

..

180 Commits

Author SHA1 Message Date
Sergio Martínez Portela
2f3c52f5f2 Push notes section to the top. 2024-07-21 19:54:23 +02:00
Sergio Martínez Portela
d630fb0f70 Add Malleable Software slides to homepage. 2024-07-21 19:48:23 +02:00
Sergio Martínez Portela
ce35091852 Add configurable subpath. 2024-05-05 12:29:26 +02:00
Sergio Martínez Portela
d9b85c8475 Fix height of arrows on external links. 2024-03-12 01:40:26 +01:00
Sergio Martínez Portela
9a020285ad Add clean styling for SOMETIME state. 2024-03-10 17:51:58 +01:00
Sergio Martínez Portela
e639df35a7 s/Mastodon/ActivityPub/ 2023-11-12 15:34:44 +01:00
Sergio Martínez Portela
89e50a6310 Fix color scheme on dark mode. 2023-11-01 23:37:57 +01:00
Sergio Martínez Portela
28122c3c31 Use dark-syntax.css only when dark mode is selected. 2023-10-05 08:53:50 +02:00
Sergio Martínez Portela
b8eadc8b1e Update CSS links on blog. 2023-10-03 00:22:13 +02:00
Sergio Martínez Portela
23f8fcefe5 Improve link checking. 2023-10-03 00:09:30 +02:00
Sergio Martínez Portela
600e737767 Re-render archives on file changes. 2023-10-03 00:09:30 +02:00
Sergio Martínez Portela
c588187ae3 Fix directory creation on re-render. 2023-10-03 00:09:30 +02:00
Sergio Martínez Portela
bd644e3788 No need to re-link videos or hrefs. 2023-10-03 00:09:30 +02:00
Sergio Martínez Portela
650b16df32 Add no-watch build on blog. 2023-10-03 00:09:30 +02:00
Sergio Martínez Portela
abfd4b16c5 Fix blog linking. 2023-10-03 00:07:45 +02:00
Sergio Martínez Portela
302ec0b764 [autoserve] Return 404 when appropriate. 2023-09-27 23:35:20 +02:00
Sergio Martínez Portela
6d78ad53c6 Fix path decoding. 2023-09-27 23:30:26 +02:00
Sergio Martínez Portela
00cb3fa203 Redirect '/directory' to '/directory/'. 2023-09-27 23:09:51 +02:00
Sergio Martínez Portela
816cedea4d wip: Script to test links for broken ones. 2023-09-22 00:05:24 +02:00
Sergio Martínez Portela
5b0873b0bd Implement RSS generation. 2023-09-21 00:16:56 +02:00
Sergio Martínez Portela
fc1a94cfcf Additional logging on loaded and reloaded posts. 2023-09-19 23:42:24 +02:00
Sergio Martínez Portela
21a857ea41 Implement handling of one post in multiple languages. 2023-09-19 23:42:24 +02:00
Sergio Martínez Portela
12b5e31e49 Handle root index autoserving. 2023-09-19 23:40:12 +02:00
Sergio Martínez Portela
7cc2407846 Link: "A sloppy guide to GnuCash's Python bindings" as post. 2023-09-18 23:56:19 +02:00
Sergio Martínez Portela
43824cc013 WIP: Explore parameters for gen_centered_graph.py 2023-09-18 23:55:32 +02:00
Sergio Martínez Portela
0003c80ad8 Miscellaneous styling changes. 2023-09-18 23:55:08 +02:00
Sergio Martínez Portela
747fbf015e Fix codehilite CSS. 2023-09-18 23:54:41 +02:00
Sergio Martínez Portela
eb1c3084c8 Implement article listing. 2023-09-17 23:39:15 +02:00
Sergio Martínez Portela
1c0b69d693 Fix navbar links. 2023-09-17 23:30:43 +02:00
Sergio Martínez Portela
f25bfb1ef6 Add category pages. 2023-09-17 23:30:24 +02:00
Sergio Martínez Portela
7d1524e270 Add links to older/newer sections of archive. 2023-09-17 22:55:45 +02:00
Sergio Martínez Portela
f1781bb1ae Implement proper article summarization for index. 2023-09-17 22:55:45 +02:00
Sergio Martínez Portela
da6536ff53 Show date but not time for each posts. 2023-09-17 22:55:45 +02:00
Sergio Martínez Portela
6275fc3694 Fix linking to blog articles from index. 2023-09-17 22:55:45 +02:00
Sergio Martínez Portela
59c9dfcf6b Fix autoserving of directory indexes. 2023-09-16 23:21:55 +02:00
Sergio Martínez Portela
7e1b71cf78 Reference notes from Homepage. Hide projects. 2023-09-16 18:44:19 +02:00
Sergio Martínez Portela
18f33b29e2 Material-er theme on dark graph render. 2023-09-16 18:44:16 +02:00
Sergio Martínez Portela
f81673d76a Merge branch 'dev/centered-graph-drawing' into develop 2023-09-16 18:43:32 +02:00
Sergio Martínez Portela
661a5e0cf8 Add centered graph as first element. 2023-06-26 23:56:32 +02:00
Sergio Martínez Portela
9784f78f1c Deduplicate graph edges. 2023-06-20 00:15:41 +02:00
Sergio Martínez Portela
dee465f6af Remap document IDs. 2023-06-20 00:09:08 +02:00
Sergio Martínez Portela
d6c8b9f3db Fix links originating from top-level of graph center. 2023-06-19 00:07:40 +02:00
Sergio Martínez Portela
539240079f Fix rendering of outgoing links on graph.
See: http://127.0.0.1:8000/notes/343fe43b-f687-4f83-8171-c966a6887898.node.html#343fe43b-f687-4f83-8171-c966a6887898
2023-06-18 23:56:30 +02:00
Sergio Martínez Portela
da20c14ae7 Fix generation of graphs where top-level headline has no backlines. 2023-06-18 23:24:33 +02:00
Sergio Martínez Portela
960a3693d3 Fix typo on intermediate dot file name. 2023-06-13 00:00:35 +02:00
Sergio Martínez Portela
fa789984f4 Implement long-operation cache. 2023-06-10 15:55:43 +02:00
Sergio Martínez Portela
135423b8e5 Wrap graph generation in python code, remove API dependency. 2023-06-10 15:35:06 +02:00
Sergio Martínez Portela
3f5ec66c3d Fix: render graph.json, a dependency, before node-centered-graph. 2023-06-09 21:04:04 +02:00
Sergio Martínez Portela
49a5ec3df2 Experiment with centered graph drawing. 2023-06-09 20:54:01 +02:00
Sergio Martínez Portela
b248f507c6 Handle file: links. 2023-06-06 18:32:19 +02:00
Sergio Martínez Portela
0e56cfbe10 Show TOC on the left side. 2023-05-14 21:06:27 +02:00
Sergio Martínez Portela
ce2ca99ebc Don't move node off center if no TOC is shown. 2023-05-14 21:06:08 +02:00
Sergio Martínez Portela
210a508a90 Add simple color loop when loading query. 2023-05-01 22:46:46 +02:00
Sergio Martínez Portela
bdda2f3676 Replace header collapse/expand for link copying. 2023-04-28 00:12:02 +02:00
Sergio Martínez Portela
fce2d3c935 Use a bluer, less-greener background color. 2023-04-28 00:11:59 +02:00
Sergio Martínez Portela
5ae3be6d0f Draw floating table-of-contents on the right when possible. 2023-04-28 00:05:40 +02:00
Sergio Martínez Portela
61b1b79f95 Don't draw tables of contents with <2 elements. 2023-04-26 23:52:49 +02:00
Sergio Martínez Portela
57af01c5be Add symbol for external links. 2023-04-26 23:31:01 +02:00
Sergio Martínez Portela
d10bd44178 requirements: Add pygments requirement. 2023-04-23 20:53:02 +02:00
Sergio Martínez Portela
9a7358a98a Fix argument order on render_table. 2023-01-12 00:19:04 +01:00
Sergio Martínez Portela
a465b409b1 Print a warning and avoid crash on orgit-rev links. 2023-01-12 00:18:46 +01:00
Sergio Martínez Portela
b916be8f0b Copy mobile viewport tags from https://eev.ee 2023-01-06 17:09:10 +01:00
Sergio Martínez Portela
84b86e456b Limit img size to max-width: 100%. 2023-01-05 20:52:26 +01:00
Sergio Martínez Portela
6d7078ae01 Link to Mastodon profile from homepage. 2022-12-28 23:00:12 +01:00
Sergio Martínez Portela
90d1c15ba7 Add markup rendering and basic styling. 2022-12-28 22:42:26 +01:00
Sergio Martínez Portela
f174be032e Softer border on results box. 2022-12-20 00:59:00 +01:00
Sergio Martínez Portela
fcb44ca1a6 Apply "unindent" to highlighted code blocks too. 2022-12-20 00:56:53 +01:00
Sergio Martínez Portela
2d0f71bca1 Hide "ATTACH" tags. 2022-12-14 23:55:30 +01:00
Sergio Martínez Portela
d004454192 Add support for attachments and image links. 2022-12-14 23:46:41 +01:00
Sergio Martínez Portela
31450effe7 Add Makefile to concatenate syntax style. 2022-11-29 23:54:53 +01:00
Sergio Martínez Portela
e268734a3f Add base table-of-contents, don't collapse headlines. 2022-11-29 00:20:31 +01:00
Sergio Martínez Portela
58bb7ccc49 Improve handling of lists with multiline items. 2022-11-24 00:09:58 +01:00
Sergio Martínez Portela
88029a27d0 Convert the linked-from list into a tagged list. 2022-11-22 20:28:14 +01:00
Sergio Martínez Portela
86e36d5b79 Fix consistency between Webkit & Gecko.
Also, minor style fixes.
2022-11-21 00:10:47 +01:00
Sergio Martínez Portela
913e2c54d3 Start exploring CSS design for tree view.
Heavily based on https://iamkate.com/code/tree-views/ .
2022-11-20 21:41:07 +01:00
Sergio Martínez Portela
b9ac52a1a9 Support light&dark syntax on code blocks. 2022-11-15 21:20:25 +01:00
Sergio Martínez Portela
fe052d3468 Separate words on different line chunks. 2022-11-15 21:12:13 +01:00
Sergio Martínez Portela
058f0caf73 Treat DB cleanup as error when not in interactive mode. 2022-11-15 21:11:58 +01:00
Sergio Martínez Portela
9a883d90dd Apply Syntax Highlight on code blocks. 2022-11-15 21:11:36 +01:00
Sergio Martínez Portela
e7dc8ad1e7 Make indentation lines clearer on dark mode. 2022-11-11 18:45:28 +01:00
Sergio Martínez Portela
87e4a8aa7d Remap document ids backlinks to main headlines. 2022-11-09 19:54:39 +01:00
Sergio Martínez Portela
ac445d2e7c Use logging.warning instead of deprecated logging.warn. 2022-11-09 01:01:49 +01:00
Sergio Martínez Portela
4849128fcd Add simple backlink connections. 2022-11-09 01:01:34 +01:00
Sergio Martínez Portela
08d35fc0b5 Handle more softly words with :// that don't match known link types. 2022-11-04 00:34:32 +01:00
Sergio Martínez Portela
76531e3cfc Add result box rendering. 2022-11-04 00:34:32 +01:00
Sergio Martínez Portela
212c41d848 Avoid having Headlines look like links when clicked. 2022-11-04 00:34:32 +01:00
Sergio Martínez Portela
1f286a0a54 Add simple style for results box. 2022-11-04 00:34:32 +01:00
Sergio Martínez Portela
38e5f57eab Use node collapse markers with better font compatibility.
These are temporal. This way of working moves the titles around when they get
     collapsed/expanded, so I should probably change it at some point.
2022-11-04 00:34:32 +01:00
Sergio Martínez Portela
0ebda876f7 Update headline expansion based on document STARTUP config. 2022-11-01 18:42:59 +01:00
Sergio Martínez Portela
847e2cfd74 Don't crash when failing to update note_search DB. 2022-11-01 12:19:52 +01:00
Sergio Martínez Portela
613aa4c88f Make result-bot TODO tag more badge-like. 2022-11-01 12:19:34 +01:00
Sergio Martínez Portela
964e2501ee Mark focused links on click. 2022-11-01 12:19:20 +01:00
Sergio Martínez Portela
208a9b2e97 Show notes TO-DO on search, mark them as such. 2022-10-30 23:58:44 +01:00
Sergio Martínez Portela
b676e2f949 Do search on all indexed fields. 2022-10-30 23:44:31 +01:00
Sergio Martínez Portela
7d20941765 Use experimental trigram tokenizer in SQLite FTS. 2022-10-30 23:38:25 +01:00
Sergio Martínez Portela
486c88c583 Take full headline body for search function.
Using just `get_contents` doesn't get list items or tables.
2022-10-30 23:31:45 +01:00
Sergio Martínez Portela
c80ada2a40 Simple style changes around search box. 2022-10-26 23:17:36 +02:00
Sergio Martínez Portela
9053bf30f6 Fix premature return when TODOs found. 2022-10-26 22:45:50 +02:00
Sergio Martínez Portela
25a65253dd Show message when no results are found on search.
Also, lower debounce time to 250ms.
2022-10-26 22:42:24 +02:00
Sergio Martínez Portela
c8b3a99e7a Mark internal links on light style too. 2022-10-25 23:52:54 +02:00
Sergio Martínez Portela
d023955ee0 Skip links to private notes. 2022-10-25 23:45:30 +02:00
Sergio Martínez Portela
250bcde6d5 Set site language on homepage. 2022-10-25 23:36:10 +02:00
Sergio Martínez Portela
34d0d2ead3 Detect local links that result in a non-public note. 2022-10-25 23:35:46 +02:00
Sergio Martínez Portela
955dc433df Visually identify internal links. 2022-10-23 21:50:33 +02:00
Sergio Martínez Portela
2eab1b4351 Fix: don't hide text until custom font is loaded. 2022-10-23 21:40:10 +02:00
Sergio Martínez Portela
20b945aa31 Add usual task states to the default settings. 2022-10-23 21:30:44 +02:00
Sergio Martínez Portela
8d8dcbfdce Fix bare links next to [ or ] characters. 2022-10-23 21:30:30 +02:00
Sergio Martínez Portela
a6607ba0f3 WIP: Support bare links (no []). 2022-10-23 21:16:22 +02:00
Sergio Martínez Portela
3e9f323b56 Remove reference to git:// links. 2022-10-23 18:33:00 +02:00
Sergio Martínez Portela
4d3997bce1 Render * Target links. 2022-10-23 18:28:08 +02:00
Sergio Martínez Portela
ce8fd431b6 Allow rendering of links that require graph knowledge.
- Fix rendering of `./filename.org` links.
2022-10-23 18:22:43 +02:00
Sergio Martínez Portela
8dd624d339 Cleanup old notes on upload before building new ones. 2022-10-21 20:18:01 +02:00
Sergio Martínez Portela
a18c94dca0 Decide on SEARCH_ENDPOINT based on current URL. 2022-10-21 20:17:41 +02:00
Sergio Martínez Portela
f66d69776b Create directory before filling it in. 2022-10-21 00:23:50 +02:00
Sergio Martínez Portela
fcd9854a17 Disable collapsing top-level headline. 2022-10-20 23:44:35 +02:00
Sergio Martínez Portela
bcfd8b1d93 Reduce unused height on headlines. 2022-10-18 23:29:12 +02:00
Sergio Martínez Portela
de92f74867 Link to notes index from title. 2022-10-18 23:18:57 +02:00
Sergio Martínez Portela
d71c28d1e8 Show top-level title in seach, hide to-do's. 2022-10-18 23:13:21 +02:00
Sergio Martínez Portela
7bad44cfb6 Redirect from subnote to parent note. 2022-10-18 22:47:04 +02:00
Sergio Martínez Portela
a313597dcf Mark tags as [tag]. 2022-10-18 01:17:11 +02:00
Sergio Martínez Portela
df7f65fa00 Test different background color. 2022-10-18 01:16:30 +02:00
Sergio Martínez Portela
b214a8148a Prepare for note deployment. 2022-10-18 01:16:14 +02:00
Sergio Martínez Portela
7ddf926fa7 Lighten dark-mode links on homepage. 2022-10-17 00:10:40 +02:00
Sergio Martínez Portela
d81db05633 Show results on notes's search-box. 2022-10-17 00:10:06 +02:00
Sergio Martínez Portela
a616d903fb Implement basic popup creation for note search. 2022-10-04 23:55:57 +02:00
Sergio Martínez Portela
d7905f5b0a Add base for search UI. 2022-10-03 23:40:57 +02:00
Sergio Martínez Portela
7fdf378cee Add script to demo search-server's docker usage. 2022-10-03 00:52:11 +02:00
Sergio Martínez Portela
14d7d0f85e Initial search-server version. 2022-10-03 00:52:11 +02:00
Sergio Martínez Portela
bc3bf30669 Full-text-search with a Quick&Dirty SQLite DB. 2022-09-30 00:13:22 +02:00
Sergio Martínez Portela
8d136312b7 Add body max width. 2022-09-29 23:49:10 +02:00
Sergio Martínez Portela
2d6a994476 Add language as class on code block. 2022-09-29 23:48:56 +02:00
Sergio Martínez Portela
cc8d7d0bc5 Render note tables. 2022-09-28 00:04:06 +02:00
Sergio Martínez Portela
f6254a6c53 Add selectable port on autoserve script. 2022-09-26 23:44:22 +02:00
Sergio Martínez Portela
8ef3c5a636 Use Atkinson-Hyperlegible font. 2022-09-26 23:44:07 +02:00
Sergio Martínez Portela
9990c77964 Add new post to homepage. 2022-09-17 21:14:49 +02:00
Sergio Martínez Portela
e73a7b7062 Avoid line-breaks inside list items. 2022-09-02 00:33:42 +02:00
Sergio Martínez Portela
856ae09b16 feat: Show note tags. 2022-09-02 00:31:16 +02:00
Sergio Martínez Portela
2a90ea8a26 Test font-selection taken from Hackliza.org blog. 2022-08-30 00:44:33 +02:00
Sergio Martínez Portela
4a64a9c732 chore(org-rw): Update headline title handling. 2022-08-28 19:37:53 +02:00
Sergio Martínez Portela
b610d356e8 Render headlines expanded by default. 2022-08-28 14:10:08 +02:00
Sergio Martínez Portela
cf7cc38bd8 Render titles with markup. 2022-08-28 14:09:57 +02:00
Sergio Martínez Portela
89bf34ed46 Remove common indentation of code blocks. 2022-08-27 13:33:47 +02:00
Sergio Martínez Portela
74493fa79d Render tokens of item-list tags. 2022-08-27 13:32:40 +02:00
Sergio Martínez Portela
08a4dc295e [style] Color TODO states. 2022-08-23 21:30:46 +02:00
Sergio Martínez Portela
0bd897d62e [style] Center note top title. 2022-08-23 21:30:26 +02:00
Sergio Martínez Portela
2cbb4f60ad [style] Make note sections more integrated. 2022-08-23 21:30:04 +02:00
Sergio Martínez Portela
246d585213 Fix headline title selection when taking ID from document. 2022-08-23 00:05:03 +02:00
Sergio Martínez Portela
a4981632e5 Fix evaluation of git paths. 2022-08-20 19:17:08 +02:00
Sergio Martínez Portela
d40b49a027 Add title to rendered notes. 2022-08-20 18:21:26 +02:00
Sergio Martínez Portela
f1b84f5615 Add simple styling to root node. 2022-08-20 17:54:41 +02:00
Sergio Martínez Portela
2b18c6a9b3 Fix text paragraph separation. 2022-08-20 17:51:24 +02:00
Sergio Martínez Portela
18ceb6bca5 Make debug print_tree more comprehensive. 2022-08-20 17:51:24 +02:00
Sergio Martínez Portela
9231013ea9 Add simplistic reloading to notes rendering script. 2022-08-20 14:27:45 +02:00
Sergio Martínez Portela
399f00a54f Add index.html generation. 2022-08-20 13:59:15 +02:00
Sergio Martínez Portela
ef1d71dc2f Merge branch 'experiment/graph-explorer-generation' into develop 2022-08-19 19:39:42 +02:00
Sergio Martínez Portela
1280de0ff9 Merge branch 'experiment/blog-generation' into develop 2022-08-19 19:39:19 +02:00
Sergio Martínez Portela
768065165f Reduce link arrow sizes. 2022-08-19 19:37:53 +02:00
Sergio Martínez Portela
f5e8fb25f7 Fill page with chart. 2022-08-19 19:36:42 +02:00
Sergio Martínez Portela
cf524ecee4 Add styling to link titles. 2022-08-19 19:36:32 +02:00
Sergio Martínez Portela
5a9fbfb253 Don't fail on unknown link types. 2022-08-19 19:30:55 +02:00
Sergio Martínez Portela
3bf1a86c31 Fix path to graph-explorer. 2022-08-19 19:30:24 +02:00
Sergio Martínez Portela
f3096eb5d9 Draft post-index. 2022-08-03 21:11:09 +02:00
Sergio Martínez Portela
9483fc1ba4 Add minor style changes on homepage. 2022-07-27 23:52:01 +02:00
Sergio Martínez Portela
189a94c930 Add simple date, tags to posts. 2022-07-25 17:58:58 +02:00
Sergio Martínez Portela
feefd1f4d5 Improve autoserve logic, reduce repeated reloads. 2022-07-25 12:19:32 +02:00
Sergio Martínez Portela
8d831af216 Add blog/post header. 2022-07-25 12:19:21 +02:00
Sergio Martínez Portela
031305f5de Add style for inline code. 2022-07-24 23:56:29 +02:00
Sergio Martínez Portela
ba9eba887b Move dark-mode to monokai. 2022-07-24 23:12:43 +02:00
Sergio Martínez Portela
830c26e333 Add light syntax, using Pygments sas style. 2022-07-24 23:12:43 +02:00
Sergio Martínez Portela
29a9d25381 Add auto-wrapping media queries for CSS static resources. 2022-07-24 23:08:38 +02:00
Sergio Martínez Portela
31a303ab55 Cleaner code blocks in dark mode. 2022-07-22 18:16:48 +02:00
Sergio Martínez Portela
4ebbd47a61 Improve link contrast against background. 2022-07-22 17:58:28 +02:00
Sergio Martínez Portela
ef6072fd27 [minor] Fast styling touches to posts's dark mode. 2022-07-22 00:23:07 +02:00
Sergio Martínez Portela
612c38711d Add basic dark-mode style. 2022-07-21 23:30:47 +02:00
Sergio Martínez Portela
85e254d75c Explore more blog-like styling. 2022-07-21 23:28:02 +02:00
Sergio Martínez Portela
7afb6be68f Add simplistic autoserve/update mechanism. 2022-07-21 21:19:03 +02:00
Sergio Martínez Portela
a3ad99cb61 Add syntax-highlighting css. 2022-07-21 21:19:03 +02:00
Sergio Martínez Portela
33f22f5f04 Add new collaboration on Hackliza. 2022-07-15 00:02:21 +02:00
Sergio Martínez Portela
57ed8fa15c Add simple mechanism for fast re-render. 2022-06-27 21:02:33 +02:00
Sergio Martínez Portela
a8c4d6ef48 Add base Markdown blog structure. 2022-06-27 20:39:21 +02:00
51 changed files with 11971 additions and 247 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
_gen _gen
static/syntax.css

7
Makefile Normal file
View File

@ -0,0 +1,7 @@
all: static/syntax.css
static/syntax.css: static/light-syntax.css static/dark-syntax.css
cat static/light-syntax.css > $@
echo '@media (prefers-color-scheme: dark) { ' >> $@
cat static/dark-syntax.css >> $@
echo '}' >> $@

142
scripts/autoserve.py Normal file
View File

@ -0,0 +1,142 @@
#!/usr/bin/env python3
import sys
import http.server
import socketserver
import threading
import os
import time
import select
import urllib.parse
import inotify.adapters
PORT = int(os.getenv('PORT', 8000))
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
MONITORED_EVENT_TYPES = (
'IN_CREATE',
# 'IN_MODIFY',
'IN_CLOSE_WRITE',
'IN_DELETE',
'IN_MOVED_FROM',
'IN_MOVED_TO',
'IN_DELETE_SELF',
'IN_MOVE_SELF',
)
WAITING_RESPONSES = []
SLEEP_TIME = 0.5
COUNTER = 0
MAX_WAITS = 100
class Server(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path.strip('/') == '__wait_for_changes':
WAITING_RESPONSES.append(self)
print(len(WAITING_RESPONSES), "waiting responses")
global COUNTER
ticket, COUNTER = COUNTER, COUNTER + 1
while self in WAITING_RESPONSES:
# This is an horribe way to wait! ... but it may work for quick tests 🤷
if COUNTER - ticket > MAX_WAITS:
# Connection closed by the other side
print("Manually closed for cleanup")
WAITING_RESPONSES.remove(self)
# send 502 response, timeout
self.send_response(502)
# send response headers
self.end_headers()
return
time.sleep(SLEEP_TIME)
return
path = urllib.parse.unquote(self.path)
if path.strip('/') == '':
path = '/index.html'
if os.path.isdir(path.strip('/')):
if path.endswith('/'):
path = path.strip('/') + '/index.html'
else:
# Redirect to + /
self.send_response(301)
self.send_header('Location', path + '/')
self.end_headers()
return
if not os.path.exists(path.strip('/')):
self.send_response(404)
self.end_headers()
return
# send 200 response
self.send_response(200)
# send response headers
self.end_headers()
with open(path.strip('/'), 'rb') as f:
# send the body of the response
self.wfile.write(f.read())
if not path.endswith('.html'):
return
else:
# Append update waiter
with open(os.path.join(THIS_DIR, 'wait_for_update.js'), 'rb') as f:
new_data = b'<script>' + f.read() + b'</script>'
self.wfile.write(new_data)
new_data_len = len(new_data)
return
def notify_reloads():
while len(WAITING_RESPONSES) > 0:
# Close opened connections
res = WAITING_RESPONSES.pop(0)
try:
# send 200 response
res.send_response(200)
# send response headers
res.end_headers()
except Exception as e:
print("ERROR:", e)
global COUNTER
COUNTER = 0
def start_notifier():
notifier = inotify.adapters.InotifyTree(os.getcwd())
should_reload = False
for event in notifier.event_gen(yield_nones=True):
if event is None:
if should_reload:
print("Reloading!")
should_reload = False
notify_reloads()
continue
(ev, types, directory, file) = event
if any([type in MONITORED_EVENT_TYPES for type in types]):
print("Detected change!", types, directory, file)
should_reload = True
def serve():
Handler = Server
notifier_thread = threading.Thread(target=start_notifier)
with http.server.ThreadingHTTPServer(("127.0.0.1", PORT), Handler) as httpd:
print("serving at port", PORT)
notifier_thread.start()
httpd.serve_forever()
if __name__ == '__main__':
serve()

596
scripts/blog.py Normal file
View File

@ -0,0 +1,596 @@
#!/usr/bin/env python3
MARKDOWN_EXTENSION = '.md'
EXTENSIONS = [
MARKDOWN_EXTENSION,
]
MARKDOWN_EXTRA_FEATURES = [
# See more in: https://python-markdown.github.io/extensions/
'markdown.extensions.fenced_code',
'markdown.extensions.codehilite',
'markdown.extensions.extra',
]
import copy
import json
import logging
import sys
import os
import datetime
import shutil
import traceback
import time
import re
from typing import List
from bs4 import BeautifulSoup as bs4
import bs4 as BeautifulSoup
import jinja2
import inotify.adapters
import yaml
import markdown
from unidecode import unidecode
SUMMARIZE_MAX_TOKENS = 1000
ITEMS_IN_RSS = 50
NIKOLA_DATE_RE = re.compile(r'^([0-2]\d|30|31)\.(0\d|1[012])\.(\d{4}), (\d{1,2}):(\d{2})$')
COMPLETE_DATE_RE = re.compile(r'^(\d{4})-(0\d|1[012])-([0-2]\d|30|31) '
+ r'(\d{2}):(\d{2})(:\d{2})( .+)?$')
SLUG_HYPHENATE_RE = re.compile(r'[\s\-]+')
SLUG_REMOVE_RE = re.compile(r'[^\s\-a-zA-Z0-9]*')
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_PATH = os.path.join(ROOT_DIR, 'static')
ARTICLE_TEMPLATE_NAME = 'article.tmpl.html'
BLOG_INDEX_TEMPLATE_NAME = 'blog_index.tmpl.html'
CATEGORY_LIST_TEMPLATE_NAME = 'category_list.tmpl.html'
ARTICLE_LIST_TEMPLATE_NAME = 'article_list.tmpl.html'
RSS_TEMPLATE_NAME = 'rss.tmpl.xml'
BLOG_INDEX_PAGE_SIZE = 10
STATIC_RESOURCES = (
('style.css', 'css/style.css'),
('light-syntax.css', 'css/light-syntax.css'),
('dark-syntax.css', 'css/dark-syntax.css', ('@media (prefers-color-scheme: dark) {\n', '\n}')),
)
JINJA_ENV = jinja2.Environment(
loader=jinja2.FileSystemLoader(STATIC_PATH),
autoescape=jinja2.select_autoescape()
)
WATCH = True
if os.getenv('WATCH_AND_REBUILD', '1') == '0':
WATCH = False
def update_statics():
global ARTICLE_TEMPLATE
ARTICLE_TEMPLATE = JINJA_ENV.get_template(ARTICLE_TEMPLATE_NAME)
global BLOG_INDEX_TEMPLATE
BLOG_INDEX_TEMPLATE = JINJA_ENV.get_template(BLOG_INDEX_TEMPLATE_NAME)
global CATEGORY_LIST_TEMPLATE
CATEGORY_LIST_TEMPLATE = JINJA_ENV.get_template(CATEGORY_LIST_TEMPLATE_NAME)
global ARTICLE_LIST_TEMPLATE
ARTICLE_LIST_TEMPLATE = JINJA_ENV.get_template(ARTICLE_LIST_TEMPLATE_NAME)
global RSS_TEMPLATE
RSS_TEMPLATE = JINJA_ENV.get_template(RSS_TEMPLATE_NAME)
update_statics()
MONITORED_EVENT_TYPES = (
'IN_CREATE',
# 'IN_MODIFY',
'IN_CLOSE_WRITE',
'IN_DELETE',
'IN_MOVED_FROM',
'IN_MOVED_TO',
'IN_DELETE_SELF',
'IN_MOVE_SELF',
)
LANG_PRIORITY = ('en', 'es', 'gl')
def parse_nikola_date(match):
return datetime.datetime(year=int(match.group(3)),
month=int(match.group(2)),
day=int(match.group(1)),
hour=int(match.group(4)),
minute=int(match.group(5)),
# Note this final assumption is not good
# and might get you in trouble if trying
# to sort closely-published posts
# when others are in complete-date format
tzinfo=datetime.timezone.utc,
)
def parse_complete_date(match):
return datetime.datetime.strptime(match.group(0), '%Y-%m-%d %H:%M:%S %Z%z')
def split_tags(tags: str) -> List[str]:
if isinstance(tags, str):
return [tag.strip() for tag in tags.split(',')]
elif isinstance(tags, list):
return tags
else:
raise NotImplementedError("Unknown tag type: {}".format(type(tags)))
def slugify(title):
"""
Made for compatibility with Nikola's slugify within CodigoParaLlevar blog.
"""
slug = unidecode(title).lower()
slug = SLUG_REMOVE_RE.sub('', slug)
slug = SLUG_HYPHENATE_RE.sub('-', slug)
slug = slug.strip('-')
return slug.strip()
def read_markdown(path):
with open(path, 'rt') as f:
data = f.read()
if data.startswith('---'):
start = data.index('\n')
if '---\n' not in data[start:]:
raise Exception('Front matter not finished on: {}'.format(path))
front_matter_str, content = data[start:].split('---\n', 1)
front_matter = yaml.load(front_matter_str, Loader=yaml.SafeLoader)
else:
raise Exception('Front matter is needed for proper rendering. Not found on: {}'.format(
path
))
doc = markdown.markdown(content, extensions=MARKDOWN_EXTRA_FEATURES)
return doc, front_matter
def get_out_path(front_matter):
if 'date' in front_matter:
if m := NIKOLA_DATE_RE.match(front_matter['date']):
front_matter['date'] = parse_nikola_date(m)
elif m := COMPLETE_DATE_RE.match(front_matter['date']):
front_matter['date'] = parse_complete_date(m)
else:
raise NotImplementedError('Unknown date format: {}'.format(
front_matter['date']))
else:
raise Exception('No date found on: {}'.format(
path
))
if 'slug' not in front_matter:
if 'title' not in front_matter:
raise Exception('No title found on: {}'.format(
path
))
front_matter['slug'] = slugify(front_matter['title'])
out_path = os.path.join(str(front_matter['date'].year), front_matter['slug'])
if front_matter.get('lang', LANG_PRIORITY[0]) != LANG_PRIORITY[0]:
out_path = os.path.join(front_matter['lang'], str(front_matter['date'].year), front_matter['slug'])
return out_path
def load_all(top_dir_relative):
top = os.path.abspath(top_dir_relative)
docs = {}
count = 0
for root, dirs, files in os.walk(top):
for name in files:
if all([not name.endswith(ext) for ext in EXTENSIONS]):
# The logic is negative... but it works
continue
if name.endswith(MARKDOWN_EXTENSION):
path = os.path.join(root, name)
doc, front_matter = read_markdown(path)
out_path = get_out_path(front_matter)
docs[path] = (doc, front_matter, out_path)
print('\rLoading posts... {}'.format(count), end='', flush=True)
count += 1
else:
raise NotImplementedError('Unknown filetype: {}'.format(name))
print(" [DONE]")
return docs
def load_doc(filepath):
doc, front_matter = read_markdown(filepath)
out_path = get_out_path(front_matter)
return (doc, front_matter, out_path)
def render_article(doc, front_matter, f, out_path):
extsep = '/' if '/' in out_path else '\\'
subdirs = len(out_path.split(extsep))
base_path = os.path.join(*(['..'] * subdirs))
result = ARTICLE_TEMPLATE.render(
content=doc,
title=front_matter['title'],
post_publication_date=front_matter['date'],
post_tags=split_tags(front_matter['tags']),
base_path=base_path,
)
f.write(result)
def summarize(doc):
tree = bs4(doc, features='lxml')
html = list(tree.children)[0]
body = list(html.children)[0]
comments = tree.find_all(string=lambda text: isinstance(text, BeautifulSoup.Comment))
teaser_end = None
for comment in comments:
if 'TEASER_END' in comment:
teaser_end = comment
break
if 'gnucash' in doc:
assert teaser_end is not None
def recur_select_to_summarize(source, dest, num_tokens):
for item in source.children:
if num_tokens + len(item.text) < SUMMARIZE_MAX_TOKENS:
# All source fits
num_tokens += len(item.text)
dest.append(item)
else:
if not isinstance(item, BeautifulSoup.NavigableString):
# Let's take as much source as we can and then stop
subsect = bs4()
recur_select_to_summarize(item, subsect, num_tokens)
if len(list(subsect.children)) > 0:
dest.append(subsect)
break
def cut_after_element(reference):
while reference.next_sibling is None:
if reference.parent is None:
logging.warning("Reached root when looking for cutting point for teaser. Doc: {}".format(doc[:100]))
return
reference = reference.parent
nxt = reference.next_sibling
while nxt is not None:
was = nxt
if reference.next_sibling is not None:
# Move to the "right"
nxt = reference.next_sibling
else:
# Move "up and right"
nxt = reference.parent
if nxt is not None:
nxt = nxt.next_sibling
was.extract()
if teaser_end is None:
result = bs4()
recur_select_to_summarize(body, result, 0)
else:
summary = copy.copy(body)
comments = summary.find_all(string=lambda text: isinstance(text, BeautifulSoup.Comment))
teaser_end = None
for comment in comments:
if 'TEASER_END' in comment:
teaser_end = comment
break
assert teaser_end is not None, 'Error finding teaser end on copy'
cut_after_element(teaser_end)
result = bs4()
for child in summary.children:
result.append(child)
return result
def render_index(docs, dest_top):
# Collect all languages accepted for all docs
docs_by_slug = {}
for (doc, front_matter, out_path) in docs.values():
if front_matter['slug'] not in docs_by_slug:
docs_by_slug[front_matter['slug']] = {}
docs_by_slug[front_matter['slug']][front_matter.get('lang', LANG_PRIORITY[0])] = (doc, front_matter, out_path)
# Remove duplicated for langs with less priority
selected_docs = []
for (doc, front_matter, out_path) in docs.values():
langs = docs_by_slug[front_matter['slug']]
lang_priority = LANG_PRIORITY.index(front_matter.get('lang', LANG_PRIORITY[0]))
min_lang_priority = min([
LANG_PRIORITY.index(lang)
for lang in langs.keys()
])
if lang_priority == min_lang_priority:
selected_docs.append((doc, front_matter, out_path, langs))
docs = sorted(selected_docs, key=lambda x: x[1]['date'], reverse=True)
index_ranges = range(0, len(docs), BLOG_INDEX_PAGE_SIZE)
for off in index_ranges:
page = docs[off: off + BLOG_INDEX_PAGE_SIZE]
posts = [
{
"doc": doc,
"title": front_matter['title'],
"post_publication_date": front_matter['date'],
"post_tags": split_tags(front_matter['tags']),
"summary": summarize(doc),
"link": out_path.rstrip('/') + '/',
}
for (doc, front_matter, out_path, _alternatives) in page
]
prev_index_num = None
next_index_num = off // BLOG_INDEX_PAGE_SIZE + 1
if off > 0:
prev_index_num = off // BLOG_INDEX_PAGE_SIZE - 1
if next_index_num >= len(index_ranges):
next_index_num = None
result = BLOG_INDEX_TEMPLATE.render(
posts=posts,
prev_index_num=prev_index_num,
next_index_num=next_index_num,
)
if off == 0:
fname = 'index.html'
else:
fname = 'index-{}.html'.format(off // BLOG_INDEX_PAGE_SIZE)
with open(os.path.join(dest_top, fname), 'wt') as f:
f.write(result)
def render_categories(docs, dest_top):
categories = {}
for (doc, front_matter, out_path) in docs.values():
for tag in split_tags(front_matter['tags']):
if tag not in categories:
categories[tag] = []
categories[tag].append((doc, front_matter, out_path))
print("Found {} tags".format(len(categories), categories))
for tag, docs in categories.items():
docs = sorted(docs, key=lambda x: x[1]['date'], reverse=True)
posts = [
{
# "doc": doc,
"title": front_matter['title'],
"post_publication_date": front_matter['date'],
"post_tags": split_tags(front_matter['tags']),
# "summary": summarize(doc),
"link": out_path.rstrip('/') + '/',
}
for (doc, front_matter, out_path) in docs
]
result = CATEGORY_LIST_TEMPLATE.render(
posts=posts,
)
path = os.path.join(dest_top, "tags", tag.replace('/', '_'), "index.html")
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'wt') as f:
f.write(result)
def render_archive(docs, dest_top):
docs = sorted(docs.values(), key=lambda x: x[1]['date'], reverse=True)
posts = [
{
# "doc": doc,
"title": front_matter['title'],
"post_publication_date": front_matter['date'],
"post_tags": split_tags(front_matter['tags']),
# "summary": summarize(doc),
"link": out_path.rstrip('/') + '/',
}
for (doc, front_matter, out_path) in docs
]
result = ARTICLE_LIST_TEMPLATE.render(
posts=posts,
)
path = os.path.join(dest_top, "articles", "index.html")
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'wt') as f:
f.write(result)
def render_rss(docs, dest_top):
# Collect all languages accepted for all docs
docs_by_slug = {}
for (doc, front_matter, out_path) in docs.values():
if front_matter['slug'] not in docs_by_slug:
docs_by_slug[front_matter['slug']] = {}
docs_by_slug[front_matter['slug']][front_matter.get('lang', LANG_PRIORITY[0])] = (doc, front_matter, out_path)
# Remove duplicated for langs with less priority
selected_docs = []
for (doc, front_matter, out_path) in docs.values():
langs = docs_by_slug[front_matter['slug']]
lang_priority = LANG_PRIORITY.index(front_matter.get('lang', LANG_PRIORITY[0]))
min_lang_priority = min([
LANG_PRIORITY.index(lang)
for lang in langs.keys()
])
if lang_priority == min_lang_priority:
selected_docs.append((doc, front_matter, out_path, langs))
docs = sorted(selected_docs, key=lambda x: x[1]['date'], reverse=True)
posts = [
{
# "doc": doc,
"title": front_matter['title'],
"post_publication_date": front_matter['date'],
"post_tags": split_tags(front_matter['tags']),
"summary": summarize(doc),
"link": out_path.rstrip('/') + '/',
}
for (doc, front_matter, out_path, langs) in docs[:ITEMS_IN_RSS]
]
result = RSS_TEMPLATE.render(
posts=posts,
last_build_date=datetime.datetime.utcnow(),
)
path = os.path.join(dest_top, "rss.xml")
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'wt') as f:
f.write(result)
def regen_all(source_top, dest_top, docs=None):
if docs is None:
docs = load_all(source_top)
# Render posts
for (doc, front_matter, out_path) in docs.values():
doc_full_path = os.path.join(dest_top, out_path)
os.makedirs(os.path.dirname(doc_full_path), exist_ok=True)
# print("==", doc_full_path)
full_out_path = doc_full_path + '/index.html'
os.makedirs(os.path.dirname(full_out_path), exist_ok=True)
with open(full_out_path, 'wt') as f:
try:
render_article(doc, front_matter, f, out_path)
except:
logging.error(traceback.format_exc())
logging.error("Rendering failed 😿")
continue
# Render statics
for static in STATIC_RESOURCES:
src_path = static[0]
dest_path = static[1]
if len(static) > 2:
before, after = static[2]
else:
before, after = '', ''
target_dest = os.path.join(dest_top, dest_path)
os.makedirs(os.path.dirname(target_dest), exist_ok=True)
with open(os.path.join(STATIC_PATH, src_path), 'rt') as src:
data = before + src.read() + after
with open(target_dest, 'wt') as f:
f.write(data)
# Render index
render_index(docs, dest_top)
# Render categories
render_categories(docs, dest_top)
# Render archive
render_archive(docs, dest_top)
# Render RSS
render_rss(docs, dest_top)
return docs
def main(source_top, dest_top):
notifier = inotify.adapters.InotifyTrees([source_top, STATIC_PATH])
## Initial load
t0 = time.time()
logging.info("Initial load...")
docs = regen_all(source_top, dest_top)
logging.info("Initial load completed in {:.2f}s".format(time.time() - t0))
if not WATCH:
logging.info("Build completed in {:.2f}s".format(time.time() - t0))
return 0
## Updating
for event in notifier.event_gen(yield_nones=False):
(ev, types, directory, file) = event
if not any([type in MONITORED_EVENT_TYPES for type in types]):
continue
filepath = os.path.join(directory, file)
if filepath.startswith(STATIC_PATH):
t0 = time.time()
try:
update_statics()
except:
logging.error(traceback.format_exc())
logging.error("Loading new templates failed 😿")
continue
is_static_resource = False
for static in STATIC_RESOURCES:
src_path = static[0]
dest_path = static[1]
if file == os.path.basename(src_path):
is_static_resource = True
if len(static) > 2:
before, after = static[2]
else:
before, after = '', ''
target_dest = os.path.join(dest_top, dest_path)
os.makedirs(os.path.dirname(target_dest), exist_ok=True)
with open(os.path.join(STATIC_PATH, src_path), 'rt') as src:
data = before + src.read() + after
with open(target_dest, 'wt') as f:
f.write(data)
if is_static_resource:
logging.info("Updated static resources in {:.2f}s".format(time.time() - t0))
else:
docs = regen_all(source_top, dest_top, docs)
logging.info("Updated all in {:.2f}s".format(time.time() - t0))
else:
try:
print("Reloading: {}".format(filepath))
(doc, front_matter, out_path) = load_doc(filepath)
except:
logging.error(traceback.format_exc())
logging.error("Skipping update 😿")
continue
t0 = time.time()
docs[filepath] = (doc, front_matter, out_path)
doc_full_path = os.path.join(dest_top, out_path)
print("Updated: {}.html".format(doc_full_path))
os.makedirs(os.path.dirname(doc_full_path + '/index.html'), exist_ok=True)
# print("==", doc_full_path)
with open(doc_full_path + '/index.html', 'wt') as f:
try:
render_article(doc, front_matter, f, out_path)
render_archive(docs, dest_top)
except:
logging.error(traceback.format_exc())
logging.error("Rendering failed 😿")
continue
logging.info("Updated all in {:.2f}s".format(time.time() - t0))
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: {} SOURCE_TOP DEST_TOP".format(sys.argv[0]))
exit(0)
logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
main(sys.argv[1], sys.argv[2])

View File

@ -1,40 +0,0 @@
#!/usr/bin/env python3
import xapian
import sys
import os
import json
def main(path, query):
db = xapian.Database(path, xapian.DB_OPEN)
docid_map_path = os.path.join(path, "docid_map.json")
with open(docid_map_path, 'rt') as f:
docid_to_node = json.load(f)
qp = xapian.QueryParser()
stemmer = xapian.Stem("english")
qp.set_stemmer(stemmer)
qp.set_database(db)
qp.set_stemming_strategy(xapian.QueryParser.STEM_SOME)
xap_query = qp.parse_query(query)
print("Parsed query is: {}".format(xap_query))
enquire = xapian.Enquire(db)
enquire.set_query(xap_query)
matches = enquire.get_mset(0, 10)
for match in matches:
print(
"ID {} {}% | DocId: {}".format(
match.rank + 1,
match.percent,
docid_to_node[str(match.document.get_docid())],
)
)
if __name__ == '__main__':
if len(sys.argv) != 3:
print("Brain-Query")
print("Usage: {} <path> <query>".format(sys.argv[0]))
exit(0)
main(sys.argv[1], sys.argv[2])

View File

@ -0,0 +1,165 @@
import subprocess
import ops_cache
import copy
import tempfile
import os
@ops_cache.cache
def gen(headline_id, graph, doc_to_headline_remapping):
reference_node = headline_id
font_name = 'monospace'
linked_from_internal = set()
g = copy.deepcopy(graph)
if 'id:' + reference_node in doc_to_headline_remapping:
reference_node = doc_to_headline_remapping['id:' + reference_node].split(':', 1)[1]
centered_graph = { reference_node: g[reference_node] }
for l in g[reference_node]['links']:
lt = l['target']
if lt.startswith("id:"):
lt = lt[3:]
linked_from_internal.add(lt)
del g[reference_node]
new_nodes = True
in_emacs_tree = {
reference_node: set(),
}
while new_nodes:
new_nodes = False
removed = set()
for k, v in g.items():
if 'id:' + k in doc_to_headline_remapping:
k = doc_to_headline_remapping['id:' + k].split(':', 1)[1]
for link in v["links"]:
if link["target"].startswith("id:"):
link["target"] = link["target"][3:]
if link['target'] in centered_graph and link.get('relation') == 'in':
centered_graph[k] = v
for l in v["links"]:
if l.get('relation') == 'in':
t = l['target']
if t.startswith("id:"):
t = t[3:]
if '[' in t:
# Special case, to be handled on org_rw
continue
if t not in in_emacs_tree:
in_emacs_tree[t] = set()
in_emacs_tree[t].add(k)
v['links'] = [
l for l in v["links"]
if l.get('relation') != 'in'
]
for l in v['links']:
lt = l['target']
if lt.startswith("id:"):
lt = lt[3:]
linked_from_internal.add(lt)
removed.add(k)
new_nodes = True
break
for k in removed:
del g[k]
in_emacs = set(centered_graph.keys())
# One more round for the rest, not requiring "in"
for k, v in g.items():
if 'id:' + k in doc_to_headline_remapping:
k = doc_to_headline_remapping['id:' + k].split(':', 1)[1]
backlinked = False
for link in v["links"]:
if link["target"].startswith("id:"):
link["target"] = link["target"][3:]
if link['target'] in in_emacs:
centered_graph[k] = v
backlinked = True
removed.add(k)
if not backlinked and (k in linked_from_internal):
centered_graph[k] = v
removed.add(k)
g = centered_graph
with tempfile.NamedTemporaryFile(suffix='.dot', mode='wt') as f:
f.write('strict digraph {\n')
f.write('maxiter=10000\n')
f.write('splines=curved\n')
# f.write('splines=spline\n') # Not supported with edges to cluster
f.write('node[shape=rect, width=0.5, height=0.5]\n')
f.write('K=0.3\n')
f.write('edge[len = 1]\n')
def draw_subgraph(node_id, depth):
f.write("subgraph cluster_{} {{\n".format(node_id.replace("-", "_")))
f.write(' URL="./{}.node.html"\n'.format(node_id))
f.write(' class="{}"\n'.format('cluster-depth-' + str(depth - 1)))
f.write(" fontname=\"{}\"\n".format(font_name))
f.write(" label=\"{}\"\n".format(g[node_id]['title'].replace("\"", "'")))
f.write("\n")
# print("T: {}".format(in_emacs_tree), file=sys.stderr)
for k in in_emacs_tree[node_id]:
v = g[k]
if k in in_emacs_tree:
draw_subgraph(k, depth=depth + 1)
else:
print(" _" + k.replace("-", "_")
+ "[label=\"" + v["title"].replace("\"", "'") + "\", "
+ "URL=\"" + k + ".node.html\", "
+ "fontname=\"" + font_name + "\", "
+ "class=\"cluster-depth-" + str(depth) + "\""
+ "];", file=f)
f.write("\n}\n")
draw_subgraph(reference_node, 1)
for k, v in g.items():
if k not in in_emacs:
print("_" + k.replace("-", "_")
+ "[label=\"" + v["title"].replace("\"", "'") + "\", "
+ "fontname=\"" + font_name + "\", "
+ "URL=\"" + k + ".node.html\"];", file=f)
for k, v in g.items():
link_src = '_' + k.replace("-", "_")
if k in in_emacs_tree:
link_src = 'cluster_{}'.format(k.replace("-", "_"))
for link in v["links"]:
if link["target"].startswith("id:"):
link["target"] = link["target"][3:]
if '[' in link['target']:
# Special case, to be handled on org_rw
continue
if link['target'] not in g:
# Irrelevant
continue
if link['target'] in in_emacs_tree:
t = 'cluster_{}'.format(link['target'].replace("-", "_"))
else:
t = "_" + link["target"].replace("-", "_")
print(link_src + "->" + t, file=f)
f.write('}\n')
f.flush()
with tempfile.NamedTemporaryFile(suffix='.svg') as fsvg:
subprocess.call(['fdp', f.name, '-Tsvg', '-o', fsvg.name])
fsvg.seek(0)
return fsvg.read().decode()

View File

@ -1,14 +1,20 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sqlite3
import time
import json import json
import html import html
import logging import logging
import os import os
import sys import sys
import uuid import uuid
import xapian
import shutil
from datetime import datetime from datetime import datetime
import traceback
import re
from itertools import chain
import shutil
import inotify.adapters
import org_rw import org_rw
from org_rw import OrgTime, dom, Link from org_rw import OrgTime, dom, Link
@ -16,12 +22,79 @@ from org_rw import dump as dump_org
from org_rw import load as load_org from org_rw import load as load_org
from org_rw import token_list_to_raw from org_rw import token_list_to_raw
import pygments
import pygments.lexers
import pygments.formatters
import gen_centered_graph
# Set custom states
for state in ("NEXT", "MEETING", "Q", "PAUSED", "SOMETIME", "TRACK", "WAITING"):
org_rw.DEFAULT_TODO_KEYWORDS.append(state)
for state in ("DISCARDED", "VALIDATING"):
org_rw.DEFAULT_DONE_KEYWORDS.append(state)
EXTENSIONS = [ EXTENSIONS = [
".org", ".org",
".org.txt", ".org.txt",
] ]
IMG_EXTENSIONS = set([
"svg",
"png",
"jpg",
"jpeg",
"gif",
])
SKIPPED_TAGS = set(['attach'])
DEFAULT_SUBPATH = "public"
WATCH = True
if os.getenv('WATCH_AND_REBUILD', '1') == '0':
WATCH = False
MIN_HIDDEN_HEADLINE_LEVEL = 2 MIN_HIDDEN_HEADLINE_LEVEL = 2
INDEX_ID = os.getenv("INDEX_ID", "ea48ec1d-f9d4-4fb7-b39a-faa7b6e2ba95")
SITE_NAME = "Código para llevar"
MONITORED_EVENT_TYPES = (
'IN_CREATE',
# 'IN_MODIFY',
'IN_CLOSE_WRITE',
'IN_DELETE',
'IN_MOVED_FROM',
'IN_MOVED_TO',
'IN_DELETE_SELF',
'IN_MOVE_SELF',
)
TEXT_OR_LINK_RE = re.compile(r'([^\s\[\]]+|.)')
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_PATH = os.path.join(ROOT_DIR, 'static')
class NonExistingLocalNoteError(AssertionError):
def __init__(self, note_id, src_headline):
AssertionError.__init__(self)
self.note_id = note_id
self.src_headline = src_headline
def get_message(self):
return ("Cannot follow link to '{}' on headline '{}' ({})"
.format(self.note_id,
self.src_headline.id,
self.src_headline.title.get_text().strip()))
def is_git_path(path):
return any([chunk == ".git" for chunk in path.split(os.sep)])
def create_db(path):
if os.path.exists(path):
os.unlink(path)
db = sqlite3.connect(path)
db.execute('CREATE VIRTUAL TABLE note_search USING fts5(note_id, title, body, top_level_title, is_done, is_todo, tokenize="trigram");')
return db
def load_all(top_dir_relative): def load_all(top_dir_relative):
top = os.path.abspath(top_dir_relative) top = os.path.abspath(top_dir_relative)
@ -48,26 +121,46 @@ def load_all(top_dir_relative):
logging.info("Collected {} files".format(len(docs))) logging.info("Collected {} files".format(len(docs)))
return docs return docs
def regen_all(src_top, dest_top, subpath, *, docs=None, db=None):
def main(src_top, dest_top):
docs = load_all(src_top)
files_generated = 0 files_generated = 0
cur = db.cursor()
cleaned_db = False
try:
cur.execute('DELETE FROM note_search;')
cleaned_db = True
except sqlite3.OperationalError as err:
if WATCH:
logging.warning("Error pre-cleaning DB, search won't be updated")
else:
raise
docs = load_all(src_top)
base_dirs = set()
doc_to_headline_remapping = {} doc_to_headline_remapping = {}
os.makedirs(dest_top, exist_ok=True) os.makedirs(dest_top, exist_ok=True)
graph = {}
## Build headline list
# This includes a virtual headline for ID-referenced documents.
all_headlines = []
main_headlines_by_path = {}
main_headline_to_docid = {}
for doc in docs: for doc in docs:
relpath = os.path.relpath(doc.path, src_top) relpath = os.path.relpath(doc.path, src_top)
changed = False changed = False
headlines = list(doc.getAllHeadlines()) headlines = list(doc.getAllHeadlines())
related = None related = None
if not relpath.startswith(subpath + "/"):
# print("Skip:", relpath)
continue
base_dirs.add(os.path.dirname(relpath))
i = len(headlines) i = len(headlines)
while i > 0: while i > 0:
i -= 1 i -= 1
headline = headlines[i] headline = headlines[i]
if headline.title.strip().lower() == "related" and headline.depth == 1: if headline.title.get_text().strip().lower() == "related" and headline.depth == 1:
if related is not None: if related is not None:
print( print(
"Found duplicated related: {} vs {}".format( "Found duplicated related: {} vs {}".format(
@ -87,10 +180,7 @@ def main(src_top, dest_top):
print("Updated", relpath) print("Updated", relpath)
save_changes(doc) save_changes(doc)
if not relpath.startswith("public/"): all_headlines.extend(headlines)
# print("Skip:", relpath)
continue
main_headline = None main_headline = None
topHeadlines = doc.getTopHeadlines() topHeadlines = doc.getTopHeadlines()
@ -98,12 +188,10 @@ def main(src_top, dest_top):
or (len(topHeadlines) == 2 and related is not None)): or (len(topHeadlines) == 2 and related is not None)):
main_headline = [h for h in topHeadlines if h != related][0] main_headline = [h for h in topHeadlines if h != related][0]
main_headlines_by_path[doc.path] = main_headline
if doc.id is not None: if doc.id is not None:
endpath = os.path.join(dest_top, doc.id + ".node.html")
with open(endpath, "wt") as f:
doc_to_headline_remapping['id:' + doc.id] = 'id:' + main_headline.id doc_to_headline_remapping['id:' + doc.id] = 'id:' + main_headline.id
main_headline_to_docid[main_headline.id] = doc.id
f.write(as_document(render(main_headline, doc, headlineLevel=0)))
files_generated += 1 files_generated += 1
elif doc.id is not None: elif doc.id is not None:
logging.error("Cannot render document from id: {}. {} headlines {} related".format( logging.error("Cannot render document from id: {}. {} headlines {} related".format(
@ -112,9 +200,10 @@ def main(src_top, dest_top):
'with' if related is not None else 'without' 'with' if related is not None else 'without'
)) ))
for headline in headlines: # Build graph
endpath = os.path.join(dest_top, headline.id + ".node.html") graph = {}
backlink_graph = {}
for headline in all_headlines:
links = [] links = []
headline_links = list(headline.get_links()) headline_links = list(headline.get_links())
if headline == main_headline and related is not None: if headline == main_headline and related is not None:
@ -140,7 +229,7 @@ def main(src_top, dest_top):
elif l.value.startswith('./'): elif l.value.startswith('./'):
pass # TODO: Properly handle pass # TODO: Properly handle
else: else:
raise NotImplementedError('On document {}, link to {}'.format(doc.path, l.value)) logging.warning('On document {}, unknown link to {}'.format(doc.path, l.value))
if headline.parent: if headline.parent:
if isinstance(headline.parent, org_rw.Headline): if isinstance(headline.parent, org_rw.Headline):
@ -148,15 +237,41 @@ def main(src_top, dest_top):
"target": headline.parent.id, "target": headline.parent.id,
"relation": "in" "relation": "in"
}) })
for backlink in links:
if 'relation' in backlink and backlink['relation'] == 'in':
continue
target = backlink['target']
if target.startswith('id:'):
target = target[len('id:'):]
if target not in backlink_graph:
backlink_graph[target] = set()
backlink_graph[target].add(headline.id)
graph[headline.id] = { graph[headline.id] = {
"title": headline.title.strip(), "title": org_rw.org_rw.token_list_to_plaintext(headline.title.contents).strip(),
"links": links, "links": links,
"depth": headline.depth, "depth": headline.depth,
} }
if headline.id in main_headline_to_docid:
graph[main_headline_to_docid[headline.id]] = graph[headline.id]
with open(endpath, "wt") as f: topLevelHeadline = headline
f.write(as_document(render(headline, doc, headlineLevel=0))) while isinstance(topLevelHeadline.parent, org_rw.Headline):
files_generated += 1 topLevelHeadline = topLevelHeadline.parent
# Save for full-text-search
cur.execute('''INSERT INTO note_search(note_id, title, body, top_level_title, is_done, is_todo) VALUES (?, ?, ?, ?, ?, ?);''',
(
headline.id,
headline.title.get_text(),
'\n'.join(headline.doc.dump_headline(headline, recursive=False)),
topLevelHeadline.title.get_text(),
headline.is_done,
headline.is_todo,
))
# Update graph, replace document ids with headline ids # Update graph, replace document ids with headline ids
for headline_data in graph.values(): for headline_data in graph.values():
@ -164,6 +279,17 @@ def main(src_top, dest_top):
if link['target'] in doc_to_headline_remapping: if link['target'] in doc_to_headline_remapping:
link['target'] = doc_to_headline_remapping[link['target']] link['target'] = doc_to_headline_remapping[link['target']]
# Remap document ids backlinks to main headlines
for doc_id, main_headline_id in doc_to_headline_remapping.items():
if doc_id.startswith('id:'):
doc_id = doc_id[len('id:'):]
if main_headline_id.startswith('id:'):
main_headline_id = main_headline_id[len('id:'):]
for backlink in backlink_graph.get(doc_id, []):
if main_headline_id not in backlink_graph:
backlink_graph[main_headline_id] = set()
backlink_graph[main_headline_id].add(backlink)
# Output graph files # Output graph files
graphpath = os.path.join(dest_top, "graph.json") graphpath = os.path.join(dest_top, "graph.json")
graph_explorer_path = os.path.join(dest_top, "graph.html") graph_explorer_path = os.path.join(dest_top, "graph.html")
@ -171,162 +297,444 @@ def main(src_top, dest_top):
json.dump(obj=graph, fp=f, indent=2) json.dump(obj=graph, fp=f, indent=2)
graph_explorer_path = os.path.join(dest_top, "graph.html") graph_explorer_path = os.path.join(dest_top, "graph.html")
with open(graph_explorer_path, 'wt') as f: with open(graph_explorer_path, 'wt') as f:
with open(os.path.join(os.path.dirname(os.path.abspath(dest_top)), 'static', 'graph_explorer.html'), 'rt') as template: with open(os.path.join(os.path.dirname(os.path.abspath(dest_top)), '..', 'static', 'graph_explorer.html'), 'rt') as template:
source = template.read() source = template.read()
f.write(source.replace('<!-- REPLACE_THIS_WITH_GRAPH -->', f.write(source.replace('<!-- REPLACE_THIS_WITH_GRAPH -->',
json.dumps(graph))) json.dumps(graph)))
logging.info("Generated {} files".format(files_generated)) logging.info("Generated {} files".format(files_generated))
# Generate index files # Render docs after we've built the graph
t0 = datetime.utcnow() # Render main headlines
logging.info("Generating text index...") full_graph_info = { "nodes": graph, "backlinks": backlink_graph, "main_headlines": main_headlines_by_path }
for _docpath, main_headline in main_headlines_by_path.items():
if main_headline.doc.id:
endpath = os.path.join(dest_top, main_headline.doc.id + ".node.html")
with open(endpath, "wt") as f:
f.write(render_as_document(main_headline, main_headline.doc, headlineLevel=0, graph=full_graph_info,
doc_to_headline_remapping=doc_to_headline_remapping,
title=org_rw.token_list_to_plaintext(main_headline.title.contents)))
xapian_db = os.path.join(dest_top, "xapian") # Render all headlines
if os.path.exists(xapian_db): for headline in all_headlines:
shutil.rmtree(xapian_db) endpath = os.path.join(dest_top, headline.id + ".node.html")
db = xapian.WritableDatabase(xapian_db, xapian.DB_CREATE)
indexer = xapian.TermGenerator() # Render HTML
stemmer = xapian.Stem("english") with open(endpath, "wt") as f:
indexer.set_stemmer(stemmer) f.write(render_as_document(headline, headline.doc, headlineLevel=0, graph=full_graph_info,
doc_to_headline_remapping=doc_to_headline_remapping,
title=org_rw.token_list_to_plaintext(headline.title.contents)))
files_generated += 1
docid_to_node = {} if headline.id == INDEX_ID:
index_endpath = os.path.join(dest_top, "index.html")
with open(index_endpath, "wt") as f:
f.write(render_as_document(headline, headline.doc, headlineLevel=0, graph=full_graph_info,
doc_to_headline_remapping=doc_to_headline_remapping,
title=org_rw.token_list_to_plaintext(headline.title.contents)))
files_generated += 1
for doc in docs: cur.close()
relpath = os.path.relpath(doc.path, src_top) db.commit()
if not relpath.startswith("public/"): logging.info("Copying attachments")
# print("Skip:", relpath) attachments_dir = os.path.join(dest_top, 'attachments')
os.makedirs(attachments_dir, exist_ok=True)
for base in base_dirs:
data_dir = os.path.join(src_top, base, 'data')
logging.info("Copying attachments from: {}".format(data_dir))
if not os.path.exists(data_dir):
continue continue
for subdir in os.listdir(data_dir):
changed = False shutil.copytree(os.path.join(data_dir, subdir),
for hl in doc.getAllHeadlines(): os.path.join(attachments_dir, subdir),
xapian_doc = xapian.Document() dirs_exist_ok=True)
content = "\n".join(doc.dump_headline(hl))
xapian_doc.set_data(content)
indexer.set_document(xapian_doc)
indexer.index_text(content)
doc_id = db.add_document(xapian_doc)
docid_to_node[doc_id] = { 'hl': hl.id, 'doc': doc.path }
docid_map_path = os.path.join(xapian_db, "docid_map.json")
with open(docid_map_path, 'wt') as f:
json.dump(docid_to_node, f)
logging.info("Text index generated in {}".format(datetime.utcnow() - t0))
def print_tree(tree, indentation=0): def main(src_top, dest_top, subpath):
notifier = inotify.adapters.InotifyTrees([src_top, STATIC_PATH])
## Initial load
t0 = time.time()
os.makedirs(dest_top, exist_ok=True)
db = create_db(os.path.join(dest_top, 'db.sqlite3'))
docs = regen_all(src_top, dest_top, subpath=subpath, db=db)
if not WATCH:
logging.info("Build completed in {:.2f}s".format(time.time() - t0))
return 0
logging.info("Initial load completed in {:.2f}s".format(time.time() - t0))
## Updating
for event in notifier.event_gen(yield_nones=False):
(ev, types, directory, file) = event
if not any([type in MONITORED_EVENT_TYPES for type in types]):
continue
if is_git_path(directory):
continue
filepath = os.path.join(directory, file)
print("CHANGED: {}".format(filepath))
t0 = time.time()
try:
docs = regen_all(src_top, dest_top, subpath=subpath, docs=docs, db=db)
except:
logging.error(traceback.format_exc())
logging.error("Loading new templates failed 😿")
continue
logging.info("Updated all in {:.2f}s".format(time.time() - t0))
def get_headline_with_name(target_name, doc):
target_name = target_name.strip()
for headline in doc.getAllHeadlines():
if headline.title.get_text().strip() == target_name:
return headline
return None
def assert_id_exists(id, src_headline, graph):
if id not in graph["nodes"]:
raise NonExistingLocalNoteError(id, src_headline)
def print_tree(tree, indentation=0, headline=None):
# if headline and headline.id != INDEX_ID:
# return
return return
for element in tree: for element in tree:
print(" " * indentation + "- " + str(type(element)))
if "children" in dir(element): if "children" in dir(element):
if len(element.children) > 0: if len(element.children) > 0:
print_tree(element.children, indentation + 1) print_element(element.children, indentation + 1, headline)
print() print()
elif "content" in dir(element):
for content in element.content:
print_element(content, indentation + 1, headline)
def render_property_drawer(element, acc): def print_element(element, indentation, headline):
if isinstance(element, org_rw.Link):
print(" " * indentation, "Link:", element.get_raw())
elif isinstance(element, str):
print(" " * indentation, "{" + element + "}", type(element))
else:
print_tree(element, indentation, headline)
def render_property_drawer(element, acc, headline, graph):
pass pass
def render_logbook_drawer(element, acc): def render_logbook_drawer(element, acc, headline, graph):
pass pass
def render_property_node(element, acc): def render_property_node(element, acc, headline, graph):
pass pass
def render_list_group(element, acc): def render_list_group(element, acc, headline, graph):
acc.append("<ul>") acc.append("<ul>")
render_tree(element.children, acc) render_tree(element.children, acc, headline, graph)
acc.append("</ul>") acc.append("</ul>")
def render_table(element, acc, headline, graph):
acc.append("<table>")
render_tree(element.children, acc, headline, graph)
acc.append("</table>")
def render_list_item(element, acc): def render_table_row(element, acc, headline, graph):
acc.append("<tr>")
for cell in element.cells:
acc.append("<td>")
acc.append(html.escape(cell))
acc.append("</td>")
acc.append("</tr>")
def render_table_separator_row(element, acc, headline, graph):
acc.append("<tr class='__table-separator'></tr>")
def render_list_item(element, acc, headline, graph):
acc.append("<li>") acc.append("<li>")
if element.tag is not None: if element.tag is not None:
acc.append("<span class='tag'>") acc.append("<span class='tag'>")
acc.append(html.escape(element.tag)) render_text_tokens(element.tag, acc, headline, graph)
acc.append("</span>") acc.append("</span>")
acc.append("<span class='item'>") acc.append("<span class='item'>")
render_text_tokens(element.content, acc) render_text_tokens(element.content, acc, headline, graph)
acc.append("</span></li>") acc.append("</span></li>")
def render_block(content, acc, _class, is_code):
acc.append('<pre class="{}">'.format(_class))
if is_code:
acc.append('<code>')
def render_code_block(element, acc): # Remove indentation common to all lines
acc.append('<pre><code>') acc.append(unindent(content))
acc.append(html.escape(element.lines)) if is_code:
acc.append('</code></pre>') acc.append('</code>')
acc.append('</pre>')
def render_results_block(element, acc): def unindent(content):
# TODO: base_indentation = min([
# acc.append('<pre class="results"><code>') len(l) - len(l.lstrip(' '))
# render_tree(element.children, acc) for l in content.split('\n')
# acc.append('</code></pre>') if len(l.strip()) > 0
])
content_lines = [
l[base_indentation:]
for l in content.split('\n')
]
return '\n'.join(content_lines)
def render_code_block(element, acc, headline, graph):
code = element.lines
if element.arguments is not None and len(element.arguments) > 0 :
try:
lexer = pygments.lexers.get_lexer_by_name(element.arguments.split()[0], stripall=True)
content = pygments.highlight(unindent(code),
lexer,
pygments.formatters.HtmlFormatter()
)
acc.append(content)
return
except pygments.util.ClassNotFound:
pass pass
logging.error("Cannot find lexer for {}".format(element.subtype.lower()))
content = html.escape(code)
render_block(content, acc, _class='code ' + element.subtype.lower(), is_code=True)
def render_text(element, acc): def render_results_block(element, acc, headline, graph):
acc.append('<span class="text">') items = [e.get_raw() for e in element.children]
render_text_tokens(element.content, acc) content = '\n'.join(items)
acc.append('</span>') if len(content.strip()) > 0:
render_block(content, acc, _class='results lang-text', is_code=False)
def render_text_tokens(tokens, acc): def render_org_text(element, acc, headline, graph):
as_dom = org_rw.text_to_dom(element.contents, element)
render_text_tokens(as_dom, acc, headline, graph)
def render_text(element, acc, headline, graph):
acc.append('<div class="text">')
render_text_tokens(element.content, acc, headline, graph)
acc.append('</div>')
def render_text_tokens(tokens, acc, headline, graph):
acc.append('<p>')
if isinstance(tokens, org_rw.Text):
tokens = tokens.contents
for chunk in tokens: for chunk in tokens:
if isinstance(chunk, str): if isinstance(chunk, str):
acc.append('{}</span> '.format(chunk)) lines = chunk.split('\n\n')
contents = []
for line in lines:
line_chunks = []
for word in TEXT_OR_LINK_RE.findall(line):
if '://' in word and not (word.startswith('org-protocol://')):
if not (word.startswith('http://')
or word.startswith('https://')
or word.startswith('ftp://')
or word.startswith('ftps://')
):
logging.warning('Is this a link? {} (on {})\nLine: {}\nChunks: {}'.format(word, headline.doc.path, line, line_chunks))
line_chunks.append(html.escape(word))
else:
line_chunks.append('<a href="{url}" class="external">{description}</a>'
.format(url=word,
description=html.escape(word)))
else:
line_chunks.append(html.escape(word))
contents.append(' '.join(line_chunks))
acc.append('<span class="line">{}</span>'.format('</p><p>'.join(contents)))
elif isinstance(chunk, Link): elif isinstance(chunk, Link):
link_target = chunk.value link_target = chunk.value
if link_target.startswith('id:'): is_internal_link = True
link_target = './' + link_target[3:] + '.node.html'
description = chunk.description description = chunk.description
if description is None: if description is None:
description = chunk.value description = chunk.value
acc.append('<a href="{}">{}</a>'.format( try:
if link_target.startswith('id:'):
assert_id_exists(link_target[3:], headline, graph)
link_target = './' + link_target[3:] + '.node.html'
elif link_target.startswith('./') or link_target.startswith('../'):
if '::' in link_target:
logging.warning('Not implemented headline links to other files. Used on {}'.format(link_target))
else:
target_path = os.path.abspath(os.path.join(os.path.dirname(headline.doc.path), link_target))
if target_path not in graph['main_headlines']:
logging.warning('Link to doc not in graph: {}'.format(target_path))
else:
assert_id_exists(graph['main_headlines'][target_path].id, headline, graph)
link_target = './' + graph['main_headlines'][target_path].id + '.node.html'
elif link_target.startswith('attachment:'):
inner_target = link_target.split(':', 1)[1]
link_target = 'attachments/{}/{}/{}'.format(headline.id[:2], headline.id[2:], inner_target)
logging.warning('Not implemented `attachment:` links. Used on {}'.format(link_target))
elif link_target.startswith('* '):
target_headline = get_headline_with_name(link_target.lstrip('* '), headline.doc)
if target_headline is None:
logging.warning('No headline found corresponding to {}. On file {}'.format(link_target, headline.doc.path))
else:
assert_id_exists(target_headline.id, headline, graph)
link_target = './' + target_headline.id + '.node.html'
else:
is_internal_link = False
if link_target.startswith('orgit-rev'):
raise NonExistingLocalNoteError(link_target, headline)
elif link_target.startswith('file:'):
raise NonExistingLocalNoteError(link_target, headline)
elif not (
link_target.startswith('https://')
or link_target.startswith('http://')
or link_target.startswith('/')
):
raise NotImplementedError('Unknown link type: {}'
.format(link_target))
if link_target.rsplit('.', 1)[-1].lower() in IMG_EXTENSIONS:
acc.append('<a href="{}" class="img img-{}" ><img src="{}" /></a>'.format(
html.escape(link_target), html.escape(link_target),
'internal' if is_internal_link else 'external',
html.escape(link_target),
))
else:
acc.append('<a href="{}" class="{}" >{}</a>'.format(
html.escape(link_target),
'internal' if is_internal_link else 'external',
html.escape(description), html.escape(description),
)) ))
# else: except NonExistingLocalNoteError as err:
# raise NotImplementedError('TextToken: {}'.format(chunk)) logging.warning(err.get_message())
acc.append(html.escape(description))
elif isinstance(chunk, org_rw.MarkerToken):
tag = '<'
if chunk.closing:
tag += '/'
tag += {
org_rw.MarkerType.BOLD_MODE: 'strong',
org_rw.MarkerType.CODE_MODE: 'code',
org_rw.MarkerType.ITALIC_MODE: 'em',
org_rw.MarkerType.STRIKE_MODE: 's',
org_rw.MarkerType.UNDERLINED_MODE: 'span class="underlined"' if not chunk.closing else 'span',
org_rw.MarkerType.VERBATIM_MODE: 'span class="verbatim"' if not chunk.closing else 'span',
}[chunk.tok_type]
tag += '>'
acc.append(tag)
else:
raise NotImplementedError('TextToken: {}'.format(chunk))
acc.append('</p>')
def render_tag(element, acc): def render_tag(element, acc, headline, graph):
return { return {
dom.PropertyDrawerNode: render_property_drawer, dom.PropertyDrawerNode: render_property_drawer,
dom.LogbookDrawerNode: render_logbook_drawer, dom.LogbookDrawerNode: render_logbook_drawer,
dom.PropertyNode: render_property_node, dom.PropertyNode: render_property_node,
dom.ListGroupNode: render_list_group, dom.ListGroupNode: render_list_group,
dom.ListItem: render_list_item, dom.ListItem: render_list_item,
dom.TableNode: render_table,
dom.TableSeparatorRow: render_table_separator_row,
dom.TableRow: render_table_row,
dom.CodeBlock: render_code_block, dom.CodeBlock: render_code_block,
dom.Text: render_text, dom.Text: render_text,
dom.ResultsDrawerNode: render_results_block, dom.ResultsDrawerNode: render_results_block,
}[type(element)](element, acc) org_rw.Text: render_org_text,
}[type(element)](element, acc, headline, graph)
def render_tree(tree, acc): def render_tree(tree, acc, headline, graph):
for element in tree: for element in tree:
render_tag(element, acc) render_tag(element, acc, headline, graph)
def render_inline(tree, f, headline, graph):
acc = []
f(tree, acc, headline, graph)
return ''.join(acc)
def render(headline, doc, headlineLevel): def render_as_document(headline, doc, headlineLevel, graph, title, doc_to_headline_remapping):
if isinstance(headline.parent, org_rw.Headline):
topLevelHeadline = headline.parent
while isinstance(topLevelHeadline.parent, org_rw.Headline):
topLevelHeadline = topLevelHeadline.parent
return f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{title} @ {SITE_NAME}</title>
<meta http-equiv="refresh" content="0;./{topLevelHeadline.id}.node.html#{headline.id}" />
<link href="../static/style.css" rel="stylesheet"/>
<link href="../static/syntax.css" rel="stylesheet"/>
</head>
<body>
<nav>
<h1><a href="./index.html">Código para llevar [Notes]</a></h1>
</nav>
<a href='./{topLevelHeadline.id}.node.html#{headline.id}'>Sending you to the main note... [{org_rw.token_list_to_plaintext(topLevelHeadline.title.contents)}]</a>
</body>
</html>
"""
else:
return as_document(render(headline, doc, graph=graph, headlineLevel=headlineLevel,
doc_to_headline_remapping=doc_to_headline_remapping),
title, render_toc(doc))
def render_toc(doc):
acc = ['<ul class="toc">']
for headline in doc.getTopHeadlines():
render_toc_headline(headline, acc)
acc.append('</ul>')
if sum([chunk == '<li>' for chunk in acc]) < 2:
# If < 2 headlines, ignore it
return None
return ''.join(acc)
def render_toc_headline(headline, acc):
acc.append('<li>')
acc.append(f'<a href="#{headline.id}">{html.escape(headline.title.get_text())}</a>')
children = list(headline.children)
if children:
acc.append('<ul>')
for child in children:
render_toc_headline(child, acc)
acc.append('</ul>')
acc.append('</li>')
def render_connections(headline_id, content, graph, doc_to_headline_remapping):
# if headline_id != 'aa29be89-70e7-4465-91ed-361cf0ce62f2':
# return
logging.info("Generating centered graph for {}".format(headline_id))
svg = gen_centered_graph.gen(headline_id, graph['nodes'], doc_to_headline_remapping)
content.append("<div class='connections'>{}</div>".format(svg))
def render(headline, doc, graph, headlineLevel, doc_to_headline_remapping):
try: try:
dom = headline.as_dom() dom = headline.as_dom()
except: except:
logging.error("Error generating DOM for {}".format(doc.path)) logging.error("Error generating DOM for {}".format(doc.path))
raise raise
print_tree(dom) print_tree(dom, indentation=2, headline=headline)
content = [] content = []
render_tree(dom, content) if headline.id and headlineLevel == 0:
render_connections(headline.id, content, graph, doc_to_headline_remapping=doc_to_headline_remapping)
render_tree(dom, content, headline, graph)
for child in headline.children: for child in headline.children:
content.append(render(child, doc, headlineLevel=headlineLevel+1)) content.append(render(child, doc, headlineLevel=headlineLevel+1, graph=graph,
doc_to_headline_remapping=doc_to_headline_remapping))
if headline.state is None: if headline.state is None:
state = "" state = ""
@ -338,17 +746,32 @@ def render(headline, doc, headlineLevel):
else: else:
todo_state = "done" todo_state = "done"
display_state = 'collapsed' tag_list = []
if headlineLevel < MIN_HIDDEN_HEADLINE_LEVEL: for tag in headline.shallow_tags:
if tag.lower() not in SKIPPED_TAGS:
tag_list.append(f'<span class="tag">{html.escape(tag)}</span>')
tags = f'<span class="tags">{"".join(tag_list)}</span>'
display_state = 'expanded' display_state = 'expanded'
# # Update display based on document STARTUP config
# visual_level = doc.get_keywords('STARTUP', 'showall')
# if visual_level.startswith('show') and visual_level.endswith('levels'):
# visual_level_num = int(visual_level[len('show'):-len('levels')]) - 1
# # Note that level is 0 indexed inside this loop
# if headlineLevel >= visual_level_num:
# display_state = 'collapsed'
title = render_inline(headline.title, render_tag, headline, graph)
if headlineLevel > 0:
title = f"<a href=\"#{html.escape(headline.id)}\">{title}</a>"
return f""" return f"""
<div id="{html.escape(headline.id)}" class="node {todo_state} {display_state}"> <div id="{html.escape(headline.id)}" class="node {todo_state} {display_state}">
<h1 class="title"> <h1 class="title">
{state} {state}
<a href=\"javascript:toggle_expand('{html.escape(headline.id)}')\"> {title}
{html.escape(headline.title)} {tags}
</a>
</h1> </h1>
<div class='contents'> <div class='contents'>
{''.join(content)} {''.join(content)}
@ -357,27 +780,40 @@ def render(headline, doc, headlineLevel):
""" """
def as_document(html): def as_document(html, title, global_toc):
body_classes = []
if global_toc is None:
toc_section = ""
body_classes.append('no-toc')
else:
toc_section = f"""
<div class="global-table-of-contents">
<h2>Table of contents</h2>
{global_toc}
</div>
"""
return f"""<!DOCTYPE html> return f"""<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8">
<title>{title} @ {SITE_NAME}</title>
<link href="../static/style.css" rel="stylesheet"/> <link href="../static/style.css" rel="stylesheet"/>
<script type="text/javascript"> <link href="../static/syntax.css" rel="stylesheet"/>
function toggle_expand(header_id) {{ <!-- v Fixes mobile viewports. -->
var e = document.getElementById(header_id); <meta name="HandheldFriendly" content="True">
if (e.classList.contains('expanded')) {{ <meta name="MobileOptimized" content="320">
e.classList.add('collapsed'); <meta name="viewport" content="width=device-width, initial-scale=1">
e.classList.remove('expanded');
}}
else {{
e.classList.add('expanded');
e.classList.remove('collapsed');
}}
}}
</script>
</head> </head>
<body> <body class="{' '.join(body_classes)}">
<nav>
<h1><a href="./index.html">Código para llevar [Notes]</a></h1>
<input type="text" id="searchbox" disabled="true" placeholder="Search (requires JS)" />
</nav>
{toc_section}
{html} {html}
<script src="../static/search-box.js"></script>
<script tye="text/javascript">_codigoparallevar_enable_search_box('#searchbox', {{placeholder: 'Search...'}})</script>
</body> </body>
</html> </html>
""" """
@ -390,9 +826,13 @@ def save_changes(doc):
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) != 3: if len(sys.argv) not in (3, 4):
print("Usage: {} SOURCE_TOP DEST_TOP".format(sys.argv[0])) print("Usage: {} SOURCE_TOP DEST_TOP <SUBPATH>".format(sys.argv[0]))
exit(0) exit(0)
logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s") logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
main(sys.argv[1], sys.argv[2]) subpath = DEFAULT_SUBPATH
if len(sys.argv) == 4:
subpath = sys.argv[3]
exit(main(sys.argv[1], sys.argv[2], subpath=subpath))

75
scripts/ops_cache.py Normal file
View File

@ -0,0 +1,75 @@
import sqlite3
import json
import logging
from typing import Optional
import xdg
import os
import datetime
CACHE_DB: Optional[sqlite3.Connection] = None
CACHE_PATH = os.path.join(xdg.xdg_cache_home(), 'codigoparallevar', 'ops.sqlite3')
def init_db():
global CACHE_DB
os.makedirs(os.path.dirname(CACHE_PATH), exist_ok=True)
CACHE_DB = sqlite3.connect(CACHE_PATH)
cur = CACHE_DB.cursor()
cur.execute('''CREATE TABLE IF NOT EXISTS ops(
in_val TEXT PRIMARY KEY,
code TEXT,
out_val TEXT,
added_at DateTime
);
''')
CACHE_DB.commit()
cur.close()
def query_cache(in_val, code):
if CACHE_DB is None:
init_db()
assert CACHE_DB is not None
cur = CACHE_DB.cursor()
cur.execute('''SELECT out_val FROM ops WHERE in_val = ? AND code = ?''', (in_val, code))
# Should return only one result, right? 🤷
results = cur.fetchall()
assert len(results) < 2
if len(results) == 0:
return None
else:
return results[0][0]
def save_cache(in_val, code, out_val):
if CACHE_DB is None:
init_db()
assert CACHE_DB is not None
cur = CACHE_DB.cursor()
cur.execute('''
INSERT INTO ops(in_val, code, out_val, added_at)
VALUES (?, ?, ?, ?);''',
(in_val, code, out_val, datetime.datetime.now()))
CACHE_DB.commit()
cur.close()
def cache(fun):
fun_code = fun.__code__.co_code.decode('latin-1')
def wrapped(*kargs, **kwargs):
in_val = json.dumps({
'kargs': kargs,
'kwargs': kwargs,
'fun_code': fun_code,
})
cache_result = query_cache(in_val, fun_code)
found_in_cache = cache_result is not None
if not found_in_cache:
out_val = fun(*kargs, **kwargs)
save_cache(in_val, fun_code, out_val)
else:
out_val = cache_result
logging.info("{} bytes in, {} bytes out (in_cache: {})".format(len(in_val), len(out_val), found_in_cache))
return out_val
return wrapped

3
scripts/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
Markdown
Jinja2
pygments

15
scripts/search-server.sh Normal file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -eu
PORT=${PORT:-3001}
cd "$(dirname "$0")/search-server"
docker build -t search-server .
cd ../../_gen/notes/
set -x
exec docker run -it --rm -p $PORT:80 -e PORT=80 -e DB_PATH=/db.sqlite3 -v `pwd`/db.sqlite3:/db.sqlite3:ro search-server

1
scripts/search-server/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
search-server

View File

@ -0,0 +1,47 @@
FROM golang:1.19-alpine as builder
# Install build dependencies
RUN apk add alpine-sdk
# Create appuser.
ENV USER=appuser
ENV UID=10001
# See https://stackoverflow.com/a/55757473/12429735
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
# Prepare dependencies
RUN mkdir /build
ADD go.mod go.sum /build/
WORKDIR /build
RUN go mod download
RUN go mod verify
# Prepare app
ADD server.go /build/
# Build as static binary
RUN CGO_ENABLED=1 go build --tags "fts5" -ldflags='-w -s -extldflags "-static"' -o /build/search-server
# Copy binary to empty image
FROM scratch
# Import the user and group files from the builder.
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Prepare environment
ENV GIN_MODE=release
# Copy executable
COPY --from=builder /build/search-server /server
# Use an unprivileged user.
USER appuser:appuser
ENTRYPOINT ["/server"]

View File

@ -0,0 +1,66 @@
module codigoparallevar/search-server
go 1.19
require github.com/gin-gonic/gin v1.8.1
require (
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/awesome-gocui/gocui v0.6.0 // indirect
github.com/awesome-gocui/keybinding v1.0.0 // indirect
github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/docker/cli v0.0.0-20190906153656-016a3232168d // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v0.7.3-0.20190309235953-33c3200e0d16 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/fsnotify/fsnotify v1.4.7 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/gogo/protobuf v1.3.0 // indirect
github.com/google/uuid v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b // indirect
github.com/lunixbochs/vtclean v1.0.0 // indirect
github.com/magiconair/properties v1.8.1 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/mattn/go-sqlite3 v1.14.15 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pelletier/go-toml v1.4.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/sirupsen/logrus v1.4.2 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.0 // indirect
github.com/spf13/cobra v0.0.5 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.4.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/wagoodman/dive v0.10.0 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
golang.org/x/text v0.3.6 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

View File

@ -0,0 +1,295 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/awesome-gocui/gocui v0.5.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=
github.com/awesome-gocui/gocui v0.6.0 h1:hhDJiQC12tEsJNJ+iZBBVaSSLFYo9llFuYpQlL5JZVI=
github.com/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=
github.com/awesome-gocui/keybinding v1.0.0 h1:CrnjCfEhWpjcqIQUan9IllaXeRGELdwfjeUmY7ljbng=
github.com/awesome-gocui/keybinding v1.0.0/go.mod h1:z0TyCwIhaT97yU+becTse8Dqh2CvYT0FLw0R0uTk0ag=
github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc h1:wGNpKcHU8Aadr9yOzsT3GEsFLS7HQu8HxQIomnekqf0=
github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/cli v0.0.0-20190906153656-016a3232168d h1:gwX/88xJZfxZV1yjhhuQpWTmEgJis7/XGCVu3iDIZYU=
github.com/docker/cli v0.0.0-20190906153656-016a3232168d/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v0.7.3-0.20190309235953-33c3200e0d16 h1:dmUn0SuGx7unKFwxyeQ/oLUHhEfZosEDrpmYM+6MTuc=
github.com/docker/docker v0.7.3-0.20190309235953-33c3200e0d16/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.0 h1:G8O7TerXerS4F6sx9OV7/nRfJdnXgHZu/S/7F2SN+UE=
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b h1:PMbSa9CgaiQR9NLlUTwKi+7aeLl3GG5JX5ERJxfQ3IE=
github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/wagoodman/dive v0.10.0 h1:JaitQBVwmfZD5mvLkBHk1LUq6jwsjvnNS6mgIl7YNZQ=
github.com/wagoodman/dive v0.10.0/go.mod h1:8IDxfzmg3+5DQwK6/sGyMpJr95ejuv511+rF9CTNYdQ=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190620144150-6af8c5fc6601/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -0,0 +1,130 @@
package main
import (
"github.com/gin-gonic/gin"
"database/sql"
"log"
"os"
"fmt"
"strconv"
_ "github.com/mattn/go-sqlite3"
)
func main() {
database_path, ok := os.LookupEnv("DB_PATH")
if !ok {
log.Fatal("Environment variable $DB_PATH must point to sqlite3 database with text indices.")
os.Exit(1)
}
port := 3000
port_str, ok := os.LookupEnv("PORT")
if ok {
port_num, err := strconv.Atoi(port_str)
if err != nil {
log.Fatal(err)
os.Exit(1)
}
if (port_num < 1) || (port_num > 65535) {
log.Fatal("Environment variale $PORT must be a number between 1 and 65535.")
os.Exit(1)
}
port = port_num
}
db, err := sql.Open("sqlite3", database_path)
if err != nil {
log.Fatal(err)
os.Exit(1)
}
r := gin.Default()
api := r.Group("/api")
api.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
api.OPTIONS("/search", func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Accept-Encoding, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
c.AbortWithStatus(204)
})
api.GET("/search", func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Accept-Encoding, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
query := c.Query("q")
stm, err := db.Prepare("SELECT note_id, title, top_level_title, is_done, is_todo FROM note_search(?)")
if err != nil {
log.Fatal(err)
c.JSON(500, gin.H{
"success": false,
"message": "Error preparing note-search query",
})
return
}
results := make([]map[string]string, 0)
rows, err := stm.Query(query)
if err != nil {
log.Fatal(err)
c.JSON(500, gin.H{
"success": false,
"message": "Error querying note DB",
})
return
}
for rows.Next() {
var note_id string
var note_title string
var note_top_level_title string
var note_is_done string
var note_is_todo string
err = rows.Scan(
&note_id,
&note_title,
&note_top_level_title,
&note_is_done,
&note_is_todo,
)
if err != nil {
log.Fatal(err)
c.JSON(500, gin.H{
"success": false,
"message": "Error reading note DB results",
})
return
}
item := make(map[string]string)
item["id"] = note_id
item["title"] = note_title
item["top_level_title"] = note_top_level_title
item["is_done"] = note_is_done
item["is_todo"] = note_is_todo
results = append(results, item)
}
c.JSON(200, gin.H{
"results": gin.H{
"notes": results,
},
})
})
r.Run(fmt.Sprintf(":%v", port))
}

59
scripts/test-links.py Normal file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
import logging
import os
import sys
import urllib.parse
from bs4 import BeautifulSoup as bs4
from tqdm import tqdm
def main(files_top):
print("Listing files...")
found_files = []
for root, dirs, files in os.walk(files_top):
for name in files:
if name.endswith('.html'):
found_files.append(os.path.join(root, name))
print("\r{} files".format(len(found_files)), end='', flush=True)
print()
found_broken = 0
for fpath in tqdm(found_files):
with open(fpath) as f:
tree = bs4(f.read(), features='lxml', parser='html5')
for tag, attr in [('a', 'href'), ('img', 'src'), ('audio', 'src'), ('video', 'src')]:
for link in tree.find_all(tag):
if attr not in link.attrs:
continue
link.attrs[attr] = link.attrs[attr].split('#')[0]
if not link.attrs[attr]:
continue
if ':' in link[attr]:
continue
if link[attr].startswith('/'):
target = os.path.join(os.path.abspath(files_top), urllib.parse.unquote(link[attr].lstrip('/')))
else:
target = os.path.join(os.path.dirname(fpath), urllib.parse.unquote(link[attr]))
if os.path.isdir(target):
pass
elif not os.path.exists(target):
print("[{}] -[ error ]-> {} | {}".format(fpath, target, link[attr]))
found_broken += 1
if found_broken:
print(f"Found {found_broken} broken links")
exit(1)
else:
exit(0)
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: {} FILES_TOP".format(sys.argv[0]))
exit(0)
logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s")
exit(main(sys.argv[1]))

View File

@ -1,8 +0,0 @@
#!/usr/bin/env bash
set -eu
cd "$(dirname "$0")/.."
cd static
scp homepage.html root@codigoparallevar.com:/mnt/vols/misc/codigoparallevar/index.html

30
scripts/upload.sh Normal file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -eu
cd "$(dirname "$0")/.."
# Upload homepage
cd static
scp homepage.html root@codigoparallevar.com:/mnt/vols/misc/codigoparallevar/index.html
# Build notes
cd ../scripts
rm -Rf ../_gen/notes
WATCH_AND_REBUILD=0 python3 generate.py ~/.logs/brain ../_gen/notes
rm -Rf ../_gen/blog
WATCH_AND_REBUILD=0 python3 blog.py ~/cloud/nextcloud/blog/posts/ ../_gen/blog
rm -Rf ../_gen/static
cp -Rv ../static ../_gen/static
# Upload notes
cd ../_gen
rsync -HPaz static/ --delete-after --exclude='*.html' root@codigoparallevar.com:/mnt/vols/misc/codigoparallevar/static/
rsync -HPaz notes/ --delete-after --exclude='xapian' --exclude='*.sqlite3' root@codigoparallevar.com:/mnt/vols/misc/codigoparallevar/notes/
rsync -HPaz notes/db.sqlite3 root@codigoparallevar.com:/mnt/vols/misc/codigoparallevar-api/
rsync -HPaz blog/ --delete-after --exclude='xapian' --exclude='*.sqlite3' root@codigoparallevar.com:/mnt/vols/misc/codigoparallevar/blog/
# Restart API server
ssh root@codigoparallevar.com docker restart notes-api-server

View File

@ -0,0 +1,19 @@
(function (){
var wait_for_update = function() {
console.debug("Waiting for changes...");
fetch('/__wait_for_changes').then(r => {
if (r.status !== 200) {
setTimeout(
wait_for_update,
1000,
);
}
else {
// Reload
window.location = window.location;
}
});
};
wait_for_update();
})();

51
static/article.tmpl.html Normal file
View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{ title }} @ Código para llevar [blog]</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ base_path }}/../static/style.css" />
<link rel="stylesheet" href="{{ base_path }}/../static/light-syntax.css" />
<link rel="stylesheet" href="{{ base_path }}/../static/dark-syntax.css" />
</head>
<body class="blog">
<div class="site-header">
<h1 class="site-name"><a href="{{ base_path }}/">Codigo para llevar [blog]</a></h1>
<nav class="site-links">
<span class="fancy-link">
<a href="{{ base_path }}/../">Home</a>
</span>
<span class="fancy-link">
<a href="{{ base_path }}/../notes/">Notes</a>
</span>
<span class="fancy-link">
<a href="https://github.com/kenkeiras">GitHub</a>
</span>
<span class="fancy-link">
<a href="https://gitlab.com/kenkeiras">GitLab</a>
</span>
<span class="fancy-link">
<a href="https://programaker.com/users/kenkeiras">PrograMaker</a>
</span>
</nav>
</div>
<div class="content">
<article class="post">
<h2 class="post-title">{{ title }}</h2>
<div class="post-metadata">
<time class="post-publication-date" datetime="{{ post_publication_date.date() }}">
{{ post_publication_date.date() }}
</time>
<ul class="post-tags">
{% for post_tag in post_tags %}
<li class="post-tag"><a href="{{ base_path }}/tags/{{ post_tag |urlencode|replace('/', '_') }}/"</a>{{ post_tag }}</a></li>
{% endfor %}
</ul>
</div>
<div class="post-content">
{{ content | safe }}
</div>
</article>
</div>
</body>
</html>

View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Código para llevar</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../static/style.css" />
<link rel="stylesheet" href="../../static/light-syntax.css" />
<link rel="stylesheet" href="../../static/dark-syntax.css" />
</head>
<body class="blog">
<div class="site-header">
<h1 class="site-name"><a href="../">Codigo para llevar [blog]</a></h1>
<nav class="site-links">
<span class="fancy-link">
<a href="../../">Home</a>
</span>
<span class="fancy-link">
<a href="../../notes/">Notes</a>
</span>
<span class="fancy-link">
<a href="https://github.com/kenkeiras">GitHub</a>
</span>
<span class="fancy-link">
<a href="https://gitlab.com/kenkeiras">GitLab</a>
</span>
<span class="fancy-link">
<a href="https://programaker.com/users/kenkeiras">PrograMaker</a>
</span>
</nav>
</div>
<div class="post-list content">
<ul>
{% for post in posts %}
<li class="post">
<div class="post-metadata">
<time class="post-publication-date" datetime="{{ post.post_publication_date.date() }}">
{{ post.post_publication_date.date() }}
</time>
</div>
<h4 class="post-title"><a href="../{{ post.link }}">{{ post.title }}</a></h4>
<div class="post-metadata">
<ul class="post-tags">
{% for post_tag in post.post_tags %}
<li class="post-tag"><a href="../tags/{{ post_tag |urlencode|replace('/', '_') }}/">{{ post_tag }}</a></li>
{% endfor %}
</ul>
</div>
</li>
{% endfor %}
</ul>
</div>
</body>
</html>

6
static/blog.css Normal file
View File

@ -0,0 +1,6 @@
body {
margin: 0 auto;
width: fit-content;
max-width: 100ex;
padding: 0 1ex;
}

View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Código para llevar</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../static/style.css" />
<link rel="stylesheet" href="../static/light-syntax.css" />
<link rel="stylesheet" href="../static/dark-syntax.css" />
</head>
<body class="blog">
<div class="site-header">
<h1 class="site-name"><a href="./">Codigo para llevar [blog]</a></h1>
<nav class="site-links">
<span class="fancy-link">
<a href="../">Home</a>
</span>
<span class="fancy-link">
<a href="../notes/">Notes</a>
</span>
<span class="fancy-link">
<a href="https://github.com/kenkeiras">GitHub</a>
</span>
<span class="fancy-link">
<a href="https://gitlab.com/kenkeiras">GitLab</a>
</span>
<span class="fancy-link">
<a href="https://programaker.com/users/kenkeiras">PrograMaker</a>
</span>
</nav>
</div>
<div class="post-index content">
{% for post in posts %}
<div class="post-container">
<article class="post">
<h2 class="post-title"><a href="{{ post.link }}">{{ post.title }}</a></h2>
<div class="post-metadata">
<time class="post-publication-date" datetime="{{ post.post_publication_date.date() }}">
{{ post.post_publication_date.date() }}
</time>
<ul class="post-tags">
{% for post_tag in post.post_tags %}
<li class="post-tag"><a href="tags/{{ post_tag |urlencode|replace('/', '_') }}/"</a>{{ post_tag }}</a></li>
{% endfor %}
</ul>
</div>
<div class="post-content">
{{ post.summary | safe }}
</div>
</article>
</div>
{% endfor %}
</div>
<div class="index-pages">
{% if prev_index_num != None %}
{% if prev_index_num == 0 %}
<a class="newer-posts" href="index.html">Newer posts</a>
{% else %}
<a class="newer-posts" href="index-{{ prev_index_num }}.html">Newer posts</a>
{% endif %}
{% endif %}
{% if next_index_num %}
<a class="older-posts" href="index-{{ next_index_num }}.html">Older posts</a>
{% endif %}
</div>
</body>
</html>

View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Código para llevar</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../../static/style.css" />
<link rel="stylesheet" href="../../../static/light-syntax.css" />
<link rel="stylesheet" href="../../../static/dark-syntax.css" />
</head>
<body class="blog">
<div class="site-header">
<h1 class="site-name"><a href="../../">Codigo para llevar [blog]</a></h1>
<nav class="site-links">
<span class="fancy-link">
<a href="../../../">Home</a>
</span>
<span class="fancy-link">
<a href="../../../notes/">Notes</a>
</span>
<span class="fancy-link">
<a href="https://github.com/kenkeiras">GitHub</a>
</span>
<span class="fancy-link">
<a href="https://gitlab.com/kenkeiras">GitLab</a>
</span>
<span class="fancy-link">
<a href="https://programaker.com/users/kenkeiras">PrograMaker</a>
</span>
</nav>
</div>
<div class="post-list content">
<ul>
{% for post in posts %}
<li class="post">
<div class="post-metadata">
<time class="post-publication-date" datetime="{{ post.post_publication_date.date() }}">
{{ post.post_publication_date.date() }}
</time>
</div>
<h4 class="post-title"><a href="../../{{ post.link }}">{{ post.title }}</a></h4>
<div class="post-metadata">
<ul class="post-tags">
{% for post_tag in post.post_tags %}
<li class="post-tag"><a href="../../tags/{{ post_tag |urlencode|replace('/', '_') }}/">{{ post_tag }}</a></li>
{% endfor %}
</ul>
</div>
</li>
{% endfor %}
</ul>
</div>
</body>
</html>

85
static/dark-syntax.css Normal file
View File

@ -0,0 +1,85 @@
/* Dark mode. */
@media (prefers-color-scheme: dark) {
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.hll { background-color: #49483e }
.c { color: #75715e } /* Comment */
.err { color: #960050; background-color: #1e0010 } /* Error */
.esc { color: #f8f8f2 } /* Escape */
.g { color: #f8f8f2 } /* Generic */
.k { color: #66d9ef } /* Keyword */
.l { color: #ae81ff } /* Literal */
.n { color: #f8f8f2 } /* Name */
.o { color: #f92672 } /* Operator */
.x { color: #f8f8f2 } /* Other */
.p { color: #f8f8f2 } /* Punctuation */
.ch { color: #75715e } /* Comment.Hashbang */
.cm { color: #75715e } /* Comment.Multiline */
.cp { color: #75715e } /* Comment.Preproc */
.cpf { color: #75715e } /* Comment.PreprocFile */
.c1 { color: #75715e } /* Comment.Single */
.cs { color: #75715e } /* Comment.Special */
.gd { color: #f92672 } /* Generic.Deleted */
.ge { color: #f8f8f2; font-style: italic } /* Generic.Emph */
.gr { color: #f8f8f2 } /* Generic.Error */
.gh { color: #f8f8f2 } /* Generic.Heading */
.gi { color: #a6e22e } /* Generic.Inserted */
.go { color: #66d9ef } /* Generic.Output */
.gp { color: #f92672; font-weight: bold } /* Generic.Prompt */
.gs { color: #f8f8f2; font-weight: bold } /* Generic.Strong */
.gu { color: #75715e } /* Generic.Subheading */
.gt { color: #f8f8f2 } /* Generic.Traceback */
.kc { color: #66d9ef } /* Keyword.Constant */
.kd { color: #66d9ef } /* Keyword.Declaration */
.kn { color: #f92672 } /* Keyword.Namespace */
.kp { color: #66d9ef } /* Keyword.Pseudo */
.kr { color: #66d9ef } /* Keyword.Reserved */
.kt { color: #66d9ef } /* Keyword.Type */
.ld { color: #e6db74 } /* Literal.Date */
.m { color: #ae81ff } /* Literal.Number */
.s { color: #e6db74 } /* Literal.String */
.na { color: #a6e22e } /* Name.Attribute */
.nb { color: #f8f8f2 } /* Name.Builtin */
.nc { color: #a6e22e } /* Name.Class */
.no { color: #66d9ef } /* Name.Constant */
.nd { color: #a6e22e } /* Name.Decorator */
.ni { color: #f8f8f2 } /* Name.Entity */
.ne { color: #a6e22e } /* Name.Exception */
.nf { color: #a6e22e } /* Name.Function */
.nl { color: #f8f8f2 } /* Name.Label */
.nn { color: #f8f8f2 } /* Name.Namespace */
.nx { color: #a6e22e } /* Name.Other */
.py { color: #f8f8f2 } /* Name.Property */
.nt { color: #f92672 } /* Name.Tag */
.nv { color: #f8f8f2 } /* Name.Variable */
.ow { color: #f92672 } /* Operator.Word */
.w { color: #f8f8f2 } /* Text.Whitespace */
.mb { color: #ae81ff } /* Literal.Number.Bin */
.mf { color: #ae81ff } /* Literal.Number.Float */
.mh { color: #ae81ff } /* Literal.Number.Hex */
.mi { color: #ae81ff } /* Literal.Number.Integer */
.mo { color: #ae81ff } /* Literal.Number.Oct */
.sa { color: #e6db74 } /* Literal.String.Affix */
.sb { color: #e6db74 } /* Literal.String.Backtick */
.sc { color: #e6db74 } /* Literal.String.Char */
.dl { color: #e6db74 } /* Literal.String.Delimiter */
.sd { color: #e6db74 } /* Literal.String.Doc */
.s2 { color: #e6db74 } /* Literal.String.Double */
.se { color: #ae81ff } /* Literal.String.Escape */
.sh { color: #e6db74 } /* Literal.String.Heredoc */
.si { color: #e6db74 } /* Literal.String.Interpol */
.sx { color: #e6db74 } /* Literal.String.Other */
.sr { color: #e6db74 } /* Literal.String.Regex */
.s1 { color: #e6db74 } /* Literal.String.Single */
.ss { color: #e6db74 } /* Literal.String.Symbol */
.bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
.fm { color: #a6e22e } /* Name.Function.Magic */
.vc { color: #f8f8f2 } /* Name.Variable.Class */
.vg { color: #f8f8f2 } /* Name.Variable.Global */
.vi { color: #f8f8f2 } /* Name.Variable.Instance */
.vm { color: #f8f8f2 } /* Name.Variable.Magic */
.il { color: #ae81ff } /* Literal.Number.Integer.Long */
}

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 169 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 172 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 138 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 136 KiB

View File

@ -7,6 +7,12 @@
<script type='text/javascript'> <script type='text/javascript'>
var NODE_GRAPH=<!-- REPLACE_THIS_WITH_GRAPH -->; var NODE_GRAPH=<!-- REPLACE_THIS_WITH_GRAPH -->;
</script> </script>
<style>
text {
font-family: sans-serif;
font-size: 10px;
}
</style>
<script> <script>
// Copyright 2021 Observable, Inc. // Copyright 2021 Observable, Inc.
// Released under the ISC license. // Released under the ISC license.
@ -80,15 +86,15 @@ function ForceGraph({
// Add arrowheads // Add arrowheads
svg.append('defs').append('marker') svg.append('defs').append('marker')
.attr('id', 'arrowhead') .attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10') .attr('viewBox', '-0 -2.5 5 5')
.attr('refX', 13) .attr('refX', 10)
.attr('refY', 0) .attr('refY', 0)
.attr('orient', 'auto') .attr('orient', 'auto')
.attr('markerWidth', 13) .attr('markerWidth', 7)
.attr('markerHeight', 13) .attr('markerHeight', 7)
.attr('xoverflow', 'visible') .attr('xoverflow', 'visible')
.append('svg:path') .append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5') .attr('d', 'M 0,-2.5 L 5 ,0 L 0,2.5')
.attr('fill', '#999') .attr('fill', '#999')
.style('stroke','none'); .style('stroke','none');
@ -263,6 +269,8 @@ function ForceGraph({
linkLabel: (d) => { const e = edges[d.index]; if (e.relation) { return e.relation; } else { return ''; } }, linkLabel: (d) => { const e = edges[d.index]; if (e.relation) { return e.relation; } else { return ''; } },
}); });
holder.appendChild(chart); holder.appendChild(chart);
chart.height = '100vh';
chart.width = '100vw';
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,15 +1,21 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Código para llevar</title> <title>Código para llevar</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<style> <style>
body { body {
background-color: white;
font-family: sans-serif;
margin: 0 auto; margin: 0 auto;
width: fit-content; width: fit-content;
max-width: 100ex; max-width: 100ex;
padding: 0 1ex; padding: 0 1ex;
color: black;
}
.header h1 {
text-align: center;
} }
.links section { .links section {
margin-top: 1em; margin-top: 1em;
@ -26,8 +32,9 @@
.links p { .links p {
margin: 0; margin: 0;
} }
h2 a { h2 a, a h2 {
color: black; color: black;
text-decoration: underline;
} }
a { a {
color: #00e; color: #00e;
@ -40,21 +47,21 @@
border-right: 1px solid #000; border-right: 1px solid #000;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
html { body {
background-color: #0f110e; background-color: #1d1f21;
color: #fafafe; color: #fafafe;
} }
h2 a { h2 a, a h2 {
color: #fafafe; color: #fafafe;
} }
a { a {
color: #66f; color: #00fdf2;
} }
#social a { #social a {
border-color: #fff; border-color: #fff;
} }
.links section { .links section {
background-color: #262826; background-color: #262628;
} }
} }
</style> </style>
@ -67,11 +74,19 @@
</div> </div>
<div class="links"> <div class="links">
<a href="/notes">
<section>
<h2>Notes</h2>
<p>Some publicly-visible notes from a sort of knowledge graph that I use as information dump.</p>
</section>
</a>
<section> <section>
<h2><a href="/blog">Blog</a></h2> <h2><a href="/blog">Blog</a></h2>
<p> <p>
Latest posts: Latest posts:
<ul> <ul>
<li><a href="https://codigoparallevar.com/blog/2023/programmatic-access-to-gnucash-using-python/">[2023] A sloppy guide to GnuCash's Python bindings</a></li>
<li><a href="https://codigoparallevar.com/blog/2022/detecting-non-halting-programs/">[2022] Detecting non halting programs</a></li>
<li><a href="https://codigoparallevar.com/blog/2022/get-process-progress-reading-file/">[2022] Get process's progress when reading a file</a></li> <li><a href="https://codigoparallevar.com/blog/2022/get-process-progress-reading-file/">[2022] Get process's progress when reading a file</a></li>
<li><a href="https://codigoparallevar.com/blog/2022/a-simple-status-indicator/">[2022] A simple status indicator</a></li> <li><a href="https://codigoparallevar.com/blog/2022/a-simple-status-indicator/">[2022] A simple status indicator</a></li>
</ul> </ul>
@ -80,19 +95,48 @@
<section> <section>
<h2>Collaborations</h2> <h2>Collaborations</h2>
<p> <p>
Latest post in <a href="https://hackliza.gal">Hackliza</a>: <a href="https://hackliza.gal/en/posts/quick_math_on_terminal/">Quick math on the terminal (english)</a> Latest posts in <a href="https://hackliza.gal">Hackliza</a>:
<ul>
<li>
<a href="https://hackliza.gal/en/posts/python-visual-profiling/">Visual profiling in Python (english)</a>
<a href="https://hackliza.gal/posts/python-visual-profiling/">(galician)</a>
</li>
<li>
<a href="https://hackliza.gal/en/posts/quick_math_on_terminal/">Quick math on the terminal (english)</a>
<a href="https://hackliza.gal/posts/contas_rapidas_no_terminal/">(galician)</a> <a href="https://hackliza.gal/posts/contas_rapidas_no_terminal/">(galician)</a>
</li>
</ul>
</p> </p>
</section> </section>
<section> <section>
<h2>Projects</h2> <h2>Talks / Slides</h2>
<p> <p>
My most stable project is <a href="https://programaker.com">PrograMaker</a>. Other work-in-progress is in <a href="https://github.com/kenkeiras">GitHub</a>. <ul>
<li>
Malleable Software
(<a href="/slides/hackliza2024/software-maleable/software-maleable.odp">galician, </a>
for <a href="https://hackliza.gal">Hackliza</a>
<a href="/slides/hackliza2024/software-maleable/software-maleable.pdf">[PDF]</a>
<a href="/slides/hackliza2024/software-maleable/software-maleable.odp">[ODP]</a>)
(<a href="/slides/eslibre2024/software-maleable.odp">spanish,</a>
for <a href="https://eslib.re/2024/">esLibre 2024</a>
<a href="/slides/eslibre2024/software-maleable.pdf">[PDF]</a>
<a href="/slides/eslibre2024/software-maleable.odp">[ODP]</a>).
</li>
</ul>
</p> </p>
</section> </section>
<!-- section>
<h2>Projects</h2>
<p>
My most stable project is <a href="https://programaker.com">PrograMaker</a>.
Other work-in-progress is in <a href="https://github.com/kenkeiras">GitHub</a>.
</p>
</section -->
<section id="social"> <section id="social">
<h2>Find me</h2> <h2>Find me</h2>
<p> <p>
<a href="https://social.codigoparallevar.com/@kenkeiras">ActivityPub</a>
<a href="https://github.com/kenkeiras">GitHub</a> <a href="https://github.com/kenkeiras">GitHub</a>
<a href="https://gitlab.com/kenkeiras">GitLab</a> <a href="https://gitlab.com/kenkeiras">GitLab</a>
<a href="https://programaker.com/users/kenkeiras">PrograMaker</a> <a href="https://programaker.com/users/kenkeiras">PrograMaker</a>

64
static/light-syntax.css Normal file
View File

@ -0,0 +1,64 @@
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.hll { background-color: #ffffcc }
.c { color: #008800; font-style: italic } /* Comment */
.err { color: #a61717; background-color: #e3d2d2 } /* Error */
.g { color: #2c2cff } /* Generic */
.k { color: #2c2cff } /* Keyword */
.x { background-color: #ffffe0 } /* Other */
.ch { color: #008800; font-style: italic } /* Comment.Hashbang */
.cm { color: #008800; font-style: italic } /* Comment.Multiline */
.cp { color: #008800; font-style: italic } /* Comment.Preproc */
.cpf { color: #008800; font-style: italic } /* Comment.PreprocFile */
.c1 { color: #008800; font-style: italic } /* Comment.Single */
.cs { color: #008800; font-style: italic } /* Comment.Special */
.gd { color: #2c2cff } /* Generic.Deleted */
.ge { color: #008800 } /* Generic.Emph */
.gr { color: #d30202 } /* Generic.Error */
.gh { color: #2c2cff } /* Generic.Heading */
.gi { color: #2c2cff } /* Generic.Inserted */
.go { color: #2c2cff } /* Generic.Output */
.gp { color: #2c2cff } /* Generic.Prompt */
.gs { color: #2c2cff } /* Generic.Strong */
.gu { color: #2c2cff } /* Generic.Subheading */
.gt { color: #2c2cff } /* Generic.Traceback */
.kc { color: #2c2cff; font-weight: bold } /* Keyword.Constant */
.kd { color: #2c2cff } /* Keyword.Declaration */
.kn { color: #2c2cff } /* Keyword.Namespace */
.kp { color: #2c2cff } /* Keyword.Pseudo */
.kr { color: #353580; font-weight: bold } /* Keyword.Reserved */
.kt { color: #2c2cff } /* Keyword.Type */
.m { color: #2c8553; font-weight: bold } /* Literal.Number */
.s { color: #800080 } /* Literal.String */
.nb { color: #2c2cff } /* Name.Builtin */
.nf { font-weight: bold; font-style: italic } /* Name.Function */
.nv { color: #2c2cff; font-weight: bold } /* Name.Variable */
.w { color: #bbbbbb } /* Text.Whitespace */
.mb { color: #2c8553; font-weight: bold } /* Literal.Number.Bin */
.mf { color: #2c8553; font-weight: bold } /* Literal.Number.Float */
.mh { color: #2c8553; font-weight: bold } /* Literal.Number.Hex */
.mi { color: #2c8553; font-weight: bold } /* Literal.Number.Integer */
.mo { color: #2c8553; font-weight: bold } /* Literal.Number.Oct */
.sa { color: #800080 } /* Literal.String.Affix */
.sb { color: #800080 } /* Literal.String.Backtick */
.sc { color: #800080 } /* Literal.String.Char */
.dl { color: #800080 } /* Literal.String.Delimiter */
.sd { color: #800080 } /* Literal.String.Doc */
.s2 { color: #800080 } /* Literal.String.Double */
.se { color: #800080 } /* Literal.String.Escape */
.sh { color: #800080 } /* Literal.String.Heredoc */
.si { color: #800080 } /* Literal.String.Interpol */
.sx { color: #800080 } /* Literal.String.Other */
.sr { color: #800080 } /* Literal.String.Regex */
.s1 { color: #800080 } /* Literal.String.Single */
.ss { color: #800080 } /* Literal.String.Symbol */
.bp { color: #2c2cff } /* Name.Builtin.Pseudo */
.fm { font-weight: bold; font-style: italic } /* Name.Function.Magic */
.vc { color: #2c2cff; font-weight: bold } /* Name.Variable.Class */
.vg { color: #2c2cff; font-weight: bold } /* Name.Variable.Global */
.vi { color: #2c2cff; font-weight: bold } /* Name.Variable.Instance */
.vm { color: #2c2cff; font-weight: bold } /* Name.Variable.Magic */
.il { color: #2c8553; font-weight: bold } /* Literal.Number.Integer.Long */

22
static/rss.tmpl.xml Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Código para llevar</title>
<link>https://codigoparallevar.com/blog/</link>
<description>Blog from a programmer adrift.</description>
<atom:link href="https://codigoparallevar.com/blog/rss.xml" rel="self" type="application/rss+xml"></atom:link>
<language>en</language>
<copyright>Contents © 2023 kenkeiras - Creative Commons License 4.0 BY-NC-SA</copyright>
<lastBuildDate>{{ last_build_date.strftime("%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate>
<ttl>3600</ttl>
{% for post in posts %}
<item>
<title>{{ post.title }}</title>
<description>{{ post.summary }}</description>
<link>https://codigoparallevar.com/blog/{{ post.link }}</link>
<pubDate>{{ post.post_publication_date.strftime("%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
</item>
{% endfor %}
</channel>
</rss>

129
static/search-box.js Normal file
View File

@ -0,0 +1,129 @@
function _codigoparallevar_enable_search_box(selector, options) {
const element = document.querySelector(selector);
if ('placeholder' in options) {
element.setAttribute('placeholder', options.placeholder);
}
// Prepare backdrop
const resultsBoxBackdrop = document.createElement('div');
resultsBoxBackdrop.setAttribute('class', 'results-box-container hidden');
const resultsBox = document.createElement('div');
resultsBox.setAttribute('class', 'results-box');
// Results box contents
const innerSearchBox = document.createElement('input');
innerSearchBox.setAttribute('type', 'text');
innerSearchBox.setAttribute('placeholder', element.getAttribute('placeholder'));
resultsBox.appendChild(innerSearchBox);
const resultsList = document.createElement('ul');
resultsBox.appendChild(resultsList);
const noResultsBox = document.createElement('div');
noResultsBox.setAttribute('class', 'no-results-box hidden');
noResultsBox.innerText = 'No results 🤷';
resultsBox.appendChild(noResultsBox);
resultsBoxBackdrop.appendChild(resultsBox);
document.body.appendChild(resultsBoxBackdrop);
// Popup cancellation
resultsBoxBackdrop.onclick = () => {
resultsBoxBackdrop.classList.add('hidden');
};
resultsBox.onclick = (ev) => {
ev.stopPropagation();
};
// Element triggers popup
element.onfocus = () => {
resultsBoxBackdrop.classList.remove('hidden');
innerSearchBox.focus();
const wasKeyDown = document.onkeydown;
document.onkeydown = (ev) => {
if (ev.key === 'Escape') {
resultsBoxBackdrop.classList.add('hidden');
document.onkeydown = wasKeyDown;
ev.stopPropagation();
}
};
};
const DEBOUNCE_TIME = 250; // Milliseconds
const MIN_LENGTH = 3;
const SEARCH_ENDPOINT = (window.location.host.startsWith('localhost')
? 'http://localhost:3001/api/search'
: 'https://api.codigoparallevar.com/api/search'
);
let debounceWaiter = null;
let currentQuery = null;
let lastVal = null;
const doQuery = () => {
const val = innerSearchBox.value.trim();
if ((val.length < MIN_LENGTH) || (val === lastVal)) {
return;
}
lastVal = val;
resultsBox.classList.add('loading');
const uri = SEARCH_ENDPOINT + '?q=' + encodeURIComponent(val);
let query = fetch(uri);
currentQuery = query;
query
.then(res => res.json())
.then((body) => {
if (query !== currentQuery) {
console.log("Query out-raced 🤷");
return;
}
resultsBox.classList.remove('loading');
resultsList.innerHTML = '';
for (const list of [
body.results.notes.filter(n => n.is_todo !== "1"),
body.results.notes.filter(n => n.is_todo === "1"),
]){
for (const note of list) {
const resultCard = document.createElement('li');
const resultContents = document.createElement('a');
resultContents.setAttribute('href', './' + note.id + '.node.html');
const resultTitle = document.createElement('h2');
resultTitle.innerText = `${note.title} (${note.top_level_title})`;
if (note.is_todo === "1") {
resultTitle.setAttribute('class', 'is-todo');
}
else if (note.is_done === "1") {
resultTitle.setAttribute('class', 'is-done');
}
resultContents.appendChild(resultTitle);
resultCard.appendChild(resultContents);
resultsList.appendChild(resultCard);
}
}
if (body.results.notes.length == 0) {
noResultsBox.classList.remove('hidden');
}
else {
noResultsBox.classList.add('hidden');
}
});
};
element.removeAttribute('disabled');
innerSearchBox.onkeyup = (ev) => {
if (debounceWaiter !== null) {
clearTimeout(debounceWaiter);
}
debounceWaiter = setTimeout(doQuery, DEBOUNCE_TIME);
};
}
// // TODO: Remove this when dev is done
// _codigoparallevar_enable_search_box('#searchbox', {placeholder: 'Search...'})
// document.querySelector('#searchbox').focus()

View File

@ -1,30 +1,311 @@
/* Default theme */
html, body {
margin: 0;
padding: 0;
font-family: 'Atkinson Hyperlegible', 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 110%;
}
body {
max-width: 80ex;
margin: 0 auto;
padding: 0.5ex 1ex;
background-color: white;
color: black;
}
body.blog {
padding: 0;
}
body nav {
text-align: center;
}
body nav h1 {
text-align: center;
color: #000;
display: inline-block;
}
a:focus {
background-color: rgb(0, 0, 238);
color: white;
}
h1 a:focus,
h2 a:focus,
h3 a:focus,
h4 a:focus,
h5 a:focus,
h6 a:focus
{
background-color: inherit;
color: #2c3e50;
}
/* Search box */
body nav input {
background-color: transparent;
color: #000;
border: none;
border-bottom: 1px solid #888;
}
.results-box-container {
z-index: 5;
position: fixed;
width: 100vw;
height: 100vh;
background-color: rgba(0,0,0,0.3);
top: 0;
left: 0;
}
.results-box-container.hidden {
display: none;
}
.results-box {
min-width: 50vw;
max-width: 90vw;
min-height: 20ex;
max-height: 90vh;
overflow: auto;
border-radius: 4px;
margin: 2rem auto;
box-shadow: 0px 1px 2px 1px rgba(0, 0, 0, 0.25);
background-color: #fff;
}
.results-box-container .results-box input {
width: 90%;
margin: 0 auto;
padding-top: 2ex;
display: block;
background-color: transparent;
color: #000;
border: none;
border-bottom: 1px solid #888;
outline: none;
font-size: 100%;
}
.results-box-container .results-box input:focus {
border-bottom: 1px solid #000;
}
@keyframes loading-query {
from {
border-bottom-color: hsl(0 80% 40%);
}
30% {
border-bottom-color: hsl(80 80% 40%);
}
60% {
border-bottom-color: hsl(180 80% 40%);
}
to {
border-bottom-color: hsl(360 80% 40%);
}
}
.results-box-container .results-box.loading input {
animation-name: loading-query;
border-bottom-width: 2px;
animation-duration: 2s;
margin-bottom: -1px;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
/* Search box results */
.results-box ul {
list-style: none;
padding: 0;
}
.results-box ul li {
padding: 0.25ex;
margin: 1ex;
border-radius: 4px;
box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.25);
}
.results-box ul li h2 {
font-size: 110%;
padding: 1.25ex;
display: block;
margin: 0;
}
.results-box li h2.is-todo::before {
content: 'TODO';
display: inline-block;
background-color: #D00;
padding: 0.25ex;
border-radius: 4px;
font-size: 90%;
margin-right: 0.5ex;
}
.no-results-box {
padding: 1rem;
}
.no-results-box.hidden {
display: none;
}
a.img {
display: block;
}
img {
max-width: 100%;
}
@font-face {
font-family: "Atkinson Hyperlegible";
src: url('./fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-Regular-102.eot');
src: url('./fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-Regular-102.eot') format('embedded-opentype'),
url('./fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-Regular-102a.woff2') format('woff2'),
url('./fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-Regular-102.woff') format('woff'),
url('./fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-Regular-102.ttf') format('truetype');
/* Make sure text is displayed ASAP, even if this font is not ready. */
font-display: swap;
}
/* Node styling */ /* Node styling */
.node { .node {
max-width: min(650px, 100ex);
margin: 0 auto; margin: 0 auto;
} }
.node .node { .node .node {
margin: 2em 0ex 2em 0.5ex;
padding: 1ex 0 1ex 1ex; padding: 1ex 0 1ex 1ex;
box-shadow: 0px 1px 3px 0 rgba(0,0,0,0.3); border-left: 1px dashed #2c3e50;
border-left: 2px solid #2c3e50;
} }
.node.collapsed > .contents { .node.collapsed > .contents {
display: none; display: none;
} }
.node .node.collapsed > .title::before { /* Item list */
content: "🮥"; .node .contents ul,
.global-table-of-contents ul {
--tree-spacing : 1rem;
--tree-radius : 0.75ex;
--tree-line-separation: 0.5rem;
--tree-color: #ccc;
--tree-border-radius: 5px;
list-style: none;
} }
.node .node.expanded > .title::before { .node .contents ul li,
content: "🮦"; .global-table-of-contents ul li{
position: relative;
padding-left: calc(var(--tree-spacing) * 2);
}
.node .contents ul li::after,
.global-table-of-contents ul li::after {
content: '';
display: block;
position: absolute;
top: calc(var(--tree-spacing) / 2 - var(--tree-radius));
left: calc(var(--tree-spacing) - var(--tree-radius) - 1px);
width: calc(2 * var(--tree-radius));
height: calc(2 * var(--tree-radius));
border-radius: 50%;
background: var(--tree-color);
}
.node .contents ul li::before,
.global-table-of-contents ul li::before {
content: ' ';
width: var(--tree-spacing);
display: inline-block;
border-bottom: 2px dashed var(--tree-color);
vertical-align: super;
margin-right: calc(var(--tree-line-separation) + 0.5ex);
margin-left: calc(0px - (var(--tree-line-separation) * 4) - 2px);
}
/* Nested item list */
.node .contents ul ul,
.global-table-of-contents ul ul {
padding: 0;
margin-left: calc(var(--tree-spacing));
list-style: none;
}
.node .contents ul > li > ul,
.global-table-of-contents ul > li > ul {
margin-left: calc(0px - var(--tree-spacing));
}
.node .contents ul ul li,
.global-table-of-contents ul ul li {
margin-left: calc(0px - var(--tree-radius) / 2 + 2px);
border-left: 2px solid var(--tree-color);
}
.node .contents ul ul li::marker,
.global-table-of-contents ul ul li::marker {
content: '';
}
.node .contents ul ul li::after,
.global-table-of-contents ul ul li::after {
left: calc(var(--tree-spacing) * 2 - 0.5ex);
}
.node .contents ul ul li::before,
.global-table-of-contents ul ul li::before {
width: calc(var(--tree-spacing) * 2);
height: calc(var(--tree-spacing) + 5px);
margin-top: -100%;
border-radius: 0;
top: calc(-0.5ex - 2px);
border-left: 2px solid var(--tree-color);
border-bottom-style: solid;
}
.node .contents ul ul li:last-of-type::before,
.global-table-of-contents ul ul li:last-of-type::before {
border-bottom-left-radius: var(--tree-border-radius);
}
.node .contents ul li:last-of-type,
.global-table-of-contents ul li:last-of-type {
border-color: transparent;
}
.node .node > .title::before {
content: "#";
display: inline-block;
color: #888;
}
.node .title {
margin: 0;
}
/* Inhibit <p> tags inside inlined items */
/* TODO: Remove need for this on generator */
.item p {
display: inline;
}
h1 p,h2 p,h3 p,h4 p,h5 p,h6 p, li p {
display: inline;
}
.connections ul {
margin-top: 0;
} }
/* Headers */ /* Headers */
h1 { body > .node > h1 {
text-align: center;
}
.node .node h1 {
font-size: 150%; font-size: 150%;
} }
@ -47,6 +328,36 @@ h1.title .state {
border-radius: 5px; border-radius: 5px;
} }
h1.title .state.todo-True {
background-color: rgba(255,0,0,0.5);
}
h1.title .state.todo-False {
background-color: rgba(0,255,0,0.25);
}
h1.title .state.todo-True.state-SOMETIME {
background-color: #ddd;
color: black;
}
h1.title .tags {
float: right;
}
h1.title .tags .tag {
font-size: 50%;
vertical-align: middle;
/* background-color: rgba(255,255,255,0.3); */
background-color: rgba(0,0,0,0.1);
padding: 4px;
margin-left: 2px;
border-radius: 5px;
}
h1.title .tags .tag:before {
content: '[';
}
h1.title .tags .tag:after {
content: ']';
}
/* Lists */ /* Lists */
li .tag { li .tag {
font-weight: bold; font-weight: bold;
@ -56,24 +367,399 @@ li .tag::after {
content: " :: "; content: " :: ";
} }
a.internal::before {
content: '{ ';
}
a.internal::after {
content: ' }';
}
a.external::after {
content: ' ↗';
vertical-align: top;
}
/* Markup */
.underlined {
text-decoration: underline;
}
/* Codehilite fix */
.codehilitetable, .codehilitetable tr, .codehilitetable td {
border: none;
}
.codehilitetable .linenodiv pre {
margin: 0;
box-shadow: none;
line-height: 1.2em;
font-family: Menlo, Monaco, "Courier New", monospace;
font-size: medium;
}
.codehilitetable .code code {
font-size: medium;
}
/* Code blocks */ /* Code blocks */
pre { pre {
overflow: auto; overflow: auto;
padding: 0.5ex; padding: 0.25ex;
padding-left: 0.5ex; box-shadow: 0px 1px 2px 1px rgba(0, 0, 0, 0.25);
padding-left: 1.5ex; border-radius: 2px;
background-color: #eee8d5;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.26);
} }
pre > code { pre > code {
display: block; display: block;
line-height: 1.2em; line-height: 1.2em;
overflow: auto; overflow: auto;
} }
code { pre code {
padding: 0.5ex; padding: 1ex;
font-size: medium; font-size: medium;
border: 2px solid #eee8d5; background: #fff;
background: #fdf6e3; color: #000;
color: #073642; border: none;
font-size: 85%;
border-radius: 4px;
}
.highlight pre {
padding: 0.5ex;
}
code, .verbatim {
padding: 0.25ex 0.5ex;
margin: 0.25ex;
background: #eee;
color: #600;
font-family: Menlo, Monaco, "Courier New", monospace;
font-size: 85%;
}
/* Results */
.results.lang-text {
border-radius: 4px;
border: 1px solid rgba(255,255,255,0.5);
padding: 1ex;
max-height: 80vh;
overflow-y: auto;
}
.content {
margin: 1ex;
}
article.post {
max-width: min(650px, 100ex);
margin: 0 auto;
}
/* Header */
.site-header {
background-color: #002b36;
border-bottom: rgba(0,0,0,0.1) 1px solid;
text-align: center;
padding: 1ex;
color: #fff;
}
.site-header h1 {
margin-top: 0;
font-size: 200%;
font-family: monospace, sans;
color: inherit;
}
.site-header .site-links a {
color: #00fdf2;
}
.site-header .site-links .fancy-link {
border-right: 1px solid #fff;
padding-left: 0.75ex;
}
.site-header .site-links .fancy-link:last-of-type {
border: none;
}
/* Post header */
.post-metadata .post-publication-date {
background-color: #024;
color: #fff;
display: inline-block;
padding: 0 0.5ex;
border-radius: 4px;
}
.post-metadata ul.post-tags {
list-style: none;
display: inline;
padding: 0;
}
.post-metadata ul.post-tags li.post-tag a::before {
content: '#';
}
.post-metadata ul.post-tags li.post-tag {
display: inline;
font-style: italic;
}
/* Post index. */
.post-index .post-container {
/* box-shadow: 0px 2px 4px 2px rgba(0, 0, 0, 0.26); */
/* border-radius: 2px; */
/* padding: 1ex; */
margin-bottom: 1em;
padding-bottom: 1em;
border-bottom: #000 2px dashed;
}
.index-pages {
display: block;
margin: 0 auto;
width: fit-content;
}
.index-pages a {
padding: 1ex;
display: inline-block;
background-color: #024;
color: #fff;
border-radius: 4px;
}
.older-posts::after {
content: ' >';
}
.newer-posts::before {
content: '< ';
}
/* Categories and archive */
.post-list .post .post-metadata,
.post-list .post h4 {
display: inline;
}
.post-list .post {
margin-top: 1rem;
}
/* Tables. */
table, th, td, tr {
border: 1px solid black;
border-collapse: collapse;
}
td {
padding: 0.5ex;
}
tr.__table-separator {
border-bottom: 0.5ex solid black;
}
.connections svg {
max-width: 100%;
height: auto;
}
.connections svg .node polygon,
.connections svg .cluster polygon {
fill: white;
}
.connections svg #graph0 > polygon {
/* Main box */
fill: transparent;
stroke: none;
}
/* Side-to-side */
@media (min-width: 120ex) {
body:not(.no-toc) {
margin: 0;
max-width: none;
}
body:not(.no-toc) > .node {
margin-left: 25rem;
}
body:not(.no-toc) .global-table-of-contents {
position: fixed;
left: 1ex;
max-width: 25rem;
top: 1ex;
overflow: auto;
max-height: 100vh;
}
}
/* Dark mode. */
@media (prefers-color-scheme: dark) {
html, body {
background-color: #1d1f21;
color: #fafafe;
}
.node .node {
border-color: #8c9ea0;
}
.node .node > .title::before {
color: #aaa;
}
h2 a {
color: #fafafe;
}
a {
color: #00fdf2;
}
a:focus {
background-color: #00fdf2;
color: black;
}
h1 a:focus,
h2 a:focus,
h3 a:focus,
h4 a:focus,
h5 a:focus,
h6 a:focus
{
background-color: inherit;
color: #f7da4a;
}
h1,h2,h3,h4,h5,h6 {
color: #f7da4a;
}
/* Header */
.site-header {
background-color: #303033;
border-bottom: rgba(0,0,0,0.1) 1px solid;
}
.site-header h1 {
color: #fff;
}
.site-header .site-links .fancy-link {
border-right: 1px solid #fff;
}
/* Nav bar */
body nav h1 {
color: #eee;
}
body nav input {
color: #ddd;
border-bottom: 1px solid #888;
}
.results-box-container .results-box input {
color: #ddd;
border-bottom: 1px solid #888;
}
.results-box {
box-shadow: none;
background-color: #262628;
}
.results-box ul li {
background-color: #303033;
box-shadow: none;
}
.results-box ul li h2 {
color: white;
}
.results-box-container .results-box input:focus {
border-bottom: 1px solid #fff;
}
/* Code blocks */
.highlight pre {
padding: 1ex;
background-color: #262628;
}
pre {
padding: 0.25ex;
background-color: inherit;
box-shadow: 0px 1px 2px 1px rgba(0, 0, 0, 0.0);
}
pre code {
padding: 1ex;
font-size: medium;
border: none;
background: #262628;
color: #fff;
}
code, .verbatim {
background: #262628;
color: #FFF;
font-family: Menlo, Monaco, "Courier New", monospace;
}
/* Results */
.results.lang-text {
border: 1px solid rgba(255,255,255,0.25);
}
.node .contents ul,
.global-table-of-contents ul {
--tree-color: #aaa;
}
/* Tables. */
table, th, td, tr {
border: 1px solid #eee;
}
tr.__table-separator {
border-bottom: 0.5ex solid #eee;
}
.connections svg polygon {
stroke: white;
fill: #222;
}
.connections svg .edge polygon {
stroke: white;
fill: white;
}
.connections svg .node polygon,
.connections svg .cluster polygon {
stroke: transparent;
fill: #303030;
}
.connections svg .cluster-depth-1 polygon {
stroke: transparent;
fill: #353535;
}
.connections svg .cluster-depth-2 polygon {
stroke: transparent;
fill: #3a3939;
}
.connections svg .cluster-depth-3 polygon {
stroke: transparent;
fill: #444444;
}
.connections svg .cluster-depth-4 polygon {
stroke: transparent;
fill: #484847;
}
.connections svg .cluster-depth-5 polygon {
stroke: transparent;
fill: #515151;
}
.connections svg .cluster-depth-6 polygon {
stroke: transparent;
fill: #565555;
}
.connections svg .cluster-depth-7 polygon {
stroke: transparent;
fill: #5a5a5a;
}
.connections svg text {
fill: white;
}
.connections svg path {
stroke: white;
}
} }