Compare commits

...

6 Commits

9 changed files with 511 additions and 232 deletions

43
Cargo.lock generated
View File

@ -87,35 +87,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "itoa"
version = "0.4.8"
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"
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",
]
[[package]]
name = "libc"
version = "0.2.134"
@ -157,12 +128,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"
@ -204,12 +169,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"
@ -267,7 +226,7 @@ dependencies = [
"ansi_term",
"bitflags",
"figlet-rs",
"lexpr",
"fnv",
"logos",
"strum",
"textwrap",

View File

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

View File

@ -16,6 +16,9 @@ pub enum TokenKind {
#[regex(r"[a-zA-Z-][a-zA-Z0-9-]*")]
Symbol,
#[regex(r"-?[0-9]*")]
Number,
#[regex(":[a-zA-Z-][a-zA-Z0-9-]*")]
Keyword,
@ -221,6 +224,13 @@ impl<'a> AstBuilder<'a> {
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()?;
@ -246,6 +256,7 @@ impl<'a> AstBuilder<'a> {
#[derive(Clone, Debug)]
pub enum ValueKind {
String(String),
Number(i32),
Symbol(String),
Tag(Tag),
List(Vec<Value>),
@ -412,6 +423,7 @@ pub enum ParseErrorKind {
UnmatchedParen,
UnexpectedToken(TokenKind, Option<TokenKind>),
UnexpectedEof,
BadNumber,
}
impl Display for ParseErrorKind {
@ -428,6 +440,7 @@ impl Display for ParseErrorKind {
}
UnexpectedToken(kind, None) => write!(f, "unexpected token {:?}", kind),
UnexpectedEof => write!(f, "unexpected end-of-file"),
BadNumber => write!(f, "badly-formed number"),
}
}
}

55
src/block.rs Normal file
View File

@ -0,0 +1,55 @@
use strum::EnumString;
#[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,
}
}
}
#[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,
}
}
}

View File

@ -1,6 +1,290 @@
use crate::tag::BlockTag;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
#[derive(Clone, Debug, Hash)]
use crate::ast::{self, TagBody, WithSource};
use crate::block::{AllowedChildren, BlockTagKind};
use crate::node::{NodeId, NodeStore};
use crate::text::{TaggedText, TextBlock, TextParseErrorKind, TextStyle, TextTagKind, TextParseError, TextLayout};
#[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 = BlockBuilder::new(BlockTagKind::Box, root);
let mut text_style = TextStyle::default();
let mut state = DomWalkState {
builder: &mut builder,
block: &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();
let kind: &NodeKind = self.nodes.get(node).unwrap();
let children: &NodeChildren = self.nodes.get(node).unwrap();
let text = self.nodes.get::<TextBlock>(node);
println!("{}{:?}: {:?} {:?}", margin, kind, children, text);
for child in children.0.iter() {
self.print_from(*child, indent + 1);
}
}
}
#[derive(Clone, Debug, Default)]
pub struct NodeChildren(Vec<NodeId>);
#[derive(Copy, Clone, Debug, Default)]
pub struct NodeParent(NodeId);
#[derive(Copy, Clone, Debug)]
pub enum NodeKind {
Block,
Text,
}
#[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 BlockBuilder {
pub id: NodeId,
pub children: Vec<NodeId>,
pub allowed_children: AllowedChildren,
pub kind: BlockTagKind,
}
impl BlockBuilder {
pub fn new(kind: BlockTagKind, id: NodeId) -> Self {
Self {
id,
children: Vec::new(),
allowed_children: kind.children(),
kind,
}
}
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);
use BlockTagKind::*;
let kind = match self.kind {
P | Figlet | Code | Li => NodeKind::Text,
Ul | Ol | Box | Section => NodeKind::Block,
};
dom.insert(self.id, kind);
self.id
}
}
#[derive(Debug)]
pub struct DomWalkState<'a> {
pub builder: &'a mut DomBuilder,
pub block: &'a mut BlockBuilder,
pub text_style: &'a mut TextStyle,
}
impl<'a> DomWalkState<'a> {
pub fn fork(&mut self) -> DomWalkState {
DomWalkState {
builder: &mut *self.builder,
block: &mut *self.block,
text_style: &mut *self.text_style,
}
}
pub fn make_text_block(&mut self, text: TaggedText) -> TextBlock {
use BlockTagKind::*;
match self.block.kind {
P => TextBlock {
text,
initial_indent: None,
subsequent_indent: None,
},
_ => unimplemented!("Text block for {:?} tag", self.block.kind),
}
}
pub fn add_text_block(&mut self, text: TaggedText) -> NodeId {
let node = self.builder.new_node();
let block = self.make_text_block(text);
self.builder.insert(node, block);
node
}
pub fn make_text_layout(&self, text: String) -> TextLayout {
use BlockTagKind::*;
match self.block.kind {
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.block.kind),
}
}
pub fn add_block_tag(&mut self, kind: BlockTagKind, body: &ast::TagBody) -> DomResult<NodeId> {
let node = self.builder.new_node();
self.block.children.push(node);
let mut block = BlockBuilder::new(kind, node);
let mut state = DomWalkState {
block: &mut block,
..self.fork()
};
match kind.children() {
AllowedChildren::TaggedText => {
let text = TaggedText::parse(&state.text_style, &body.children)
.map_err(|e| e.map(e.inner.clone().into()))?;
let block = state.make_text_block(text);
self.builder.insert(node, block);
}
AllowedChildren::PlainText => {
let text = crate::text::parse_plain(&body.children)
.map_err(|e| e.map(e.inner.clone().into()))?;
let layout = state.make_text_layout(text);
self.builder.insert(node, layout);
}
_ => {
state.add_children(&body.children)?;
}
}
block.build(&mut self.builder);
Ok(node)
}
pub fn add_tag(mut self, tag: &ast::Tag) -> DomResult<()> {
if let Ok(kind) = TextTagKind::from_str(&tag.id) {
if !self.block.allowed_children.allows_styled_text() {
return Err(tag.id.map(DomErrorKind::InvalidChild));
}
let style = &self.text_style;
let styled = TaggedText::parse_tag_body(style, &kind, &tag.body)
.map_err(|e| e.map(e.inner.clone().into()))?;
self.add_text_block(styled);
} else if let Ok(kind) = BlockTagKind::from_str(&tag.id) {
if !self.block.allowed_children.allows_block(&kind) {
return Err(tag.id.map(DomErrorKind::InvalidChild));
}
self.add_block_tag(kind, &tag.body)?;
} else {
return Err(tag.id.map(DomErrorKind::InvalidTag));
}
Ok(())
}
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,
TextError(TextParseErrorKind),
}
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);
}
write!(f, "DOM error: ")?;
match self {
InvalidTag => write!(f, "invalid tag"),
InvalidChild => write!(f, "invalid child"),
_ => unimplemented!(),
}
}
}

View File

@ -1,22 +1,13 @@
pub mod ast;
pub mod block;
pub mod dom;
pub mod layout;
pub mod parse;
pub mod style;
pub mod tag;
pub mod node;
pub mod text;
use logos::Logos;
use style::Stylesheet;
fn main() {
let src_path = std::env::args().nth(1).unwrap();
let src = std::fs::read_to_string(src_path).unwrap();
let lexer = ast::TokenKind::lexer(&src);
let tokens: Vec<_> = lexer.collect();
println!("{:?}", tokens);
let source = std::sync::Arc::new(ast::Source::from_string(src.clone()));
let mut ast_builder = ast::AstBuilder::new(&source);
let body = match ast_builder.parse_tag_body() {
@ -26,30 +17,11 @@ fn main() {
println!("{:#?}", body);
let style = text::TextStyle::default();
let text = match text::StyledText::parse(&style, &body.children) {
Ok(text) => text,
Err(e) => panic!("Text parse error:\n{}", e),
let dom = match dom::Dom::parse(&body) {
Ok(dom) => dom,
Err(e) => panic!("DOM parse error:\n{}", e),
};
println!("{:#?}", text);
let options = lexpr::parse::Options::new()
.with_string_syntax(lexpr::parse::StringSyntax::Elisp)
.with_keyword_syntax(lexpr::parse::KeywordSyntax::ColonPrefix);
let expr = lexpr::from_str_custom(&src, options).unwrap();
println!("{:#?}", expr);
let dom = parse::Parser::parse(&expr);
let style = Stylesheet::default();
let width = 40;
for tag in dom.tags.iter() {
for line in tag.layout(&style, width) {
println!("{}", line);
}
println!("");
}
dom.print();
}

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,143 +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 mut parser = Self::from_value(document);
let (args, children) = parser.parse_args();
let mut tags = Vec::new();
for val in children.into_iter() {
let mut parser = Self::from_value(&val);
let kind = parser.parse_block_kind();
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 mut args = Vec::new();
let mut children = Vec::new();
while let Some(next) = self.vals.pop() {
if let Value::Keyword(arg_name) = next {
let arg_val = self.vals.pop().expect("Expected value after keyword");
args.push((arg_name.to_string(), arg_val));
} else {
children.push(next);
break;
}
}
children.extend(self.vals.drain(..).rev());
(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_text_kind();
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_block_tag(&mut self, kind: BlockTagKind) -> BlockTag {
let (_args, children_vals) = self.parse_args();
let allowed_children = kind.allowed_children();
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 symbol = parser.parse_symbol();
if let Ok(child_kind) = BlockTagKind::from_str(&symbol) {
if allowed_children.allows_block(&child_kind) {
BlockNode::Block(parser.parse_block_tag(child_kind))
} else {
panic!("{:?} tag cannot contain {:?} tag", kind, child_kind);
}
} else if let Ok(child_kind) = TextTagKind::from_str(&symbol) {
if allowed_children.allows_styled_text() {
BlockNode::Text(parser.parse_text_tag(child_kind))
} else {
panic!("{:?} tag cannot contain styled text", kind);
}
} else {
panic!("Unrecognized tag {}", symbol);
}
}
Value::String(string) => {
if allowed_children.allows_plain_text() {
BlockNode::Text(TextTag {
kind: TextTagKind::Plain,
children: vec![TextNode::Text(string.to_string())],
})
} else {
panic!("{:?} tag cannot contain plain text", kind);
}
}
_ => panic!("Block tag child must be either a string or a cons"),
};
children.push(node);
}
BlockTag { kind, children }
}
pub fn parse_block_kind(&mut self) -> BlockTagKind {
let symbol = self.parse_symbol();
BlockTagKind::from_str(&symbol).expect("Unrecognized block tag")
}
pub fn parse_text_kind(&mut self) -> TextTagKind {
let symbol = self.parse_symbol();
TextTagKind::from_str(&symbol).expect("Unrecognized text tag")
}
pub fn parse_symbol(&mut self) -> String {
self.vals
.pop()
.as_ref()
.map(|v| v.as_symbol())
.flatten()
.expect("Expected symbol")
.to_string()
}
}

View File

@ -82,7 +82,7 @@ impl Default for TextStyle {
}
impl TextStyle {
pub fn apply_tag(&mut self, kind: TextTagKind) {
pub fn apply_tag(&mut self, kind: &TextTagKind) {
if let Some(color) = kind.to_color() {
self.fg = color;
return;
@ -108,12 +108,27 @@ pub struct StyledString {
pub style: TextStyle,
}
impl StyledString {
pub fn from_plain(plain: &str) -> Self {
Self {
string: plain.to_string(),
style: TextStyle::default(),
}
}
}
#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
pub struct StyledText {
pub struct TaggedText {
pub strings: Vec<StyledString>,
}
impl StyledText {
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);
}
@ -128,7 +143,7 @@ impl StyledText {
}),
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::UnexpectedSymbol)),
_ => return Err(val.map(TextParseErrorKind::UnexpectedValue)),
}
}
@ -138,18 +153,41 @@ impl StyledText {
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, &tag.body.children)
Self::parse(&style, &body.children)
}
}
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(Debug)]
#[derive(Clone, Debug)]
pub enum TextParseErrorKind {
InvalidTag,
UnexpectedSymbol,
UnexpectedValue,
UnexpectedTag,
}
impl std::fmt::Display for TextParseErrorKind {
@ -159,7 +197,28 @@ impl std::fmt::Display for TextParseErrorKind {
use TextParseErrorKind::*;
match self {
InvalidTag => write!(f, "invalid tag"),
UnexpectedSymbol => write!(f, "unexpected symbol"),
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,
}
#[derive(Clone, Debug)]
pub struct TextLayout {
pub lines: Vec<TaggedText>,
}
impl TextLayout {
pub fn from_plain(plain: &str) -> Self {
Self {
lines: plain.lines().map(|s| TaggedText::from_plain(s)).collect(),
}
}
}