Compare commits

...

38 Commits
main ... main

Author SHA1 Message Date
mars 7c8e913f0d Horizontal layout (woohoo!) 2022-10-24 00:31:34 -06:00
mars 63b985d230 Lots of axis logic + min-sized based calc_sizes() 2022-10-23 23:15:55 -06:00
mars 8477e4c1f1 Add horizontal box demo 2022-10-23 22:27:41 -06:00
mars 0ec3b49eae Better box building 2022-10-23 22:24:20 -06:00
mars 847de268aa Calculate individual child block sizes 2022-10-23 20:37:53 -06:00
mars d7b465b438 Dimensions + ParseScoped 2022-10-23 19:54:49 -06:00
mars bfe3db415c Fix text block line width inconsistencies 2022-10-23 00:26:12 -06:00
mars 3d214a8230 Rudimentary layout 2022-10-23 00:10:55 -06:00
mars 8f92067869 Render text block padding 2022-10-22 22:57:25 -06:00
mars 8fb89ad70b Refactor to layout module 2022-10-22 22:23:47 -06:00
mars 2355d2c768 Add auto margins 2022-10-22 22:12:04 -06:00
mars bda4432d25 Add border kind parsing 2022-10-22 22:06:26 -06:00
mars c7ccaaa0e5 Clean up use of ParseError 2022-10-22 21:50:49 -06:00
mars 0cbff0802e Add and parse AxisSize 2022-10-22 21:18:09 -06:00
mars 88d3794eb9 Better ParseValue + Color parsing + border colors 2022-10-22 17:52:33 -06:00
mars d9dc49b584 Args parsing traits + block margins and padding 2022-10-22 17:38:17 -06:00
mars d60ee159a7 Refactor Args into args module 2022-10-22 12:58:50 -06:00
mars 293fa8af2b Refactor Source into source module 2022-10-22 12:42:46 -06:00
mars 3df84a24a1 Remove old files 2022-10-22 12:36:32 -06:00
mars 0e3b8ea611 Text layout + rendering 2022-10-13 00:49:47 -06:00
mars dfdf8d73b0 Rename styled text to tagged text + convert plain text to text layouts + figlet and code block layouts 2022-10-12 02:59:13 -06:00
mars b89496f15b Remove test lexing from main 2022-10-12 02:47:22 -06:00
mars bb11a857da Remove importing of obsolete modules 2022-10-12 01:55:38 -06:00
mars c8fd64bd79 Remove old lexpr parsing code 2022-10-12 01:54:43 -06:00
mars 461e9b6529 WIP component-based DOM 2022-10-12 01:53:48 -06:00
mars b1b5b187ba Parse number values 2022-10-11 18:54:51 -06:00
mars 5616237eea Basic styled text parsing 2022-10-11 03:42:07 -06:00
mars 3091cc09f4 Return-carriage newlines + properly aligned newline starts 2022-10-11 03:16:57 -06:00
mars 9d68e04d75 Support inline list tags + better ParseError display 2022-10-11 02:47:45 -06:00
mars 0bbc6e2a62 Display ParseErrors with source 2022-10-11 02:35:34 -06:00
mars 10a324172b Minor WithSource cleanup 2022-10-11 01:56:11 -06:00
mars a0edf84c01 Add AST parser 2022-10-11 01:34:03 -06:00
mars 8a6b714c1a WIP text module 2022-10-10 22:12:44 -06:00
mars a6cfb7c180 Parse arguments 2022-10-10 15:55:51 -06:00
mars 8cbd303510 Add more test content 2022-10-09 19:27:45 -06:00
mars ac6eac1375 Color text tags 2022-10-09 17:10:58 -06:00
mars dec3406a91 Figlet and code block layout 2022-10-09 15:44:38 -06:00
mars 04517ad670 Remove leaf tags + better child semantics 2022-10-09 15:22:45 -06:00
18 changed files with 2013 additions and 503 deletions

99
Cargo.lock generated
View File

@ -31,12 +31,42 @@ dependencies = [
"winapi",
]
[[package]]
name = "beef"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "either"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "figlet-rs"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01af5c4fc8506204d2280509e9d57d1b41e54e587c185ac09baef30d21ae63ed"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "getrandom"
version = "0.2.7"
@ -64,32 +94,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "itoa"
version = "0.4.8"
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "lexpr"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ceee0b80e0043f17bf81130471e1b0975179af75fe657af45577d80e2698fe3b"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"itoa",
"lexpr-macros",
"proc-macro-hack",
"ryu",
]
[[package]]
name = "lexpr-macros"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd627fb38e19c00d8d068618259205f7a91c91aeade5c15bc35dbca037bb1c35"
dependencies = [
"proc-macro-hack",
"proc-macro2",
"quote",
"either",
]
[[package]]
@ -98,6 +108,29 @@ version = "0.2.134"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
[[package]]
name = "logos"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf8b031682c67a8e3d5446840f9573eb7fe26efe7ec8d195c9ac4c0647c502f1"
dependencies = [
"logos-derive",
]
[[package]]
name = "logos-derive"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d849148dbaf9661a6151d1ca82b13bb4c4c128146a88d05253b38d4e2f496c"
dependencies = [
"beef",
"fnv",
"proc-macro2",
"quote",
"regex-syntax",
"syn",
]
[[package]]
name = "memchr"
version = "2.5.0"
@ -110,12 +143,6 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "proc-macro-hack"
version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.46"
@ -157,12 +184,6 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]]
name = "smawk"
version = "0.3.1"
@ -218,7 +239,11 @@ name = "tml"
version = "0.1.0"
dependencies = [
"ansi_term",
"lexpr",
"bitflags",
"figlet-rs",
"fnv",
"itertools",
"logos",
"strum",
"textwrap",
]

View File

@ -4,7 +4,11 @@ version = "0.1.0"
edition = "2021"
[dependencies]
lexpr = "0.2"
strum = { version = "0.24", features = ["derive"] }
ansi_term = "0.12"
bitflags = "1.3"
figlet-rs = "0.1"
fnv = "1.0"
itertools = "0.10"
logos = "0.12"
strum = { version = "0.24", features = ["derive"] }
textwrap = "0.15"

8
assets/horizontal.tml Normal file
View File

@ -0,0 +1,8 @@
(p "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
(box :axis horizontal
(p :min-width 20 "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
(p :min-width 20 "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
(p :min-width 20 "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."))
(p "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")

1
assets/styled.tml Normal file
View File

@ -0,0 +1 @@
("unstyled" bold "bold" (red "bold red") italic "bold italic")

View File

@ -1,6 +1,17 @@
(
(h1 "Hello world!")
(figlet "Hello figlet!")
(code "
#include <stdio.h>
int main(int argc, const char *argv[]) {
printf(\"Hello, world!\\n\");
return 0;
}
")
(p "Text goes here.")
(h2 "Heading 2")
@ -30,6 +41,29 @@ lines.")
(strikethrough (bold (italic (underline "EVERYTHING AT ONCE"))))
".")
(p
"You can write every color in the "
(red "te")
(green "rm")
(yellow "in")
(blue "al")
" "
(magenta "ra")
(cyan "in")
(white "bo")
(black "w")
".")
(p "To " (bold "be") ", or " (bold "NOT") " to " (bold "be") ": " (italic "that is the question."))
(p "Super long paragraph: According to all known laws of aviation, there is no way that a bee should be able to fly. Its wings are too small to get its fat little body off the ground. The bee, of course, flies anyway because bees don't care what humans think is impossible.")
(p "Paragraph with big words:
The implement, the device of my extinction
The terminating clockwork of my gleeful bane
The definitive scourge of its mockery
The end-art instruments lethality attained")
(h3 "Styling " (italic "even ") "works " (bold "in ") (strikethrough "headers."))
(h3 "Super long header: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")

95
src/args.rs Normal file
View File

@ -0,0 +1,95 @@
use std::str::FromStr;
use crate::ast::{ParseErrorKind, ParseResult, Value, ValueKind};
use crate::source::WithSource;
#[derive(Clone, Debug, Default)]
pub struct Args(Vec<Arg>);
impl Args {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn contains(&mut self, name: &str) -> bool {
self.0.iter().find(|arg| arg.name.inner == name).is_some()
}
pub fn get(&mut self, name: &str) -> Option<Arg> {
let idx = self.0.iter().position(|old| old.name.inner == name);
idx.map(|idx| self.0.remove(idx))
}
pub fn insert(&mut self, arg: Arg) -> Option<Arg> {
let existing = self
.0
.iter()
.position(|old| old.name.inner == arg.name.inner);
self.0.push(arg);
existing.map(|idx| self.0.remove(idx))
}
}
#[derive(Clone, Debug)]
pub struct Arg {
pub name: WithSource<String>,
pub val: Value,
}
pub trait ParseArgs: Sized {
fn parse_args(args: &mut Args) -> ParseResult<Self>;
}
pub trait ParseValue: Sized {
fn parse_value(val: &Value) -> ParseResult<Self>;
}
pub trait ParseScoped: Sized {
type Default;
fn parse_scoped(args: &mut Args, scope: &str, default: Self::Default) -> ParseResult<Self>;
}
impl<T: ParseValue> ParseScoped for T {
type Default = T;
fn parse_scoped(args: &mut Args, scope: &str, default: Self::Default) -> ParseResult<Self> {
if let Some(arg) = args.get(scope) {
T::parse_value(&arg.val)
} else {
Ok(default)
}
}
}
impl ParseValue for usize {
fn parse_value(val: &Value) -> ParseResult<Self> {
match val.inner {
ValueKind::Number(num) => Ok(num as Self),
_ => Err(val.map(ParseErrorKind::InvalidValue)),
}
}
}
impl<T: ParseValue> ParseValue for Option<T> {
fn parse_value(val: &Value) -> ParseResult<Self> {
if let ValueKind::Symbol(symbol) = &val.inner {
if symbol.to_lowercase() == "none" {
return Ok(None);
}
}
T::parse_value(val).map(|t| Some(t))
}
}
pub(crate) trait ParseSymbol: FromStr {}
impl<T: ParseSymbol> ParseValue for T {
fn parse_value(val: &Value) -> ParseResult<Self> {
match &val.inner {
ValueKind::Symbol(symbol) => {
T::from_str(&symbol).map_err(|_| val.map(ParseErrorKind::InvalidValue))
}
_ => Err(val.map(ParseErrorKind::InvalidValue)),
}
}
}

339
src/ast.rs Normal file
View File

@ -0,0 +1,339 @@
use std::fmt::{Debug, Display};
use std::ops::Range;
use std::sync::Arc;
use logos::{Lexer, Logos};
use crate::args::{Arg, Args};
use crate::source::{Source, SourcePosition, SourceRange, WithSource};
#[derive(Logos, Copy, Clone, Debug, PartialEq, Eq)]
#[logos(subpattern ident = r"[a-zA-Z][a-zA-Z0-9]*")]
pub enum TokenKind {
#[token("(")]
ParenOpen,
#[token(")")]
ParenClose,
#[regex(r"[a-zA-Z-][a-zA-Z0-9-]*")]
Symbol,
#[regex(r"-?[0-9]*")]
Number,
#[regex(":[a-zA-Z-][a-zA-Z0-9-]*")]
Keyword,
#[regex(r#""([^"\\]|\\t|\\u|\\n|\\")*""#)]
String,
#[regex(r";[^\n]*", logos::skip)]
Comment,
#[token("\n")]
Newline,
#[regex(r"[ \t\f\r]+", logos::skip)]
Whitespace,
#[logos(ignore)]
Eof,
#[error]
Error,
}
#[derive(Debug, Clone)]
pub struct TokenInner {
pub kind: TokenKind,
pub content: String,
}
pub type Token = WithSource<TokenInner>;
#[derive(Debug)]
pub struct AstBuilder<'a> {
lexer: Lexer<'a, TokenKind>,
source: &'a Arc<Source>,
current_row_idx: usize,
current_row_start: usize,
paren_level: usize,
}
impl<'a> AstBuilder<'a> {
pub fn new(source: &'a Arc<Source>) -> Self {
Self {
lexer: TokenKind::lexer(&source.full),
current_row_idx: 0,
current_row_start: 0,
paren_level: 0,
source,
}
}
pub fn idx_to_position(&self, idx: usize) -> SourcePosition {
SourcePosition {
row: self.current_row_idx,
col: idx - self.current_row_start,
}
}
pub fn span_to_range(&self, span: Range<usize>) -> SourceRange {
Range {
start: self.idx_to_position(span.start),
end: self.idx_to_position(span.end),
}
}
pub fn lexer_range(&self) -> SourceRange {
self.span_to_range(self.lexer.span())
}
pub fn with_source<T>(&self, inner: T, range: SourceRange) -> WithSource<T> {
WithSource {
inner,
source: self.source.clone(),
range,
}
}
pub fn token_to_string(&self, token: Token) -> ParseResult<WithSource<String>> {
match token.kind {
TokenKind::String => {
let slice = self.lexer.slice();
let end = slice.len() - 1;
let inner = slice[1..end].to_string();
Ok(WithSource {
inner,
source: token.source,
range: token.range,
})
}
TokenKind::Symbol => Ok(WithSource {
inner: token.content.clone(),
source: token.source,
range: token.range,
}),
TokenKind::Keyword => Ok(WithSource {
inner: token.content[1..].to_string(),
source: token.source,
range: token.range,
}),
_ => Err(token.map(ParseErrorKind::UnexpectedToken(token.kind, None))),
}
}
pub fn next(&mut self) -> ParseResult<Token> {
loop {
let token = self.lexer.next().unwrap_or(TokenKind::Eof);
let span = self.lexer.span();
if let TokenKind::Newline = token {
self.current_row_idx += 1;
self.current_row_start = span.end;
} else if let TokenKind::Error = token {
let range = self.span_to_range(span);
break Err(self.with_source(ParseErrorKind::InvalidToken, range));
} else {
let range = self.span_to_range(span);
if let TokenKind::ParenOpen = token {
self.paren_level += 1;
}
if let TokenKind::ParenClose = token {
if self.paren_level == 0 {
return Err(self.with_source(ParseErrorKind::UnmatchedParen, range));
}
self.paren_level -= 1;
}
if let TokenKind::Eof = token {
if self.paren_level > 0 {
return Err(self.with_source(ParseErrorKind::UnmatchedParen, range));
}
}
break Ok(Token {
inner: TokenInner {
kind: token,
content: self.lexer.slice().to_string(),
},
source: self.source.to_owned(),
range,
});
}
}
}
pub fn expect_next(&mut self) -> ParseResult<Token> {
let tok = self.next()?;
if let TokenKind::Eof = tok.kind {
Err(tok.map(ParseErrorKind::UnexpectedEof))
} else {
Ok(tok)
}
}
pub fn parse_tag_body(&mut self) -> ParseResult<TagBody> {
let mut args = Args::new();
let children = loop {
let token = self.next()?;
match token.kind {
TokenKind::Eof => break vec![],
TokenKind::ParenClose => break vec![],
TokenKind::Keyword => {
let name = self.token_to_string(token.clone())?;
if args.contains(&name) {
return Err(token.map(ParseErrorKind::DuplicateArgument(name.inner)));
}
let val = self.expect_value()?;
args.insert(Arg { name, val });
}
_ => break self.parse_list_from(token)?,
}
};
Ok(TagBody { args, children })
}
pub fn parse_list_from(&mut self, mut token: Token) -> ParseResult<Vec<Value>> {
let mut vals = Vec::new();
loop {
if let TokenKind::Symbol = token.kind {
let start = token.range.start.clone();
let id = self.token_to_string(token)?;
let body = self.parse_tag_body()?;
let end = self.idx_to_position(self.lexer.span().end);
let val = Value {
inner: ValueKind::Tag(Tag { id, body }),
source: self.source.to_owned(),
range: start..end,
};
vals.push(val);
break;
} else if let Some(val) = self.parse_value_from(token)? {
vals.push(val);
token = self.next()?;
} else {
break;
}
}
Ok(vals)
}
pub fn parse_value_from(&mut self, token: Token) -> ParseResult<Option<Value>> {
match token.kind {
TokenKind::Eof | TokenKind::ParenClose => Ok(None),
TokenKind::String => Ok(Some(self.token_to_string(token)?.to_val_string())),
TokenKind::Symbol => Ok(Some(self.token_to_string(token)?.to_val_symbol())),
TokenKind::Number => {
if let Ok(val) = token.content.parse() {
Ok(Some(token.map(ValueKind::Number(val))))
} else {
Err(token.map(ParseErrorKind::BadNumber))
}
}
TokenKind::ParenOpen => {
let start = token.range.end;
let next = self.expect_next()?;
let vals = self.parse_list_from(next)?;
let end = self.idx_to_position(self.lexer.span().end);
Ok(Some(Value {
inner: ValueKind::List(vals),
source: self.source.to_owned(),
range: start..end,
}))
}
_ => Err(token.map(ParseErrorKind::UnexpectedToken(token.kind, None))),
}
}
pub fn expect_value(&mut self) -> ParseResult<Value> {
let next = self.expect_next()?;
self.parse_value_from(next)?
.ok_or_else(|| self.with_source(ParseErrorKind::UnexpectedEof, self.lexer_range()))
}
}
#[derive(Clone, Debug)]
pub enum ValueKind {
String(String),
Number(i32),
Symbol(String),
Tag(Tag),
List(Vec<Value>),
}
pub type Value = WithSource<ValueKind>;
impl WithSource<String> {
pub fn to_val_string(self) -> Value {
Value {
inner: ValueKind::String(self.inner),
source: self.source,
range: self.range,
}
}
pub fn to_val_symbol(self) -> Value {
Value {
inner: ValueKind::Symbol(self.inner),
source: self.source,
range: self.range,
}
}
}
#[derive(Clone, Debug)]
pub struct Tag {
pub id: WithSource<String>,
pub body: TagBody,
}
#[derive(Clone, Debug, Default)]
pub struct TagBody {
pub args: Args,
pub children: Vec<Value>,
}
pub type ParseError = WithSource<ParseErrorKind>;
pub type ParseResult<T> = Result<T, ParseError>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ParseErrorKind {
InvalidToken,
DuplicateArgument(String),
MissingArgument(String),
UnmatchedParen,
UnexpectedToken(TokenKind, Option<TokenKind>),
UnexpectedEof,
InvalidValue,
BadNumber,
}
impl Display for ParseErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Parse error: ")?;
use ParseErrorKind::*;
match self {
InvalidToken => write!(f, "invalid token"),
DuplicateArgument(name) => write!(f, "duplicate {} argument", name),
MissingArgument(name) => write!(f, "missing {} argument", name),
UnmatchedParen => write!(f, "unmatched parentheses"),
UnexpectedToken(kind, Some(expected)) => {
write!(f, "unexpected {:?} token (expected {:?})", kind, expected)
}
UnexpectedToken(kind, None) => write!(f, "unexpected token {:?}", kind),
UnexpectedEof => write!(f, "unexpected end-of-file"),
InvalidValue => write!(f, "invalid value"),
BadNumber => write!(f, "badly-formed number"),
}
}
}

117
src/block.rs Normal file
View File

@ -0,0 +1,117 @@
use strum::EnumString;
use crate::args::{Args, ParseArgs, ParseScoped};
use crate::ast::ParseResult;
use crate::border::BorderKind;
use crate::layout::{AxisSize, Dimensions, Margin, Rect, Axis};
use crate::text::{Color, TextLayout};
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, EnumString)]
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
pub enum BlockTagKind {
Section,
P,
Figlet,
Code,
Ul,
Ol,
Li,
Box,
}
impl BlockTagKind {
pub fn children(&self) -> AllowedChildren {
use BlockTagKind::*;
match self {
Section => AllowedChildren::Block,
P => AllowedChildren::TaggedText,
Figlet | Code => AllowedChildren::PlainText,
Ul | Ol => AllowedChildren::Specific(&[Li, Ul, Ol]),
Li => AllowedChildren::TaggedText,
Box => AllowedChildren::Block,
}
}
pub fn make_text_layout(&self, text: String) -> TextLayout {
use BlockTagKind::*;
match self {
Figlet => {
let font = figlet_rs::FIGfont::standard().unwrap();
let figure = font.convert(&text).expect("Failed to FIG-ify text");
TextLayout::from_plain(&figure.to_string())
}
Code => TextLayout::from_plain(&text),
_ => unimplemented!("Text layout for {:?} tag", self),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum AllowedChildren {
Block,
PlainText,
TaggedText,
Specific(&'static [BlockTagKind]),
}
impl AllowedChildren {
pub fn allows_styled_text(&self) -> bool {
use AllowedChildren::*;
match self {
TaggedText => true,
_ => false,
}
}
pub fn allows_block(&self, kind: &BlockTagKind) -> bool {
use AllowedChildren::*;
match self {
Block => true,
Specific(allowed) => allowed.contains(kind),
_ => false,
}
}
}
#[derive(Clone, Debug)]
pub struct Block {
pub size: Dimensions<AxisSize>,
pub margin: Rect<Margin>,
pub padding: Rect<usize>,
pub border: Rect<Option<BorderKind>>,
pub border_color: Rect<Color>,
}
impl ParseArgs for Block {
fn parse_args(args: &mut Args) -> ParseResult<Self> {
let size = Dimensions::parse(args, 1)?;
let margin = Rect::parse_scoped(args, "margin", Margin::Fixed(1))?;
let padding = Rect::parse_scoped(args, "padding", 0)?;
let border = Rect::parse_scoped(args, "border", None)?;
let border_color = Rect::parse_scoped(args, "border-color", Color::White)?;
Ok(Self {
size,
margin,
padding,
border,
border_color,
})
}
}
impl Block {
pub fn required_axis_size(&self, axis: Axis) -> AxisSize {
let mut size = self.size.axis(axis).to_owned();
let (ps, pe) = self.padding.axis_sides(axis);
size.add_size(*ps);
size.add_size(*pe);
let (ms, me) = self.margin.axis_sides(axis);
size.add_margin(ms);
size.add_margin(me);
size
}
}

9
src/border.rs Normal file
View File

@ -0,0 +1,9 @@
use strum::EnumString;
#[derive(Copy, Clone, Debug, EnumString)]
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
pub enum BorderKind {
Single,
}
impl crate::args::ParseSymbol for BorderKind {}

View File

@ -1,6 +1,334 @@
use crate::tag::BlockTag;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
#[derive(Clone, Debug, Hash)]
use crate::args::{Args, ParseArgs, ParseScoped};
use crate::ast::{self, ParseErrorKind, ParseResult, TagBody};
use crate::block::{AllowedChildren, Block, BlockTagKind};
use crate::layout::{Axis, BoxLayout, Dimensions, RenderedBlock};
use crate::node::{NodeId, NodeStore};
use crate::source::WithSource;
use crate::text::{TaggedText, TextBlock, TextLayout, TextParseErrorKind, TextStyle};
#[derive(Clone, Debug)]
pub struct Dom {
pub tags: Vec<BlockTag>,
pub root: NodeId,
pub nodes: NodeStore,
}
impl Dom {
pub fn parse(body: &TagBody) -> DomResult<Self> {
let mut builder = DomBuilder::default();
let root = builder.new_node();
let mut block = BoxBuilder::root(root);
let mut text_style = TextStyle::default();
let mut state = DomWalkState {
builder: &mut builder,
box_builder: &mut block,
text_style: &mut text_style,
};
state.add_children(&body.children)?;
block.build(&mut builder);
Ok(Self {
root,
nodes: builder.node_store,
})
}
pub fn print(&self) {
self.print_from(self.root, 0);
}
pub fn print_from(&self, node: NodeId, indent: usize) {
let margin: String = std::iter::repeat(" ").take(indent * 4).collect();
if let Some(NodeChildren(children)) = self.nodes.get(node) {
println!("{}{}: {:?}", margin, node, children);
for child in children.iter() {
self.print_from(*child, indent + 1);
}
} else {
let text = self.nodes.get::<TextBlock>(node);
println!("{}{}: {:?}", margin, node, text);
}
}
pub fn render(&mut self, size: Dimensions<usize>) -> &RenderedBlock {
self.render_from(self.root, size);
self.nodes.get(self.root).unwrap()
}
pub fn render_from(&mut self, node: NodeId, size: Dimensions<usize>) {
let rendered = if let Some(NodeChildren(children)) = self.nodes.get(node) {
let children = children.to_owned();
let axis: Axis = self.nodes.get::<Axis>(node).unwrap().to_owned();
let layout = BoxLayout {
axis,
children: self.get_many::<Block>(&children),
};
let sizes = layout.calc_sizes(size).to_owned();
drop(layout);
for (size, child) in sizes.into_iter().zip(children) {
self.render_from(child, size);
}
let children = self.get_children(node);
let rendered = self.get_many::<RenderedBlock>(&children);
let layout = BoxLayout {
axis,
children: self.get_many::<Block>(&children),
};
layout.render(&rendered)
} else {
let style = TextStyle::default();
let layout: TextLayout = if let Some(layout) = self.nodes.get::<TextLayout>(node) {
layout.to_owned()
} else if let Some(block) = self.nodes.get::<TextBlock>(node) {
block.layout(&style, size.width)
} else {
panic!("Couldn't find text info on node {}", node);
};
let rendered = layout.render(&style);
let block: &Block = self.nodes.get(node).unwrap();
rendered.add_padding(&block.padding)
};
self.nodes.insert(node, rendered);
}
pub fn get_children(&self, node: NodeId) -> Vec<NodeId> {
self.nodes.get::<NodeChildren>(node).unwrap().0.to_owned()
}
pub fn get_many<T: 'static>(&self, children: &[NodeId]) -> Vec<&T> {
children
.iter()
.map(|child| self.nodes.get(*child).unwrap())
.collect()
}
}
#[derive(Clone, Debug, Default)]
pub struct NodeChildren(Vec<NodeId>);
#[derive(Copy, Clone, Debug, Default)]
pub struct NodeParent(NodeId);
#[derive(Debug, Default)]
pub struct DomBuilder {
pub node_store: NodeStore,
pub next_node: NodeId,
}
impl Deref for DomBuilder {
type Target = NodeStore;
fn deref(&self) -> &NodeStore {
&self.node_store
}
}
impl DerefMut for DomBuilder {
fn deref_mut(&mut self) -> &mut NodeStore {
&mut self.node_store
}
}
impl DomBuilder {
pub fn new_node(&mut self) -> NodeId {
let node = self.next_node;
self.next_node += 1;
node
}
}
#[derive(Debug)]
pub struct BoxBuilder {
pub id: NodeId,
pub axis: Axis,
pub children: Vec<NodeId>,
}
impl BoxBuilder {
pub fn new(id: NodeId, args: &mut Args) -> ParseResult<Self> {
Ok(Self {
id,
axis: Axis::parse_scoped(args, "axis", Axis::Vertical)?,
children: Vec::new(),
})
}
pub fn root(id: NodeId) -> Self {
Self {
id,
axis: Axis::Vertical,
children: Vec::new(),
}
}
pub fn build(self, dom: &mut DomBuilder) -> NodeId {
for child in self.children.iter() {
dom.insert(*child, NodeParent(self.id));
}
let children = NodeChildren(self.children);
dom.insert(self.id, children);
dom.insert(self.id, self.axis);
self.id
}
}
#[derive(Debug)]
pub struct DomWalkState<'a> {
pub builder: &'a mut DomBuilder,
pub box_builder: &'a mut BoxBuilder,
pub text_style: &'a mut TextStyle,
}
impl<'a> DomWalkState<'a> {
pub fn fork(&mut self) -> DomWalkState {
DomWalkState {
builder: &mut *self.builder,
box_builder: &mut *self.box_builder,
text_style: &mut *self.text_style,
}
}
pub fn add_text_block(&mut self, kind: BlockTagKind, text: TaggedText) -> NodeId {
let node = self.builder.new_node();
let block = text.make_text_block(kind);
self.builder.insert(node, block);
node
}
pub fn add_block_tag(&mut self, kind: BlockTagKind, body: &ast::TagBody) -> DomResult<NodeId> {
let node = self.builder.new_node();
let mut args = body.args.clone();
let block = Block::parse_args(&mut args).map_err(|e| e.map(e.inner.clone().into()))?;
println!("{:#?}", block);
self.builder.insert(node, block);
if let BlockTagKind::Box = kind {
self.box_builder.children.push(node);
let mut box_builder =
BoxBuilder::new(node, &mut args).map_err(|e| e.map(e.inner.clone().into()))?;
let mut state = DomWalkState {
box_builder: &mut box_builder,
..self.fork()
};
state.add_children(&body.children)?;
box_builder.build(&mut self.builder);
return Ok(node);
}
match kind.children() {
AllowedChildren::TaggedText => {
let text = TaggedText::parse(&self.text_style, &body.children)
.map_err(|e| e.map(e.inner.clone().into()))?;
let block = text.make_text_block(kind);
self.builder.insert(node, block);
self.box_builder.children.push(node);
}
AllowedChildren::PlainText => {
let text = crate::text::parse_plain(&body.children)
.map_err(|e| e.map(e.inner.clone().into()))?;
let layout = kind.make_text_layout(text);
self.builder.insert(node, layout);
self.box_builder.children.push(node);
}
_ => {
self.add_children(&body.children)?;
}
}
Ok(node)
}
pub fn add_tag(mut self, tag: &ast::Tag) -> DomResult<()> {
if let Ok(kind) = BlockTagKind::from_str(&tag.id) {
self.add_block_tag(kind, &tag.body)?;
Ok(())
} else {
Err(tag.id.map(DomErrorKind::InvalidTag))
}
}
pub fn add_children(&mut self, vals: &[ast::Value]) -> DomResult<()> {
for child in vals.iter() {
self.add_child(child)?;
}
Ok(())
}
pub fn add_child(&mut self, val: &ast::Value) -> DomResult<()> {
use ast::ValueKind::*;
match &val.inner {
Tag(tag) => {
let state = self.fork();
state.add_tag(tag)
}
List(list) => self.add_children(list),
String(_string) => {
println!("{}", val.map("Insert string code here:"));
Ok(())
}
_ => Err(val.map(DomErrorKind::InvalidChild)),
}
}
}
pub type DomResult<T> = Result<T, WithSource<DomErrorKind>>;
#[derive(Clone, Debug)]
pub enum DomErrorKind {
InvalidTag,
InvalidChild,
ParseError(ParseErrorKind),
TextError(TextParseErrorKind),
}
impl From<ParseErrorKind> for DomErrorKind {
fn from(e: ParseErrorKind) -> Self {
DomErrorKind::ParseError(e)
}
}
impl From<TextParseErrorKind> for DomErrorKind {
fn from(e: TextParseErrorKind) -> Self {
DomErrorKind::TextError(e)
}
}
impl std::fmt::Display for DomErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use DomErrorKind::*;
if let TextError(e) = &self {
return e.fmt(f);
}
if let ParseError(e) = &self {
return e.fmt(f);
}
write!(f, "DOM error: ")?;
match self {
InvalidTag => write!(f, "invalid tag"),
InvalidChild => write!(f, "invalid child"),
_ => unimplemented!(),
}
}
}

View File

@ -1,184 +1,479 @@
use crate::style::Stylesheet;
use crate::tag::*;
use itertools::Itertools;
use strum::EnumString;
impl TextTag {
pub fn layout<'a>(
&self,
style: &Stylesheet,
options: impl Into<textwrap::Options<'a>>,
) -> Vec<String> {
let styled = self.style(style);
textwrap::wrap(&styled, options)
.into_iter()
.map(|s| s.to_string())
.collect()
}
use crate::args::*;
use crate::ast::{ParseErrorKind, ParseResult, Value, ValueKind};
use crate::block::Block;
use crate::source::WithSource;
pub fn style_children(&self, style: &Stylesheet) -> String {
let mut string = String::new();
for child in self.children.iter() {
let child_string = match child {
TextNode::Tag(tag) => tag.style(style),
TextNode::Text(string) => string.to_owned(),
};
string.push_str(&child_string);
}
string
}
pub fn style(&self, style: &Stylesheet) -> String {
let string = self.style_children(style);
use TextTagKind::*;
let painter = match self.kind {
Plain => return string,
Underline => style.underline,
Bold => style.bold,
Italic => style.italic,
Strikethrough => style.strikethrough,
};
painter.paint(string).to_string()
}
#[derive(Clone, Debug)]
pub struct RenderedBlock {
pub lines: Vec<String>,
pub size: Dimensions<usize>,
}
impl BlockTag {
pub fn layout(&self, style: &Stylesheet, available_width: usize) -> Vec<String> {
use BlockTagKind::*;
match self.kind {
H1 | H2 | H3 => {
const INDENT: usize = 4;
impl RenderedBlock {
pub fn add_padding(&self, padding: &Rect<usize>) -> Self {
let make_spaces = |num: usize| -> String { std::iter::repeat(" ").take(num).collect() };
let new_width = self.size.width + padding.l + padding.r;
let left_pad = make_spaces(padding.l);
let right_pad = make_spaces(padding.r);
let vertical_pad = make_spaces(new_width);
let styled = self.style_text_children(style);
let mut lines = Vec::new();
let bullets = match self.kind {
H1 => "# ",
H2 => "## ",
H3 => "### ",
_ => unreachable!(),
};
lines.extend(std::iter::repeat(vertical_pad.clone()).take(padding.t));
let indent: String = std::iter::repeat(" ").take(INDENT).collect();
lines.extend(
self.lines
.iter()
.map(|line| format!("{}{}{}", left_pad, line, right_pad)),
);
let options = textwrap::Options::new(available_width)
.initial_indent(&bullets)
.subsequent_indent(&indent);
lines.extend(std::iter::repeat(vertical_pad.clone()).take(padding.b));
let lines = textwrap::wrap(&styled, options);
let mut max_len = 0;
lines
.iter()
.enumerate()
.map(|(index, line)| {
let mut line = line.to_string();
max_len = max_len.max(line.len());
let painter = if index == lines.len() - 1 {
while line.len() < max_len {
line.push(' ');
}
&style.header_style_floor
} else {
&style.header_style_float
};
painter.paint(line).to_string()
})
.collect()
}
P => self.layout_text_children(style, available_width),
Ul => self.layout_list_items(style, available_width, false),
Ol => self.layout_list_items(style, available_width, true),
Li | Tr | Td => panic!("{:?} tag cannot be directly laid out", self.kind),
_ => unimplemented!(),
Self {
lines,
size: Dimensions {
width: new_width,
height: self.size.height + padding.t + padding.b,
},
}
}
pub fn layout_list_items(
&self,
style: &Stylesheet,
available_width: usize,
ordered: bool,
) -> Vec<String> {
let indent = style.list_indent;
let mut lines = Vec::new();
let mut index = 1;
let spacer: String = std::iter::repeat(" ").take(indent).collect();
pub fn make_blank(size: Dimensions<usize>) -> Self {
let line: String = std::iter::repeat(" ").take(size.width).collect();
Self {
lines: std::iter::repeat(line).take(size.height).collect(),
size,
}
}
let prefix = format!("{:>width$} ", style.ul_prefix, width = indent - 2);
let mut prefix = style.bullet.paint(prefix).to_string();
let available_width = if available_width > indent {
available_width - indent
} else {
0
};
for child in self.children.iter() {
if ordered {
prefix = format!("{:>width$}. ", index, width = indent - 2);
prefix = style.bullet.paint(prefix).to_string();
}
let options = textwrap::Options::new(available_width)
.initial_indent(&prefix)
.subsequent_indent(&spacer);
match child {
BlockNode::Text(tag) => {
index += 1;
let mut li = tag.layout(style, options);
lines.append(&mut li);
}
BlockNode::Block(tag) => {
let li = match tag.kind {
BlockTagKind::Ol => tag.layout_list_items(style, available_width, true),
BlockTagKind::Ul => tag.layout_list_items(style, available_width, false),
BlockTagKind::Li => {
index += 1;
let mut li = tag.layout_text_children(style, options);
lines.append(&mut li);
continue;
}
_ => panic!("Unexpected {:?} in {:?}", tag.kind, self.kind),
pub fn concat(mut self, axis: Axis, mut other: Self) -> Self {
match axis {
Axis::Vertical => Self {
size: Dimensions {
width: self.size.width.max(other.size.width),
height: self.size.height + other.size.height,
},
lines: self.lines.into_iter().chain(other.lines).collect(),
},
Axis::Horizontal => {
if self.size.height < other.size.height {
let padding = Dimensions {
width: self.size.width,
height: other.size.height - self.size.height,
};
lines.extend(li.into_iter().map(|line| format!("{}{}", spacer, line)));
let padding = Self::make_blank(padding);
self = self.concat(Axis::Vertical, padding);
}
if other.size.height < self.size.height {
let padding = Dimensions {
width: other.size.width,
height: self.size.height - other.size.height,
};
let padding = Self::make_blank(padding);
other = other.concat(Axis::Vertical, padding);
}
Self {
size: Dimensions {
width: self.size.width + other.size.width,
height: self.size.height.max(other.size.height),
},
lines: self
.lines
.into_iter()
.zip(other.lines)
.map(|(first, other)| format!("{}{}", first, other))
.collect(),
}
BlockNode::Leaf(_) => panic!("Unexpected leaf tag in {:?}", self.kind),
}
}
lines
}
pub fn layout_text_children<'a>(
&self,
style: &Stylesheet,
options: impl Into<textwrap::Options<'a>>,
) -> Vec<String> {
let styled = self.style_text_children(style);
textwrap::wrap(&styled, options)
.into_iter()
.map(|s| s.to_string())
.collect()
}
pub fn style_text_children(&self, style: &Stylesheet) -> String {
let mut string = String::new();
for child in self.children.iter() {
match child {
BlockNode::Text(tag) => string.push_str(&tag.style(style)),
_ => panic!("{:?} node attempted to style a non-text child", self.kind),
}
}
string
}
}
#[derive(Copy, Clone, Debug, EnumString)]
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
pub enum Axis {
Horizontal,
Vertical,
}
impl ParseSymbol for Axis {}
impl Axis {
pub fn cross(&self) -> Self {
match self {
Axis::Horizontal => Axis::Vertical,
Axis::Vertical => Axis::Horizontal,
}
}
}
#[derive(Clone, Debug)]
pub struct BoxLayout<'a> {
pub axis: Axis,
pub children: Vec<&'a Block>,
}
impl<'a> BoxLayout<'a> {
pub fn calc_sizes(&self, available: Dimensions<usize>) -> Vec<Dimensions<usize>> {
let cross = self.axis.cross();
let sizes = self
.children
.iter()
.map(|block| {
let available = *available.axis(cross);
let required = block.required_axis_size(cross);
let min_size = required.min_size();
let remaining = available - min_size.min(available);
let cross_size = block.size.axis(cross).clamp(remaining);
let axis_size = block.size.axis(self.axis).min_size();
match self.axis {
Axis::Horizontal => Dimensions {
width: axis_size,
height: cross_size,
},
Axis::Vertical => Dimensions {
width: cross_size,
height: axis_size,
},
}
})
.collect();
sizes
}
pub fn render(&self, blocks: &[&RenderedBlock]) -> RenderedBlock {
let axis = self.axis;
let cross = axis.cross();
let cross_size = blocks
.iter()
.zip(&self.children)
.map(|(rendered, block)| {
let (ms, me) = block.margin.axis_sides(cross);
ms.required_size() + rendered.size.axis(cross) + me.required_size()
})
.max()
.unwrap_or(0);
let make_spacer = |size: usize| -> RenderedBlock {
let (width, height) = match axis {
Axis::Horizontal => (size, cross_size),
Axis::Vertical => (cross_size, size),
};
RenderedBlock::make_blank(Dimensions { width, height })
};
let mut spacers = Vec::new();
let mut last_margin = 0;
for block in self.children.iter() {
let (ms, me) = block.margin.axis_sides(axis);
let size = ms.required_size().max(last_margin);
spacers.push(make_spacer(size));
last_margin = me.required_size();
}
spacers.push(make_spacer(last_margin));
let mut padded = Vec::new();
for (block, rendered) in self.children.iter().zip(blocks) {
let (ms, me) = block.margin.axis_sides(cross);
let available = cross_size - rendered.size.axis(cross).min(&cross_size);
let (ms, me) = Self::calc_margins(ms, me, available);
let padding = match axis {
Axis::Horizontal => Rect {
l: 0,
r: 0,
t: ms,
b: me,
},
Axis::Vertical => Rect {
t: 0,
b: 0,
l: ms,
r: me,
},
};
padded.push(rendered.add_padding(&padding));
}
spacers
.into_iter()
.interleave(padded)
.reduce(|r1, r2| r1.concat(axis, r2))
.unwrap()
}
pub fn calc_margins(start: &Margin, end: &Margin, available: usize) -> (usize, usize) {
let split_available = |available: usize| -> (usize, usize) {
if available & 2 == 0 {
(available >> 1, available >> 1)
} else {
(available >> 1, (available >> 1) | 1)
}
};
use Margin::*;
match (start, end) {
(Auto, Auto) => split_available(available),
(Fixed(start), Auto) => (*start, available - start),
(Auto, Fixed(end)) => (available - end, *end),
(Fixed(fixed_start), Fixed(fixed_end)) => {
let taken = fixed_start + fixed_end;
let available = available - taken.min(available);
let (start, end) = split_available(available);
(fixed_start + start, fixed_end + end)
}
}
}
}
#[derive(Clone, Debug)]
pub enum AxisSize {
Fixed(usize),
Bounded(usize, Option<usize>),
}
impl ParseScoped for AxisSize {
type Default = usize;
fn parse_scoped(args: &mut Args, name: &str, mut min: usize) -> ParseResult<Self> {
if let Some(arg) = args.get(name) {
Ok(AxisSize::Fixed(ParseValue::parse_value(&arg.val)?))
} else {
let min_name = format!("min-{}", name);
if let Some(arg) = args.get(&min_name) {
min = ParseValue::parse_value(&arg.val)?;
}
let max_name = format!("max-{}", name);
let max = if let Some(arg) = args.get(&max_name) {
Some(ParseValue::parse_value(&arg.val)?)
} else {
None
};
Ok(AxisSize::Bounded(min, max))
}
}
}
impl AxisSize {
pub fn add_size(&mut self, size: usize) {
match self {
AxisSize::Fixed(fixed) => *fixed += size,
AxisSize::Bounded(min, None) => *min += size,
AxisSize::Bounded(min, Some(max)) => {
*min += size;
*max += size;
}
}
}
pub fn add_margin(&mut self, margin: &Margin) {
match margin {
Margin::Fixed(margin) => self.add_size(*margin),
Margin::Auto => {}
}
}
pub fn clamp(&self, size: usize) -> usize {
match *self {
AxisSize::Fixed(fixed) => fixed,
AxisSize::Bounded(min, None) => size.max(min),
AxisSize::Bounded(min, Some(max)) => size.max(min).min(max),
}
}
pub fn min_size(&self) -> usize {
match self {
AxisSize::Fixed(fixed) => *fixed,
AxisSize::Bounded(min, _) => *min,
}
}
}
#[derive(Clone, Debug)]
pub enum Margin {
Auto,
Fixed(usize),
}
impl ParseValue for Margin {
fn parse_value(val: &Value) -> ParseResult<Self> {
match &val.inner {
ValueKind::Symbol(symbol) if symbol.to_lowercase() == "auto" => Ok(Margin::Auto),
_ => Ok(Margin::Fixed(usize::parse_value(val)?)),
}
}
}
impl Margin {
pub fn required_size(&self) -> usize {
match self {
Margin::Auto => 0,
Margin::Fixed(fixed) => *fixed,
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct Dimensions<T> {
pub width: T,
pub height: T,
}
impl<T: ParseScoped> Dimensions<T>
where
T::Default: Clone,
{
pub fn parse(args: &mut Args, default: T::Default) -> ParseResult<Self> {
let width = T::parse_scoped(args, "width", default.clone())?;
let height = T::parse_scoped(args, "height", default)?;
Ok(Self { width, height })
}
}
impl<T> Dimensions<T> {
pub fn axis(&self, axis: Axis) -> &T {
match axis {
Axis::Horizontal => &self.width,
Axis::Vertical => &self.height,
}
}
}
impl Dimensions<AxisSize> {
pub fn min_size(&self) -> Dimensions<usize> {
Dimensions {
width: self.width.min_size(),
height: self.height.min_size(),
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct Rect<T> {
pub l: T,
pub t: T,
pub r: T,
pub b: T,
}
impl<T> Rect<T> {
pub fn map<O>(&self, f: impl Fn(&T) -> O) -> Rect<O> {
Rect::<O> {
l: f(&self.l),
t: f(&self.t),
r: f(&self.r),
b: f(&self.b),
}
}
pub fn axis_sides(&self, axis: Axis) -> (&T, &T) {
match axis {
Axis::Horizontal => (&self.l, &self.r),
Axis::Vertical => (&self.t, &self.b),
}
}
}
impl<T: Clone + ParseValue> Rect<T> {
pub fn parse_shorthand(parsed: WithSource<&[T]>) -> ParseResult<Self> {
match parsed.len() {
1 => Ok(Self {
l: parsed[0].clone(),
t: parsed[0].clone(),
r: parsed[0].clone(),
b: parsed[0].clone(),
}),
2 => Ok(Self {
t: parsed[0].clone(),
b: parsed[0].clone(),
l: parsed[1].clone(),
r: parsed[1].clone(),
}),
3 => Ok(Self {
t: parsed[0].clone(),
l: parsed[1].clone(),
r: parsed[1].clone(),
b: parsed[2].clone(),
}),
4 => Ok(Self {
t: parsed[0].clone(),
r: parsed[1].clone(),
b: parsed[2].clone(),
l: parsed[3].clone(),
}),
_ => Err(parsed.map(ParseErrorKind::InvalidValue)),
}
}
}
impl<T: Clone + ParseValue> ParseScoped for Rect<T> {
type Default = T;
fn parse_scoped(args: &mut Args, name: &str, default: T) -> ParseResult<Self> {
let mut get_side = |side: &str| -> Option<Value> {
let name = format!("{}-{}", name, side);
args.get(&name).map(|arg| arg.val)
};
let l = get_side("left");
let t = get_side("top");
let r = get_side("right");
let b = get_side("bottom");
if let Some(short) = args.get(name) {
if l.is_some() || t.is_some() || r.is_some() || b.is_some() {
let e = ParseErrorKind::DuplicateArgument(name.to_string());
return Err(short.name.map(e));
}
match &short.val.inner {
ValueKind::List(vals) => {
let mut parsed = Vec::new();
for val in vals.iter() {
parsed.push(T::parse_value(val)?);
}
Self::parse_shorthand(short.val.map(&parsed))
}
_ => {
let val = T::parse_value(&short.val)?;
Self::parse_shorthand(short.val.map(&[val]))
}
}
} else {
let parse_value = |val: Option<Value>| -> ParseResult<T> {
if let Some(val) = val {
T::parse_value(&val)
} else {
Ok(default.clone())
}
};
Ok(Self {
l: parse_value(l)?,
t: parse_value(t)?,
r: parse_value(r)?,
b: parse_value(b)?,
})
}
}
}

View File

@ -1,32 +1,40 @@
use std::str::FromStr;
use strum::EnumString;
pub mod args;
pub mod ast;
pub mod block;
pub mod border;
pub mod dom;
pub mod layout;
pub mod parse;
pub mod style;
pub mod tag;
use style::Stylesheet;
pub mod node;
pub mod source;
pub mod text;
fn main() {
let src_path = std::env::args().nth(1).unwrap();
let src = std::fs::read_to_string(src_path).unwrap();
let options =
lexpr::parse::Options::new().with_string_syntax(lexpr::parse::StringSyntax::Elisp);
let expr = lexpr::from_str_custom(&src, options).unwrap();
println!("{:#?}", expr);
let source = std::sync::Arc::new(source::Source::from_string(src.clone()));
let mut ast_builder = ast::AstBuilder::new(&source);
let body = match ast_builder.parse_tag_body() {
Ok(body) => body,
Err(e) => panic!("Parse error:\n{}", e),
};
let dom = parse::Parser::parse(&expr);
let style = Stylesheet::default();
let width = 40;
println!("{:#?}", body);
for tag in dom.tags.iter() {
for line in tag.layout(&style, width) {
println!("{}", line);
}
let mut dom = match dom::Dom::parse(&body) {
Ok(dom) => dom,
Err(e) => panic!("DOM parse error:\n{}", e),
};
println!("");
let size = layout::Dimensions {
width: 80,
height: 20,
};
dom.print();
let rendered = dom.render(size);
for line in rendered.lines.iter() {
println!("{}", line);
}
}

80
src/node.rs Normal file
View File

@ -0,0 +1,80 @@
use std::any::{Any, TypeId};
use std::fmt::Debug;
use fnv::FnvHashMap as HashMap;
pub type NodeId = u32;
pub type ComponentStorage<T> = HashMap<NodeId, T>;
#[derive(Debug, Default)]
pub struct NodeStore {
storage: HashMap<TypeId, Box<dyn ComponentStorageTrait>>,
}
impl NodeStore {
pub fn get_storage<T: 'static>(&self) -> Option<&ComponentStorage<T>> {
let id = TypeId::of::<T>();
let boxed = self.storage.get(&id)?;
boxed.as_any().downcast_ref()
}
pub fn get_storage_mut<T: 'static>(&mut self) -> Option<&mut ComponentStorage<T>> {
let id = TypeId::of::<T>();
let boxed = self.storage.get_mut(&id)?;
boxed.as_any_mut().downcast_mut()
}
pub fn get<T: 'static>(&self, id: NodeId) -> Option<&T> {
let storage = self.get_storage::<T>()?;
storage.get(&id)
}
pub fn get_mut<T: 'static>(&mut self, id: NodeId) -> Option<&mut T> {
let storage = self.get_storage_mut::<T>()?;
storage.get_mut(&id)
}
pub fn insert<T: 'static + Debug + Clone>(&mut self, id: NodeId, component: T) -> Option<T> {
if let Some(storage) = self.get_storage_mut::<T>() {
storage.insert(id, component)
} else {
let mut storage = ComponentStorage::<T>::default();
let old = storage.insert(id, component);
let id = TypeId::of::<T>();
self.storage.insert(id, Box::new(storage));
old
}
}
}
impl Clone for NodeStore {
fn clone(&self) -> Self {
let mut storage = HashMap::default();
for (id, src) in self.storage.iter() {
storage.insert(*id, src.as_ref().clone());
}
Self { storage }
}
}
pub trait ComponentStorageTrait: Any + Debug {
fn clone(&self) -> Box<dyn ComponentStorageTrait + 'static>;
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
impl<T: Clone + Debug + 'static> ComponentStorageTrait for ComponentStorage<T> {
fn clone(&self) -> Box<dyn ComponentStorageTrait + 'static> {
Box::new(Clone::clone(self))
}
fn as_any(&self) -> &dyn Any {
self as _
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self as _
}
}

View File

@ -1,127 +0,0 @@
use crate::dom::Dom;
use crate::tag::*;
use lexpr::{Cons, Value};
use std::str::FromStr;
pub struct Parser {
vals: Vec<Value>,
}
impl Parser {
pub fn parse(document: &Value) -> Dom {
let parser = Self::from_value(document);
let mut tags = Vec::new();
for val in parser.vals.into_iter().rev() {
let mut parser = Self::from_value(&val);
let kind = parser
.parse_any_tag_kind()
.as_block()
.expect("Root tag must be a block tag");
let tag = parser.parse_block_tag(kind);
tags.push(tag);
}
Dom { tags }
}
pub fn from_value(value: &Value) -> Self {
let cons = value.as_cons().expect("Must be a cons");
Self::from_cons(cons)
}
pub fn from_cons(cons: &Cons) -> Self {
let mut vals = cons.to_vec().0;
vals.reverse();
Self { vals }
}
pub fn parse_args(&mut self) -> (Vec<(String, Value)>, Vec<Value>) {
let args = Vec::new();
let mut children = std::mem::replace(&mut self.vals, Vec::new());
children.reverse();
(args, children)
}
pub fn parse_text_tag(&mut self, kind: TextTagKind) -> TextTag {
let (_args, children_vals) = self.parse_args();
let mut children = Vec::new();
for child in children_vals.into_iter() {
let node = match child {
Value::Cons(cons) => {
let mut parser = Self::from_cons(&cons);
let kind = parser
.parse_any_tag_kind()
.as_text()
.expect("Text tag can only have other text tags as children");
TextNode::Tag(parser.parse_text_tag(kind))
}
Value::String(string) => TextNode::Text(string.to_string()),
_ => panic!("Text tag child must be a string or a cons"),
};
children.push(node);
}
TextTag { kind, children }
}
pub fn parse_leaf_tag(&mut self, kind: LeafTagKind) -> LeafTag {
let (_args, children) = self.parse_args();
if !children.is_empty() {
panic!("Leaf tag cannot have children");
}
LeafTag { kind }
}
pub fn parse_block_tag(&mut self, kind: BlockTagKind) -> BlockTag {
let (_args, children_vals) = self.parse_args();
let mut children = Vec::new();
for child in children_vals.into_iter() {
let node = match child {
Value::Cons(cons) => {
let mut parser = Self::from_cons(&cons);
match parser.parse_any_tag_kind() {
AnyTagKind::Text(kind) => BlockNode::Text(parser.parse_text_tag(kind)),
AnyTagKind::Leaf(kind) => BlockNode::Leaf(parser.parse_leaf_tag(kind)),
AnyTagKind::Block(kind) => BlockNode::Block(parser.parse_block_tag(kind)),
}
}
Value::String(string) => BlockNode::Text(TextTag {
kind: TextTagKind::Plain,
children: vec![TextNode::Text(string.to_string())],
}),
_ => panic!("Block tag child must be either a string or a cons"),
};
children.push(node);
}
BlockTag { kind, children }
}
pub fn parse_any_tag_kind(&mut self) -> AnyTagKind {
let symbol = self.parse_symbol();
AnyTagKind::from_str(&symbol).expect("Unrecognized tag")
}
pub fn parse_symbol(&mut self) -> String {
self.vals
.pop()
.as_ref()
.map(|v| v.as_symbol())
.flatten()
.expect("Expected symbol")
.to_string()
}
}

92
src/source.rs Normal file
View File

@ -0,0 +1,92 @@
use std::fmt::{Debug, Display};
use std::ops::{Deref, Range};
use std::sync::Arc;
#[derive(Clone)]
pub struct WithSource<T> {
pub inner: T,
pub source: Arc<Source>,
pub range: SourceRange,
}
impl<T: Debug> Debug for WithSource<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.inner.fmt(f)
}
}
impl<T: Debug + Display> Display for WithSource<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, " {}", self.inner)?;
for line_idx in self.range.start.row..=self.range.end.row {
let line = &self.source.lines[line_idx];
writeln!(f, " {:<4}| {}", line_idx + 1, line)?;
let start = if line_idx == self.range.start.row {
self.range.start.col
} else {
0
};
let end = if line_idx == self.range.end.row {
self.range.end.col
} else {
line.len()
};
let spaces: String = std::iter::repeat(" ").take(start).collect();
let carats: String = std::iter::repeat("^").take(end - start).collect();
writeln!(f, " | {}{}", spaces, carats)?;
}
Ok(())
}
}
impl<T> AsRef<T> for WithSource<T> {
fn as_ref(&self) -> &T {
&self.inner
}
}
impl<T> Deref for WithSource<T> {
type Target = T;
fn deref(&self) -> &T {
&self.inner
}
}
impl<T> WithSource<T> {
pub fn map<O>(&self, other: O) -> WithSource<O> {
WithSource {
inner: other,
source: self.source.to_owned(),
range: self.range.clone(),
}
}
}
#[derive(Clone, Debug)]
pub struct Source {
pub full: String,
pub lines: Vec<String>,
}
impl Source {
pub fn from_string(full: String) -> Self {
Self {
lines: full.lines().map(ToString::to_string).collect(),
full,
}
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct SourcePosition {
pub row: usize,
pub col: usize,
}
pub type SourceRange = Range<SourcePosition>;

View File

@ -1,36 +0,0 @@
use ansi_term::Style;
#[derive(Clone, Debug)]
pub struct Stylesheet {
pub header_style_float: Style,
pub header_style_floor: Style,
pub header_prefix: String,
pub p: Style,
pub underline: Style,
pub bold: Style,
pub italic: Style,
pub strikethrough: Style,
pub bullet: Style,
pub ul_prefix: String,
pub list_indent: usize,
}
impl Default for Stylesheet {
fn default() -> Self {
use ansi_term::Color::*;
Self {
header_style_float: Blue.bold(),
header_style_floor: Blue.bold().underline(),
header_prefix: "#".to_string(),
p: Style::new(),
underline: Style::new().underline(),
bold: Style::new().bold(),
italic: Style::new().italic(),
strikethrough: Style::new().strikethrough(),
bullet: Black.into(),
ul_prefix: "*".to_string(),
list_indent: 4,
}
}
}

View File

@ -1,114 +0,0 @@
use std::str::FromStr;
use strum::EnumString;
#[derive(Clone, Debug, Hash, PartialEq, Eq, EnumString)]
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
pub enum TextTagKind {
Plain,
Italic,
Bold,
Underline,
Strikethrough,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum TextNode {
Text(String),
Tag(TextTag),
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct TextTag {
pub kind: TextTagKind,
pub children: Vec<TextNode>,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq, EnumString)]
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
pub enum LeafTagKind {
Figlet,
Code,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct LeafTag {
pub kind: LeafTagKind,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq, EnumString)]
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
pub enum BlockTagKind {
H1,
H2,
H3,
P,
Ul,
Ol,
Li,
Div,
Table,
Tr,
Td,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum BlockNode {
Text(TextTag),
Leaf(LeafTag),
Block(BlockTag),
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct BlockTag {
pub kind: BlockTagKind,
pub children: Vec<BlockNode>,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum AnyTagKind {
Text(TextTagKind),
Leaf(LeafTagKind),
Block(BlockTagKind),
}
impl AnyTagKind {
pub fn as_text(self) -> Option<TextTagKind> {
if let AnyTagKind::Text(text) = self {
Some(text)
} else {
None
}
}
pub fn as_leaf(self) -> Option<LeafTagKind> {
if let AnyTagKind::Leaf(leaf) = self {
Some(leaf)
} else {
None
}
}
pub fn as_block(self) -> Option<BlockTagKind> {
if let AnyTagKind::Block(block) = self {
Some(block)
} else {
None
}
}
}
impl FromStr for AnyTagKind {
type Err = strum::ParseError;
fn from_str(string: &str) -> Result<Self, strum::ParseError> {
if let Ok(text) = TextTagKind::from_str(string) {
Ok(AnyTagKind::Text(text))
} else if let Ok(leaf) = LeafTagKind::from_str(string) {
Ok(AnyTagKind::Leaf(leaf))
} else if let Ok(block) = BlockTagKind::from_str(string) {
Ok(AnyTagKind::Block(block))
} else {
Err(strum::ParseError::VariantNotFound)
}
}
}

352
src/text.rs Normal file
View File

@ -0,0 +1,352 @@
use std::str::FromStr;
use strum::EnumString;
use crate::ast;
use crate::block::BlockTagKind;
use crate::layout::{RenderedBlock, Dimensions};
use crate::source::WithSource;
#[derive(Clone, Debug, Hash, PartialEq, Eq, EnumString)]
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
pub enum TextTagKind {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
Italic,
Bold,
Underline,
Strike,
Style,
}
impl TextTagKind {
pub fn to_color(&self) -> Option<Color> {
use Color as C;
use TextTagKind::*;
match self {
Black => Some(C::Black),
Red => Some(C::Red),
Green => Some(C::Green),
Yellow => Some(C::Yellow),
Blue => Some(C::Blue),
Magenta => Some(C::Magenta),
Cyan => Some(C::Cyan),
White => Some(C::White),
_ => None,
}
}
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, EnumString)]
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
pub enum Color {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
}
impl crate::args::ParseSymbol for Color {}
impl From<Color> for ansi_term::Color {
fn from(c: Color) -> Self {
use ansi_term::Color as C;
use Color::*;
match c {
Black => C::Black,
Red => C::Red,
Green => C::Green,
Yellow => C::Yellow,
Blue => C::Blue,
Magenta => C::Purple,
Cyan => C::Cyan,
White => C::White,
}
}
}
bitflags::bitflags! {
pub struct TextStyleFlags: u32 {
const ITALIC = 1 << 0;
const BOLD = 1 << 1;
const UNDERLINE = 1 << 2;
const STRIKETHROUGH = 1 << 3;
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct TextStyle {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub flags: TextStyleFlags,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
fg: None,
bg: None,
flags: TextStyleFlags::empty(),
}
}
}
impl TextStyle {
pub fn apply_tag(&mut self, kind: &TextTagKind) {
if let Some(color) = kind.to_color() {
self.fg = Some(color);
return;
}
use TextStyleFlags as F;
use TextTagKind::*;
let flags = match kind {
Italic => F::ITALIC,
Bold => F::BOLD,
Strike => F::STRIKETHROUGH,
Underline => F::UNDERLINE,
_ => unreachable!(),
};
self.flags |= flags;
}
pub fn inherit(&self, base: &TextStyle) -> Self {
Self {
fg: self.fg.or(base.fg),
bg: self.bg.or(base.bg),
flags: self.flags | base.flags,
}
}
pub fn make_painter(&self) -> ansi_term::Style {
ansi_term::Style {
foreground: self.fg.map(Into::into),
background: self.bg.map(Into::into),
is_bold: self.flags.contains(TextStyleFlags::BOLD),
is_italic: self.flags.contains(TextStyleFlags::ITALIC),
is_underline: self.flags.contains(TextStyleFlags::UNDERLINE),
is_strikethrough: self.flags.contains(TextStyleFlags::STRIKETHROUGH),
..Default::default()
}
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct StyledString {
pub string: String,
pub style: TextStyle,
}
impl StyledString {
pub fn from_plain(plain: &str) -> Self {
Self {
string: plain.to_string(),
style: TextStyle::default(),
}
}
pub fn render(&self, base: &TextStyle) -> String {
let painter = self.style.inherit(base).make_painter();
painter.paint(&self.string).to_string()
}
}
#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
pub struct TaggedText {
pub strings: Vec<StyledString>,
}
impl TaggedText {
pub fn from_plain(plain: &str) -> Self {
Self {
strings: vec![StyledString::from_plain(plain)],
}
}
pub fn concat(&mut self, mut other: Self) {
self.strings.append(&mut other.strings);
}
pub fn parse(style: &TextStyle, vals: &[ast::Value]) -> Result<Self, TextParseError> {
let mut text = Self::default();
for val in vals.iter() {
match &val.inner {
ast::ValueKind::String(string) => text.strings.push(StyledString {
string: string.clone(),
style: style.clone(),
}),
ast::ValueKind::List(items) => text.concat(Self::parse(style, items)?),
ast::ValueKind::Tag(tag) => text.concat(Self::parse_tag(style, tag)?),
_ => return Err(val.map(TextParseErrorKind::UnexpectedValue)),
}
}
Ok(text)
}
pub fn parse_tag(style: &TextStyle, tag: &ast::Tag) -> Result<Self, TextParseError> {
let kind = TextTagKind::from_str(&tag.id)
.map_err(|_| tag.id.map(TextParseErrorKind::InvalidTag))?;
Self::parse_tag_body(style, &kind, &tag.body)
}
pub fn parse_tag_body(
style: &TextStyle,
kind: &TextTagKind,
body: &ast::TagBody,
) -> Result<Self, TextParseError> {
let mut style = style.clone();
style.apply_tag(kind);
Self::parse(&style, &body.children)
}
pub fn render(&self, base: &TextStyle) -> String {
let mut rendered = String::new();
for string in self.strings.iter() {
let string = string.render(base);
rendered.push_str(&string);
}
rendered
}
pub fn make_text_block(&self, kind: BlockTagKind) -> TextBlock {
use BlockTagKind::*;
match kind {
P => TextBlock {
text: self.to_owned(),
initial_indent: None,
subsequent_indent: None,
},
_ => unimplemented!("Text block for {:?} tag", kind),
}
}
}
pub fn parse_plain(vals: &[ast::Value]) -> Result<String, TextParseError> {
let mut text = String::new();
for val in vals.iter() {
match &val.inner {
ast::ValueKind::List(vals) => text.push_str(&parse_plain(vals)?),
ast::ValueKind::String(string) => text.push_str(&string),
ast::ValueKind::Tag(_) => return Err(val.map(TextParseErrorKind::UnexpectedTag)),
_ => return Err(val.map(TextParseErrorKind::UnexpectedValue)),
}
}
Ok(text)
}
pub type TextParseError = WithSource<TextParseErrorKind>;
#[derive(Clone, Debug)]
pub enum TextParseErrorKind {
InvalidTag,
UnexpectedValue,
UnexpectedTag,
}
impl std::fmt::Display for TextParseErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Text parse error: ")?;
use TextParseErrorKind::*;
match self {
InvalidTag => write!(f, "invalid tag"),
UnexpectedValue => write!(f, "unexpected value"),
UnexpectedTag => write!(f, "unexpected tag"),
}
}
}
#[derive(Clone, Debug)]
pub struct TextBlock {
pub initial_indent: Option<StyledString>,
pub subsequent_indent: Option<StyledString>,
pub text: TaggedText,
}
impl TextBlock {
// FIXME this function discards tag information
pub fn layout(&self, base: &TextStyle, width: usize) -> TextLayout {
let text = self.text.render(&base);
let initial_indent = self
.initial_indent
.as_ref()
.map(|text| text.render(&base))
.unwrap_or("".to_string());
let subsequent_indent = self
.subsequent_indent
.as_ref()
.map(|text| text.render(&base))
.unwrap_or("".to_string());
let options = textwrap::Options::new(width)
.initial_indent(&initial_indent)
.subsequent_indent(&subsequent_indent);
TextLayout {
lines: textwrap::wrap(&text, options)
.into_iter()
.map(|line| TaggedText {
strings: vec![StyledString::from_plain(&line)],
})
.collect(),
width,
}
}
}
#[derive(Clone, Debug)]
pub struct TextLayout {
pub lines: Vec<TaggedText>,
pub width: usize,
}
impl TextLayout {
pub fn from_plain(plain: &str) -> Self {
let mut lines = Vec::new();
let mut width = 0;
for line in plain.lines() {
width = width.max(textwrap::core::display_width(&line));
lines.push(TaggedText::from_plain(line));
}
Self { lines, width }
}
pub fn render(&self, base: &TextStyle) -> RenderedBlock {
let mut lines = Vec::new();
for line in self.lines.iter() {
let mut line = line.render(base);
let used_width = textwrap::core::display_width(&line);
let needed_width = self.width - used_width.min(self.width);
line.extend(std::iter::repeat(' ').take(needed_width));
lines.push(line);
}
RenderedBlock {
size: Dimensions {
width: self.width,
height: lines.len(),
},
lines,
}
}
}