Change repo layout

This commit is contained in:
Jeff 2024-06-06 20:27:39 -04:00
parent 4d507e2f0f
commit d01372e431
24 changed files with 53 additions and 5759 deletions

View File

@ -1,3 +0,0 @@
# jeffa.io
Source code for https://jeffa.io.

17
compose.yaml Normal file
View File

@ -0,0 +1,17 @@
services:
jeffa.io:
image: git.jeffa.io/jeff/www.jeffa.io
ports:
- "80:80"
- "443:443"
- "8000:8000"
volumes:
- storage:/srv
hotbot:
image: git.jeffa.io/jeff/hotbot
environment:
- STATE_FILE=/srv/wiglic.txt
volumes:
- storage:/srv
volumes:
storage:

1
id_ed25519.pub Normal file
View File

@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDR7q2FfdizNkvel66N/jHcwlEA8R5JEOneKXshMWqkX jeff@station-1

35
server.bu Normal file
View File

@ -0,0 +1,35 @@
variant: fcos
version: 1.5.0
passwd:
users:
- name: core
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDR7q2FfdizNkvel66N/jHcwlEA8R5JEOneKXshMWqkX jeff@station-1
systemd:
units:
- name: jeffa.io.service
enabled: true
contents: |
[Unit]
Description=hotbot
[Service]
Type=oneshot
After=network-online.target
Wants=network-online.target
ExecStart=/usr/bin/podman compose up
[Install]
WantedBy=multi-user.target
storage:
disks:
- device: /dev/vdb
wipe_table: true
partitions:
- number: 1
label: jeffa.io-storage
filesystems:
- path: /jeffa.io-storage
device: /dev/disk/by-partlabel/jeffa.io-storage
format: btrfs
wipe_filesystem: true
label: jeffa.io-storage
with_mount_unit: true

2
shuttle/.gitignore vendored
View File

@ -1,2 +0,0 @@
/target
Secrets*.toml

5151
shuttle/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
[package]
name = "jeffa"
version = "0.1.0"
edition = "2021"
[dependencies]
dust-lang = "0.1.1"
rocket = "0.5.0-rc.2"
shuttle-rocket = "0.25.0"
shuttle-runtime = "0.25.0"
tokio = "1.26.0"

View File

@ -1,24 +0,0 @@
use dust_lib;
#[macro_use]
extern crate rocket;
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
#[get("/dust/<code>")]
fn dust(code: &str) -> String {
match dust_lib::eval(code) {
Ok(result) => result.to_string(),
Err(error) => error.to_string(),
}
}
#[shuttle_runtime::main]
async fn rocket() -> shuttle_rocket::ShuttleRocket {
let rocket = rocket::build().mount("/", routes![index, dust]);
Ok(rocket.into())
}

View File

@ -1,7 +0,0 @@
jeffa.io,
www.jeffa.io,
localhost:8080 {
root * /srv/
file_server
try_files {path} {path}.html {path}/ 404
}

View File

@ -1,15 +0,0 @@
FROM alpine as build
RUN apk update && apk add pandoc fish
WORKDIR /www.jeffa.io/
COPY . .
RUN fish build.fish
FROM caddy:2-alpine
COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=build /www.jeffa.io/out /srv

View File

@ -1,103 +0,0 @@
/* Basic.css */
* {box-sizing: border-box}
:root{
--sans: 1em/1.6 system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Droid Sans, Helvetica Neue, Fira Sans, sans-serif;
--mono: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, 'Courier New', monospace;
--c1:#0074d9;
--c2:#eee;
--c3:#fff;
--c4:#000;
--c5:#fff;
--m1: 8px;
--rc: 8px;
}
@media (prefers-color-scheme: dark) {
:root {
--c2:#333;
--c3:#1e1f20;
--c4:#fff;
}
}
html {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* General settings */
body {
margin: 10px 15% 10px 15%;
font: var(--sans);
font-weight: 400;
font-style: normal;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
background-color: var(--c3);
color: var(--c4);
}
img, iframe {border: none; max-width: 100%}
a {color: var(--c1); text-decoration:none}
a:hover {color: var(--c1); text-decoration: underline}
pre {font: 1em/1.6 var(--mono); background: var(--c2); padding: 1em; overflow: auto}
code {font: 1em/1.6 var(--mono);}
blockquote {border-left: 5px solid var(--c2); padding: 1em 1.5em; margin: 0}
hr {border:0; border-bottom: 1px solid var(--c4)}
/* Headlines */
h1,h2,h3,h4,h5,h6 {margin: 0.6em 0; font-weight: normal}
h1 {font-size: 2.625em; line-height: 1.2}
h2 {font-size: 1.625em; line-height: 1.2}
h3 {font-size: 1.3125em; line-height: 1.24}
h4 {font-size: 1.1875em; line-height: 1.23}
h5,h6 {font-size: 1em; font-weight:bold}
/* Table */
table {border-collapse: collapse; border-spacing: 0; margin:1em 0}
th, td {text-align: left; vertical-align: top; border: 1px solid; padding: 0.4em}
thead,tfoot {background: var(--c2)}
/* Rounded Corners*/
pre,code,input,select,textarea,button,img {border-radius: var(--rc)}
/* Forms */
input, select, textarea {font-size: 1em; color:var(--c4); background: var(--c2); border: 0; padding: 0.6em}
button, input[type=submit], input[type=reset], input[type="button"] { -webkit-appearance: none; font-size: 1em; display: inline-block; color: var(--c5); background: var(--c1); border: 0; margin: 4px; padding: 0.6em; cursor: pointer; text-align: center}
button:hover, button:focus, input:hover, textarea:hover, select:hover {opacity: 0.8}
/* Infinite Grid */
section {display: flex; flex-flow: row wrap}
[style*="--c:"], section>section, aside, article {flex:var(--c,1); margin:var(--m1)}
/* Cards */
article {background: var(--c2); border-radius: var(--rc); padding: 1em; box-shadow: 0px 1px 0px rgba(0,0,0,0.3)}
[style*="--c:"]:first-child, section>section:first-child, article:first-child {margin-left:0}
[style*="--c:"]:last-child, section>section:last-child, article:last-child {margin-right:0}

View File

@ -1,19 +0,0 @@
if ! test -d out/
mkdir out/
end
cp basic.css out/index.css
for file in src/**/*.md
set -l output (string replace .md .html $file)
set -l output (string replace src/ out/ $output)
pandoc \
--to html \
--embed-resources \
--standalone \
--template templates/page.html \
--email-obfuscation references \
$file \
> $output
end

View File

@ -1,2 +0,0 @@
podman build -t www.jeffa.io .
podman run -p 8080:8080 www.jeffa.io:latest

View File

@ -1,15 +0,0 @@
variant: fcos
version: 1.5.0
systemd:
units:
- name: hello.service
enabled: true
contents: |
[Unit]
Description=Run jeffa.io
[Service]
Type=oneshot
RemainAfterExit=no
ExecStart=/usr/bin/podman run git.jeffa.io/jeff/www.jeffa.io
[Install]
WantedBy=multi-user.target

View File

@ -1,84 +0,0 @@
---
date: Jan 26th, 2021
---
# An Auto-Increment Crate for Rust
...now exists! [Check it out.][1]
## The Problem
I was recently working on a Rust program and needed a way to give instances a unique identifier. These IDs did not need to be *universally* unique. I wanted the simplest way to recognize two instances as unique even if they were identical aside from the ID. A serial (or auto-increment) ID seemed like an appropriate choice.
As a rule, I try to write my own code to avoid over-reliance on dependencies. But this case seemed to warrant a dependency. It would be a simple thing to implement. What I wanted was essentially a counter, after all. I expected to find a reliable crate with lots of downloads.
Surprisingly, no such crate existed. I tried searching for "serial", "increment", "auto increment", "serial id", etc. and found nothing. One [crate][2] had a promising name but no documentation and, from what I could tell from the code, did not quite do what I was looking for.
I decided to write and publish such a crate myself. It seemed like a good opportunity to contribute a small but useful crate with relatively simple code to the greater Rust ecosystem.
## The Solution
### Are Universally Unique IDs a Universal Solution?
Serial IDs are used as identifiers because they are simple. Universally unique identifiers (or UUIDs), on the other hand, are usually used when a program needs to create identifiers without access to information about IDs created elsewhere. This is why a distributed database would use them. Rust developers have access to the [rand] crate and the [uuid] crate, which uses rand internally but creates IDs in formats that conform to formal standards. The basic strategy is to create a pseudo-random value or to capture a value (such as a UTC timestamp) and hash it. The details of UUID generation are too extensive to get into here. The uuid crate is both well-documented and cleanly coded, and there are countless other descriptions of the standards it implements.
The important difference between serial IDs and UUIDs is the performance cost. Hashing and random number generation are more expensive than increasing the value of an integer by one. Even if threads have to wait to get exclusive access to an ID generator, each thread will only hold it for the number of nanoseconds that it takes to calculate "x + 1". Serial IDs are also free from the issue of collisions, allowing them to be smaller. A 32-bit serial ID can have 4,294,967,296 unique instances. There is no probability to consider.
*UUIDs are always good enough, but their cost is not always necessary.* Git, for example, creates an SHA-1 has of a commit's content to create a commit ID. The IDs need to be unique when commits from different machines are pushed to a single remote. But many IDs do not need a high probability of being universally unique. CLI applications or other client-side programs that only work with in-memory values or store data locally are such examples.
### How Has This Been Implemented Before?
Serial IDs are familiar to users of database systems. In SQL, auto-increment integers can be used by setting `GENERATE BY DEFAULT AS IDENTITY` as the default value for a column.
```sql
CREATE TABLE distributors (
did integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
name varchar(40) NOT NULL CHECK (name <> '')
);
```
[Source][Postgres: CREATE TABLE]
Postgres has its own `SERIAL` type that mimics this behavior.
```sql
CREATE TABLE tablename (
colname SERIAL
);
```
[Source][Postgres: SERIAL]
Postgres provides a reasonable standard to replicate: 32-bit integers by default with support for any other unsigned integer. By using a trait to define the types that can be generated, users can make their own types compatible with the API provided by the crate. My crate just needs to provide the generator, the trait and implementations for unsigned integers in the standard library.
## Goals
- Small: The crate should be tiny. This is just a counter.
- Safe: IDs should be unique to each generator. There should be no panics.
- Compatible: Prefer primitives as the output values instead of a new type.
- Usable: Use feature flags to offer serde implementations, atomic primitives and other features
that may be useful to some users.
## Takeaway
This process proved something about Rust that wasn't readily apparent to me before. Encouraging flexibility by defining behavior (through traits) makes it intuitive to implement new features. When you know what you want to do the code starts to seem obvious. If you need a single type that can output many types, define the common behavior of those types and make the single type operate based on that shared behavior.
Rust is often described as flexible, but many languages are flexible. With Rust, it feels like the API writes itself once you clearly define what it should do. The code is not just expressive, it's obvious.
I also realized that there are still plenty of opportunities to contribute Rust code. Both widely-applicable and more niche libraries have provided the tools that developers need to adopt the language. But while Rust has become mature enough to fit many use cases, the ecosystem is far from bloated. There is still plenty of space to do new or different things and make an impact.
The serial_int crate is on [crates.io][1] and [lib.rs][lib.rs]. Contributions and feedback on the API are welcome!
[1]: https://crates.io/crates/serial_int
[2]: https://crates.io/crates/increment
[lib.rs]: https://lib.rs/crates/serial_int
[Postgres: CREATE TABLE]: https://www.postgresql.org/docs/13/sql-createtable.html
[Postgres: SERIAL]: https://www.postgresql.org/docs/13/datatype-numeric.html#DATATYPE-SERIAL
[rand]: https://crates.io/crates/rand
[uuid]: https://crates.io/crates/uuid

View File

@ -1,36 +0,0 @@
# Personal Cookbook
## Convert file formats
### Markdown to HTML
Convert a file
```sh
pandoc --to html --standalone file.md > file.html
```
Add local CSS
```sh
pandoc --to html --css file.css --self-contained file.md > file.html
```
## Kitty
### Desktop launchers
The default group uses the main kitty.conf settings. Having the socket set prevents other windows
from using these settings by default.
```.desktop
[Desktop Entry]
Version=1.0
Type=Application
Name=kitty
GenericName=Terminal emulator
Comment=Fast, feature-rich, GPU based terminal
Exec=kitty --instance-group default --listen-on unix:@kitty
Icon=kitty
Categories=System;TerminalEmulator;
```

View File

@ -1,14 +0,0 @@
I am a programmer in New York. This is my website.
<!--toc:start-->
- [Résumé](resume)
- Sites
- [jeffa.io](https://www.jeffa.io)
- [git.jeffa.io](https://git.jeffa.io)
- [Sourcehut](https://sr.ht/~jeffa)
- [Github](https://github.com/solaeus)
- Articles
- [It's Time to Rethink the Command Line Shell](its_time_to_rethink_the_command_line_shell)
- [An Auto-Increment Crate for Rust](an_auto_increment_crate_for_rust)
- [Rust Packages vs Crates](rust_packages_vs_crates)
<!--toc:end-->

View File

@ -1,96 +0,0 @@
# It's Time to Rethink the Command Line Shell
I recently wrote a command-line shell and programming language called [whale]. I did so for fun and because I wanted to work out some frustrations with life on the command line by trying to build solutions. The [nushell] project was a big inspiration and assistance (I borrowed their readline library) and they were probably the first to make me wish for structured data.
## The String Problem
Traditional shells (e.g. sh, bash, fish and zsh) differ in meaningful ways. For example, control flow, variable assignment, how they read startup scripts, etc. But fundamentally, they all represent data the same way: as text strings. This is a problem because, unless the user entered it, a string is rarely the most meaningful way to present data.
String-based shells work well when you are evoking commands with flags and arguments because those are all user-entered strings. But when you fetch some JSON data from the web or export a database as CSV, you are suddenly left with an unwieldy wall of text that the shell can't handle on its own. This is not actually a design flaw, these shells were designed to solve problems related to text because they ran on systems that were entirely text-based.
> All the core UNIX system tools were designed so that they could operate
> together. The original text-based editors (and even TeX and LaTeX) use ASCII
> (the American text encoding standard; an open standard) and you can use tools
> such as; sed, awk, vi, grep, cat, more, tr and various other text-based tools
> in conjunction with these editors.
>
> [The Unix Tools Philosophy]
But structured data has come a long way since those shells were designed. Three of the most popular shells (sh, bash and zsh) are older than both JSON and CSV. The CSV format was formally defined in 2005, the year that fish was released. And JSON's formal definition did not come until 2017. These shells use a single ambiguous data type (the string) because it is universal and ad-hoc parsing through sed and awk was the best that could be done.
## The Unix Tool Problem
The whole point of a command-line shell, in my estimation, is to allow the user to pass data to and between single-purpose programs. The unix tool philosophy is to do one thing and do it well. One writes programs by processing input into output. This is a simple yet monumental computing and programming paradigm.
Is it possible to disambiguate data on the command line by using more than just strings without violating this philosophy? In this paradigm, it may seem that the shell *should* only recognize data in the form of an ambiguous type like the string because the meaningful work is supposed to happen inside of programs, not between them.
But a shell that implements structured data correctly *can* work within the Unix tools philosophy and can even compliment its features.
## Solving a Problem with Strings
Let's write a function in fish that will allow us to save a quick note.
```fish
function new_note
set -l message $argv[1]
set -l time (date)
set -l note "$time | $message"
note >> notes.txt
end
new_note "buy milk"
new_note "take out garbage"
```
This will give us a file like this.
```txt
Thu Aug 17 01:00:00 AM EDT 2023 | buy milk
Thu Aug 17 02:00:00 AM EDT 2023 | take out garbage
```
In order to process this file, we have to start by treating the entire thing as single string. Then a program like awk, sed or grep may divide it by line before searching and interpolating positional values. This will work as long as the strings are uniform enough to pose as structured data, but it is a feeble framework that can easily be compromised by something as simple as two programs parsing dates differently.
## Solving the Same Problem with Structured Data
Here's how the still-experimental shell [whale] handles the same job of defining a function that saves a note to a file.
```whale
new_note = '
note.message = input;
note.time = now();
notes = from_toml(read_file("notes.toml"));
notes:push(note);
data = notes:to_toml();
data:write("notes.toml");
';
new_note("buy milk");
new_note("take out garbage");
```
Notice that we first declare variables with dot notation to group them. Then we read our existing notes from a TOML file into a variable. This variable is a **list**, a data structure that is a simple contiguous sequence. We can then call **push** on the list to add new item to the end. Finally, we write the file using our new list of notes.
TOML, unlike JSON, has a time data type that makes it a good choice when saving dates and times. That solves the problem of different programs processing dates differently, `from_toml` and `to_toml`are the shell's universal means of conversion for this format. There are also conversion macros for CSV and JSON. These are infallible: they create only valid output and will convert any valid input. This improvement is virtually without downsides: any structured data can still be treated as a string and *structured data can be converted between formats*, which improves interoperability.
```whale
toml_string = read_file("data.toml");
parsed_data = from_toml(toml_string);
json_string = to_json(parsed_data);
```
## Writing Unix Tools for Structured Data
Rust users are lucky to have [serde], a library that can create in-memory values from data formats and vica versa. If you are compiling a program to be used as a command-line tool, I would urge you to think about how you might use structured data as input and output. [Pandoc], for example, can use a YAML file as an alternative to command-line arguments but I would prefer it to have an additional option to pass that YAML to standard input. In the meantime, it is easy enough to implement pandoc's features in whale by translating a data structure into command line arguments. It is not a perfect solution but it goes a long way to prove that the commacnd line shell is still the gold standard of computing.
If you are interested in structured data on the command line, [whale] is in its experimental phase and I'm happy to talk more about it. [Nushell] is a more mature example of a command-line shell with structured data and [jq] is an excellent resource for querying JSON.
[Pandoc]: https://pandoc.org/
[serde]: https://serde.rs/
[nushell]: https://www.nushell.sh/
[whale]: https://git.jeffa.io/jeff/whale
[jq]: https://jqlang.github.io/jq/
[The Unix Tools Philosophy]: https://tldp.org/LDP/GNU-Linux-Tools-Summary/html/c1089.htm

View File

@ -1,5 +0,0 @@
- [jeffa.io](/index)
- Articles
- [How To Code]()
- [/cookbook](/cookbook)

View File

@ -1,43 +0,0 @@
# Jeff Anderson
## About Me
I was a butcher at Bread Garden Market in Iowa City, IA before moving to Manhattan in March of 2020. Since the beginning of the pandemic, I have sharpened the skills I learned at the Hack Reactor bootcamp by learning Typescript and Rust as well as dozens of other skills in programming and computing.
- Strong communication and language skills
- Meticulous attention to detail
- Fastidious about preparation and planning
- Receptive to feedback and criticism
## Projects
### Dust
Data-oriented programming language and interactive shell.
- Implemented a powerful tool using the Rust programming language.
- Designed a separate parser and syntax highlighter for Dust using tree sitter.
- Worked extensively with the standard library and major community crates.
### serial_int
Auto-increment integer library for Rust.
- Wrote a minimal library with tests and documentation.
- Interfaced with global open-source contributors.
- Reviewed and accepted pull requests from volunteers.
### jeffa.io
Self-hosted blog and point of contact.
- Deployed reliably and automatically with Fedora CoreOS
- Published content updates by pushing to a personally-owned container registry
- Achieved top scores from Google PageSpeed Insights including perfect scores in performance and SEO.
## Training
- Hack Reactor: Graduated in the HRNYC16 cohort in 2018.
- International Culinary Center: Graduated 2014 at California Campus.

View File

@ -1,57 +0,0 @@
---
date: Nov 18th, 2021
---
# Rust Packages vs Crates
It is a common misconception that packages are crates and visa versa. I will
admit that this is a pet peeve. But for Rust coders, it is important to know the
difference because otherwise you are denied an understanding of how Rust code is
organized, shared and consumed.
## Crate
A **crate**, like a **module** inside of a crate, is a means of organizing code.
A **crate** is either a binary or a library.
A **crate** is not published independently, but rather as a member of its
**package**.
The compiler knows what a **crate** is and uses **crates** as namespaces for
items. If `std::hash::Hash` is not in scope, you can define your own trait
called `Hash`.
## Package
A **package** is a wrapper for at least one crate.
A **package** is publishable.
A **package** can contain one or zero library **crates**.
A **package** can contain any number of binary **crates**.
When you add a **package** to your dependencies, you consume the one library
**crate** inside of that **package**.
When you use **cargo run** or **cargo install** without specificying a
**crate**, you consume the one binary **crate** in that **package**.
When you use **cargo run --bin** or **cargo install --bin** followed by a
**crate** name, you consume the specified binary **crate** in that **package**.
## Why Is There Confusion About This?
I think the reason people get mixed up is because [crates.io] is actually a
repository for packages. If you find a library on [crates.io], you add the
*package* to the dependencies in your Cargo.toml file. You don't have to
specificy the crate because a package can only have one library crate. If the
default repository were called "packages.io", this may be less of an issue.
As always, The Book is there as an easy-to-read authority that clears up these
little confusions.
[The Rust Programming Language: Packages and Crates](https://doc.rust-lang.org/stable/book/ch07-01-packages-and-crates.html)
[crates.io]:https://crates.io

View File

@ -1,48 +0,0 @@
# Visualizing Data with Dust
Dust is an experimental data-oriented programming language with powerful tools for working with structured data from external sources. Despite being a command line tool, dust is able to spin up GUI windows with plots, charts and graphs. This is done through the language itself and dust is distributed with this cross-platform capability built in. So it works in the REPL, in scripts or in ad-hoc commands through your shell.
To read more about dust, the project status and to learn the language, see the [README].
## Load Data
Loading data into a dust program or environment can be done by downloading it or loading it from a file. Let's use an example that downloads some raw data and saves it so we don't have to keep fetching the it.
```
raw_data = download "faithful.csv";
raw_data:write "faithful.csv";
```
Now we can load the data any time we like by reading the file.
```
raw_data = read("faithful.csv");
```
The second step is parsing the data into dust values. This is as easy as knowing the format of the data you just assigned to variable. In this case we'll use `from_csv` to turn `raw_data` into a table. The `from_json` and `from_toml` macros are also available as well as `to_csv`, etc.
## Explore
Printing the parsed data to the terminal will show that `from_csv` has created a table with three columns and 272 rows. Dust has pretty terminal output for tables but it's still a lot of scrolling and a wall of text does not make the data easy to understand. But if we want the data to fit in a bar graph, we will have to take an extra step to make it clear what the graph should display.
## Transform
The simplest bar graph just needs a list of values. The index of each value can provide the x axis position and a flaoting point value will provide the height. But that's not what were' working with. Our data is in a variable that is a table, not a list. If we want the rows of the table as list, we simple call `rows()`, which shows us the following output.
As you can see, each row is represented as another list. We want to *transform* each list into just the second value, which is a floating point.
```
table = from_csv(read("faithful.csv"));
rows = table:rows();
list = rows:transform('input.1');
```
The resulting list looks like this.
## Visualize
Now each item in the list can represent a single bar on the graph at they will be arranged by their position in the list. Just one last step to spin up our GUI!
```
bar_graph(list)
```

View File

@ -1 +0,0 @@

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<meta name="author" content="Jeff Anderson" />
$if(date)$
<meta name="date" content=\"$date$\" />
$endif$
<link rel="stylesheet" href="index.css" />
<title>jeffa.io</title>
</head>
<body>
<h1><a href="https://www.jeffa.io">jeffa.io</a></h1>
$body$
<br />
<footer>
<p> Please contact me by <a href = "mailto: jeff@jeffa.io">email</a> with questions or corrections. You can view this website's <a href = "https://git.jeffa.io/jeff/jeffa.io">source</a>.
</p>
<a rel="license" href="http://creativecommons.org/licenses/by/4.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by/4.0/88x31.png" /></a><br />This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International License</a>.
</footer>
</body>
</html>