feat: inline code, resource links

fixes Text::is_empty() and allows formatted
text in the context section of quotes (so you can link to /ref)

slight changes to the css of the html generator,
this should fix document preview on link hovering
and might improve nesting if you edit the generated
html in weird ways (should be more robust now).
This commit is contained in:
Mark
2026-08-03 17:53:07 +02:00
parent 8492d0b636
commit b53e6e7bb6
5 changed files with 151 additions and 95 deletions

View File

@@ -15,9 +15,9 @@ in most markdown flavors.
as someone using mharkup, you as someone using mharkup, you
` can read and write it in your text editor ` can read and write it in your text editor
` can export and `/printable(print) it ` can export and `/printable[print] it
` can easily (re)organize your document ` can easily (re)organize your document
` probably won't apply formatting on accident `/backtick() ` probably won't apply formatting on accident `/backtick[]
# the format # the format
@@ -46,9 +46,9 @@ for example, here are the sections you just read:
each mharkup file starts with a title, each mharkup file starts with a title,
the first line of the file, which doesn't have the first line of the file, which doesn't have
any leading hashtags. a file `+(should) also have a small any leading hashtags. a file `+[should] also have a small
description of what it will contain, and the file's description of what it will contain, and the file's
actual content `+(should) be in subdocuments. actual content `+[should] be in subdocuments.
for example, here's how this file starts: for example, here's how this file starts:
[[ [[
@@ -68,19 +68,20 @@ features of mharkup.
in most cases, a section is just in most cases, a section is just
a few lines of text. you've probably a few lines of text. you've probably
already noticed that linebreaks are usually already noticed that linebreaks are usually
ignored `-(or, more accurately, replaced with spaces). ignored `-[or, more accurately, replaced with spaces].
sections beginning with a hashtag (#) are special. sections beginning with a hashtag (`![#]) are special.
the first line specifies the type of section you want, the first line specifies the type of section you want,
and the other lines are the section's content. and the other lines are the section's content.
you've already seen this with #!mharkup in the you've already seen this with `![#!mharkup] in the
first example. first example.
the types of sections you can create like this are: the types of sections you can create like this are:
` #@ quotes ` `![#@] quotes
` #! code blocks ` `![#!] code blocks
` #\ various extensions ` `![#:] resource links
` `![#\] various extensions
### backticks /here ### backticks /here
@@ -90,18 +91,18 @@ with a backtick followed by a space to create a list.
here's a list of things the backtick can do: here's a list of things the backtick can do:
` create a list ` create a list
` represent itself ``: ```` ` represent itself ``: `!{``}
` add a `-(note): ``-(note) ` add a `-[note]: `!{`-[note]}
` mark something that's `~(wrong): ``~(wrong) ` mark something that's `~[wrong]: `!{`~[wrong]}
` use monospace for `!(code): ``!(code) ` include some `![code.f()]: `!{`![code.f()]}
` mark a non-word or name, like `@(mharkup): ``@(mharkup) ` mark a non-word or name, like `@[mharkup]: ``@[mharkup]
` highlight a `_(special) element: ``_(special) ` highlight a `_[special] element: `!{`_[special]}
` add emphasis to `+(something): ``+(something) ` add emphasis to `+[something]: `!{`+[something]}
` highlight something `*(important): ``*(important) ` highlight something `*[important]: `!{`*[important]}
` insert raw `{`text`}: `<`{`text`}>: `2{`<`{`text`}>}}: `2<`2{`<`{`text`}>}}>>: ... ` insert raw `{`text`}: `!<`{`text`}>: `2!{`<`{`text`}>}}: `2!<`2{`<`{`text`}>}}>>: ...
` link `/here(somewhere): ``/here(somewhere) ` link `/here[somewhere]: ``/here[somewhere]
` use an extension: ``\spec{...} or ``\spec<...> ` use an extension: ``\spec{...} or ``\spec<...>
` combine any of these: `*_+(whoah): ``*_+(whoah) ` combine any of these: `*_+[whoa`~(h)]: ``*_+[whoa``~(h)]
## ##
@@ -155,21 +156,32 @@ to use the different section types in more detail.
### quotes ### quotes
#@mark @21st century #@mark @21st century `/ref[]
i've solved many problems i've solved many problems
that noone's ever had. that noone's ever had.
in this case, the first line in this case, the first line
of the section was #@mark @21st century. of the section was `!{#@mark @21st century `/ref[]}.
### code ### code blocks
#!python #!python
def do_something_productive(): def do_something_productive():
pass pass
in this case, the first line in this case, the first line
of the section was #!python. of the section was `!{#!python}.
### resource links /ref
#:other_doc
A document you should read.
#https://example.com/hello
Something external.
The first lines of these sections were `!{#:other_doc} and `!{#https://example.com/hello}.
If the section starts with `!{#:}, the link is relative.
### extensions ### extensions
@@ -198,14 +210,14 @@ or inline: `\texm<a^2> or `2\texm{ \frac{1}{2} }}
i really don't want to think about how many special i really don't want to think about how many special
characters i can't use while writing my plaintext documents. characters i can't use while writing my plaintext documents.
i know that writing `<`*(word)> instead of **word** is inconvenient, i know that writing `{`*(word)} instead of **word** is inconvenient,
but using *, _, and even \ without any escaping may `-[(?)] already be worth it. but using *, _, and even \ without any escaping may `-[(?)] already be worth it.
Also, * and _ can't be nested, so if you took `+(this **cool** section) Also, * and _ can't be nested, so if you took `+[this **cool** section]
and copy-pasted it into an already bold section, you would get and copy-pasted it into an already bold section, you would get
`+(**this **cool** section**) which is just... ew. `+[**this **cool** section**] which is just... ew.
Meanwhile `{`*(this `*(cool) section)} works just fine Meanwhile `{`*[this `*(cool) section]} works just fine
and produces `*(this `*(cool) section). and produces `*[this `*(cool) section].
## why does it need to be printable? /printable ## why does it need to be printable? /printable
@@ -237,7 +249,7 @@ and here's 10 layers of documents in mharkup:
########## whoah, that's a lot ########## whoah, that's a lot
but it's still perfectly readable but it's still perfectly readable.
## why are there no nested lists? /listnesting ## why are there no nested lists? /listnesting

View File

@@ -11,7 +11,8 @@ pub enum Section<'a> {
Paragraph(Text<'a>), Paragraph(Text<'a>),
List(Vec<Text<'a>>), List(Vec<Text<'a>>),
Code(&'a str, Text<'a>), Code(&'a str, Text<'a>),
Quote(&'a str, &'a str, Text<'a>), Quote(&'a str, Text<'a>, Text<'a>),
Link(&'a str, &'a str, Text<'a>),
Ext(&'a str, &'a str), Ext(&'a str, &'a str),
Document(Document<'a>), Document(Document<'a>),
} }
@@ -25,34 +26,32 @@ pub enum Component<'a> {
Text(&'a str), Text(&'a str),
Note(Text<'a>), Note(Text<'a>),
Wrong(Text<'a>), Wrong(Text<'a>),
Name(Text<'a>),
Code(Text<'a>),
Special(Text<'a>), Special(Text<'a>),
Emphasis(Text<'a>), Emphasis(Text<'a>),
Important(Text<'a>), Important(Text<'a>),
Name(Text<'a>),
Code(Text<'a>),
Link(&'a str, Text<'a>), Link(&'a str, Text<'a>),
Ext(&'a str, &'a str), Ext(&'a str, &'a str),
} }
impl<'a> Text<'a> { impl<'a> Text<'a> {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.0.iter().any(|c| c.is_empty()) self.0.iter().all(|c| c.is_empty())
} }
} }
impl<'a> Component<'a> { impl<'a> Component<'a> {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
match self { match self {
Component::Char(_) => false, Component::Char(_) | Component::Link(_, _) | Component::Ext(_, _) => false,
Component::Text(t) => t.is_empty(), Component::Text(t) => t.is_empty(),
Component::Note(t) Component::Note(t)
| Component::Wrong(t) | Component::Wrong(t)
| Component::Name(t)
| Component::Code(t)
| Component::Special(t) | Component::Special(t)
| Component::Emphasis(t) | Component::Emphasis(t)
| Component::Important(t) | Component::Important(t) => t.is_empty(),
| Component::Name(t)
| Component::Code(t) => t.is_empty(),
Component::Link(target, t) => target.is_empty() && t.is_empty(),
Component::Ext(_, _) => false,
} }
} }
} }

View File

@@ -225,6 +225,12 @@ pub fn parse_text_impl<'a>(
'~' if opt.is_empty() => { '~' if opt.is_empty() => {
inner = Ok(text(inner, Component::Wrong)); inner = Ok(text(inner, Component::Wrong));
} }
'@' if opt.is_empty() => {
inner = Ok(text(inner, Component::Name));
}
'!' if opt.is_empty() => {
inner = Ok(text(inner, Component::Code));
}
'_' if opt.is_empty() => { '_' if opt.is_empty() => {
inner = Ok(text(inner, Component::Special)); inner = Ok(text(inner, Component::Special));
} }
@@ -234,12 +240,6 @@ pub fn parse_text_impl<'a>(
'*' if opt.is_empty() => { '*' if opt.is_empty() => {
inner = Ok(text(inner, Component::Important)); inner = Ok(text(inner, Component::Important));
} }
'@' if opt.is_empty() => {
inner = Ok(text(inner, Component::Name));
}
'!' if opt.is_empty() => {
inner = Ok(text(inner, Component::Code));
}
'/' if !opt.is_empty() => { '/' if !opt.is_empty() => {
inner = Ok(text(inner, |v| Component::Link(opt, v))); inner = Ok(text(inner, |v| Component::Link(opt, v)));
} }
@@ -254,11 +254,11 @@ pub fn parse_text_impl<'a>(
Component::Link(_, text) Component::Link(_, text)
| Component::Note(text) | Component::Note(text)
| Component::Wrong(text) | Component::Wrong(text)
| Component::Name(text)
| Component::Code(text)
| Component::Special(text) | Component::Special(text)
| Component::Emphasis(text) | Component::Emphasis(text)
| Component::Important(text) | Component::Important(text) => inner = &mut text.0[0],
| Component::Name(text)
| Component::Code(text) => inner = &mut text.0[0],
Component::Char(_) | Component::Ext(_, _) => { Component::Char(_) | Component::Ext(_, _) => {
unreachable!() unreachable!()
} }
@@ -426,7 +426,31 @@ fn parse_sections<'a>(
attributed.rsplit_once('@').unwrap_or((attributed, "")); attributed.rsplit_once('@').unwrap_or((attributed, ""));
sections.push(Section::Quote( sections.push(Section::Quote(
attributed.strip_suffix(' ').unwrap_or(attributed), attributed.strip_suffix(' ').unwrap_or(attributed),
context.strip_prefix(' ').unwrap_or(context), parse_text(&mut context.strip_prefix(' ').unwrap_or(context), |o| {
o.remove_linebreaks()
})
.map_err(|ParseError(e, i)| ParseError(e, i + pos))?,
parse_text(&mut inner, |o| o.remove_linebreaks())
.map_err(|ParseError(e, i)| ParseError(e, i + pos))?,
));
}
Some(_)
if posthash
.trim_start_matches(|c: char| !(c == ':' || c.is_whitespace()))
.starts_with(":") =>
{
let i = posthash.find(':').unwrap();
let (mut link, mut inner) = if let Some(j) = posthash.find('\n') {
(&posthash[0..j], &posthash[j + 1..])
} else {
(posthash, "")
};
if i == 0 {
link = &link[1..];
}
sections.push(Section::Link(
&posthash[0..i],
link,
parse_text(&mut inner, |o| o.remove_linebreaks()) parse_text(&mut inner, |o| o.remove_linebreaks())
.map_err(|ParseError(e, i)| ParseError(e, i + pos))?, .map_err(|ParseError(e, i)| ParseError(e, i + pos))?,
)); ));

View File

@@ -40,7 +40,7 @@ pub fn page_to<'a>(
<style> <style>
@media (prefers-color-scheme: light) {{ :root {{ --bg: #FFF; --bg2: #FFF4F4; --fg: #000; }} }} @media (prefers-color-scheme: light) {{ :root {{ --bg: #FFF; --bg2: #FFF4F4; --fg: #000; }} }}
@media (prefers-color-scheme: dark) {{ :root {{ --bg: #070002; --bg2: #070010; --fg: #E0B8A0; }} }} @media (prefers-color-scheme: dark) {{ :root {{ --bg: #070002; --bg2: #070010; --fg: #E0B8A0; }} }}
:root {{ color: var(--fg); background: var(--bg); font-family: sans-serif; :root {{ background: var(--bg);
--fgl: color-mix(var(--fg), var(--bg) 50%); --fgd: color-mix(var(--fg), var(--bg) 65%); --fgt: color-mix(var(--fg), var(--bg) 80%); --fgl: color-mix(var(--fg), var(--bg) 50%); --fgd: color-mix(var(--fg), var(--bg) 65%); --fgt: color-mix(var(--fg), var(--bg) 80%);
--fsmall: 66%; --fless: 77%; --fmore: 110%; --ftitle: 132%; }} --fsmall: 66%; --fless: 77%; --fmore: 110%; --ftitle: 132%; }}
/* /*
@@ -54,6 +54,11 @@ pub fn page_to<'a>(
max-width: 90%; max-width: 90%;
width: fit-content; width: fit-content;
}} }}
.doc {{
color: var(--fg); font-family: sans-serif;
text-align: left; text-decoration: none;
}}
a {{ color: inherit; text-decoration: none; }}
.z {{ .z {{
width: fit-content; width: fit-content;
max-width: 100%; max-width: 100%;
@@ -108,64 +113,67 @@ pub fn page_to<'a>(
padding-left: 1em; padding-left: 1em;
}} }}
.y.code .head {{ .y.code > .head {{
color: var(--fgl); color: var(--fgl);
font-size: var(--fsmall); font-size: var(--fsmall);
}} }}
.y.code .head::before {{ .y.code > .head::before {{
content: "#! "; content: "#! ";
font-size: var(--fless); font-size: var(--fless);
}} }}
.y.quote .head {{ .y.quote > .head {{
color: var(--fgl); color: var(--fgl);
font-size: var(--fless); font-size: var(--fless);
}} }}
.y.quote .tail {{ .y.quote > .tail {{
color: var(--fgl); color: var(--fgl);
font-size: var(--fsmall); font-size: var(--fsmall);
text-align: right; text-align: right;
}} }}
.y.quote .head::before {{ .y.quote > .head::before {{
content: "@ "; content: "@ ";
font-size: var(--fless); font-size: var(--fless);
}} }}
.y.quote:not(.c-) .tail::before {{ .y.quote > .tail::before {{
content: " ("; content: " (";
font-size: var(--fless); font-size: var(--fless);
}} }}
.y.quote:not(.c-) .tail::after {{ .y.quote > .tail::after {{
content: ") "; content: ") ";
font-size: var(--fless); font-size: var(--fless);
}} }}
.y.quote.c- .x::after {{
color: var(--fgl); .y.rlink > .head {{
font-size: var(--fsmall); font-size: var(--fsmall);
content: "”"; > * {{ text-decoration: underline var(--fgl); }}
}}
.y.rlink > .head::before {{
content: "➤ ";
color: var(--fgl);
}} }}
.y.ext.spec .head {{ .y.ext.spec > .head {{
color: var(--fgl); color: var(--fgl);
}} }}
.y.ext.nospec .head {{ .y.ext.nospec > .head {{
color: red; color: red;
}} }}
.y.ext .head {{ .y.ext > .head {{
font-size: var(--fsmall); font-size: var(--fsmall);
}} }}
.y.ext .head::before {{ .y.ext > .head::before {{
content: "#/ "; content: "#/ ";
font-size: var(--fless); font-size: var(--fless);
}} }}
.y.ext.spec.e-html .head {{ .y.ext.spec.e-html > .head {{
display: none; display: none;
}} }}
.y.ext.spec.e-texm .head {{ .y.ext.spec.e-texm > .head {{
display: none; display: none;
}} }}
.link {{ .link {{
color: var(--fg);
text-decoration: underline var(--fgl); text-decoration: underline var(--fgl);
position: relative; position: relative;
}} }}
@@ -303,6 +311,8 @@ fn doc_to_impl<'a>(
Section::List(..) => "y list", Section::List(..) => "y list",
Section::Code(..) => "y code", Section::Code(..) => "y code",
Section::Quote(..) => "y quote", Section::Quote(..) => "y quote",
Section::Link("", ..) => "y rlink path",
Section::Link(..) => "y rlink uri",
Section::Ext(..) => "y ext", Section::Ext(..) => "y ext",
Section::Document(..) => "sub", Section::Document(..) => "sub",
}, },
@@ -333,16 +343,27 @@ fn doc_to_impl<'a>(
} }
Section::Quote(attributed, context, text) => { Section::Quote(attributed, context, text) => {
out.push_str(&format!( out.push_str(&format!(
r#" a-{} c-{}"><div class="head">{}</div><div class="x">"#, r#" a-{}"><div class="head">{}</div><div class="x">"#,
escape_quot(&attributed.replace(char::is_whitespace, "-")), escape_quot(&attributed.replace(char::is_whitespace, "-")),
escape_quot(&context.replace(char::is_whitespace, "-")),
escape(attributed), escape(attributed),
)); ));
text_to(text, specs, out, doc_args); text_to(text, specs, out, doc_args);
out.push_str("</div>");
if !context.is_empty() {
out.push_str(r#"<div class="x tail">"#);
text_to(context, specs, out, doc_args);
out.push_str("</div>");
}
}
Section::Link(scheme, link, text) => {
out.push_str(&format!( out.push_str(&format!(
r#"</div><div class="tail">{}</div>"#, r#" ls-{}"><div class="head"><a href="{}">{}</a></div><div class="x">"#,
escape(context), escape_quot(scheme),
escape_quot(link),
escape(link),
)); ));
text_to(text, specs, out, doc_args);
out.push_str("</div>");
} }
Section::Ext(spec @ "html", source) if specs.has(spec) => { Section::Ext(spec @ "html", source) if specs.has(spec) => {
out.push_str(&format!( out.push_str(&format!(
@@ -417,6 +438,16 @@ fn text_to<'a: 'b, 'b>(
text_to(text, specs, out, args); text_to(text, specs, out, args);
out.push_str("</del>"); out.push_str("</del>");
} }
Component::Name(text) => {
out.push_str(r#"<span class="tname">"#);
text_to(text, specs, out, args);
out.push_str("</span>");
}
Component::Code(text) => {
out.push_str(r#"<pre class="tcode">"#);
text_to(text, specs, out, args);
out.push_str("</pre>");
}
Component::Special(text) => { Component::Special(text) => {
out.push_str(r#"<u class="special">"#); out.push_str(r#"<u class="special">"#);
text_to(text, specs, out, args); text_to(text, specs, out, args);
@@ -432,16 +463,6 @@ fn text_to<'a: 'b, 'b>(
text_to(text, specs, out, args); text_to(text, specs, out, args);
out.push_str("</strong>"); out.push_str("</strong>");
} }
Component::Name(text) => {
out.push_str(r#"<span class="tname">"#);
text_to(text, specs, out, args);
out.push_str("</span>");
}
Component::Code(text) => {
out.push_str(r#"<pre class="tcode">"#);
text_to(text, specs, out, args);
out.push_str("</pre>");
}
Component::Link(target, text) => { Component::Link(target, text) => {
let link_target: Option<&'a Document<'a>> = args let link_target: Option<&'a Document<'a>> = args
.max_len .max_len

View File

@@ -35,6 +35,18 @@ fn text_to(text: &Text<'_>, complete: bool, specs: &[impl Spec], out: &mut Strin
out.push('~'); out.push('~');
} }
} }
Component::Name(text) => {
text_to(text, complete, specs, out);
}
Component::Code(text) => {
if complete {
out.push('`');
}
text_to(text, complete, specs, out);
if complete {
out.push('`');
}
}
Component::Special(text) => { Component::Special(text) => {
if complete { if complete {
out.push('_'); out.push('_');
@@ -66,18 +78,6 @@ fn text_to(text: &Text<'_>, complete: bool, specs: &[impl Spec], out: &mut Strin
out.push('*'); out.push('*');
} }
} }
Component::Name(text) => {
text_to(text, complete, specs, out);
}
Component::Code(text) => {
if complete {
out.push('`');
}
text_to(text, complete, specs, out);
if complete {
out.push('`');
}
}
Component::Link(target, text) => { Component::Link(target, text) => {
if complete { if complete {
out.push('_'); out.push('_');