Initial commit, added all backend files
This commit is contained in:
commit
8b85ea5952
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.css
|
||||||
|
*.map
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
FROM google/dart:2.7
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Get Dart dependencies
|
||||||
|
RUN mkdir -p /pub-cache
|
||||||
|
ENV PUB_CACHE=/pub-cache
|
||||||
|
ENV PATH="${PATH}:/pub-cache/bin"
|
||||||
|
RUN pub global activate webdev
|
||||||
|
ADD pubspec.* /app/
|
||||||
|
RUN pub get
|
||||||
|
RUN pub get --offline
|
||||||
|
|
||||||
|
RUN apt update && apt install ruby-sass ruby-dev build-essential -y
|
||||||
|
RUN gem install sass-listen
|
||||||
|
|
||||||
|
ADD . /app/
|
||||||
|
|
||||||
|
CMD ["./start.sh"]
|
144
README.org
Normal file
144
README.org
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
#+title: Org Website Backend
|
||||||
|
#+author: Lucien Cartier-Tilet
|
||||||
|
#+STARTUP: content
|
||||||
|
|
||||||
|
* Org Website Backend
|
||||||
|
** Introduction
|
||||||
|
Org Website Backend, or OWB, is a backend I created for my org-generated
|
||||||
|
static websites. It was originally developed as a backend for my linguistics
|
||||||
|
website [[https://langue.phundrak.com][langue.phundrak.com]], but eventually I began using its Dart and Scss
|
||||||
|
source code with other generated websites and pages of mine, such as my
|
||||||
|
configuration website hosted on [[https://phundrak.com/config][phundrak.com/config]]. I now want to just have
|
||||||
|
it stand on its own, while my org-generated websites stay Dart and Scss code
|
||||||
|
free.
|
||||||
|
|
||||||
|
** What this backend does
|
||||||
|
This has one goal: provide my org-generated websites a beautiful and unified
|
||||||
|
interface. This is achieved by reorganizing the HTML generated by Emacs when
|
||||||
|
publishing my org files, and by reading dynamically the website’s sitemap in
|
||||||
|
order to generate some user menus so they can navigate freely on the website
|
||||||
|
without the need to go back to the main page.
|
||||||
|
|
||||||
|
Visually, it also provides the user three themes:
|
||||||
|
- a light theme, enabled by default
|
||||||
|
- a dark theme, easier on the eyes
|
||||||
|
- a black theme, easier on smartphones’ battery if they have an AMOLED screen
|
||||||
|
The user’s preferences are kept on their browser’s local storage, so no
|
||||||
|
cookies are used.
|
||||||
|
|
||||||
|
** Why Dart?
|
||||||
|
Dart is a programming language developed by Google, which aims to be
|
||||||
|
compilable as native code or as Javascript code. In this case, I use it
|
||||||
|
compiled as Javascript. Why not Javascript then? I personally find Dart much
|
||||||
|
easier to work with, and to be a way saner language than Javascript is. It
|
||||||
|
also ensures type-safety and —to some extent— some compile-time code
|
||||||
|
verification. The dart compiler also performs some optimization at
|
||||||
|
compile-time, which is really benificial.
|
||||||
|
|
||||||
|
** Why SCSS?
|
||||||
|
SCSS is a superset of CSS which aims at simplifying CSS users’ life, and it’s
|
||||||
|
really good at its job. I especially enjoy being able to nest blocks within
|
||||||
|
one another, there’s no more need to rewrite endlessly some lines that could
|
||||||
|
simply be generated by SCSS. Why SCSS and not SASS? The answer is simple: I
|
||||||
|
have a buggy SASS installation, but SCSS works fine. Yep, simple as that.
|
||||||
|
|
||||||
|
Another thing is that I use the Ruby implementation of SASS. The reason for
|
||||||
|
that is also simple: it is the only one that provides a ~--watch~ option so
|
||||||
|
it automatically recompiles SCSS code to CSS when the SCSS code is changed.
|
||||||
|
|
||||||
|
** How to run this backend
|
||||||
|
This backend delivers only two main files:
|
||||||
|
- =/dart/main.dart.js= The main dart file compiled to Javascript (you don’t
|
||||||
|
need to worry about the others),
|
||||||
|
- =/style/style.css= The main style file compiled to CSS.
|
||||||
|
This is everything you need for beautiful org-generated websites.
|
||||||
|
|
||||||
|
*** Running locally
|
||||||
|
You could install Dart on your machine, as well as the Ruby implementation
|
||||||
|
of SASS with its dependencies. Then, all you have to do is run ~start.sh~,
|
||||||
|
and you’re good to go! Content will be delivered on the 8080 port. If you
|
||||||
|
wish to deliver content to another port, you can edit this file.
|
||||||
|
|
||||||
|
*** Docker
|
||||||
|
A Dockerfile is also provided so you can run this server inside a Docker
|
||||||
|
container, and thus you can avoid the hassle of installing Dart and Ruby
|
||||||
|
Sass. In order to run OWB, you can run the following:
|
||||||
|
#+BEGIN_SRC sh
|
||||||
|
$ docker build . --tag owb:1.0
|
||||||
|
$ docker run \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-e RELEASE="release" \
|
||||||
|
--restart always \
|
||||||
|
--detach \
|
||||||
|
--name owb \
|
||||||
|
owb:1.0
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
If you wish to run this in development mode, you could attach a volume so
|
||||||
|
the Dart and SCSS code are linked between your container and your
|
||||||
|
filesystem. This will also make ~webserver~ run in development mode; expect
|
||||||
|
shorter Dart compilation time, but slower Dart code execution.
|
||||||
|
#+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
|
||||||
|
|
||||||
|
*** Docker-compose
|
||||||
|
This repository also provides a ~docker-compose.yml~ file for easier Docker
|
||||||
|
usage with ~docker-compose~. If you wish to run your backend in
|
||||||
|
release-mode, simply run the following:
|
||||||
|
#+BEGIN_SRC sh
|
||||||
|
docker-compose up --detach
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
If, as above, you wish to enter development mode, you could modify the
|
||||||
|
~docker-compose.yml~ file like so:
|
||||||
|
#+BEGIN_SRC yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
- RELEASE="debug"
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./web/:/app/web/
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** 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
|
||||||
|
the top of your org file:
|
||||||
|
#+BEGIN_SRC org
|
||||||
|
,#+HTML_HEAD_EXTRA: <link rel="stylesheet" href="https://org.example.com/style/style.css"/>
|
||||||
|
,#+HTML_HEAD_EXTRA: <script defer src="https://org.example.com/dart/main.dart.js"></script>
|
||||||
|
,#+HTML_HEAD_EXTRA: <script src="https://kit.fontawesome.com/yourtokenhere.js" crossorigin="anonymous"></script>
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
You will need to obtain a (free) license on Fontawesome to use fontawesome’s
|
||||||
|
icons. Then, once you have this license, use your token provided by them to
|
||||||
|
edit the third header above.
|
||||||
|
|
||||||
|
Another option is to redirect any request of your website directed to ~/dart~
|
||||||
|
or ~/style~ to your running instance with the help of your reverse proxy,
|
||||||
|
such as Nginx. You could have for example the following lines:
|
||||||
|
#+BEGIN_SRC nginx
|
||||||
|
location /dart {
|
||||||
|
root http://localhost:8080/dart;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /style {
|
||||||
|
root http://localhost:8080/style;
|
||||||
|
}
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
Of course, be careful to write the same port in the rules above as the port
|
||||||
|
your backend is serving on.
|
14
analysis_options.yaml
Normal file
14
analysis_options.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Defines a default set of lint rules enforced for
|
||||||
|
# projects at Google. For details and rationale,
|
||||||
|
# see https://github.com/dart-lang/pedantic#enabled-lints.
|
||||||
|
include: package:pedantic/analysis_options.yaml
|
||||||
|
|
||||||
|
# For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
|
||||||
|
# Uncomment to specify additional rules.
|
||||||
|
# linter:
|
||||||
|
# rules:
|
||||||
|
# - camel_case_types
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
# exclude:
|
||||||
|
# - path/to/excluded/files/**
|
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- 8010:8080
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./web/:/app/web/
|
16
pubspec.yaml
Normal file
16
pubspec.yaml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
name: languephundrakcom
|
||||||
|
description: A bare-bone server for my linguistics website.
|
||||||
|
version: 1.0.0
|
||||||
|
homepage: https://langue.phundrak.com
|
||||||
|
author: Lucien Cartier-Tilet <lucien@phundrak.com>
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=2.5.0 <3.0.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
html: '^0.14.0+3'
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
build_runner: ^1.8.0
|
||||||
|
build_web_compilers: ^2.9.0
|
||||||
|
pedantic: ^1.9.0
|
3
start.sh
Executable file
3
start.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
sass --watch web/style/:web/style -tcompressed &
|
||||||
|
webdev serve --release --hostname 0.0.0.0
|
9
web/dart/main.dart
Normal file
9
web/dart/main.dart
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import './reorganize_html.dart' show reorganizeHtml;
|
||||||
|
import './theme.dart' show enableThemeChanger, setTheme;
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
await setTheme();
|
||||||
|
await reorganizeHtml().then((_) {
|
||||||
|
enableThemeChanger();
|
||||||
|
});
|
||||||
|
}
|
149
web/dart/navbar.dart
Normal file
149
web/dart/navbar.dart
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import 'dart:html';
|
||||||
|
|
||||||
|
import './parse_sitemap.dart' show parseSitemap;
|
||||||
|
|
||||||
|
// Returns the title of the current webpage
|
||||||
|
String getPageTitle() {
|
||||||
|
return querySelector('title').text;
|
||||||
|
}
|
||||||
|
|
||||||
|
Element makeIcon(List<String> classes, [String id]) {
|
||||||
|
final icon = Element.tag('i')..classes.addAll(classes);
|
||||||
|
if (id != null) {
|
||||||
|
icon.attributes['id'] = id;
|
||||||
|
}
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Element> makeToc() async {
|
||||||
|
return Element.li()
|
||||||
|
..attributes['id'] = 'toc-drop'
|
||||||
|
..classes.addAll(['nav-item', 'has-dropdown'])
|
||||||
|
..append(Element.a()
|
||||||
|
..attributes['href'] = 'javascript:void(0)'
|
||||||
|
..append(makeIcon(['fas', 'fa-list-ol'], 'tocBtn')));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Element makeShareLink(Element icon, String url) {
|
||||||
|
return Element.li()
|
||||||
|
..classes.add('dropdown-item')
|
||||||
|
..append(Element.a()
|
||||||
|
..attributes['href'] = url
|
||||||
|
..attributes['target'] = '_blank'
|
||||||
|
..append(icon));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Element> makeShare() async {
|
||||||
|
return Element.li()
|
||||||
|
..classes.addAll(['nav-item', 'has-dropdown'])
|
||||||
|
..append(Element.a()
|
||||||
|
..attributes['href'] = 'javascript:void(0)'
|
||||||
|
..append(makeIcon(['fas', 'fa-share-alt'])))
|
||||||
|
..append(Element.ul()
|
||||||
|
..classes.add('dropdown')
|
||||||
|
..attributes['id'] = 'drop-share'
|
||||||
|
..append(makeShareLink(
|
||||||
|
makeIcon(['fab', 'fa-twitter-square']),
|
||||||
|
'https://twitter.com/share?text=${getPageTitle()}'
|
||||||
|
'&url=${window.location.href}'))
|
||||||
|
..append(makeShareLink(makeIcon(['fab', 'fa-reddit-square']),
|
||||||
|
'https://www.reddit.com/submit?title=${getPageTitle()}s&url=${window.location.href}'))
|
||||||
|
..append(makeShareLink(makeIcon(['fas', 'fa-envelope-square']),
|
||||||
|
'mailto:?subject=${getPageTitle}&body=${window.location.href}'))
|
||||||
|
..append(makeShareLink(
|
||||||
|
makeIcon(['fab', 'fa-linkedin']),
|
||||||
|
'https://www.linkedin.com/shareArticle?mini=true&url=${window.location.href}'
|
||||||
|
'&title=${getPageTitle()}'))
|
||||||
|
..append(makeShareLink(makeIcon(['fab', 'fa-facebook-square']),
|
||||||
|
'https://www.facebook.com/sharer/sharer.php?u=${window.location.href}')));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Element> makeThemeChanger() async {
|
||||||
|
Element makeThemeItem(String t_btnId, Element t_icon, String t_text) {
|
||||||
|
return Element.li()
|
||||||
|
..classes.add('dropdown-item')
|
||||||
|
..append(Element.span()
|
||||||
|
..attributes['id'] = t_btnId
|
||||||
|
..append(t_icon)
|
||||||
|
..appendText(' $t_text'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Element.li()
|
||||||
|
..classes.addAll(['nav-item', 'has-dropdown'])
|
||||||
|
..append(Element.a()
|
||||||
|
..attributes['href'] = 'javascript:void(0)'
|
||||||
|
..append(Element.span()
|
||||||
|
..classes.add('fa-stack')
|
||||||
|
..style.verticalAlign = 'top'
|
||||||
|
..append(makeIcon(['fas', 'fa-sun', 'fa-stack-1x'])
|
||||||
|
..style.fontSize = '0.9em')
|
||||||
|
..append(makeIcon(['fas', 'fa-moon', 'fa-stack-1x']))))
|
||||||
|
..append(Element.ul()
|
||||||
|
..classes.add('dropdown')
|
||||||
|
..attributes['id'] = 'theme-dropdown'
|
||||||
|
..append(makeThemeItem('lightBtn', makeIcon(['fas', 'fa-sun']), 'Clair'))
|
||||||
|
..append(
|
||||||
|
makeThemeItem('darkBtn', makeIcon(['fas', 'fa-lightbulb']), 'Sombre'))
|
||||||
|
..append(
|
||||||
|
makeThemeItem('blackBtn', makeIcon(['fas', 'fa-moon']), 'Noir')));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Element> makeHome() async {
|
||||||
|
return Element.li()
|
||||||
|
..classes.add('nav-item')
|
||||||
|
..insertAdjacentElement(
|
||||||
|
'afterBegin',
|
||||||
|
Element.a()
|
||||||
|
..attributes['href'] = '/'
|
||||||
|
..append(makeIcon(['fas', 'fa-home'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a navbar atop of the HTML body, containing two buttons:
|
||||||
|
// - One back to home
|
||||||
|
// - A dropdown to each page detected in the sitemap
|
||||||
|
Future<Element> makeNavbar() async {
|
||||||
|
final navbar_content = Element.ul()..classes.add('navbar-nav');
|
||||||
|
final home = await makeHome();
|
||||||
|
final pages = await makePages();
|
||||||
|
final toc = await makeToc();
|
||||||
|
final share = await makeShare();
|
||||||
|
final theme = await makeThemeChanger();
|
||||||
|
|
||||||
|
navbar_content
|
||||||
|
..append(home)
|
||||||
|
..append(pages)
|
||||||
|
..append(toc)
|
||||||
|
..append(share)
|
||||||
|
..append(theme);
|
||||||
|
|
||||||
|
// Navbar content added to navbar
|
||||||
|
final navbar = Element.nav()
|
||||||
|
..classes.add('navbar')
|
||||||
|
..append(navbar_content);
|
||||||
|
|
||||||
|
return navbar;
|
||||||
|
}
|
43
web/dart/parse_sitemap.dart
Normal file
43
web/dart/parse_sitemap.dart
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import 'dart:html' show HttpRequest;
|
||||||
|
|
||||||
|
import 'package:html/parser.dart' show parse;
|
||||||
|
import 'package:html/dom.dart' show Element;
|
||||||
|
|
||||||
|
// Get the sitemap content
|
||||||
|
Future<String> getSitemap() async {
|
||||||
|
const path = 'sitemap.html';
|
||||||
|
try {
|
||||||
|
return await HttpRequest.getString(path);
|
||||||
|
} catch (e) {
|
||||||
|
print('Couldn’t open $path');
|
||||||
|
}
|
||||||
|
return 'Error';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the list of elements and detect pages from this list
|
||||||
|
Map<String, String> detectPages(List<Element> sitemap, [String prefix]) {
|
||||||
|
final links = <String, String>{};
|
||||||
|
for (var elem in sitemap) {
|
||||||
|
if (elem.outerHtml.contains('index')) {
|
||||||
|
continue;
|
||||||
|
} else if (elem.innerHtml.startsWith('<a')) {
|
||||||
|
elem = elem.firstChild;
|
||||||
|
final url = elem.attributes['href'];
|
||||||
|
final text = elem.firstChild.text;
|
||||||
|
links[url] = (prefix == null) ? text : '$text ($prefix)';
|
||||||
|
} else {
|
||||||
|
final prefix = elem.firstChild.text;
|
||||||
|
final ul = elem.children[0].children;
|
||||||
|
links.addAll(detectPages(ul, prefix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 sitemap = parse(content).getElementsByClassName('org-ul')[0].children;
|
||||||
|
return detectPages(sitemap);
|
||||||
|
}
|
71
web/dart/reorganize_html.dart
Normal file
71
web/dart/reorganize_html.dart
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import 'dart:html';
|
||||||
|
|
||||||
|
import './navbar.dart' show makeNavbar;
|
||||||
|
|
||||||
|
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['heigh'] = '150px'
|
||||||
|
..attributes['width'] = '150px')
|
||||||
|
..append(querySelector('h1'));
|
||||||
|
var subt = querySelector('.subtitle');
|
||||||
|
if (subt != null) {
|
||||||
|
header.append(subt);
|
||||||
|
}
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> wrapTables() async {
|
||||||
|
for (var table in querySelectorAll('table')) {
|
||||||
|
var largetable = DivElement()..className = 'largetable';
|
||||||
|
table.before(largetable);
|
||||||
|
largetable.children.add(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All images that are not nested inside a link will be linkified to themselves.
|
||||||
|
void linkifyImg() {
|
||||||
|
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);
|
||||||
|
link.append(img);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reorganizeHtml() async {
|
||||||
|
final content = querySelector('#content');
|
||||||
|
|
||||||
|
// Make navbar
|
||||||
|
await makeNavbar().then((navbar) {
|
||||||
|
querySelector('body').insertAdjacentElement('afterBegin', navbar);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make header
|
||||||
|
await makeHeader().then((header) {
|
||||||
|
content.insertAdjacentElement('beforeBegin', header);
|
||||||
|
final subtitle = querySelector('.subtitle');
|
||||||
|
if (subtitle != null) {
|
||||||
|
header.append(subtitle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// wrap tables in container for better SCSS display
|
||||||
|
await wrapTables();
|
||||||
|
|
||||||
|
linkifyImg();
|
||||||
|
|
||||||
|
// Add correct class to TOC
|
||||||
|
querySelector('#toc-drop')
|
||||||
|
.append(querySelector('#table-of-contents')..classes.add('dropdown'));
|
||||||
|
|
||||||
|
// Remove all <br> tags from HTML
|
||||||
|
querySelectorAll('br').forEach((br) => br.remove());
|
||||||
|
}
|
53
web/dart/theme.dart
Normal file
53
web/dart/theme.dart
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import 'dart:html';
|
||||||
|
|
||||||
|
class Theme {
|
||||||
|
String _name;
|
||||||
|
String _icon;
|
||||||
|
|
||||||
|
Theme(String t_name, String t_icon) {
|
||||||
|
_name = t_name;
|
||||||
|
_icon = t_icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getIcon() => _icon;
|
||||||
|
String getName() => _name;
|
||||||
|
}
|
||||||
|
|
||||||
|
final themes = {
|
||||||
|
'light': Theme('light', 'fa-sun'),
|
||||||
|
'dark': Theme('dark', 'fa-lightbulb'),
|
||||||
|
'black': Theme('black', 'fa-moon')
|
||||||
|
};
|
||||||
|
|
||||||
|
final localStorage = window.localStorage;
|
||||||
|
|
||||||
|
var currentTheme = themes[localStorage['theme']];
|
||||||
|
|
||||||
|
void enableThemeChanger() {
|
||||||
|
final darkBtn = querySelector('#darkBtn');
|
||||||
|
final lightBtn = querySelector('#lightBtn');
|
||||||
|
final blackBtn = querySelector('#blackBtn');
|
||||||
|
|
||||||
|
lightBtn.onClick.listen((_) => switchTheme(themes['light']));
|
||||||
|
darkBtn.onClick.listen((_) => switchTheme(themes['dark']));
|
||||||
|
blackBtn.onClick.listen((_) => switchTheme(themes['black']));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setTheme() async {
|
||||||
|
if (currentTheme == null) {
|
||||||
|
currentTheme = themes['light'];
|
||||||
|
localStorage['theme'] = currentTheme.getName();
|
||||||
|
}
|
||||||
|
querySelector('body')
|
||||||
|
..classes.clear()
|
||||||
|
..classes.add(currentTheme.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
void switchTheme(Theme currentTheme) {
|
||||||
|
// Set HTML theme
|
||||||
|
querySelector('body')
|
||||||
|
..classes.clear()
|
||||||
|
..classes.add(currentTheme.getName());
|
||||||
|
// Set storage theme
|
||||||
|
localStorage['theme'] = currentTheme.getName();
|
||||||
|
}
|
591
web/style/style.scss
Normal file
591
web/style/style.scss
Normal file
@ -0,0 +1,591 @@
|
|||||||
|
@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)";
|
||||||
|
$switch-smaller-screen: "only screen and (max-width: 400px)";
|
||||||
|
|
||||||
|
$navbar-height: 70px;
|
||||||
|
$postamble-height: 55px;
|
||||||
|
|
||||||
|
// Themes /////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
$dark: rgba( 52, 73, 94, 1);
|
||||||
|
$black: rgba( 0, 0, 0, 1);
|
||||||
|
$accent1: rgba( 93, 115, 126, 1);
|
||||||
|
$accent2: rgba( 92, 172, 126, 1);
|
||||||
|
$accent3: rgba(197, 193, 155, 1);
|
||||||
|
$light: #eee;
|
||||||
|
$grey1: #f8f8f8;
|
||||||
|
$grey2: #dbe1e8;
|
||||||
|
$grey3: #b2becd;
|
||||||
|
$grey4: #6c7983;
|
||||||
|
$grey5: #454e56;
|
||||||
|
$grey6: #12181b;
|
||||||
|
|
||||||
|
// Accent 1
|
||||||
|
// Black
|
||||||
|
$gradient-accent1-black-left: linear-gradient(to left, $black, $accent1, $accent1);
|
||||||
|
$gradient-accent1-black-right: linear-gradient(to right, $black, $accent1, $accent1);
|
||||||
|
// Dark
|
||||||
|
$gradient-accent1-dark-left: linear-gradient(to left, $dark, $accent1);
|
||||||
|
$gradient-accent1-dark-right: linear-gradient(to right, $dark, $accent1);
|
||||||
|
// Light
|
||||||
|
$gradient-accent1-light-left: linear-gradient(to left, $light, $accent1);
|
||||||
|
$gradient-accent1-light-right: linear-gradient(to right, $light, $accent1);
|
||||||
|
// Accent 2
|
||||||
|
// Black
|
||||||
|
$gradient-accent2-black-left: linear-gradient(to left, $black, $accent2, $accent2);
|
||||||
|
$gradient-accent2-black-right: linear-gradient(to right, $black, $accent2, $accent2);
|
||||||
|
// Dark
|
||||||
|
$gradient-accent2-dark-left: linear-gradient(to left, $dark, $accent2);
|
||||||
|
$gradient-accent2-dark-right: linear-gradient(to right, $dark, $accent2);
|
||||||
|
// Light
|
||||||
|
$gradient-accent2-light-left: linear-gradient(to left, $light, $accent2);
|
||||||
|
$gradient-accent2-light-right: linear-gradient(to right, $light, $accent2);
|
||||||
|
// Accent 3
|
||||||
|
// Black
|
||||||
|
$gradient-accent3-black-left: linear-gradient(to left, $black, $accent3, $accent3);
|
||||||
|
$gradient-accent3-black-right: linear-gradient(to right, $black, $accent3, $accent3);
|
||||||
|
// Dark
|
||||||
|
$gradient-accent3-dark-left: linear-gradient(to left, $dark, $accent3);
|
||||||
|
$gradient-accent3-dark-right: linear-gradient(to right, $dark, $accent3);
|
||||||
|
// Light
|
||||||
|
$gradient-accent3-light-left: linear-gradient(to left, $light, $accent3);
|
||||||
|
$gradient-accent3-light-right: linear-gradient(to right, $light, $accent3);
|
||||||
|
|
||||||
|
.light {
|
||||||
|
$bg-nav: $gradient-accent3-light-right;
|
||||||
|
$border-color: $accent1;
|
||||||
|
|
||||||
|
color: $dark;
|
||||||
|
background: $light;
|
||||||
|
|
||||||
|
transition: background 500ms ease-in-out, color 1s ease-in-out;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
box-shadow: 3px 3px $dark;
|
||||||
|
border-color: $light;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.src {
|
||||||
|
&::before {
|
||||||
|
background-color: $light;
|
||||||
|
color: $dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar, header {
|
||||||
|
background: $bg-nav;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
background: $gradient-accent3-light-left;
|
||||||
|
color: $dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
border-bottom: 1px dotted $accent3;
|
||||||
|
|
||||||
|
.tooltiptext {
|
||||||
|
background-color: $accent3;
|
||||||
|
color: $dark;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border-color: $accent3 transparent transparent transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
background: $accent3;
|
||||||
|
color: $dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
a {
|
||||||
|
box-shadow: inset 0 -3px 0 $accent3;
|
||||||
|
transition: box-shadow 300ms ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: inset 0 -23px 0 $accent3;
|
||||||
|
transition: box-shadow 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid $dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: darken($light, 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gentree {
|
||||||
|
filter: invert(0%);
|
||||||
|
transition: filter 1s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark, .black {
|
||||||
|
$bg-nav: $gradient-accent2-dark-right;
|
||||||
|
$border-color: $dark;
|
||||||
|
|
||||||
|
color: $light;
|
||||||
|
background: $dark;
|
||||||
|
|
||||||
|
transition: background 500ms ease-in-out, color 1s ease-in-out;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
box-shadow: 3px 3px $dark;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.src {
|
||||||
|
&::before {
|
||||||
|
background-color: $dark;
|
||||||
|
color: $light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar, header {
|
||||||
|
background: $bg-nav;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
background: $gradient-accent2-dark-left;
|
||||||
|
color: $light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
border-bottom: 1px dotted $accent1;
|
||||||
|
|
||||||
|
.tooltiptext {
|
||||||
|
background-color: $accent1;
|
||||||
|
color: $light;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border-color: $accent1 transparent transparent transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
background: $accent3;
|
||||||
|
color: $dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
a {
|
||||||
|
box-shadow: inset 0 -3px 0 $accent2;
|
||||||
|
transition: box-shadow 300ms ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: inset 0 -23px 0 $accent2;
|
||||||
|
transition: box-shadow 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid $accent1;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: darken($dark, 2.5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gentree {
|
||||||
|
filter: invert(100%);
|
||||||
|
transition: filter 1s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.black {
|
||||||
|
$bg-nav: $gradient-accent1-black-right;
|
||||||
|
|
||||||
|
background: $black;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
box-shadow: 3px 3px $light;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.src {
|
||||||
|
&::before {
|
||||||
|
background-color: $black;
|
||||||
|
color: $light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar, header {
|
||||||
|
background: $bg-nav;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
background: $gradient-accent1-black-left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
background: $dark;
|
||||||
|
color: $light;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
a {
|
||||||
|
box-shadow: inset 0 -3px 0 $accent1;
|
||||||
|
transition: box-shadow 300ms ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: inset 0 -23px 0 $accent1;
|
||||||
|
transition: box-shadow 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid $light;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: lighten($black, 15%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style *********************************************************************/
|
||||||
|
|
||||||
|
* {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: "Noto Sans Runes", "DoulosSIL", "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif;
|
||||||
|
font-size: 1.2em;
|
||||||
|
|
||||||
|
transition: background 500ms ease-in-out, color 1s ease-in-out;
|
||||||
|
|
||||||
|
header, .navbar {
|
||||||
|
transition: background 500ms ease-in-out, color 1s ease-in-out;
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: currentColor;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
z-index: 4;
|
||||||
|
height: $navbar-height;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 1em;
|
||||||
|
margin-top: $navbar-height;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding-bottom: 6em;
|
||||||
|
text-align: center;
|
||||||
|
clip-path: polygon(50% 0%, 100% 0, 100% 80%, 50% 100%, 0 80%, 0 0);
|
||||||
|
transition: background 500ms ease-in-out;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 5em;
|
||||||
|
margin: 0;
|
||||||
|
@media #{$switch-small-screen} {
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
border-bottom-right-radius: 8px;
|
||||||
|
border-bottom-left-radius: 8px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
min-height: 3rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-dropdown {
|
||||||
|
&:focus-within {
|
||||||
|
.dropdown {
|
||||||
|
opacity: 1;
|
||||||
|
top: $navbar-height;
|
||||||
|
z-index: 5;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table-of-contents {
|
||||||
|
top: $navbar-height / 1.3;
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 5;
|
||||||
|
height: 500%;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#theme-dropdown {
|
||||||
|
width: 250px;
|
||||||
|
flex-direction: row;
|
||||||
|
transform: translateX(-75%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#drop-page {
|
||||||
|
flex-direction: column;
|
||||||
|
transform: translateX(-40%);
|
||||||
|
li {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#drop-share {
|
||||||
|
li {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
a {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
size: 0.7rem;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text-table-of-contents {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
padding: 50px;
|
||||||
|
padding-top: 0;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: justify;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#postamble {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: 'author email date';
|
||||||
|
|
||||||
|
@media #{$switch-small-screen} {
|
||||||
|
grid-template-areas: 'author date' 'email email';
|
||||||
|
}
|
||||||
|
|
||||||
|
@media #{$switch-smaller-screen} {
|
||||||
|
grid-template-areas: 'author' 'date' 'email';
|
||||||
|
}
|
||||||
|
|
||||||
|
font-size: 0.8em;
|
||||||
|
align-content: space-evenly;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author {
|
||||||
|
grid-area: author;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email {
|
||||||
|
grid-area: email;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
grid-area: date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
@media #{$switch-small-screen} {
|
||||||
|
font-size: 1.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 2em;
|
||||||
|
|
||||||
|
@media #{$switch-small-screen} {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
|
||||||
|
@media #{$switch-small-screen} {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
|
||||||
|
@media #{$switch-small-screen} {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.tooltiptext {
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
margin-left: -60px; /* Half the width */
|
||||||
|
bottom: 100%;
|
||||||
|
left: 10%;
|
||||||
|
padding: 5px 10px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5;
|
||||||
|
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
top: 100%; /* At the bottom of the tooltip */
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -5px;
|
||||||
|
border-width: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.tooltiptext {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
padding: 0;
|
||||||
|
margin: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.largetable {
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top:20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 600px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.figure {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-inline-start: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.src {
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user