Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
8ca55cb710
|
|||
|
aa4600b588
|
|||
|
2f297b6374
|
|||
|
9f6a32f5d2
|
|||
|
34ac1480d3
|
|||
|
fc1556128c
|
|||
|
3c942b4b8f
|
|||
|
90106df0f6
|
|||
|
a82fa74a6b
|
|||
|
e81986683c
|
|||
|
cc6519c302
|
|||
|
705e35e971
|
|||
|
61fe6f71a4
|
|||
|
404e79211e
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
||||
/.packages
|
||||
/pubspec.lock
|
||||
/.sass-cache/
|
||||
/build/
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@@ -1,7 +1,11 @@
|
||||
FROM google/dart:2.7
|
||||
FROM google/dart:2.8
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Get Ruby Sass
|
||||
RUN apt update && apt install ruby-sass ruby-dev build-essential -y
|
||||
RUN gem install sass-listen
|
||||
|
||||
# Get Dart dependencies
|
||||
RUN mkdir -p /pub-cache
|
||||
ENV PUB_CACHE=/pub-cache
|
||||
@@ -11,10 +15,8 @@ ADD pubspec.* /app/
|
||||
RUN pub get
|
||||
RUN pub get --offline
|
||||
|
||||
# Get Ruby Sass
|
||||
RUN apt update && apt install ruby-sass ruby-dev build-essential -y
|
||||
RUN gem install sass-listen
|
||||
|
||||
ADD . /app/
|
||||
# ADD . /app/
|
||||
ADD web /app/
|
||||
ADD start.sh /app/
|
||||
|
||||
CMD ["./start.sh"]
|
||||
|
||||
38
README.org
38
README.org
@@ -100,11 +100,39 @@
|
||||
#+END_SRC
|
||||
|
||||
** Running in development mode
|
||||
To run this backend in development mode, you will have to remove the
|
||||
~--release~ option from the ~webdev~ command in the ~start.sh~ file. This
|
||||
will allow webdev to compile Dart files faster, but at the price of slower
|
||||
compiled Javascript files. If you use Docker, don’t forget to rebuild your
|
||||
image.
|
||||
# To run this backend in development mode, you will have to remove the
|
||||
# ~--release~ option from the ~webdev~ command in the ~start.sh~ file. This
|
||||
# will allow webdev to compile Dart files faster, but at the price of slower
|
||||
# compiled Javascript files. If you use Docker, don’t forget to rebuild your
|
||||
# image.
|
||||
To run this backend in development mode, you can add to your environment the
|
||||
variable ~RELEASE~ with the value ~debug~. Running the backend locally, you
|
||||
would start it like so:
|
||||
#+BEGIN_SRC sh
|
||||
RELEASE=debug ./start.sh
|
||||
#+END_SRC
|
||||
|
||||
Running it with Docker, you would use the following command:
|
||||
#+BEGIN_SRC sh
|
||||
docker run \
|
||||
-p 8080:8080 \
|
||||
-v ./web:/app/web \
|
||||
-e RELEASE=debug \
|
||||
--restart always \
|
||||
--detach \
|
||||
--name owb \
|
||||
owb:1.0
|
||||
#+END_SRC
|
||||
|
||||
And with docker-compose, you would add the following line to your ~owb~
|
||||
service:
|
||||
#+BEGIN_SRC yaml
|
||||
environment:
|
||||
- RELEASE=debug
|
||||
#+END_SRC
|
||||
|
||||
Any other value to this environment variable will make your backend run in
|
||||
release mode (actually, it will only make ~webdev~ run in release mode).
|
||||
|
||||
** How can I use this in my org files?
|
||||
Let’s say you serve your files on org.example.com, add the following lines to
|
||||
|
||||
@@ -6,5 +6,7 @@ services:
|
||||
ports:
|
||||
- 8010:8080
|
||||
restart: always
|
||||
environment:
|
||||
- RELEASE=debug
|
||||
volumes:
|
||||
- ./web:/app/web
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
name: languephundrakcom
|
||||
description: A bare-bone server for my linguistics website.
|
||||
name: orgwebsitebackend
|
||||
description: A bare-bone server for org-generated websites.
|
||||
version: 1.0.0
|
||||
homepage: https://langue.phundrak.com
|
||||
homepage: https://labs.phundrak.com/phundrak/org-website-backend
|
||||
author: Lucien Cartier-Tilet <lucien@phundrak.com>
|
||||
|
||||
environment:
|
||||
sdk: '>=2.5.0 <3.0.0'
|
||||
sdk: '>=2.7.0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
html: '^0.14.0+3'
|
||||
|
||||
7
start.sh
7
start.sh
@@ -1,3 +1,8 @@
|
||||
#!/bin/bash
|
||||
sass --watch web/style/:web/style -tcompressed &
|
||||
webdev serve --release --hostname 0.0.0.0
|
||||
|
||||
[ "$RELEASE" == "debug" ] \
|
||||
&& webdev serve --hostname 0.0.0.0 \
|
||||
|| webdev serve --release --hostname 0.0.0.0
|
||||
|
||||
# webdev serve --release --hostname 0.0.0.0
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import './reorganize_html.dart' show reorganizeHtml;
|
||||
import './theme.dart' show enableThemeChanger, setTheme;
|
||||
import './parse_sitemap.dart' show getSitemap;
|
||||
|
||||
Future<void> main() async {
|
||||
await setTheme();
|
||||
await reorganizeHtml().then((_) {
|
||||
enableThemeChanger();
|
||||
});
|
||||
await reorganizeHtml().then((_) => enableThemeChanger());
|
||||
await getSitemap();
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@ String getPageTitle() {
|
||||
|
||||
Element makeIcon(List<String> classes, [String id]) {
|
||||
final icon = Element.tag('i')..classes.addAll(classes);
|
||||
if (id != null) {
|
||||
icon.attributes['id'] = id;
|
||||
}
|
||||
icon.attributes['id'] = id ?? '';
|
||||
return icon;
|
||||
}
|
||||
|
||||
@@ -28,24 +26,12 @@ Future<Element> makePages() async {
|
||||
var pages = Element.ul()
|
||||
..attributes['id'] = 'drop-page'
|
||||
..classes.add('dropdown');
|
||||
await parseSitemap().then((final sitemap) => {
|
||||
sitemap.forEach((url, name) {
|
||||
final link = Element.li()
|
||||
..classes.add('dropdown-item')
|
||||
..insertAdjacentElement(
|
||||
'afterBegin',
|
||||
Element.a()
|
||||
..attributes['href'] = url
|
||||
..innerText = name);
|
||||
pages.insertAdjacentElement('beforeEnd', link);
|
||||
})
|
||||
});
|
||||
return Element.li()
|
||||
..append(Element.a()
|
||||
..attributes['href'] = 'javascript:void(0)'
|
||||
..append(makeIcon(['fas', 'fa-flag'])))
|
||||
..classes.addAll(['nav-item', 'has-dropdown'])
|
||||
..insertAdjacentElement('beforeEnd', pages);
|
||||
..append(pages);
|
||||
}
|
||||
|
||||
Element makeShareLink(Element icon, String url) {
|
||||
@@ -54,6 +40,7 @@ Element makeShareLink(Element icon, String url) {
|
||||
..append(Element.a()
|
||||
..attributes['href'] = url
|
||||
..attributes['target'] = '_blank'
|
||||
..attributes['rel'] = 'noreferrer'
|
||||
..append(icon));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'dart:html' show HttpRequest;
|
||||
import 'dart:html' as html show HttpRequest, Element, querySelector;
|
||||
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:html/dom.dart' show Element;
|
||||
import 'package:html/dom.dart' as dom show Element;
|
||||
|
||||
final excluded_keywords = ['index', 'CONTRIBUTING', 'LICENSE', 'README'];
|
||||
final excluded_keywords = {'index', 'CONTRIBUTING', 'LICENSE', 'README'};
|
||||
|
||||
// Get the sitemap content
|
||||
Future<String> getSitemap() async {
|
||||
Future<String> fetchRemoteSitemap() async {
|
||||
const path = 'sitemap.html';
|
||||
try {
|
||||
return await HttpRequest.getString(path);
|
||||
return await html.HttpRequest.getString(path);
|
||||
} catch (e) {
|
||||
print('Couldn’t open $path');
|
||||
}
|
||||
@@ -17,10 +17,10 @@ Future<String> getSitemap() async {
|
||||
}
|
||||
|
||||
// Parse the list of elements and detect pages from this list
|
||||
Map<String, String> detectPages(List<Element> sitemap, [String prefix]) {
|
||||
Map<String, String> detectPages(List<dom.Element> sitemap, [String prefix]) {
|
||||
final links = <String, String>{};
|
||||
for (var elem in sitemap) {
|
||||
for(var kw in excluded_keywords) {
|
||||
for (var kw in excluded_keywords) {
|
||||
if (elem.outerHtml.contains(kw)) {
|
||||
continue;
|
||||
}
|
||||
@@ -31,7 +31,9 @@ Map<String, String> detectPages(List<Element> sitemap, [String prefix]) {
|
||||
final text = elem.firstChild.text;
|
||||
links[url] = (prefix == null) ? text : '$text ($prefix)';
|
||||
} else {
|
||||
final prefix = elem.firstChild.text;
|
||||
prefix = (prefix == null)
|
||||
? elem.firstChild.text
|
||||
: '$prefix / ${elem.firstChild.text}';
|
||||
final ul = elem.children[0].children;
|
||||
links.addAll(detectPages(ul, prefix));
|
||||
}
|
||||
@@ -42,7 +44,31 @@ Map<String, String> detectPages(List<Element> sitemap, [String prefix]) {
|
||||
// This function returns a Map which contains all links to languages detected
|
||||
// from the sitemap.
|
||||
Future<Map<String, String>> parseSitemap() async {
|
||||
final content = await getSitemap();
|
||||
final content = await fetchRemoteSitemap();
|
||||
final sitemap = parse(content).getElementsByClassName('org-ul')[0].children;
|
||||
return detectPages(sitemap);
|
||||
}
|
||||
|
||||
Future sleep(Duration time) async {
|
||||
return Future.delayed(time);
|
||||
}
|
||||
|
||||
Future<html.Element> getSitemap() async {
|
||||
final sitemap = await parseSitemap();
|
||||
final pages = <html.Element>[];
|
||||
sitemap.forEach((url, name) {
|
||||
final link = html.Element.li()
|
||||
..classes.add('dropdown-item')
|
||||
..append(html.Element.a()
|
||||
..attributes['href'] = url
|
||||
..innerText = name);
|
||||
pages.add(link);
|
||||
});
|
||||
var drop_container;
|
||||
do {
|
||||
await sleep(Duration(seconds: 1));
|
||||
drop_container = html.querySelector('#drop-page');
|
||||
} while (drop_container != null);
|
||||
pages.forEach((link) => drop_container.append(link));
|
||||
return drop_container;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ import 'dart:html';
|
||||
|
||||
import './navbar.dart' show makeNavbar;
|
||||
|
||||
const image_header =
|
||||
'https://phundrak.fra1.cdn.digitaloceanspaces.com/img/mahakala-monochrome.png';
|
||||
|
||||
Future<Element> makeHeader() async {
|
||||
var header = Element.tag('header');
|
||||
header
|
||||
..append(Element.img()
|
||||
..attributes['src'] =
|
||||
'https://cdn.phundrak.com/img/mahakala-monochrome.png'
|
||||
..attributes['alt'] = 'Logo de Phundrak'
|
||||
..attributes['src'] = image_header
|
||||
..attributes['alt'] = 'Logo'
|
||||
..attributes['heigh'] = '150px'
|
||||
..attributes['width'] = '150px')
|
||||
..append(querySelector('h1'));
|
||||
@@ -28,10 +30,8 @@ Future<void> wrapTables() async {
|
||||
}
|
||||
|
||||
// All images that are not nested inside a link will be linkified to themselves.
|
||||
void linkifyImg() {
|
||||
Future<void> linkifyImg() async {
|
||||
querySelectorAll('img').forEach((img) {
|
||||
print(img.attributes['src']);
|
||||
print(img.parent.tagName);
|
||||
if (img.parent.tagName == 'P') {
|
||||
final link = Element.a()..attributes['href'] = img.attributes['src'];
|
||||
img.insertAdjacentElement('beforeBegin', link);
|
||||
@@ -41,30 +41,31 @@ void linkifyImg() {
|
||||
}
|
||||
|
||||
Future<void> reorganizeHtml() async {
|
||||
final content = querySelector('#content');
|
||||
|
||||
// Make navbar
|
||||
await makeNavbar().then((navbar) {
|
||||
querySelector('body').insertAdjacentElement('afterBegin', navbar);
|
||||
});
|
||||
final navbar = await makeNavbar();
|
||||
|
||||
// Make header
|
||||
await makeHeader().then((header) {
|
||||
content.insertAdjacentElement('beforeBegin', header);
|
||||
final subtitle = querySelector('.subtitle');
|
||||
if (subtitle != null) {
|
||||
header.append(subtitle);
|
||||
}
|
||||
});
|
||||
final header = await makeHeader();
|
||||
|
||||
// wrap tables in container for better SCSS display
|
||||
await wrapTables();
|
||||
|
||||
linkifyImg();
|
||||
// Make images not linking somewhere link to themselves
|
||||
await linkifyImg();
|
||||
|
||||
// Add navbar to page
|
||||
querySelector('body').insertAdjacentElement('afterBegin', navbar);
|
||||
|
||||
// Add headet to page
|
||||
querySelector('#content').insertAdjacentElement('beforeBegin', header);
|
||||
|
||||
// Add correct class to TOC
|
||||
querySelector('#toc-drop')
|
||||
.append(querySelector('#table-of-contents')..classes.add('dropdown'));
|
||||
final toc = (querySelector('#table-of-contents') ??
|
||||
(Element.div()
|
||||
..attributes['id'] = 'table-of-contents'
|
||||
..innerText = 'Table of Contents Unavailable'))
|
||||
..classes.add('dropdown');
|
||||
navbar.querySelector('#toc-drop').append(toc);
|
||||
|
||||
// Remove all <br> tags from HTML
|
||||
querySelectorAll('br').forEach((br) => br.remove());
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic|Roboto+Slab:400,700|Inconsolata:400,700);
|
||||
@font-face {
|
||||
font-family: "DoulosSIL";
|
||||
font-display: swap;
|
||||
src: url("/fonts/DoulosSIL-R.woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Noto Sans Runes";
|
||||
font-display: swap;
|
||||
src: url("../fonts/NotoSansRunic-Regular.ttf");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Helvetica Neue";
|
||||
font-display: swap;
|
||||
src: url("../fonts/HelveticaNeue.ttf");
|
||||
}
|
||||
|
||||
/* Variables *****************************************************************/
|
||||
|
||||
$switch-small-screen: "only screen and (max-width: 600px)";
|
||||
@@ -79,7 +62,7 @@ $gradient-accent3-light-right: linear-gradient(to right, $light, $accent3);
|
||||
transition: background 500ms ease-in-out, color 1s ease-in-out;
|
||||
|
||||
pre {
|
||||
box-shadow: 3px 3px $dark;
|
||||
box-shadow: 3px 5px $dark;
|
||||
border-color: $light;
|
||||
}
|
||||
|
||||
@@ -277,7 +260,7 @@ $gradient-accent3-light-right: linear-gradient(to right, $light, $accent3);
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Noto Sans Runes", "DoulosSIL", "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif;
|
||||
font-family: "Lato", "proxima-nova", Arial, sans-serif;
|
||||
font-size: 1.2em;
|
||||
|
||||
transition: background 500ms ease-in-out, color 1s ease-in-out;
|
||||
@@ -353,7 +336,7 @@ header {
|
||||
box-shadow: rgba(2, 8, 20, 0.1) 0px 0.175em 0.5em;
|
||||
transform: translateX(-40%);
|
||||
|
||||
transition: opacity 500ms ease-in-out, top 500ms ease-in-out;
|
||||
transition: opacity 500ms ease-in-out, top 500ms ease-in-out, height 500ms ease-in-out;
|
||||
}
|
||||
|
||||
.has-dropdown {
|
||||
@@ -365,7 +348,7 @@ header {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#table-of-contents {
|
||||
#table-of-contents, #drop-page {
|
||||
top: $navbar-height / 1.3;
|
||||
opacity: 1;
|
||||
z-index: 5;
|
||||
@@ -381,15 +364,26 @@ header {
|
||||
transform: translateX(-75%);
|
||||
}
|
||||
|
||||
#drop-page {
|
||||
#drop-page, #table-of-contents {
|
||||
flex-direction: column;
|
||||
transform: translateX(-40%);
|
||||
li {
|
||||
padding: 5px;
|
||||
|
||||
transform: translateX(-45%);
|
||||
top: -40px;
|
||||
|
||||
height: 0;
|
||||
min-width: 350px;
|
||||
overflow-y: auto;
|
||||
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
@media #{$switch-small-screen} {
|
||||
#drop-page {
|
||||
transform: translateX(-27.5%);
|
||||
}
|
||||
}
|
||||
|
||||
#drop-share {
|
||||
#drop-share, #drop-page {
|
||||
li {
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -401,7 +395,6 @@ header {
|
||||
a {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
size: 0.7rem;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
font-weight: bold;
|
||||
@@ -409,17 +402,9 @@ header {
|
||||
}
|
||||
|
||||
#table-of-contents {
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
float: right;
|
||||
overflow-y: auto;
|
||||
height: 0%;
|
||||
width: 75%;
|
||||
min-width: 350px;
|
||||
transform: translateX(-45%);
|
||||
font-size: 0.9em;
|
||||
top: -40px;
|
||||
|
||||
transition: height 500ms ease-in-out, opacity 500ms ease-in-out, top 500ms ease-in-out;
|
||||
|
||||
li {
|
||||
@@ -442,6 +427,10 @@ header {
|
||||
margin: 0 auto;
|
||||
text-align: justify;
|
||||
|
||||
@media #{$switch-small-screen} {
|
||||
padding: 50px 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user