sidoc

Check-in Differences
Login

Check-in Differences

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Difference From sidoc-0.1.0 To sidoc-0.1.1

2024-11-07
09:13
change log. check-in: ef4f5138fd user: jan tags: trunk
08:53
Update crate metadata. Happy pedantic clippy. check-in: c84ce1ad85 user: jan tags: trunk, sidoc-0.1.1
2024-11-06
18:42
Happy pedantic clippy. check-in: c1d63406c5 user: jan tags: trunk
2022-09-29
18:48
Move repo. Clippy. check-in: 98b35525c4 user: jan tags: trunk
2020-09-12
13:56
Ignore gitignore. check-in: 4bf4749324 user: jan tags: trunk, sidoc-0.1.0
13:54
Move from temporary repository. check-in: b274896be6 user: jan tags: trunk

Changes to .efiles.

1



2




3
Cargo.toml



src/*.rs




tests/*.rs

>
>
>
|
>
>
>
>

1
2
3
4
5
6
7
8
9
10
Cargo.toml
README.md
www/index.md
www/changelog.md
src/err.rs
src/lib.rs
src/builder.rs
src/doc.rs
src/render.rs
tests/*.rs

Changes to .fossil-settings/ignore-glob.

1
2
3
4
.*.swp
.gitignore
Cargo.lock
target

<


1

2
3
.*.swp

Cargo.lock
target

Changes to Cargo.toml.

1
2
3
4
5
6


7
8
9







10





11







[package]
name = "sidoc"
version = "0.1.0"
authors = ["Jan Danielsson <jan.danielsson@qrnch.com>"]
edition = "2018"
license = "0BSD"


keywords = [ "text", "document", "html" ]
repository = "https://github.com/openqrnch/sidoc"
description = "Library for generating structured/scoped indented documents."













[dependencies]









|
<
|

>
>

|
|
>
>
>
>
>
>
>
|
>
>
>
>
>

>
>
>
>
>
>
>
1
2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[package]
name = "sidoc"
version = "0.1.1"

edition = "2021"
license = "0BSD"
# https://crates.io/category_slugs
categories = [ "template-engine" ]
keywords = [ "text", "document", "html" ]
repository = "https://repos.qrnch.tech/pub/sidoc"
description = "Generate structured/scoped indented documents."
exclude = [
  ".fossil-settings",
  ".efiles",
  ".fslckout",
  "www",
  "bacon.toml",
  "rustfmt.toml"
]

# https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section
[badges]
maintenance = { status = "passively-maintained" }

[dependencies]

[lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
cargo = { level = "warn", priority = -1 }

Added README.md.









>
>
>
>
1
2
3
4
# sidoc

Template engine designed to generate indented documents.

Added bacon.toml.





























































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# This is a configuration file for the bacon tool
#
# Complete help on configuration: https://dystroy.org/bacon/config/
# 
# You may check the current default at
#   https://github.com/Canop/bacon/blob/main/defaults/default-bacon.toml

default_job = "clippy-all"

[jobs.check]
command = ["cargo", "check", "--color", "always"]
need_stdout = false

[jobs.check-all]
command = ["cargo", "check", "--all-targets", "--color", "always"]
need_stdout = false

# Run clippy on the default target
[jobs.clippy]
command = [
    "cargo", "clippy",
    "--color", "always",
]
need_stdout = false

# Run clippy on all targets
# To disable some lints, you may change the job this way:
#    [jobs.clippy-all]
#    command = [
#        "cargo", "clippy",
#        "--all-targets",
#        "--color", "always",
#    	 "--",
#    	 "-A", "clippy::bool_to_int_with_if",
#    	 "-A", "clippy::collapsible_if",
#    	 "-A", "clippy::derive_partial_eq_without_eq",
#    ]
# need_stdout = false
[jobs.clippy-all]
command = [
    "cargo", "clippy",
    "--all-targets",
    "--color", "always",
]
need_stdout = false

# This job lets you run
# - all tests: bacon test
# - a specific test: bacon test -- config::test_default_files
# - the tests of a package: bacon test -- -- -p config
[jobs.test]
command = [
    "cargo", "test", "--color", "always",
    "--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true

[jobs.nextest]
command = [
    "cargo", "nextest", "run",
    "--color", "always",
    "--hide-progress-bar", "--failure-output", "final"
]
need_stdout = true
analyzer = "nextest"

[jobs.doc]
command = ["cargo", "doc", "--color", "always", "--no-deps"]
need_stdout = false

# If the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"]
need_stdout = false
on_success = "back" # so that we don't open the browser at each change

# You can run your application and have the result displayed in bacon,
# if it makes sense for this crate.
# Don't forget the `--color always` part or the errors won't be
# properly parsed.
[jobs.run]
command = [
    "cargo", "run",
    "--color", "always",
    # put launch parameters for your program behind a `--` separator
]
need_stdout = true
allow_warnings = true
background = true

# Run your long-running application (eg server) and have the result displayed in bacon.
# For programs that never stop (eg a server), `background` is set to false
# to have the cargo run output immediately displayed instead of waiting for
# program's end.
# 'on_change_strategy' is set to `kill_then_restart` to have your program restart
# on every change (an alternative would be to use the 'F5' key manually in bacon).
# If you often use this job, it makes sense to override the 'r' key by adding
# a binding `r = job:run-long` at the end of this file .
[jobs.run-long]
command = [
    "cargo", "run",
    "--color", "always",
    # put launch parameters for your program behind a `--` separator
]
need_stdout = true
allow_warnings = true
background = false
on_change_strategy = "kill_then_restart"

# This parameterized job runs the example of your choice, as soon
# as the code compiles.
# Call it as
#    bacon ex -- my-example
[jobs.ex]
command = ["cargo", "run", "--color", "always", "--example"]
need_stdout = true
allow_warnings = true

# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"
c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target

Changes to src/builder.rs.

1
2
3

4
5
6
7
8
9
10

11
12
13
14
15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39



40
41
42
43
44
45
46
47
48
49
50




51
52
53
54
55
56
57

58
59
60
61
62
63
64
65
66

67
68
69
70
71
72
73
74
75
76

77
78
79
80
81
82
83
84
85



86
87
88
89
90
91
92
93
94
95
96
97
98
use crate::{Doc, Error, Node};

/// Constructor for `Doc` objects.

pub struct Builder {
  nodes: Vec<Node>,
  scope_stack: Vec<Option<String>>
}

impl Builder {
  /// Create a new `Doc` builder context.

  pub fn new() -> Self {
    Builder {
      nodes: Vec::new(),
      scope_stack: Vec::new()
    }
  }

  /// Begin a scope, pushing an optional scope terminator to the internal scope
  /// stack.
  ///
  /// If the scope generated using a terminator line, that line will appended
  /// to the document when the scope is closed using the `exit()` method.

  pub fn scope<L: ToString, K: ToString>(
    &mut self,
    begin_line: L,
    term_line: Option<K>
  ) -> &mut Self {
    self.nodes.push(Node::BeginScope(begin_line.to_string()));
    if let Some(ln) = term_line {
      self.scope_stack.push(Some(ln.to_string()));
    } else {
      self.scope_stack.push(None);
    }
    self
  }

  /// Leave a previously entered scope.
  ///
  /// If the `scope()` call that created the current scope



  pub fn exit(&mut self) -> &mut Self {
    if let Some(s) = self.scope_stack.pop().unwrap() {
      self.nodes.push(Node::EndScope(Some(s)));
    } else {
      self.nodes.push(Node::EndScope(None));
    }
    self
  }

  /// Leave previously entered scope, adding a line passed by the caller rather
  /// than the scope stack.




  pub fn exit_line<L: ToString>(&mut self, line: L) -> &mut Self {
    let _ = self.scope_stack.pop().unwrap();
    self.nodes.push(Node::EndScope(Some(line.to_string())));
    self
  }

  /// Add a new line at current scope.

  pub fn line<L: ToString>(&mut self, line: L) -> &mut Self {
    self.nodes.push(Node::Line(line.to_string()));
    self
  }

  /// Add a named optional reference.
  ///
  /// References are placeholders for other documents.  An optional reference
  /// means that this reference does not need to be resolved by the renderer.

  pub fn optref<N: ToString>(&mut self, name: N) -> &mut Self {
    self.nodes.push(Node::OptRef(name.to_string()));
    self
  }

  /// Add a named required reference.
  ///
  /// References are placeholders for other documents.  A required reference
  /// must be resolved by the renderer or it will return an error to its
  /// caller.

  pub fn reqref<N: ToString>(&mut self, name: N) -> &mut Self {
    self.nodes.push(Node::ReqRef(name.to_string()));
    self
  }

  /// Generate a `Doc` object from this document.
  ///
  /// The document must be properly nested before calling this function,
  /// meaning all scopes it opened must be closed.



  pub fn build(self) -> Result<Doc, Error> {
    if self.scope_stack.is_empty() {
      Ok(Doc { nodes: self.nodes })
    } else {
      Err(Error::BadNesting(format!(
        "{} scope(s) remaining",
        self.scope_stack.len()
      )))
    }
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :



>







>

|
<
<
<







>

















>
>
>











>
>
>
>







>









>










>









>
>
>













1
2
3
4
5
6
7
8
9
10
11
12
13
14



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
use crate::{Doc, Error, Node};

/// Constructor for `Doc` objects.
#[derive(Default)]
pub struct Builder {
  nodes: Vec<Node>,
  scope_stack: Vec<Option<String>>
}

impl Builder {
  /// Create a new `Doc` builder context.
  #[must_use]
  pub fn new() -> Self {
    Self::default()



  }

  /// Begin a scope, pushing an optional scope terminator to the internal scope
  /// stack.
  ///
  /// If the scope generated using a terminator line, that line will appended
  /// to the document when the scope is closed using the `exit()` method.
  #[allow(clippy::needless_pass_by_value)]
  pub fn scope<L: ToString, K: ToString>(
    &mut self,
    begin_line: L,
    term_line: Option<K>
  ) -> &mut Self {
    self.nodes.push(Node::BeginScope(begin_line.to_string()));
    if let Some(ln) = term_line {
      self.scope_stack.push(Some(ln.to_string()));
    } else {
      self.scope_stack.push(None);
    }
    self
  }

  /// Leave a previously entered scope.
  ///
  /// If the `scope()` call that created the current scope
  ///
  /// # Panics
  /// The scope stack must not be empty.
  pub fn exit(&mut self) -> &mut Self {
    if let Some(s) = self.scope_stack.pop().unwrap() {
      self.nodes.push(Node::EndScope(Some(s)));
    } else {
      self.nodes.push(Node::EndScope(None));
    }
    self
  }

  /// Leave previously entered scope, adding a line passed by the caller rather
  /// than the scope stack.
  ///
  /// # Panics
  /// The scope stack must not be empty.
  #[allow(clippy::needless_pass_by_value)]
  pub fn exit_line<L: ToString>(&mut self, line: L) -> &mut Self {
    let _ = self.scope_stack.pop().unwrap();
    self.nodes.push(Node::EndScope(Some(line.to_string())));
    self
  }

  /// Add a new line at current scope.
  #[allow(clippy::needless_pass_by_value)]
  pub fn line<L: ToString>(&mut self, line: L) -> &mut Self {
    self.nodes.push(Node::Line(line.to_string()));
    self
  }

  /// Add a named optional reference.
  ///
  /// References are placeholders for other documents.  An optional reference
  /// means that this reference does not need to be resolved by the renderer.
  #[allow(clippy::needless_pass_by_value)]
  pub fn optref<N: ToString>(&mut self, name: N) -> &mut Self {
    self.nodes.push(Node::OptRef(name.to_string()));
    self
  }

  /// Add a named required reference.
  ///
  /// References are placeholders for other documents.  A required reference
  /// must be resolved by the renderer or it will return an error to its
  /// caller.
  #[allow(clippy::needless_pass_by_value)]
  pub fn reqref<N: ToString>(&mut self, name: N) -> &mut Self {
    self.nodes.push(Node::ReqRef(name.to_string()));
    self
  }

  /// Generate a `Doc` object from this document.
  ///
  /// The document must be properly nested before calling this function,
  /// meaning all scopes it opened must be closed.
  ///
  /// # Errors
  /// [`Error::BadNesting`] means one or more scopes are still open.
  pub fn build(self) -> Result<Doc, Error> {
    if self.scope_stack.is_empty() {
      Ok(Doc { nodes: self.nodes })
    } else {
      Err(Error::BadNesting(format!(
        "{} scope(s) remaining",
        self.scope_stack.len()
      )))
    }
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :

Changes to src/doc.rs.

1
2
3

4
5
6
7
8

9
10
11
12
13
14
use crate::Node;

/// A "Doc" represents a set of lines and references to other Doc's.

pub struct Doc {
  pub(crate) nodes: Vec<Node>
}

impl Doc {

  pub fn new() -> Self {
    Doc { nodes: Vec::new() }
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :



>





>

|




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use crate::Node;

/// A "Doc" represents a set of lines and references to other Doc's.
#[derive(Default)]
pub struct Doc {
  pub(crate) nodes: Vec<Node>
}

impl Doc {
  #[must_use]
  pub fn new() -> Self {
    Self::default()
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :

Changes to src/err.rs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::fmt;

#[derive(Debug)]
pub enum Error {
  BadRef(String),
  BadNesting(String)
}

impl std::error::Error for Error {}

impl fmt::Display for Error {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    match &*self {
      Error::BadRef(s) => write!(f, "Bad reference error; {}", s),
      Error::BadNesting(s) => write!(f, "Bad nesting error; {}", s)
    }
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :












|
|
|





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::fmt;

#[derive(Debug)]
pub enum Error {
  BadRef(String),
  BadNesting(String)
}

impl std::error::Error for Error {}

impl fmt::Display for Error {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    match self {
      Self::BadRef(s) => write!(f, "Bad reference error; {s}"),
      Self::BadNesting(s) => write!(f, "Bad nesting error; {s}")
    }
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :

Changes to src/render.rs.

1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22









23

24
25
26
27
28



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
use std::collections::HashMap;
use std::sync::Arc;

use crate::{Doc, Error, Node};


pub struct RenderContext {
  ichar: char,
  iwidth: usize,
  dict: HashMap<String, Arc<Doc>>
}


impl RenderContext {
  /// Create a new render context object.
  pub fn new() -> Self {
    RenderContext {
      ichar: ' ',
      iwidth: 2,
      dict: HashMap::new()
    }
  }










  /// Add a shared `Doc` document to the render context.

  pub fn doc<N: ToString>(&mut self, id: N, doc: Arc<Doc>) {
    self.dict.insert(id.to_string(), Arc::clone(&doc));
  }

  /// Render a root `Doc`, resolving all references.



  pub fn render(&self, name: &str) -> Result<String, Error> {
    struct IterNode<'a> {
      lst: &'a Vec<Node>,
      idx: usize
    }
    let mut iterstack = Vec::new();
    let mut out = String::new();
    let mut indent: usize = 0;

    // Generate single indent string
    let istr = self.ichar.to_string().repeat(self.iwidth);

    if let Some(dict) = self.dict.get(name) {
      iterstack.push(IterNode {
        lst: &dict.nodes,
        idx: 0
      });
    } else {
      return Err(Error::BadRef(format!("Missing root document '{}'", name)));
    }

    'outer: while !iterstack.is_empty() {
      let mut it = iterstack.pop().unwrap();

      while it.idx < it.lst.len() {
        match &it.lst[it.idx] {
          Node::BeginScope(s) => {
            let is = istr.repeat(indent);
            out.push_str(&is);
            out.push_str(&s);
            out.push('\n');
            indent += 1;
          }
          Node::EndScope(s) => {
            indent -= 1;
            if let Some(s) = s {
              let is = istr.repeat(indent);
              out.push_str(&is);
              out.push_str(&s);
              out.push('\n');
            }
          }
          Node::Line(s) => {
            let is = istr.repeat(indent);
            out.push_str(&is);
            out.push_str(&s);
            out.push('\n');
          }
          Node::OptRef(name) => {
            if let Some(dict) = self.dict.get(name) {
              iterstack.push(IterNode {
                lst: it.lst,
                idx: it.idx + 1
|
<



>






<
|
<
|
|





|
>
>
>
>
>
>
>
>
>

>





>
>
>


















|


<
|
<





|








|






|







1

2
3
4
5
6
7
8
9
10
11

12

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

61

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
use std::{collections::HashMap, sync::Arc};


use crate::{Doc, Error, Node};

#[allow(clippy::module_name_repetitions)]
pub struct RenderContext {
  ichar: char,
  iwidth: usize,
  dict: HashMap<String, Arc<Doc>>
}


impl Default for RenderContext {

  fn default() -> Self {
    Self {
      ichar: ' ',
      iwidth: 2,
      dict: HashMap::new()
    }
  }
}


impl RenderContext {
  /// Create a new render context object.
  #[must_use]
  pub fn new() -> Self {
    Self::default()
  }

  /// Add a shared `Doc` document to the render context.
  #[allow(clippy::needless_pass_by_value)]
  pub fn doc<N: ToString>(&mut self, id: N, doc: Arc<Doc>) {
    self.dict.insert(id.to_string(), Arc::clone(&doc));
  }

  /// Render a root `Doc`, resolving all references.
  ///
  /// # Errors
  /// [`Error::BadRef`] means a referenced document does not exist.
  pub fn render(&self, name: &str) -> Result<String, Error> {
    struct IterNode<'a> {
      lst: &'a Vec<Node>,
      idx: usize
    }
    let mut iterstack = Vec::new();
    let mut out = String::new();
    let mut indent: usize = 0;

    // Generate single indent string
    let istr = self.ichar.to_string().repeat(self.iwidth);

    if let Some(dict) = self.dict.get(name) {
      iterstack.push(IterNode {
        lst: &dict.nodes,
        idx: 0
      });
    } else {
      return Err(Error::BadRef(format!("Missing root document '{name}'")));
    }


    'outer: while let Some(mut it) = iterstack.pop() {

      while it.idx < it.lst.len() {
        match &it.lst[it.idx] {
          Node::BeginScope(s) => {
            let is = istr.repeat(indent);
            out.push_str(&is);
            out.push_str(s);
            out.push('\n');
            indent += 1;
          }
          Node::EndScope(s) => {
            indent -= 1;
            if let Some(s) = s {
              let is = istr.repeat(indent);
              out.push_str(&is);
              out.push_str(s);
              out.push('\n');
            }
          }
          Node::Line(s) => {
            let is = istr.repeat(indent);
            out.push_str(&is);
            out.push_str(s);
            out.push('\n');
          }
          Node::OptRef(name) => {
            if let Some(dict) = self.dict.get(name) {
              iterstack.push(IterNode {
                lst: it.lst,
                idx: it.idx + 1
96
97
98
99
100
101
102
103

104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
              });

              iterstack.push(IterNode {
                lst: &dict.nodes,
                idx: 0
              });
              continue 'outer;
            } else {

              return Err(Error::BadRef(format!(
                "Missing required document '{}'",
                name
              )));
            }
          }
        }
        it.idx += 1;
      }
    }

    Ok(out)
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :







<
>
|
|
<
|
<











105
106
107
108
109
110
111

112
113
114

115

116
117
118
119
120
121
122
123
124
125
126
              });

              iterstack.push(IterNode {
                lst: &dict.nodes,
                idx: 0
              });
              continue 'outer;

            }
            return Err(Error::BadRef(format!(
              "Missing required document '{name}'"

            )));

          }
        }
        it.idx += 1;
      }
    }

    Ok(out)
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :

Added www/changelog.md.













































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Change Log

⚠️  indicates a breaking change.

## [Unreleased]

[Details](/vdiff?from=sidoc-0.1.0&to=trunk)

### Added

- Derive `Default` on `Doc` and `RenderContext`

### Changed

### Removed

---

## [0.1.0] - 2020-09-12

Initial release

Added www/index.md.



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# sidoc

Template engine designed to generate indented documents.


## Change log

The details of changes can always be found in the timeline, but for a
high-level view of changes between released versions there's a manually
maintained [Change Log](./changelog.md).


## Project Status

sidoc is passively maintained.  It will receive updates and fixes and needed,
but is currently considered to be feature complete.