Merge remote-tracking branch 'origin/parse-toml' into ebooks
This commit is contained in:
commit
2cae064040
13
Cargo.toml
13
Cargo.toml
|
@ -15,13 +15,14 @@ exclude = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
clap = "2.2.1"
|
||||
handlebars = { version = "0.20.0", features = ["serde_type"] }
|
||||
serde = "0.8.17"
|
||||
serde_json = "0.8.3"
|
||||
clap = "2.19.2"
|
||||
handlebars = { version = "0.23.0", features = ["serde_type"] }
|
||||
serde = "0.8"
|
||||
serde_json = "0.8"
|
||||
pulldown-cmark = "0.0.8"
|
||||
log = "0.3"
|
||||
env_logger = "0.3.4"
|
||||
env_logger = "0.3"
|
||||
toml = { version = "0.2", features = ["serde"] }
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "2.5.5", optional = true }
|
||||
|
@ -33,12 +34,10 @@ iron = { version = "0.4", optional = true }
|
|||
staticfile = { version = "0.3", optional = true }
|
||||
ws = { version = "0.5.1", optional = true}
|
||||
|
||||
|
||||
# Tests
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3.4"
|
||||
|
||||
|
||||
[features]
|
||||
default = ["output", "watch", "serve"]
|
||||
debug = []
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"title": "mdBook Documentation",
|
||||
"description": "Create book from markdown files. Like Gitbook but implemented in Rust",
|
||||
"author": "Mathieu David"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
author = "Mathieu David"
|
|
@ -10,7 +10,7 @@ mdBook supports a `test` command that will run all available tests in mdBook. At
|
|||
- checking for unused files
|
||||
- ...
|
||||
|
||||
In the future I would like the user to be able to enable / disable test from the `book.json` configuration file and support custom tests.
|
||||
In the future I would like the user to be able to enable / disable test from the `book.toml` configuration file and support custom tests.
|
||||
|
||||
**How to use it:**
|
||||
```bash
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
# Configuration
|
||||
|
||||
You can configure the parameters for your book in the ***book.json*** file.
|
||||
You can configure the parameters for your book in the ***book.toml*** file.
|
||||
|
||||
Here is an example of what a ***book.json*** file might look like:
|
||||
We encourage using the TOML format, but JSON is also recognized and parsed.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Example book",
|
||||
"author": "Name",
|
||||
"description": "The example book covers examples.",
|
||||
"dest": "output/my-book"
|
||||
}
|
||||
Here is an example of what a ***book.toml*** file might look like:
|
||||
|
||||
```toml
|
||||
title = "Example book"
|
||||
author = "Name"
|
||||
description = "The example book covers examples."
|
||||
dest = "output/my-book"
|
||||
```
|
||||
|
||||
#### Supported variables
|
||||
|
|
|
@ -4,5 +4,5 @@ In this section you will learn how to:
|
|||
|
||||
- Structure your book correctly
|
||||
- Format your `SUMMARY.md` file
|
||||
- Configure your book using `book.json`
|
||||
- Configure your book using `book.toml`
|
||||
- Customize your theme
|
||||
|
|
|
@ -19,7 +19,7 @@ Here is a list of the properties that are exposed:
|
|||
|
||||
- ***language*** Language of the book in the form `en`. To use in <code class="language-html">\<html lang="{{ language }}"></code> for example.
|
||||
At the moment it is hardcoded.
|
||||
- ***title*** Title of the book, as specified in `book.json`
|
||||
- ***title*** Title of the book, as specified in `book.toml`
|
||||
|
||||
- ***path*** Relative path to the original markdown file from the source directory
|
||||
- ***content*** This is the rendered markdown.
|
||||
|
|
|
@ -47,7 +47,7 @@ Will render as
|
|||
# }
|
||||
```
|
||||
|
||||
**At the moment, this only works for code examples that are annotated with `rust`. Because it would collide with semantics of some programming languages. In the future, we want to make this configurable through the `book.json` so that everyone can benefit from it.**
|
||||
**At the moment, this only works for code examples that are annotated with `rust`. Because it would collide with semantics of some programming languages. In the future, we want to make this configurable through the `book.toml` so that everyone can benefit from it.**
|
||||
|
||||
|
||||
## Improve default theme
|
||||
|
|
|
@ -13,7 +13,7 @@ fn main() {
|
|||
let mut book = MDBook::new(Path::new("my-book")) // Path to root
|
||||
.set_src(Path::new("src")) // Path from root to source directory
|
||||
.set_dest(Path::new("book")) // Path from root to output directory
|
||||
.read_config(); // Parse book.json file for configuration
|
||||
.read_config(); // Parse book.toml or book.json file for configuration
|
||||
|
||||
book.build().unwrap(); // Render the book
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
use serde_json;
|
||||
extern crate toml;
|
||||
|
||||
use std::process::exit;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use serde_json;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BookConfig {
|
||||
|
@ -18,7 +23,6 @@ pub struct BookConfig {
|
|||
multilingual: bool,
|
||||
}
|
||||
|
||||
|
||||
impl BookConfig {
|
||||
pub fn new(root: &Path) -> Self {
|
||||
BookConfig {
|
||||
|
@ -40,40 +44,88 @@ impl BookConfig {
|
|||
|
||||
debug!("[fn]: read_config");
|
||||
|
||||
// If the file does not exist, return early
|
||||
let mut config_file = match File::open(root.join("book.json")) {
|
||||
Ok(f) => f,
|
||||
let read_file = |path: PathBuf| -> String {
|
||||
let mut data = String::new();
|
||||
let mut f: File = match File::open(&path) {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
debug!("[*]: Failed to open {:?}", root.join("book.json"));
|
||||
return self;
|
||||
},
|
||||
error!("[*]: Failed to open {:?}", &path);
|
||||
exit(2);
|
||||
}
|
||||
};
|
||||
if let Err(_) = f.read_to_string(&mut data) {
|
||||
error!("[*]: Failed to read {:?}", &path);
|
||||
exit(2);
|
||||
}
|
||||
data
|
||||
};
|
||||
|
||||
debug!("[*]: Reading config");
|
||||
let mut data = String::new();
|
||||
// Read book.toml or book.json if exists
|
||||
|
||||
// Just return if an error occured.
|
||||
// I would like to propagate the error, but I have to return `&self`
|
||||
if let Err(_) = config_file.read_to_string(&mut data) {
|
||||
return self;
|
||||
if Path::new(root.join("book.toml").as_os_str()).exists() {
|
||||
|
||||
debug!("[*]: Reading config");
|
||||
let data = read_file(root.join("book.toml"));
|
||||
self.parse_from_toml_string(&data);
|
||||
|
||||
} else if Path::new(root.join("book.json").as_os_str()).exists() {
|
||||
|
||||
debug!("[*]: Reading config");
|
||||
let data = read_file(root.join("book.json"));
|
||||
self.parse_from_json_string(&data);
|
||||
|
||||
} else {
|
||||
debug!("[*]: No book.toml or book.json was found, using defaults.");
|
||||
}
|
||||
|
||||
// Convert to JSON
|
||||
if let Ok(config) = serde_json::from_str::<serde_json::Value>(&data) {
|
||||
// Extract data
|
||||
self
|
||||
}
|
||||
|
||||
let config = config.as_object().unwrap();
|
||||
pub fn parse_from_toml_string(&mut self, data: &String) -> &mut Self {
|
||||
|
||||
let mut parser = toml::Parser::new(&data);
|
||||
|
||||
let config = match parser.parse() {
|
||||
Some(x) => {x},
|
||||
None => {
|
||||
error!("[*]: Toml parse errors in book.toml: {:?}", parser.errors);
|
||||
exit(2);
|
||||
}
|
||||
};
|
||||
|
||||
self.parse_from_btreemap(&config);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Parses the string to JSON and converts it to BTreeMap<String, toml::Value>.
|
||||
pub fn parse_from_json_string(&mut self, data: &String) -> &mut Self {
|
||||
|
||||
let c: serde_json::Value = match serde_json::from_str(&data) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
error!("[*]: JSON parse errors in book.json: {:?}", e);
|
||||
exit(2);
|
||||
}
|
||||
};
|
||||
|
||||
let config = json_object_to_btreemap(&c.as_object().unwrap());
|
||||
self.parse_from_btreemap(&config);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn parse_from_btreemap(&mut self, config: &BTreeMap<String, toml::Value>) -> &mut Self {
|
||||
|
||||
debug!("[*]: Extracting data from config");
|
||||
// Title, author, description
|
||||
if let Some(a) = config.get("title") {
|
||||
self.title = a.to_string().replace("\"", "")
|
||||
self.title = a.to_string().replace("\"", "");
|
||||
}
|
||||
if let Some(a) = config.get("author") {
|
||||
self.author = a.to_string().replace("\"", "")
|
||||
self.author = a.to_string().replace("\"", "");
|
||||
}
|
||||
if let Some(a) = config.get("description") {
|
||||
self.description = a.to_string().replace("\"", "")
|
||||
self.description = a.to_string().replace("\"", "");
|
||||
}
|
||||
|
||||
// Destination folder
|
||||
|
@ -105,8 +157,6 @@ impl BookConfig {
|
|||
self.set_theme_path(&theme_path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -146,3 +196,33 @@ impl BookConfig {
|
|||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json_object_to_btreemap(json: &serde_json::Map<String, serde_json::Value>) -> BTreeMap<String, toml::Value> {
|
||||
let mut config: BTreeMap<String, toml::Value> = BTreeMap::new();
|
||||
|
||||
for (key, value) in json.iter() {
|
||||
config.insert(
|
||||
String::from_str(key).unwrap(),
|
||||
json_value_to_toml_value(value.to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
pub fn json_value_to_toml_value(json: serde_json::Value) -> toml::Value {
|
||||
match json {
|
||||
serde_json::Value::Null => toml::Value::String("".to_string()),
|
||||
serde_json::Value::Bool(x) => toml::Value::Boolean(x),
|
||||
serde_json::Value::I64(x) => toml::Value::Integer(x),
|
||||
serde_json::Value::U64(x) => toml::Value::Integer(x as i64),
|
||||
serde_json::Value::F64(x) => toml::Value::Float(x),
|
||||
serde_json::Value::String(x) => toml::Value::String(x),
|
||||
serde_json::Value::Array(x) => {
|
||||
toml::Value::Array(x.iter().map(|v| json_value_to_toml_value(v.to_owned())).collect())
|
||||
},
|
||||
serde_json::Value::Object(x) => {
|
||||
toml::Value::Table(json_object_to_btreemap(&x))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,349 @@
|
|||
#[cfg(test)]
|
||||
|
||||
use std::path::Path;
|
||||
use serde_json;
|
||||
use book::bookconfig::*;
|
||||
|
||||
#[test]
|
||||
fn it_parses_json_config() {
|
||||
let text = r#"
|
||||
{
|
||||
"title": "mdBook Documentation",
|
||||
"description": "Create book from markdown files. Like Gitbook but implemented in Rust",
|
||||
"author": "Mathieu David"
|
||||
}"#;
|
||||
|
||||
// TODO don't require path argument, take pwd
|
||||
let mut config = BookConfig::new(Path::new("."));
|
||||
|
||||
config.parse_from_json_string(&text.to_string());
|
||||
|
||||
let mut expected = BookConfig::new(Path::new("."));
|
||||
expected.title = "mdBook Documentation".to_string();
|
||||
expected.author = "Mathieu David".to_string();
|
||||
expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string();
|
||||
|
||||
assert_eq!(format!("{:#?}", config), format!("{:#?}", expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_parses_toml_config() {
|
||||
let text = r#"
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
author = "Mathieu David"
|
||||
"#;
|
||||
|
||||
// TODO don't require path argument, take pwd
|
||||
let mut config = BookConfig::new(Path::new("."));
|
||||
|
||||
config.parse_from_toml_string(&text.to_string());
|
||||
|
||||
let mut expected = BookConfig::new(Path::new("."));
|
||||
expected.title = "mdBook Documentation".to_string();
|
||||
expected.author = "Mathieu David".to_string();
|
||||
expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string();
|
||||
|
||||
assert_eq!(format!("{:#?}", config), format!("{:#?}", expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_parses_json_nested_array_to_toml() {
|
||||
|
||||
// Example from:
|
||||
// toml-0.2.1/tests/valid/arrays-nested.json
|
||||
|
||||
let text = r#"
|
||||
{
|
||||
"nest": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "array", "value": [
|
||||
{"type": "string", "value": "a"}
|
||||
]},
|
||||
{"type": "array", "value": [
|
||||
{"type": "string", "value": "b"}
|
||||
]}
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let c: serde_json::Value = serde_json::from_str(&text).unwrap();
|
||||
|
||||
let result = json_object_to_btreemap(&c.as_object().unwrap());
|
||||
|
||||
let expected = r#"{
|
||||
"nest": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"a"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"b"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
}"#;
|
||||
|
||||
assert_eq!(format!("{:#?}", result), expected);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn it_parses_json_arrays_to_toml() {
|
||||
|
||||
// Example from:
|
||||
// toml-0.2.1/tests/valid/arrays.json
|
||||
|
||||
let text = r#"
|
||||
{
|
||||
"ints": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "integer", "value": "1"},
|
||||
{"type": "integer", "value": "2"},
|
||||
{"type": "integer", "value": "3"}
|
||||
]
|
||||
},
|
||||
"floats": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "float", "value": "1.1"},
|
||||
{"type": "float", "value": "2.1"},
|
||||
{"type": "float", "value": "3.1"}
|
||||
]
|
||||
},
|
||||
"strings": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "string", "value": "a"},
|
||||
{"type": "string", "value": "b"},
|
||||
{"type": "string", "value": "c"}
|
||||
]
|
||||
},
|
||||
"dates": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "datetime", "value": "1987-07-05T17:45:00Z"},
|
||||
{"type": "datetime", "value": "1979-05-27T07:32:00Z"},
|
||||
{"type": "datetime", "value": "2006-06-01T11:00:00Z"}
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let c: serde_json::Value = serde_json::from_str(&text).unwrap();
|
||||
|
||||
let result = json_object_to_btreemap(&c.as_object().unwrap());
|
||||
|
||||
let expected = r#"{
|
||||
"dates": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"datetime"
|
||||
),
|
||||
"value": String(
|
||||
"1987-07-05T17:45:00Z"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"datetime"
|
||||
),
|
||||
"value": String(
|
||||
"1979-05-27T07:32:00Z"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"datetime"
|
||||
),
|
||||
"value": String(
|
||||
"2006-06-01T11:00:00Z"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
"floats": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"float"
|
||||
),
|
||||
"value": String(
|
||||
"1.1"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"float"
|
||||
),
|
||||
"value": String(
|
||||
"2.1"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"float"
|
||||
),
|
||||
"value": String(
|
||||
"3.1"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
"ints": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"integer"
|
||||
),
|
||||
"value": String(
|
||||
"1"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"integer"
|
||||
),
|
||||
"value": String(
|
||||
"2"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"integer"
|
||||
),
|
||||
"value": String(
|
||||
"3"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
"strings": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"a"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"b"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"c"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
}"#;
|
||||
|
||||
assert_eq!(format!("{:#?}", result), expected);
|
||||
}
|
|
@ -8,6 +8,8 @@ pub use self::metadata::{Author, Language, BookMetadata};
|
|||
pub use self::chapter::Chapter;
|
||||
pub use self::book::Book;
|
||||
|
||||
pub mod bookconfig_test;
|
||||
|
||||
pub use self::bookitem::{BookItem, BookItems};
|
||||
pub use self::bookconfig::BookConfig;
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{VecDeque, BTreeMap};
|
||||
|
||||
use serde_json;
|
||||
use serde_json::value::ToJson;
|
||||
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Context, Renderable};
|
||||
|
||||
|
||||
// Handlebars helper for navigation
|
||||
|
||||
pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
|
@ -14,9 +15,9 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
|
|||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = c.navigate(rc.get_path(), "chapters");
|
||||
let chapters = c.navigate(rc.get_path(), &VecDeque::new(), "chapters");
|
||||
|
||||
let current = c.navigate(rc.get_path(), "path")
|
||||
let current = c.navigate(rc.get_path(), &VecDeque::new(), "path")
|
||||
.to_string()
|
||||
.replace("\"", "");
|
||||
|
||||
|
@ -114,9 +115,9 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
|
|||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = c.navigate(rc.get_path(), "chapters");
|
||||
let chapters = c.navigate(rc.get_path(), &VecDeque::new(), "chapters");
|
||||
|
||||
let current = c.navigate(rc.get_path(), "path")
|
||||
let current = c.navigate(rc.get_path(), &VecDeque::new(), "path")
|
||||
.to_string()
|
||||
.replace("\"", "");
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{VecDeque, BTreeMap};
|
||||
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
|
||||
|
@ -15,8 +15,8 @@ impl HelperDef for RenderToc {
|
|||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = c.navigate(rc.get_path(), "chapters");
|
||||
let current = c.navigate(rc.get_path(), "path").to_string().replace("\"", "");
|
||||
let chapters = c.navigate(rc.get_path(), &VecDeque::new(), "chapters");
|
||||
let current = c.navigate(rc.get_path(), &VecDeque::new(), "path").to_string().replace("\"", "");
|
||||
try!(rc.writer.write("<ul class=\"chapter\">".as_bytes()));
|
||||
|
||||
// Decode json format
|
||||
|
|
Loading…
Reference in New Issue