diff --git a/.gitignore b/.gitignore
index ea8c4bf..ea8c4bf 100644..100755
--- a/.gitignore
+++ b/.gitignore
diff --git a/.rustfmt.toml b/.rustfmt.toml
index 218e203..218e203 100644..100755
--- a/.rustfmt.toml
+++ b/.rustfmt.toml
diff --git a/Cargo.lock b/Cargo.lock
index 4c4488d..5aa59d6 100644..100755
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,21 +3,6 @@
version = 3
[[package]]
-name = "camino"
-version = "1.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
-
-[[package]]
-name = "converge"
-version = "0.1.0"
-dependencies = [
- "camino",
- "cutie",
- "thiserror",
-]
-
-[[package]]
name = "cutie"
version = "0.1.0"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
index d62ac1d..d35799a 100644..100755
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,5 +9,5 @@ license = "ISC"
[dependencies]
thiserror = "1.0.52"
-[workspace]
-members = ["converge"]
+#[workspace]
+#members = ["converge"]
diff --git a/LICENSE b/LICENSE
index 8a55383..8a55383 100644..100755
--- a/LICENSE
+++ b/LICENSE
diff --git a/converge/Cargo.toml b/converge/Cargo.toml
index 4dc9e4b..98481f6 100644..100755
--- a/converge/Cargo.toml
+++ b/converge/Cargo.toml
@@ -6,6 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+bempline = "0.8.1"
camino = "1.1.6"
cutie = { path = "../", version = "0.1.0" }
thiserror = "1.0.52"
+time = { version = "0.3.31", features = ["formatting", "macros"] }
+scurvy = { path = "../../scurvy" }
diff --git a/converge/example.html b/converge/example.html
new file mode 100755
index 0000000..970cc06
--- /dev/null
+++ b/converge/example.html
@@ -0,0 +1,185 @@
+/Users/gen/src/inf/served/touching-grass.html
+/Users/gen/src/inf/fixtures/post.html
+/Users/gen/src/inf/fixtures/base.html
+Looking for html
+Looking for head
+Looking for title
+ closed title
+ closed head
+Looking for body
+Looking for ul
+Looking for li
+Looking for a
+ closed a
+ closed li
+ closed ul
+Looking for main
+ closed main
+Looking for footer
+Looking for ul
+Looking for li
+ closed li
+Looking for li
+Looking for abbr
+ closed abbr
+ closed li
+Looking for li
+Looking for abbr
+ closed abbr
+ closed li
+ closed ul
+ closed footer
+ closed body
+ closed html
+Looking for nav
+Looking for a
+ closed a
+ closed nav
+BEFORE
+Looking for main
+ closed main
+Looking for section
+Looking for video
+ closed video
+Looking for ul
+Looking for li
+ closed li
+Looking for li
+Looking for a
+ closed a
+ closed li
+Looking for li
+Looking for a
+ closed a
+ closed li
+Looking for li
+ closed li
+Looking for li
+Looking for input
+Looking for label
+ closed label
+ closed input
+ closed li
+ closed ul
+ closed section
+Looking for script
+ parse special
+Looking for p
+ closed p
+Looking for p
+ closed p
+Looking for p
+Looking for span
+ closed span
+Looking for i
+ closed i
+Looking for span
+ closed span
+ closed p
+Looking for p
+ closed p
+<html>
+
+<head>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta charset="utf-8" />
+
+ <link rel="icon" type="image/png" href="/3directions.png" />
+ <link rel="icon" type="image/svg+xml" href="/3directions.svg" />
+
+ <title></title>
+
+ <link rel="stylesheet" href="/styles/common.css" />
+</head>
+
+<body>
+ <ul id="nav-access">
+ <li><a href="#content">Skip to main content</a></li>
+ </ul>
+
+ <nav class="sized">
+ <a id="home" href="/" alt="home">← home</a>
+</nav><main id="content" class="writing sized"><section style="width: 100%; text-align: right; padding: 8px 0">
+ <video id="grass" controls width="100%" poster="grass_poster.jpg" preload="metadata" loop>
+ <source src="grass_720p.mp4" type="video/mp4" />
+ <track default src="grass.vtt" />
+ </video>
+ <ul>
+ <li class="small">download</li>
+ <li><a href="grass.mp4" download>[1080p | 7.1MB]</a></li>
+ <li><a href="grass_720p.mp4" download>[720p | 1.9MB]</a></li>
+ <li id="long"></li>
+ <li><input type="checkbox" checked id="loop"><label> loop video?</label></input></li>
+ </ul>
+</section>
+
+<script>
+ document.addEventListener("DOMContentLoaded", setup);
+
+ const video = document.getElementById("grass");
+ function setup() {
+ let marks = document.getElementsByClassName("mark");
+ for (let i = 0; i < marks.length; ++i) {
+ marks[i].addEventListener('click', markClick)
+ }
+ console.log(marks);
+
+ let loop = document.getElementById('loop');
+ loop.addEventListener('change', loopChanged)
+
+ // Fire once with a fake event so we aren't desynced
+ loopChanged({ 'target': loop });
+ }
+
+ function markClick(event) {
+ let target = event.target;
+ let time = parseFloat(target.getAttribute('data-time'));
+ console.log(`seeking to ${time}`)
+ video.currentTime = time;
+ }
+
+ function loopChanged(event) {
+ let target = event.target;
+ let checked = target.checked;
+ console.log(`video looping: ${checked}`);
+ video.loop = checked;
+ }
+</script>
+
+<p>
+ I took this short video to send to a friend one day and accidentally
+ made a 106 frame masterpiece. Well, that's hyperbolic. But there's a
+ lot I enjoy about it.
+</p>
+
+<p>
+ The contrast between my orange painted nails and the surprisingly
+ green grass is pretty pleasing. I think the gentle brown of the brick
+ in the bottom-left corner helps keep from an overwhelming greenery.
+ Thanks bricks :)
+</p>
+
+<p>
+ The accidental camera movement is nice, too. I like the angle the camera is at and the way it wobbles.
+ It's clearly hand-held. At <span class="mark" data-time="1.3">1.3-ish-seconds</span>
+ you can see I gently sway forward, but seem to be less wobbly. <i>I'm braced;
+ I'm stable! I have-three-points-of-contact-with-the-ground!</i> Then, at roughly
+ <span class="mark" data-time="1.9">2 seconds</span> when the grass finally
+ yields, I get rocked back and wobbles resume.
+</p>
+
+<p>
+ The sound! And the sound. The ripping of the grass. That planty matter finally giving way. The fibrous tearing.
+ I just like it; I enjoy it! I greatly appreciate it.
+</p></main>
+
+ <footer class="sized">
+ <ul id="dates">
+ <li id="dates-title"> times</li>
+ <li><abbr title="creation time">c</abbr> {created}</li>
+ <li><abbr title="modification time">m</abbr> {modified}</li>
+ </ul>
+ </footer>
+</body>
+
+</html>
diff --git a/converge/readme.md b/converge/readme.md
index 5a778c3..7fcecb5 100644..100755
--- a/converge/readme.md
+++ b/converge/readme.md
@@ -6,4 +6,9 @@ Setup: <path>
{directions}
content
-{directions} content
\ No newline at end of file
+{directions} content
+
+**variables converge tries to fill**
+
+- `{file_time_created}`: time the file was created
+- `{file_time_modified}`: time the file was modified
\ No newline at end of file
diff --git a/converge/src/main.rs b/converge/src/main.rs
index 20f2683..74abe67 100644..100755
--- a/converge/src/main.rs
+++ b/converge/src/main.rs
@@ -1,32 +1,30 @@
use std::str::FromStr;
use camino::Utf8PathBuf;
+use scurvy::Argument;
fn main() {
- let mut args = std::env::args().skip(1);
- let argc = args.len();
- if argc == 0 {
- eprintln!("usage: converge <content file> [supporting files ...]");
- return;
+ let arguments = [
+ Argument::arg("timdb", "path").help("time database generated with whenwasit"),
+ Argument::arg("part", "path").help("part converge is supposed to build"),
+ ];
+ let cli = scurvy::parse(&arguments);
+
+ let content: Utf8PathBuf = cli.parse_opt_or_die("part");
+ let supporting: Vec<Utf8PathBuf> = cli.free_opts().into_iter().map(Utf8PathBuf::from).collect();
+
+ let time = std::time::Instant::now();
+ for idx in 0..100 {
+ let supporting = supporting.clone();
+ let html = process(content.clone(), supporting);
}
+ println!("{}ms for 100", time.elapsed().as_millis());
- let content = match args.next() {
- None => {
- eprintln!("usage: converge <content file> [supporting files ...]");
- return;
- }
- Some(path) => Utf8PathBuf::from(path),
- };
-
- let supporting = args.map(Utf8PathBuf::from).collect();
-
- let html = process(content, supporting);
- println!("{html}")
+ //println!("{html}")
}
fn process(content_file: Utf8PathBuf, mut supporting: Vec<Utf8PathBuf>) -> cutie::Html {
- println!("{content_file}");
- let raw = std::fs::read_to_string(&content_file).unwrap();
+ let raw = std::fs::read_to_string(content_file).unwrap();
match Part::from_str(&raw) {
Err(PartError::NoSetup) => cutie::Html::parse(raw),
@@ -73,7 +71,7 @@ fn process(content_file: Utf8PathBuf, mut supporting: Vec<Utf8PathBuf>) -> cutie
html: &'a mut cutie::Html,
ident: &str,
) -> &'a mut cutie::Tag {
- match html.get_parent_that_contains_tag_name_mut(&ident) {
+ match html.get_parent_that_contains_tag_name_mut(ident) {
None => {
eprintln!("error processing file");
eprintln!("failed to find element with tag {ident}");
@@ -93,7 +91,6 @@ fn process(content_file: Utf8PathBuf, mut supporting: Vec<Utf8PathBuf>) -> cutie
tag.children.extend(content_html.nodes);
}
Opcode::Before => {
- println!("BEFORE");
let predicate = |node: &cutie::Node| -> bool {
if let cutie::Node::Tag(tag) = node {
if tag.name == ident {
diff --git a/converge/src/timedb.rs b/converge/src/timedb.rs
new file mode 100755
index 0000000..718115d
--- /dev/null
+++ b/converge/src/timedb.rs
@@ -0,0 +1,97 @@
+use std::path::Path;
+
+use time::OffsetDateTime;
+
+#[derive(Debug)]
+pub struct TimeDb {
+ data: Vec<TimedFile>,
+}
+
+impl TimeDb {
+ pub fn load<P: AsRef<Path>>(path: P) -> Self {
+ let file = std::fs::read_to_string(path).unwrap();
+
+ let mut data = vec![];
+ for line in file.lines() {
+ let it = TimedFile::parse_line(line);
+ data.push(it);
+ }
+
+ Self { data }
+ }
+
+ pub fn get_times(&self, path: &str) -> Option<&TimedFile> {
+ for file in &self.data {
+ if &file.path == path {
+ return Some(file);
+ }
+ }
+
+ None
+ }
+}
+
+#[derive(Debug)]
+pub struct TimedFile {
+ path: String,
+ pub creation: Option<OffsetDateTime>,
+ pub modification: Option<OffsetDateTime>,
+ pub access: Option<OffsetDateTime>,
+}
+
+impl TimedFile {
+ pub fn parse_line<S: AsRef<str>>(raw: S) -> Self {
+ let mut values = raw.as_ref().rsplitn(4, ",").collect::<Vec<&str>>();
+ values.reverse();
+
+ let to_odt = |str: &&str| -> Option<OffsetDateTime> {
+ str.parse::<u64>()
+ .ok()
+ .map(|t| OffsetDateTime::from_unix_timestamp(t as i64).unwrap())
+ };
+
+ let path = unescape(values[0]);
+ let creation = values.get(1).map(to_odt).flatten();
+ let modification = values.get(2).map(to_odt).flatten();
+ let access = values.get(3).map(to_odt).flatten();
+
+ Self {
+ path,
+ creation,
+ modification,
+ access,
+ }
+ }
+}
+
+// Permissive unescape. Everything that's not \\ or \, is passed
+// unchanged, while those get their slash removed
+fn unescape<S: AsRef<str>>(raw: S) -> String {
+ let raw = raw.as_ref();
+
+ if !raw.contains('\\') {
+ return raw.to_owned();
+ }
+
+ let mut unescape = String::with_capacity(raw.len());
+ let mut escaped = false;
+ for ch in raw.chars() {
+ match (escaped, ch) {
+ (false, '\\') => {
+ escaped = true;
+ }
+ (false, c) => unescape.push(c),
+ (true, '\\') | (true, ',') => {
+ unescape.push(ch);
+ escaped = false;
+ }
+ (true, c) => {
+ unescape.push('\\');
+ unescape.push(c);
+ escaped = false;
+ }
+ }
+ }
+
+ unescape
+}
diff --git a/readme.md b/readme.md
index 49cdf8a..252c728 100644..100755
--- a/readme.md
+++ b/readme.md
@@ -3,7 +3,11 @@ Not particularly fast, probably.
- all tags must close *(even `<br/>`, `<meta/>`, `<link/>`)*
- tag names must be separated from the tag-body *(where the attributes go)* by a space character *(` `, 0x20)*
-- self-closing tags must have the closing `/` at the ver y end of the body *(directly before the `>`)*
+- attributes must use `"` as their quoting character AND attributes must be
+ quoted *(no `src=image.png` nonsense. is that- does that ever even happen? i hope not)*
+- attribute keys, if they have a value, must have the `=` directly following the
+ key *(good: `src="image.png"`, bad: `src = "image.png`)*
+- self-closing tags must have the closing `/` at the very end of the body *(directly before the `>`)*
- no > in tags except at the end (not even in attributes)
- inline `<script>` and `<style>` must have their closing-tag be
first-of-line *(excluding whitespace)*
\ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
index 8b5c47d..bc8b629 100644..100755
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -87,6 +87,31 @@ impl Html {
None
}
+ pub fn get_by_id(&self, id: &str) -> Option<&Tag> {
+ // depth first
+ fn find_node<'a>(tag: &'a Tag, id: &str) -> Option<&'a Tag> {
+ if tag.id().unwrap_or_default() == id {
+ return Some(tag);
+ }
+
+ for child in tag.child_tags() {
+ if let Some(tag) = find_node(child, id) {
+ return Some(tag);
+ }
+ }
+
+ None
+ }
+
+ for child in self.child_tags() {
+ if let Some(tag) = find_node(child, id) {
+ return Some(tag);
+ }
+ }
+
+ None
+ }
+
fn parse_node(raw: &str) -> Consumed {
match Self::is_tag(raw) {
Some(_) => {
@@ -133,14 +158,11 @@ impl Html {
};
}
- println!("Looking for {}", root_tag.name);
-
loop {
// Special case <script> and <style>
if root_tag.name == "script" && tag.get_attribute("src").is_none()
|| root_tag.name == "style"
{
- println!("\tparse special");
let special = Self::special_parse(rest.unwrap(), root_tag.name);
match special {
@@ -166,7 +188,6 @@ impl Html {
// Find the closing end of out root_tag
if let Some((parsed, remaining)) = Self::is_tag(rest.unwrap()) {
if parsed.closing && parsed.name == root_tag.name {
- println!("\tclosed {}", parsed.name);
break Consumed {
node: Node::Tag(tag),
remaining,
diff --git a/src/query.rs b/src/query.rs
index 26b02aa..26b02aa 100644..100755
--- a/src/query.rs
+++ b/src/query.rs
diff --git a/src/tag.rs b/src/tag.rs
index 0a97061..43c4e0c 100644..100755
--- a/src/tag.rs
+++ b/src/tag.rs
@@ -1,4 +1,5 @@
use core::fmt;
+use std::{iter::Peekable, str::CharIndices};
use crate::Node;
@@ -208,6 +209,8 @@ impl<'a> Iterator for TagIteratorMut<'a> {
mod test {
use crate::Tag;
+ use super::peek_skip_while;
+
#[test]
fn tag_finds_boolen_attribute() {
let tag = Tag {
@@ -240,4 +243,54 @@ mod test {
};
assert!(tag.get_attribute("contenteditable").is_some());
}
+
+ // yes, this is a bad name.
+ #[test]
+ fn peek_skip_while_works() {
+ let str = "\t no whitespace";
+ let mut chari = str.char_indices().peekable();
+
+ peek_skip_while(&mut chari, |c| c.is_whitespace());
+
+ let next_idx = chari.next().unwrap().0;
+ assert_eq!(&str[next_idx..], "no whitespace");
+ }
+}
+
+struct Attribute<'body> {
+ name: &'body str,
+ content: Option<&'body str>,
+ start: usize,
+ len: usize,
+}
+
+fn find_attribute<'body>(body: &'body str, attribute: &str) -> Option<Attribute<'body>> {
+ let mut chari = body.char_indices().peekable();
+
+ 'big: loop {
+ // skip whitespace
+ peek_skip_while(&mut chari, |c| c.is_whitespace());
+
+ let key_start = chari.next().unwrap();
+ // find end of key
+ peek_skip_while(&mut chari, |c| c.is_alphanumeric());
+
+ let key_after = chari.next().unwrap();
+ }
+
+ todo!()
+}
+
+fn peek_skip_while<P>(iter: &mut Peekable<CharIndices>, mut predicate: P)
+where
+ P: FnMut(&char) -> bool,
+{
+ loop {
+ match iter.peek() {
+ Some((_, c)) if predicate(c) => {
+ iter.next();
+ }
+ _ => break,
+ }
+ }
}
diff --git a/tests/nyble.html b/tests/nyble.html
index 41614ab..41614ab 100644..100755
--- a/tests/nyble.html
+++ b/tests/nyble.html
diff --git a/tests/nyble_pages.rs b/tests/nyble_pages.rs
index 2a8fc75..2a8fc75 100644..100755
--- a/tests/nyble_pages.rs
+++ b/tests/nyble_pages.rs
diff --git a/tests/touching_grass.html b/tests/touching_grass.html
index de90798..de90798 100644..100755
--- a/tests/touching_grass.html
+++ b/tests/touching_grass.html
|