1
0
Fork 0
mirror of https://github.com/lise-henry/crowbook synced 2024-05-10 12:46:17 +02:00

Compare commits

...

8 Commits

Author SHA1 Message Date
Elisabeth Henry c1f54fdbd4 Remove old parser file style there for no reason 2023-08-19 04:11:35 +02:00
Elisabeth Henry 0f8a4f3026 More i18n fixing 2023-08-19 04:10:53 +02:00
Elisabeth Henry b2f17cb4a2 Remove unused proofread error 2023-08-19 04:00:28 +02:00
Elisabeth Henry 1a84b58e9c Still more i18n work 2023-08-19 03:58:13 +02:00
Elisabeth Henry 1bdcdaa309 Continue i18n change 2023-08-19 03:45:47 +02:00
Elisabeth Henry e2fa7aff3c Continue i18n change 2023-08-19 02:41:12 +02:00
Elisabeth Henry 682cc039a3 Start replacing crowbook-int in lib 2023-08-19 02:37:24 +02:00
Elisabeth Henry 0ff24f94f4 Use rust-i18n instead of crowbook-intl 2023-08-19 02:19:41 +02:00
15 changed files with 666 additions and 986 deletions

336
Cargo.lock generated
View File

@ -41,6 +41,15 @@ dependencies = [
"libc",
]
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "anstream"
version = "0.3.2"
@ -90,6 +99,23 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi 0.1.19",
"libc",
"winapi 0.3.9",
]
[[package]]
name = "autocfg"
version = "0.1.8"
@ -147,6 +173,16 @@ version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
[[package]]
name = "bstr"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.13.0"
@ -188,6 +224,21 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "clap"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"ansi_term",
"atty",
"bitflags 1.3.2",
"strsim 0.8.0",
"textwrap 0.11.0",
"unicode-width",
"vec_map",
]
[[package]]
name = "clap"
version = "4.3.21"
@ -208,7 +259,7 @@ dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
"strsim 0.10.0",
"terminal_size",
]
@ -251,7 +302,7 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "482aa5695bca086022be453c700a40c02893f1ba7098a2c88351de55341ae894"
dependencies = [
"clap",
"clap 4.3.21",
"entities",
"memchr",
"once_cell",
@ -340,7 +391,7 @@ name = "crowbook"
version = "0.17.0"
dependencies = [
"base64",
"clap",
"clap 4.3.21",
"comrak",
"console",
"crowbook-intl",
@ -356,10 +407,11 @@ dependencies = [
"numerals",
"punkt",
"rayon",
"rust-i18n",
"simplelog",
"syntect",
"tempfile",
"textwrap",
"textwrap 0.16.0",
"upon",
"uuid",
"walkdir 2.3.3",
@ -443,6 +495,12 @@ dependencies = [
"zip",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.2"
@ -529,18 +587,63 @@ dependencies = [
"wasi",
]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d"
dependencies = [
"aho-corasick 1.0.3",
"bstr",
"fnv",
"log",
"regex 1.9.3",
]
[[package]]
name = "globwalk"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
"bitflags 1.3.2",
"ignore",
"walkdir 2.3.3",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.3.2"
@ -602,6 +705,23 @@ dependencies = [
"cc",
]
[[package]]
name = "ignore"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
dependencies = [
"globset",
"lazy_static 1.4.0",
"log",
"memchr",
"regex 1.9.3",
"same-file 1.0.6",
"thread_local 1.1.7",
"walkdir 2.3.3",
"winapi-util",
]
[[package]]
name = "indenter"
version = "0.3.3"
@ -615,7 +735,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg 1.1.0",
"hashbrown",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
]
[[package]]
@ -646,7 +776,7 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi",
"hermit-abi 0.3.2",
"libc",
"windows-sys 0.48.0",
]
@ -657,11 +787,20 @@ version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi",
"hermit-abi 0.3.2",
"rustix 0.38.8",
"windows-sys 0.48.0",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.9"
@ -862,7 +1001,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"hermit-abi 0.3.2",
"libc",
]
@ -970,7 +1109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06"
dependencies = [
"base64",
"indexmap",
"indexmap 1.9.3",
"line-wrap",
"quick-xml",
"serde",
@ -1214,7 +1353,7 @@ dependencies = [
"aho-corasick 0.6.10",
"memchr",
"regex-syntax 0.5.6",
"thread_local",
"thread_local 0.3.6",
"utf8-ranges",
]
@ -1262,6 +1401,77 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca136c6f6d53a2de7264bb392ea7c1f83357e00d131a24275b1661ea1c23c3af"
[[package]]
name = "rust-i18n"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "074e2507aedea43bdeb742cb55fc339a0704625050c9532a226c7ddbd1a05f62"
dependencies = [
"anyhow",
"clap 2.34.0",
"globwalk",
"itertools",
"once_cell",
"quote 1.0.32",
"regex 1.9.3",
"rust-i18n-extract",
"rust-i18n-macro",
"rust-i18n-support",
"serde",
"serde_derive",
"toml",
]
[[package]]
name = "rust-i18n-extract"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89ac25fb50c8d0893ee6436056fb4a0cc6f6e1df99239d7c104421d007d445e"
dependencies = [
"anyhow",
"ignore",
"proc-macro2 1.0.66",
"quote 1.0.32",
"regex 1.9.3",
"rust-i18n-support",
"serde",
"serde_json",
"serde_yaml",
"syn 1.0.109",
]
[[package]]
name = "rust-i18n-macro"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e09ef5c1e310112eea3c19c4e18e3e62968b002eb535ff5b242ca1200742f996"
dependencies = [
"glob",
"once_cell",
"proc-macro2 1.0.66",
"quote 1.0.32",
"rust-i18n-support",
"serde",
"serde_json",
"serde_yaml",
"syn 1.0.109",
]
[[package]]
name = "rust-i18n-support"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14eb094cd0072c5f09f333eea36fcd8c64961f9eb61dbd09e82eff51c58e8414"
dependencies = [
"globwalk",
"once_cell",
"proc-macro2 1.0.66",
"serde",
"serde_json",
"serde_yaml",
"toml",
]
[[package]]
name = "rustc-serialize"
version = "0.3.24"
@ -1363,6 +1573,27 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
dependencies = [
"indexmap 1.9.3",
"ryu",
"serde",
"yaml-rust",
]
[[package]]
name = "shell-words"
version = "1.1.0"
@ -1401,6 +1632,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "strsim"
version = "0.10.0"
@ -1418,6 +1655,17 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2 1.0.66",
"quote 1.0.32",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.28"
@ -1483,6 +1731,15 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.16.0"
@ -1523,6 +1780,16 @@ dependencies = [
"lazy_static 1.4.0",
]
[[package]]
name = "thread_local"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "time"
version = "0.3.25"
@ -1553,6 +1820,40 @@ dependencies = [
"time-core",
]
[[package]]
name = "toml"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [
"indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "typed-arena"
version = "2.0.2"
@ -1642,6 +1943,12 @@ dependencies = [
"getrandom",
]
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.4"
@ -1913,6 +2220,15 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97"
dependencies = [
"memchr",
]
[[package]]
name = "xdg"
version = "2.5.2"

View File

@ -46,6 +46,7 @@ nightly = ["punkt", "hyphenation"]
crowbook-intl = "0.2"
[dependencies]
rust-i18n = "2"
html-escape = "0.2"
mime_guess = "2"
comrak = "0.18"

68
lang/bin/en.yml Normal file
View File

@ -0,0 +1,68 @@
msg:
autograph: "Enter autograph:"
default_book: |
"author: Your name"
"title: Your title"
"lang: en"
"## Output formats"
"# Uncomment and fill to generate files"
"# output.html: some_file.html"
"# output.epub: some_file.epub"
"# output.pdf: some_file.pdf"
"# Or uncomment the following to generate PDF, HTML and EPUB files based on this file's name"
"# output: [pdf, epub, html]"
"# Uncomment and fill to set cover image (for EPUB)"
"# cover: some_cover.png"
chapter_list: "\n## List of chapters\n"
created: "Created %{file}, now you'll have to complete it!"
cmd:
about: Render a Markdown book in EPUB, PDF or HTML.
single: Use a single Markdown file instead of a book configuration file
emoji: Force emoji usage even if it might not work on your system
verbose: Print warnings in parsing/rendering
quiet: Don't print info/error messages
create: Create a new book with existing Markdown files
autograph: Prompts for an autograph for this book
output: Specify output file
lang: Set the runtime language used by Crowbook
to: Generate specific format
set: Set a list of book options
no_fancy: Disably fancy UI
list_options: List all possible options
list_options_md: List all possible options, formatted in Markdown
template: Prints the default content of a template
book: File containing the book configuration file, or a Markdown file when called with --single
stats: Print some project statistics
clap:
template: |
{bin} {version} by {author}
{about}
USAGE:
{usage}
OPTIONS:
{options}
ARGS:
{positionals}
error:
invalid_template: "%{template} is not a valid template name"
no_file: |
You must pass the name of a book configuration file.
For more information try --help.
autograph: could not read autograph from stdin
occurred: "Crowbook exited successfully, but the following errors occurred:"
warning: WARNING
error: ERROR
odd_number: |
An odd number of arguments was passed to --set, but it takes
a list of key value pairs.
set_key: "Error in setting key %{key}: %{error}"
create: "Could not create file %{file}: it already exists!"

95
lang/lib/en.yml Normal file
View File

@ -0,0 +1,95 @@
ui:
parsing: Parsing...
parsing_file: "Parsing %{file}"
rendering: Rendering...
rendering_format: rendering...
waiting: waiting...
options: setting options
chapters: Parsing chapters
processing: Processing...
processing_file: "Processing %{file}..."
finished: Finished
generated: "generated %{path}"
error: ERROR
error:
markdown: "Error parsing markdown: %{error}"
config: "Error parsing configuration file: "
template: "Error compiling template: %{template}"
render_error: "Error during rendering: "
zipper: "Error during temporary files editing: "
bookoption: "Error converting BookOption: "
invalid_option: "Error accessing book option: "
syntect: "Error higligting syntax: "
file_not_found: "Could not find file '%{file}' for %{description}"
utf8_error: "UTF-8 error: %{error}"
initial: empty str token, could not find initial
no_string: "%{s} is not a string"
no_string_vector: "%{s} is not a string vector"
no_path: "%{s} is not a path"
no_bool: "%{s} is not a boolean"
no_char: "%{s} is not a char"
no_i32: "%{s} is not an i32"
no_f32: "%{s} is not a f32"
book_init: "Error initializing book: could not set %{key} to %{value}: %{error}"
parse_book: |
"could not parse %{file} as a book file."
Maybe you meant to run crowbook with the --single argument?
yaml_block: "YAML block was not valid YAML: %{error}"
yaml_hash: YAML part of the book is not a valid hashmap
chapter_whitspace: chapter filenames must not contain whitespace
no_chapter_name: "no chapter name specified"
source: "could not read source: %{error}"
format_line: ill-formatted line specifying chapter number
chapter_number: "error parsing chapter number: %{error}"
part_number_line: ill-formatted line specifying part number
part_number: "error parsing part number: %{error}"
part_definition: found invalid part definition in the chapter list
chapter_definition: found invalid chapter definition in the chapter list
rendering: "Error rendering %{name}: %{error}"
infer: "output to %{format} set to auto but can't find book file name to infer it"
support: "the %{format} renderer does not support auto for output path"
unknown: "unknown format %{format}"
unknown_short: "unknown format"
utf8: "file %{file} contains invalid UTF-8"
heading: "this subchapter contains a heading that, when adjusted, is not in the right range (%{n} instead of [0-6])"
invalid_template: "invalid template '%{template}'"
read_file: "file '%{file}' could not be read"
compile_template: "could not compile '%{template}': %{error}"
roman_numerals: "can not use roman numerals with zero or negative chapter numbers (%{n})"
render_key: "could not render `%{key}` for metadata:\n%{error}"
yaml_set: "Inline YAML block could not set %{key} to %{value}: %{err}"
renderer:
no_output: This renderer does not support the auto output
file_creation: "could not create file '%{file}': '%{err}"
write: "could not write book content to file '%{file}': %{err}"
warn:
above: "Warning: book contains chapter '%{file}' in a directory above the book file, this might cause problems"
format:
book: book
book_chapter: book chapter
html_single: HTML (standalone page)
html_dir: HTML (multiple pages)
tex: LaTeX
pdf: PDF
epub: EPUB
html_if: HTML (interactive fiction)
debug:
yaml_replace: "Inline YAML block replaced %{key} previously set to %{old_val} to %{new_val}"
yaml_set: "Inline YAML block set %{key} to %{value}"
yaml_ignore: "Ignoring YAML block:\n%{block}"
found_yaml_block: "Found something that looked like a YAML block:\n%{block}"
found_yaml_block2: "... but it didn't parse correctly as YAML('%{error}'), so treating it like Markdown."
epub:
zip_command: "Could not run zip command, falling back to zip library"
cover: cover
image_or_cover: image or cover
resources: additional resource from resources.files
ambiguous: "EPUB (%{source}): detected two chapters inside the same markdown file."
ambiguous_invisible: "EPUB (%{source}): detected two chapter titles inside the same markdown file, in a file where chapter titles are not even rendered."
title_conflict: "EPUB ({source}): conflict between: %{title1} and %{title2}"
guess: "EPUB: could not guess the format of %{file} based on extension. Assuming png."
msg:
attempting: "Attempting to generate %{format}..."
generated: "Succesfully generated %{format}: %{path}"
generated_short: "Succesfully generated %{format}"

View File

@ -1,4 +1,4 @@
// Copyright (C) 2016-2022Élisabeth HENRY.
// Copyright (C) 2016-2023Élisabeth HENRY.
//
// This file is part of Crowbook.
//
@ -18,6 +18,7 @@
use clap::{Arg, ArgAction, ArgMatches, Command};
use console::style;
use crowbook::Book;
use rust_i18n::t;
use std::env;
use std::fs;
@ -33,7 +34,7 @@ pub fn print_warning(msg: &str, emoji: bool) {
if emoji {
eprint!("{}", style(WARNING).yellow());
}
eprintln!("{} {}", style(lformat!("WARNING")).bold().yellow(), msg);
eprintln!("{} {}", style(t!("error.warning")).bold().yellow(), msg);
}
/// Prints an error
@ -41,7 +42,7 @@ pub fn print_error(s: &str, emoji: bool) {
if emoji {
eprint!("{}", style(ERROR).red());
}
eprintln!("{} {}", style(lformat!("ERROR")).bold().red(), s);
eprintln!("{} {}", style(t!("error.error")).bold().red(), s);
}
/// Prints an error on stderr and exit the program
@ -82,10 +83,7 @@ pub fn get_book_options(matches: &ArgMatches) -> Vec<(&str, &str)> {
let v: Vec<_> = iter.collect();
if v.len() % 2 != 0 {
print_error_and_exit(
&lformat!(
"An odd number of arguments was passed to --set, but it takes \
a list of key value pairs."
),
&t!("error.odd_number"),
false,
);
}
@ -96,9 +94,6 @@ pub fn get_book_options(matches: &ArgMatches) -> Vec<(&str, &str)> {
output.push((key.as_str(), value.as_str()));
}
}
if matches.get_flag("proofread") {
output.push(("proofread", "true"));
}
output
}
@ -113,7 +108,7 @@ pub fn set_book_options(book: &mut Book, matches: &ArgMatches) -> String {
for (key, value) in options {
let res = book.options.set(key, value);
if let Err(err) = res {
print_error_and_exit(&lformat!("Error in setting key {}: {}", key, err), false);
print_error_and_exit(&t!("error.set_key", key = key, error = err), false);
}
output.push_str(&format!("{key}: {value}\n"));
}
@ -126,7 +121,7 @@ pub fn create_book(matches: &ArgMatches) -> ! {
let mut f: Box<dyn Write> = if let Some(book) = matches.get_one::<String>("BOOK") {
if fs::metadata(book).is_ok() {
print_error_and_exit(
&lformat!("Could not create file {}: it already exists!", book),
&t!("error.create", file = book),
false,
);
}
@ -142,29 +137,12 @@ pub fn create_book(matches: &ArgMatches) -> ! {
f.write_all(s.as_bytes()).unwrap();
} else {
f.write_all(
lformat!(
"author: Your name
title: Your title
lang: en
## Output formats
# Uncomment and fill to generate files
# output.html: some_file.html
# output.epub: some_file.epub
# output.pdf: some_file.pdf
# Or uncomment the following to generate PDF, HTML and EPUB files based on this file's name
# output: [pdf, epub, html]
# Uncomment and fill to set cover image (for EPUB)
# cover: some_cover.png\n"
)
t!("msg.default_book")
.as_bytes(),
)
.unwrap();
}
f.write_all(lformat!("\n## List of chapters\n").as_bytes())
f.write_all(t!("msg.chapter_list").as_bytes())
.unwrap();
for file in values {
f.write_all(format!("+ {file}\n").as_bytes()).unwrap();
@ -172,7 +150,7 @@ lang: en
if let Some(s) = matches.get_one::<String>("BOOK") {
println!(
"{}",
lformat!("Created {}, now you'll have to complete it!", s)
t!("msg.created", file = s)
);
}
exit(0);
@ -188,37 +166,24 @@ pub fn create_matches() -> ArgMatches {
// in its own function for testing purpose
fn app() -> clap::Command {
lazy_static! {
static ref ABOUT: String = lformat!("Render a Markdown book in EPUB, PDF or HTML.");
static ref SINGLE: String = lformat!("Use a single Markdown file instead of a book configuration file");
static ref EMOJI: String = lformat!("Force emoji usage even if it might not work on your system");
static ref VERBOSE: String = lformat!("Print warnings in parsing/rendering");
static ref QUIET: String = lformat!("Don't print info/error messages");
static ref PROOFREAD: String = lformat!("Enable proofreading");
static ref CREATE: String = lformat!("Create a new book with existing Markdown files");
static ref AUTOGRAPH: String = lformat!("Prompts for an autograph for this book");
static ref OUTPUT: String = lformat!("Specify output file");
static ref LANG: String = lformat!("Set the runtime language used by Crowbook");
static ref TO: String = lformat!("Generate specific format");
static ref SET: String = lformat!("Set a list of book options");
static ref NO_FANCY: String = lformat!("Disably fancy UI");
static ref LIST_OPTIONS: String = lformat!("List all possible options");
static ref LIST_OPTIONS_MD: String = lformat!("List all possible options, formatted in Markdown");
static ref PRINT_TEMPLATE: String = lformat!("Prints the default content of a template");
static ref BOOK: String = lformat!("File containing the book configuration file, or a Markdown file when called with --single");
static ref STATS: String = lformat!("Print some project statistics");
static ref TEMPLATE: String = lformat!("\
{{bin}} {{version}} by {{author}}
{{about}}
USAGE:
{{usage}}
OPTIONS:
{{options}}
ARGS:
{{positionals}}
");
static ref ABOUT: String = t!("cmd.about");
static ref SINGLE: String = t!("cmd.single");
static ref EMOJI: String = t!("cmd.emoji");
static ref VERBOSE: String = t!("cmd.verbose");
static ref QUIET: String = t!("cmd.quiet");
static ref CREATE: String = t!("cmd.create");
static ref AUTOGRAPH: String = t!("cmd.autograph");
static ref OUTPUT: String = t!("cmd.output");
static ref LANG: String = t!("cmd.lang");
static ref TO: String = t!("cmd.to");
static ref SET: String = t!("cmd.set");
static ref NO_FANCY: String = t!("cmd.no_fancy");
static ref LIST_OPTIONS: String = t!("cmd.list_options");
static ref LIST_OPTIONS_MD: String = t!("cmd.list_options_md");
static ref PRINT_TEMPLATE: String = t!("cmd.template");
static ref BOOK: String = t!("cmd.book");
static ref STATS: String = t!("cmd.stats");
static ref TEMPLATE: String = t!("clap.template");
}
let app = Command::new("crowbook")
@ -269,13 +234,6 @@ ARGS:
.help(QUIET.as_str())
.conflicts_with("verbose"),
)
.arg(
Arg::new("proofread")
.short('p')
.long("poofread")
.action(ArgAction::SetTrue)
.help(PROOFREAD.as_str()),
)
.arg(
Arg::new("files")
.short('c')
@ -305,10 +263,6 @@ ARGS:
"tex",
"odt",
"html.dir",
"proofread.html",
"proofread.html.dir",
"proofread.pdf",
"proofread.tex",
])
.help(TO.as_str()),
)

View File

@ -1,7 +1,3 @@
extern crate crowbook;
extern crate crowbook_intl_runtime;
extern crate yaml_rust;
#[macro_use]
mod localize_macros;
#[cfg(feature = "binary")]
@ -13,6 +9,9 @@ mod real_main;
#[macro_use]
extern crate lazy_static;
rust_i18n::i18n!("lang/bin", fallback="en");
#[cfg(feature = "binary")]
fn main() {
crate::real_main::real_main();

View File

@ -1,4 +1,4 @@
// Copyright (C) 2016-2022 Élisabeth HENRY.
// Copyright (C) 2016-2023 Élisabeth HENRY.
//
// This file is part of Crowbook.
//
@ -17,11 +17,10 @@
use crate::helpers::*;
use clap::ArgMatches;
use crowbook::Stats;
use crowbook::{Book, BookOptions, Result};
use crowbook_intl_runtime::set_lang;
use clap::ArgMatches;
use simplelog::{ConfigBuilder, LevelFilter, SimpleLogger, TermLogger, WriteLogger};
use std::env;
use std::fs::File;
@ -29,6 +28,8 @@ use std::io;
use std::io::Read;
use std::process::exit;
use yaml_rust::Yaml;
use rust_i18n::t;
/// Render a book to specific format
fn render_format(book: &mut Book, emoji: bool, matches: &ArgMatches, format: &str) {
@ -66,9 +67,9 @@ pub fn try_main() -> Result<()> {
});
if let Some(val) = lang {
if val.starts_with("fr") {
set_lang("fr");
rust_i18n::set_locale("fr");
} else {
set_lang("en");
rust_i18n::set_locale("en");
}
}
@ -110,7 +111,7 @@ pub fn try_main() -> Result<()> {
exit(0);
}
Err(_) => print_error_and_exit(
&lformat!("{} is not a valid template name.", template),
&t!("error.invalid_template", template = template),
emoji,
),
}
@ -122,10 +123,7 @@ pub fn try_main() -> Result<()> {
let book = matches.get_one::<String>("BOOK");
if book.is_none() {
print_error_and_exit(
&lformat!(
"You must pass the file of a book configuration \
file.\nFor more information try --help."
),
&t!("error.no_file"),
emoji,
);
}
@ -173,7 +171,7 @@ pub fn try_main() -> Result<()> {
{
let mut book = Book::new();
if matches.get_flag("autograph") {
println!("{}", &lformat!("Enter autograph: "));
println!("{}", &t!("msg.autograph"));
let mut autograph = String::new();
match io::stdin().read_to_string(&mut autograph) {
Ok(_) => {
@ -184,7 +182,7 @@ pub fn try_main() -> Result<()> {
)
.unwrap();
}
Err(_) => print_error(&lformat!("could not read autograph from stdin"), emoji),
Err(_) => print_error(&t!("error.autograph") , emoji),
}
}
@ -236,7 +234,7 @@ pub fn try_main() -> Result<()> {
file.read_to_string(&mut errors).unwrap();
if !errors.is_empty() {
print_warning(
&lformat!("Crowbook exited successfully, but the following errors occurred:"),
&t!("error.occurred"),
emoji,
);
// Non-efficient dedup algorithm but we need to keep the order

View File

@ -48,6 +48,7 @@ use std::path::{Path, PathBuf};
use numerals::roman::Roman;
use rayon::prelude::*;
use yaml_rust::{Yaml, YamlLoader};
use rust_i18n::t;
/// Type of header (part or chapter)
#[derive(Copy, Clone, Debug)]
@ -170,20 +171,20 @@ impl<'a> Book<'a> {
book.add_format(
"html",
lformat!("HTML (standalone page)"),
t!("format.html_single"),
Box::new(HtmlSingle {}),
)
.add_format(
"html.dir",
lformat!("HTML (multiple pages)"),
t!("format.html_dir"),
Box::new(HtmlDir {}),
)
.add_format("tex", lformat!("LaTeX"), Box::new(Latex {}))
.add_format("pdf", lformat!("PDF"), Box::new(Pdf {}))
.add_format("epub", lformat!("EPUB"), Box::new(Epub {}))
.add_format("tex", t!("format.tex"), Box::new(Latex {}))
.add_format("pdf", t!("format.pdf"), Box::new(Pdf {}))
.add_format("epub", t!("format.epub"), Box::new(Epub {}))
.add_format(
"html.if",
lformat!("HTML (interactive fiction)"),
t!("html_if"),
Box::new(HtmlIf {}),
);
book
@ -256,8 +257,8 @@ impl<'a> Book<'a> {
if let Err(err) = self.options.set(key, value) {
error!(
"{}",
lformat!(
"Error initializing book: could not set {key} to {value}: {error}",
t!(
"error.book_init",
key = key,
value = value,
error = err
@ -290,7 +291,7 @@ impl<'a> Book<'a> {
self.options.source = Source::new(filename.as_str());
let f = File::open(path.as_ref()).map_err(|_| {
Error::file_not_found(Source::empty(), lformat!("book"), filename.clone())
Error::file_not_found(Source::empty(), t!("format.book"), filename.clone())
})?;
// Set book path to book's directory
if let Some(parent) = path.as_ref().parent() {
@ -304,11 +305,8 @@ impl<'a> Book<'a> {
if err.is_config_parser() && path.as_ref().ends_with(".md") {
let err = Error::default(
Source::empty(),
lformat!(
"could not parse {file} as a book \
file.\nMaybe you meant to run crowbook \
with the --single argument?",
file = misc::normalize(path)
t!("error.parse_book",
file = misc::normalize(path)
),
);
Err(err)
@ -395,7 +393,7 @@ impl<'a> Book<'a> {
Err(err) => {
return Err(Error::config_parser(
&self.source,
lformat!("YAML block was not valid YAML: {error}", error = err),
t!("error.yaml_block", error = err),
))
}
Ok(mut docs) => {
@ -412,10 +410,7 @@ impl<'a> Book<'a> {
} else {
return Err(Error::config_parser(
&self.source,
lformat!(
"YAML part of the book is not a \
valid hashmap"
),
t!("error.parse_book"),
));
}
}
@ -458,27 +453,24 @@ impl<'a> Book<'a> {
if words.len() > 1 {
return Err(Error::config_parser(
source,
lformat!(
"chapter filenames must not contain \
whitespace"
),
t!("error.chapter_whitespace"),
));
} else if words.is_empty() {
return Err(Error::config_parser(
source,
lformat!("no chapter name specified"),
t!("error.no_chapter_name"),
));
}
Ok(words[0])
}
self.bar_set_message(Crowbar::Main, &lformat!("setting options"));
self.bar_set_message(Crowbar::Main, &t!("ui.options"));
let mut s = String::new();
source.read_to_string(&mut s).map_err(|err| {
Error::config_parser(
Source::empty(),
lformat!("could not read source: {error}", error = err),
t!("error.source", error = err),
)
})?;
@ -552,11 +544,11 @@ impl<'a> Book<'a> {
// Update cleaner according to options (autoclean/lang)
self.update_cleaner();
self.bar_set_message(Crowbar::Main, &lformat!("Parsing chapters"));
self.bar_set_message(Crowbar::Main, &t!("ui.chapters"));
// Parse chapters
let lines: Vec<_> = lines.collect();
self.add_second_bar(&lformat!("Processing..."), lines.len() as u64);
self.add_second_bar(&t!("ui.processing"), lines.len() as u64);
for line in lines {
self.inc_second_bar();
line_number += 1;
@ -599,17 +591,14 @@ impl<'a> Book<'a> {
if parts.len() != 2 {
return Err(Error::config_parser(
&self.source,
lformat!(
"ill-formatted line specifying \
chapter number"
),
t!("error.format_line"),
));
}
let file = get_filename(&self.source, parts[1])?;
let number = parts[0].parse::<i32>().map_err(|err| {
Error::config_parser(
&self.source,
lformat!("error parsing chapter number: {error}", error = err),
t!("error.chapter_number", error = err),
)
})?;
self.add_chapter(Number::Specified(number), file, true)?;
@ -637,33 +626,27 @@ impl<'a> Book<'a> {
if parts.len() != 2 {
return Err(Error::config_parser(
&self.source,
lformat!(
"ill-formatted line specifying \
part number"
),
t!("error.part_number_line")
));
}
let file = get_filename(&self.source, parts[1])?;
let number = parts[0].parse::<i32>().map_err(|err| {
Error::config_parser(
&self.source,
lformat!("error parsing part number: {error}", error = err),
t!("error.part_number", error = err),
)
})?;
self.add_chapter(Number::SpecifiedPart(number), file, true)?;
} else {
return Err(Error::config_parser(
&self.source,
lformat!("found invalid part definition in the chapter list"),
t!("error.part_definition"),
));
}
} else {
return Err(Error::config_parser(
&self.source,
lformat!(
"found invalid chapter definition in \
the chapter list"
),
t!("error.chapter_definition"),
));
}
}
@ -675,13 +658,6 @@ impl<'a> Book<'a> {
Ok(())
}
/// Determine whether proofreading is activated or not
fn is_proofread(&self) -> bool {
self.options.get_bool("proofread").unwrap()
&& (self.options.get("output.proofread.html").is_ok()
|| self.options.get("output.proofread.html.dir").is_ok()
|| self.options.get("output.proofread.pdf").is_ok())
}
/// Generates output files acccording to book options.
///
@ -709,9 +685,6 @@ impl<'a> Book<'a> {
.formats
.keys()
.filter(|fmt| {
if !self.is_proofread() && fmt.contains("proofread") {
return false;
}
self.options.get_path(&format!("output.{fmt}")).is_ok()
})
.map(|s| s.to_string())
@ -735,7 +708,7 @@ impl<'a> Book<'a> {
self.render_format_with_bar(fmt, i);
});
self.bar_finish(Crowbar::Main, CrowbarState::Success, &lformat!("Finished"));
self.bar_finish(Crowbar::Main, CrowbarState::Success, &t!("ui.finished"));
// if handles.is_empty() {
// Logger::display_warning(lformat!("Crowbook generated no file because no output file was \
@ -748,7 +721,7 @@ impl<'a> Book<'a> {
let mut key = String::from("output.");
key.push_str(format);
if let Ok(path) = self.options.get_path(&key) {
self.bar_set_message(Crowbar::Spinner(bar), &lformat!("rendering..."));
self.bar_set_message(Crowbar::Spinner(bar), &t!("ui.rendering_format"));
let result = self.render_format_to_file_with_bar(format, path, bar);
if let Err(err) = result {
self.bar_finish(
@ -758,8 +731,7 @@ impl<'a> Book<'a> {
);
error!(
"{}",
lformat!(
"Error rendering {name}: {error}",
t!("error.rendering",
name = format,
error = err
)
@ -776,7 +748,7 @@ impl<'a> Book<'a> {
) -> Result<()> {
debug!(
"{}",
lformat!("Attempting to generate {format}...", format = format)
t!("msg.attempting", format = format)
);
let path = path.into();
match self.formats.get(format) {
@ -790,14 +762,13 @@ impl<'a> Book<'a> {
{
s.to_string_lossy().into_owned()
} else {
return Err(Error::default(&self.source, lformat!("output to {format} set to auto but can't find book file name to infer it",
return Err(Error::default(&self.source, t!("error.infer",
format = description)));
};
let file = renderer.auto_path(&file).map_err(|_| {
Error::default(
&self.source,
lformat!(
"the {format} renderer does not support auto for output path",
t!("error.support",
format = description
),
)
@ -808,8 +779,8 @@ impl<'a> Book<'a> {
};
renderer.render_to_file(self, &path)?;
let path = misc::normalize(path);
let msg = lformat!(
"Succesfully generated {format}: {path}",
let msg = t!(
"msg.generated",
format = description,
path = &path
);
@ -817,13 +788,13 @@ impl<'a> Book<'a> {
self.bar_finish(
Crowbar::Spinner(bar),
CrowbarState::Success,
&lformat!("generated {path}", path = path),
&t!("ui.generated", path = path),
);
Ok(())
}
None => Err(Error::default(
Source::empty(),
lformat!("unknown format {format}", format = format),
t!("error.unknown", format = format),
)),
}
}
@ -842,7 +813,7 @@ impl<'a> Book<'a> {
pub fn render_format_to<T: Write>(&mut self, format: &str, f: &mut T) -> Result<()> {
debug!(
"{}",
lformat!("Attempting to generate {format}...", format = format)
t!("msg.attempting", format = format)
);
let bar = self.add_spinner_to_multibar(format);
match self.formats.get(format) {
@ -851,12 +822,12 @@ impl<'a> Book<'a> {
self.bar_finish(
Crowbar::Spinner(bar),
CrowbarState::Success,
&lformat!("generated {format}", format = format),
&t!("ui.generated", path = format),
);
self.bar_finish(Crowbar::Main, CrowbarState::Success, &lformat!("Finished"));
self.bar_finish(Crowbar::Main, CrowbarState::Success, &t!("ui.finished"));
info!(
"{}",
lformat!("Succesfully generated {format}", format = description)
t!("msg.generated_short", format = description)
);
Ok(())
}
@ -864,9 +835,9 @@ impl<'a> Book<'a> {
self.bar_finish(
Crowbar::Spinner(bar),
CrowbarState::Error,
&lformat!("{error}", error = e),
&format!("{error}", error = e),
);
self.bar_finish(Crowbar::Main, CrowbarState::Error, &lformat!("ERROR"));
self.bar_finish(Crowbar::Main, CrowbarState::Error, &t!("ui.error"));
Err(e)
}
},
@ -874,11 +845,11 @@ impl<'a> Book<'a> {
self.bar_finish(
Crowbar::Spinner(bar),
CrowbarState::Error,
&lformat!("unknown format"),
&t!("error.unknown_short"),
);
Err(Error::default(
Source::empty(),
lformat!("unknown format {format}", format = format),
t!("error.unknown", format = format),
))
}
}
@ -904,7 +875,7 @@ impl<'a> Book<'a> {
pub fn render_format_to_file<P: Into<PathBuf>>(&mut self, format: &str, path: P) -> Result<()> {
let bar = self.add_spinner_to_multibar(format);
self.render_format_to_file_with_bar(format, path, bar)?;
self.bar_finish(Crowbar::Main, CrowbarState::Success, &lformat!("Finished"));
self.bar_finish(Crowbar::Main, CrowbarState::Success, &t!("ui.finished"));
Ok(())
}
@ -920,21 +891,21 @@ impl<'a> Book<'a> {
) -> Result<&mut Self> {
self.bar_set_message(
Crowbar::Main,
&lformat!("Processing {file}...", file = file),
&t!("ui.processing_file", file = file),
);
let mut content = String::new();
source.read_to_string(&mut content).map_err(|_| {
Error::parser(
&self.source,
lformat!(
"file {file} contains invalid UTF-8",
t!(
"error.utf8",
file = misc::normalize(file)
),
)
})?;
// parse the file
self.bar_set_message(Crowbar::Second, &lformat!("Parsing..."));
self.bar_set_message(Crowbar::Second, &t!("ui.parsing..."));
let mut parser = Parser::from(self);
parser.set_source_file(file);
@ -954,9 +925,8 @@ impl<'a> Book<'a> {
if offset.starts_with("..") {
debug!(
"{}",
lformat!(
"Warning: book contains chapter '{file}' in a directory above \
the book file, this might cause problems",
t!(
"warn.above",
file = misc::normalize(file)
)
);
@ -1020,7 +990,7 @@ impl<'a> Book<'a> {
let new = *n + level;
if !(0..=6).contains(&new) {
return Err(Error::parser(Source::new(file),
lformat!("this subchapter contains a heading that, when adjusted, is not in the right range ({} instead of [0-6])", new)));
t!("error.heading", n = new)));
}
*n = new;
}
@ -1050,11 +1020,7 @@ impl<'a> Book<'a> {
) -> Result<&mut Self> {
self.bar_set_message(
Crowbar::Main,
&lformat!("Parsing {file}", file = misc::normalize(file)),
);
debug!(
"{}",
lformat!("Parsing chapter: {file}...", file = misc::normalize(file))
&t!("ui.parsing_file", file = misc::normalize(file)),
);
// try to open file
@ -1062,7 +1028,7 @@ impl<'a> Book<'a> {
let f = File::open(&path).map_err(|_| {
Error::file_not_found(
&self.source,
lformat!("book chapter"),
t!("format.book_chapter"),
format!("{}", path.display()),
)
})?;
@ -1137,7 +1103,7 @@ impl<'a> Book<'a> {
_ => {
return Err(Error::config_parser(
&self.source,
lformat!("invalid template '{template}'"),
t!("error.invalid_template"),
))
}
};
@ -1149,7 +1115,7 @@ impl<'a> Book<'a> {
f.read_to_string(&mut res).map_err(|_| {
Error::config_parser(
&self.source,
lformat!("file '{file}' could not be read", file = s),
t!("error.read_file", file = s),
)
})?;
Ok(Cow::Owned(res))
@ -1171,8 +1137,8 @@ impl<'a> Book<'a> {
self.options.get_str(tpl).unwrap().to_owned())
.map_err(|e| Error::template(
&self.source,
lformat!(
"could not compile '{template}': {error}",
t!(
"error.compile_template",
template = "tpl",
error = e
))
@ -1197,8 +1163,8 @@ impl<'a> Book<'a> {
if n <= 0 {
return Err(Error::render(
Source::empty(),
lformat!(
"can not use roman numerals with zero or negative chapter numbers ({n})",
t!(
"error.roman_numerals",
n = n
),
));
@ -1314,9 +1280,8 @@ impl<'a> Book<'a> {
Err(err) => {
return Err(Error::render(
&self.source,
lformat!(
"could not render `{key}` for \
metadata:\n{error}",
t!(
"error.render_key",
key = &key,
error = err
),
@ -1349,9 +1314,10 @@ impl<'a> Book<'a> {
Ok(result) => Ok(result),
Err(err) => Err(Error::template(
source,
lformat!(
"could not compile '{template_name}': {:#}",
err
t!(
"error.compile_template",
template = template_name,
error = format!("{:#}", err)
),
)),
}
@ -1384,24 +1350,18 @@ impl<'a> Book<'a> {
if let Some(old_value) = opt {
debug!(
"{}",
lformat!(
"Inline YAML block \
replaced {:?} \
previously set to \
{:?} to {:?}",
key,
old_value,
value
t!("debug.yaml_replace",
key = format!("{:?}", key),
old_val = format!("{:?}", old_value),
new_val = format!("{:?}", value)
)
);
} else {
debug!(
"{}",
lformat!(
"Inline YAML block \
set {:?} to {:?}",
key,
value
t!("debug.yaml_set",
key = format!("{:?}", key),
value = format!("{:?}", value)
)
);
}
@ -1409,12 +1369,11 @@ impl<'a> Book<'a> {
Err(e) => {
error!(
"{}",
lformat!(
"Inline YAML block could \
not set {:?} to {:?}: {}",
key,
value,
e
t!(
"error.yaml_set",
key = format!("{:?}", key,),
value = format!("{:?}", value),
err = e
)
)
}
@ -1425,10 +1384,8 @@ impl<'a> Book<'a> {
} else {
debug!(
"{}",
lformat!(
"Ignoring YAML \
block:
\n{block}",
t!(
"debug.yaml_ignore",
block = &yaml_block
)
);
@ -1437,18 +1394,13 @@ impl<'a> Book<'a> {
Err(err) => {
error!(
"{}",
lformat!(
"Found something that looked like a \
YAML block:\n{block}",
t!("debug.found_yaml_block",
block = &yaml_block
)
);
error!(
"{}",
lformat!(
"... but it didn't parse correctly as \
YAML('{error}'), so treating it like \
Markdown.",
t!("debug.found_yaml_block2",
error = err
)
);

View File

@ -21,6 +21,7 @@
use crate::book::{Book, Crowbar, CrowbarState};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use rust_i18n::t;
use std::sync::Arc;
use std::time::Duration;
@ -79,19 +80,7 @@ impl Book<'_> {
.add(ProgressBar::new_spinner());
b.enable_steady_tick(Duration::from_millis(200));
self.bars.mainbar = Some(b);
// let sty = ProgressStyle::default_spinner()
// .tick_chars("🕛🕐🕑🕒🕓🕔🕔🕕🕖🕗🕘🕘🕙🕚V")
// .tick_chars("/|\\-V")
// .template("{spinner:.dim.bold.yellow} {prefix} {wide_msg}");
self.bar_set_style(Crowbar::Main, CrowbarState::Running);
// self.bars.guard = Some(thread::spawn(move || {
// if let Err(_) = multibar.join() {
// error!(
// "{}",
// lformat!("could not display fancy UI, try running crowbook with --no-fancy")
// );
// }
// }));
}
/// Sets a finished message to the progress bar, if it is set
@ -148,12 +137,12 @@ impl Book<'_> {
pub fn add_spinner_to_multibar(&mut self, key: &str) -> usize {
if let Some(ref multibar) = self.bars.multibar {
if let Some(ref mainbar) = self.bars.mainbar {
mainbar.set_message(lformat!("Rendering..."));
mainbar.set_message(t!("ui.rendering"));
}
let bar = multibar.add(ProgressBar::new_spinner());
bar.enable_steady_tick(Duration::from_millis(200));
bar.set_message(lformat!("waiting..."));
bar.set_message(t!("ui.waiting"));
bar.set_prefix(format!("{key}:"));
let i = self.bars.spinners.len();
self.bars.spinners.push(bar);

View File

@ -21,6 +21,7 @@ use crate::error::{Error, Result, Source};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use rust_i18n::t;
/// Trait that must be implemented by the various renderers to render a whole book.
@ -29,7 +30,7 @@ pub trait BookRenderer: Sync {
fn auto_path(&self, _book_file: &str) -> Result<String> {
Err(Error::default(
Source::empty(),
lformat!("This renderer does not support the auto output"),
t!("error.renderer.no_output"),
))
}
@ -47,8 +48,8 @@ pub trait BookRenderer: Sync {
let mut file = File::create(path).map_err(|err| {
Error::default(
Source::empty(),
lformat!(
"could not create file '{file}': {err}",
t!(
"error.renderer.file_creation",
file = path.display(),
err = err
),
@ -57,8 +58,8 @@ pub trait BookRenderer: Sync {
file.write_all(&content).map_err(|err| {
Error::default(
Source::empty(),
lformat!(
"could not write book content to file '{file}': {err}",
t!(
"error.renderer.write",
file = path.display(),
err = err
),

View File

@ -1,3 +1,5 @@
use rust_i18n::t;
use crate::error::{Error, Result, Source};
/// Structure for storing a book option
@ -50,7 +52,7 @@ impl BookOption {
BookOption::String(ref s) => Ok(s),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not a string", self),
t!("error.no_string", s = format!("{:?}", self)),
)),
}
}
@ -61,7 +63,7 @@ impl BookOption {
BookOption::StringVec(ref v) => Ok(v),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not a string vector", self),
t!("error.no_string_vector", s = format!("{:?}", self)),
)),
}
}
@ -72,7 +74,7 @@ impl BookOption {
BookOption::Path(ref s) => Ok(s),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not a path", self),
t!("error.no_path", s = format!("{:?}", self)),
)),
}
}
@ -83,7 +85,7 @@ impl BookOption {
BookOption::Bool(b) => Ok(b),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not a bool", self),
t!("error.no_bool", s = format!("{:?}", self)),
)),
}
}
@ -94,7 +96,7 @@ impl BookOption {
BookOption::Char(c) => Ok(c),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not a char", self),
t!("error.no_char", s = format!("{:?}", self)),
)),
}
}
@ -105,7 +107,7 @@ impl BookOption {
BookOption::Int(i) => Ok(i),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not an i32", self),
t!("error.no_i32", s = format!("{:?}", self)),
)),
}
}
@ -116,7 +118,7 @@ impl BookOption {
BookOption::Float(f) => Ok(f),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not a f32", self),
t!("error.no_f32", s = format!("{:?}", self)),
)),
}
}

View File

@ -35,6 +35,7 @@ use epub_builder::{
ZipLibrary,
};
use upon::Template;
use rust_i18n::t;
use std::borrow::Cow;
use std::convert::{AsMut, AsRef};
@ -90,7 +91,7 @@ impl<'a> EpubRenderer<'a> {
} else {
warn!(
"{}",
lformat!("Could not run zip command, falling back to zip library")
t!("epub.zip_command")
);
ZipCommandOrLibrary::Library(ZipLibrary::new()
.map_err(|err| Error::render(Source::empty(), format!("{}", err)))?)
@ -254,7 +255,7 @@ impl<'a> EpubRenderer<'a> {
let f = fs::canonicalize(source).and_then(File::open).map_err(|_| {
Error::file_not_found(
&self.html.source,
lformat!("image or cover"),
t!("epub.image_or_cover"),
source.to_owned(),
)
})?;
@ -290,7 +291,7 @@ impl<'a> EpubRenderer<'a> {
.map_err(|_| {
Error::file_not_found(
&self.html.book.source,
lformat!("additional resource from resources.files"),
t!("epub.resources"),
abs_path.to_string_lossy().into_owned(),
)
})?;
@ -327,7 +328,7 @@ impl<'a> EpubRenderer<'a> {
if fs::metadata(&cover).is_err() {
return Err(Error::file_not_found(
&self.html.book.source,
lformat!("cover"),
t!("epub.cover"),
cover,
));
}
@ -349,10 +350,7 @@ impl<'a> EpubRenderer<'a> {
.into());
Ok(template.render(&data).to_string()?)
} else {
panic!(
"{}",
lformat!("Why is this method called if cover is None???")
);
unreachable!();
}
}
@ -422,10 +420,8 @@ impl<'a> EpubRenderer<'a> {
} else {
warn!(
"{}",
lformat!(
"EPUB ({source}): detected two chapter titles inside the \
same markdown file, in a file where chapter titles are \
not even rendered.",
t!(
"epub.ambiguous_invisible",
source = self.html.source
)
);
@ -459,16 +455,15 @@ impl<'a> EpubRenderer<'a> {
} else {
warn!(
"{}",
lformat!(
"EPUB ({source}): detected two chapters inside the same \
markdown file.",
t!(
"epub.ambiguous",
source = self.html.source
)
);
warn!(
"{}",
lformat!(
"EPUB ({source}): conflict between: {title1} and {title2}",
t!(
"epub.title_conflict",
source = self.html.source,
title1 = self.chapter_title,
title2 = s
@ -487,9 +482,8 @@ impl<'a> EpubRenderer<'a> {
None => {
error!(
"{}",
lformat!(
"EPUB: could not guess the format of {file} based on \
extension. Assuming png.",
t!(
"epub.guess",
file = s
)
);
@ -529,10 +523,7 @@ impl<'a> EpubRenderer<'a> {
let initial = chars.next().ok_or_else(|| {
Error::parser(
&html.book.source,
lformat!(
"empty str token, could not find \
initial"
),
t!("error.initial"),
)
})?;
let mut new_content = if initial.is_alphanumeric() {

View File

@ -21,6 +21,8 @@ use std::fmt;
use std::result;
use std::string::FromUtf8Error;
use rust_i18n::t;
#[derive(Debug, PartialEq, Clone)]
/// Source of an error.
///
@ -107,15 +109,6 @@ impl Error {
}
}
/// Creates a new grammar check error.
///
/// Used when there is a problem connecting to languagetool
pub fn grammar_check<S: Into<Cow<'static, str>>, O: Into<Source>>(source: O, msg: S) -> Error {
Error {
source: source.into(),
inner: Inner::GrammarCheck(msg.into()),
}
}
/// Creates a new parser error.
///
/// Error when parsing markdown file.
@ -275,9 +268,7 @@ impl error::Error for Error {
| Inner::InvalidOption(ref s)
| Inner::Render(ref s)
| Inner::Template(ref s)
| Inner::Syntect(ref s)
| Inner::GrammarCheck(ref s) => s.as_ref(),
| Inner::Syntect(ref s) => s.as_ref(),
Inner::FileNotFound(..) => "File not found",
}
}
@ -296,30 +287,23 @@ impl fmt::Display for Error {
match self.inner {
Inner::Default(ref s) => write!(f, "{s}"),
Inner::GrammarCheck(ref s) => {
write!(
f,
"{}",
lformat!("Error while trying to check grammar: {error}", error = s)
)
}
Inner::Parser(ref s) => {
write!(
f,
"{}",
lformat!("Error parsing markdown: {error}", error = s)
t!("error.markdown", error = s)
)
}
Inner::ConfigParser(ref s) => {
f.write_str(&lformat!("Error parsing configuration file: "))?;
f.write_str(&t!("error.config"))?;
f.write_str(s)
}
Inner::FileNotFound(ref description, ref file) => {
write!(
f,
"{}",
lformat!(
"Could not find file '{file}' for {description}",
t!(
"error.file_not_found",
file = file,
description = description
)
@ -329,27 +313,27 @@ impl fmt::Display for Error {
write!(
f,
"{}",
lformat!("Error compiling template: {template}", template = s)
t!("error.template", template = s)
)
}
Inner::Render(ref s) => {
f.write_str(&lformat!("Error during rendering: "))?;
f.write_str(&t!("error.render_error"))?;
f.write_str(s)
}
Inner::Zipper(ref s) => {
f.write_str(&lformat!("Error during temporary files editing: "))?;
f.write_str(&t!("error.zipper"))?;
f.write_str(s)
}
Inner::BookOption(ref s) => {
f.write_str(&lformat!("Error converting BookOption: "))?;
f.write_str(&t!("error.bookoption"))?;
f.write_str(s)
}
Inner::InvalidOption(ref s) => {
f.write_str(&lformat!("Error accessing book option: "))?;
f.write_str(&t!("error.invalid_option"))?;
f.write_str(s)
}
Inner::Syntect(ref s) => {
f.write_str(&lformat!("Error higligting syntax: "))?;
f.write_str(&t!("error.syntect"))?;
f.write_str(s)
}
}?;
@ -372,7 +356,7 @@ impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Error {
Error::render(
Source::empty(),
lformat!("UTF-8 error: {error}", error = err),
t!("error.utf8_error", error = err),
)
}
}
@ -381,7 +365,7 @@ impl From<std::str::Utf8Error> for Error {
fn from(err: std::str::Utf8Error) -> Error {
Error::render(
Source::empty(),
lformat!("UTF-8 error: {error}", error = err),
t!("error.utf8_error", error = err),
)
}
}
@ -424,8 +408,6 @@ enum Inner {
InvalidOption(Cow<'static, str>),
/// Error when compiling template
Template(Cow<'static, str>),
/// Error when connecting to LanguageTool
GrammarCheck(Cow<'static, str>),
/// Error when parsing code syntax
Syntect(Cow<'static, str>),
}

View File

@ -1,4 +1,4 @@
// Copyright (C) 2016, 2017, 2018 Élisabeth HENRY.
// Copyright (C) 2016-2023 Élisabeth HENRY.
//
// This file is part of Crowbook.
//
@ -109,10 +109,6 @@ extern crate log;
#[macro_use]
extern crate lazy_static;
#[cfg(feature = "proofread")]
#[macro_use]
extern crate serde_derive;
pub use book::Book;
pub use book_renderer::BookRenderer;
pub use bookoption::BookOption;
@ -127,6 +123,8 @@ pub use stats::Stats;
pub use token::Data;
pub use token::Token;
rust_i18n::i18n!("lang/lib", fallback="en");
#[macro_use]
#[doc(hidden)]
mod localize_macros;

View File

@ -1,666 +0,0 @@
// Copyright (C) 2016 Élisabeth HENRY.
//
// This file is part of Crowbook.
//
// Crowbook is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 2.1 of the License, or
// (at your option) any later version.
//
// Caribon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received ba copy of the GNU Lesser General Public License
// along with Crowbook. If not, see <http://www.gnu.org/licenses/>.
use crate::token::Token;
use crate::error::{Result, Error, Source};
use crate::book::Book;
use std::mem;
use std::fs::File;
use std::path::Path;
use std::convert::AsRef;
use std::io::Read;
use std::collections::HashMap;
use std::ops::BitOr;
use cmark::{Parser as CMParser, Event, Tag, Options};
#[derive(Debug, Copy, Clone, PartialEq)]
/// The list of features used in a document.
pub struct Features {
pub image: bool,
pub blockquote: bool,
pub codeblock: bool,
pub ordered_list: bool,
pub footnote: bool,
pub table: bool,
pub url: bool,
pub subscript: bool,
pub superscript: bool,
}
impl Features {
/// Creates a new set of features where all are set to false
pub fn new() -> Features {
Features {
image: false,
blockquote: false,
codeblock: false,
ordered_list: false,
footnote: false,
table: false,
url: false,
subscript: false,
superscript: false,
}
}
}
impl BitOr for Features {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Features {
image: self.image | rhs.image,
blockquote: self.blockquote | rhs.blockquote,
codeblock: self.codeblock | rhs.codeblock,
ordered_list: self.ordered_list | rhs.ordered_list,
footnote: self.footnote | rhs.footnote,
table: self.table | rhs.table,
url: self.url | rhs.url,
subscript: self.subscript | rhs.subscript,
superscript: self.superscript | rhs.superscript,
}
}
}
/// A parser that reads markdown and convert it to AST (a vector of `Token`s)
///
/// This AST can then be used by various renderes.
///
/// As this Parser uses Pulldown-cmark's one, it should be able to parse most
/// *valid* CommonMark variant of Markdown.
///
/// Compared to other Markdown parser, it might fail more often on invalid code, e.g.
/// footnotes references that are not defined anywhere.
///
/// # Examples
///
/// ```
/// use crowbook::Parser;
/// let mut parser = Parser::new();
/// let result = parser.parse("Some *valid* Markdown[^1]\n\n[^1]: with a valid footnote");
/// assert!(result.is_ok());
/// ```
///
/// ```
/// use crowbook::Parser;
/// let mut parser = Parser::new();
/// let result = parser.parse("Some footnote pointing to nothing[^1] ");
/// assert!(result.is_err());
/// ```
pub struct Parser {
footnotes: HashMap<String, Vec<Token>>,
source: Source,
features: Features,
html_as_text: bool,
superscript: bool,
}
impl Parser {
/// Creates a parser
pub fn new() -> Parser {
Parser {
footnotes: HashMap::new(),
source: Source::empty(),
features: Features::new(),
html_as_text: true,
superscript: false,
}
}
/// Creates a parser with options from a book configuration file
pub fn from(book: &Book) -> Parser {
let mut parser = Parser::new();
parser.html_as_text = book.options.get_bool("crowbook.html_as_text").unwrap();
parser.superscript = book.options.get_bool("crowbook.markdown.superscript").unwrap();
parser
}
/// Enable/disable HTML as text
pub fn html_as_text(&mut self, b: bool) {
self.html_as_text = b;
}
/// Sets a parser's source file
pub fn set_source_file(&mut self, s: &str) {
self.source = Source::new(s);
}
/// Parse a file and returns an AST or an error
pub fn parse_file<P: AsRef<Path>>(&mut self, filename: P) -> Result<Vec<Token>> {
let path: &Path = filename.as_ref();
let mut f = File::open(path)
.map_err(|_| {
Error::file_not_found(&self.source,
lformat!("markdown file"),
format!("{}", path.display()))
})?;
let mut s = String::new();
f.read_to_string(&mut s)
.map_err(|_| {
Error::parser(&self.source,
lformat!("file {file} contains invalid UTF-8, could not parse it",
file = path.display()))
})?;
self.parse(&s)
}
/// Parse a string and returns an AST an Error.
pub fn parse(&mut self, s: &str) -> Result<Vec<Token>> {
let mut opts = Options::empty();
opts.insert(Options::ENABLE_TABLES);
opts.insert(Options::ENABLE_FOOTNOTES);
let mut p = CMParser::new_ext(s, opts);
let mut res = vec![];
self.parse_events(&mut p, &mut res, None)?;
self.parse_footnotes(&mut res)?;
collapse(&mut res);
find_standalone(&mut res);
// Transform superscript and subscript
if self.superscript {
self.parse_super_vec(&mut res);
self.parse_sub_vec(&mut res);
}
Ok(res)
}
/// Parse an inline string and returns a list of `Token`.
///
/// This function removes the outermost `Paragraph` in most of the
/// cases, as it is meant to be used for an inline string (e.g. metadata)
pub fn parse_inline(&mut self, s: &str) -> Result<Vec<Token>> {
let mut tokens = self.parse(s)?;
// Unfortunately, parser will put all this in a paragraph, so we might need to remove it.
if tokens.len() == 1 {
let res = match tokens[0] {
Token::Paragraph(ref mut v) => Some(mem::replace(v, vec![])),
_ => None,
};
match res {
Some(tokens) => Ok(tokens),
_ => Ok(tokens),
}
} else {
Ok(tokens)
}
}
/// Returns the list of features used by this parser
pub fn features(&self) -> Features {
self.features
}
/// Replace footnote reference with their definition
fn parse_footnotes(&mut self, v: &mut Vec<Token>) -> Result<()> {
for token in v {
match *token {
Token::Footnote(ref mut content) => {
let reference = if let Token::Str(ref text) = content[0] {
text.clone()
} else {
panic!("Reference is not a vector of a single Token::Str");
};
if let Some(in_vec) = self.footnotes.get(&reference) {
*content = in_vec.clone();
} else {
return Err(Error::parser(&self.source,
lformat!("footnote reference {reference} does \
not have a matching definition",
reference = &reference)));
}
}
Token::Paragraph(ref mut vec) |
Token::Header(_, ref mut vec) |
Token::Emphasis(ref mut vec) |
Token::Strong(ref mut vec) |
Token::Code(ref mut vec) |
Token::BlockQuote(ref mut vec) |
Token::CodeBlock(_, ref mut vec) |
Token::List(ref mut vec) |
Token::OrderedList(_, ref mut vec) |
Token::Item(ref mut vec) |
Token::Table(_, ref mut vec) |
Token::TableHead(ref mut vec) |
Token::TableRow(ref mut vec) |
Token::TableCell(ref mut vec) |
Token::Link(_, _, ref mut vec) |
Token::Image(_, _, ref mut vec) => self.parse_footnotes(vec)?,
_ => (),
}
}
Ok(())
}
/// Looks for super script in a vector of tokens
fn parse_super_vec(&mut self, v: &mut Vec<Token>) {
for i in 0..v.len() {
let new = if v[i].is_str() {
if let Token::Str(ref s) = v[i] {
parse_super_sub(s, b'^')
} else {
unreachable!()
}
} else {
if v[i].is_code() || !v[i].is_container() {
continue;
}
if let Some(ref mut inner) = v[i].inner_mut() {
self.parse_super_vec(inner);
}
None
};
if let Some(mut new) = new {
self.features.superscript = true;
let mut post = v.split_off(i);
post.remove(0);
self.parse_super_vec(&mut post);
v.append(&mut new);
v.append(&mut post);
return;
}
}
}
/// Looks for subscript in a vector of token
fn parse_sub_vec(&mut self, v: &mut Vec<Token>) {
for i in 0..v.len() {
let new = if v[i].is_str() {
if let Token::Str(ref s) = v[i] {
parse_super_sub(s, b'~')
} else {
unreachable!()
}
} else {
if v[i].is_code() || !v[i].is_container() {
continue;
}
if let Some(ref mut inner) = v[i].inner_mut() {
self.parse_sub_vec(inner);
}
None
};
if let Some(mut new) = new {
self.features.subscript = true;
let mut post = v.split_off(i);
post.remove(0);
self.parse_sub_vec(&mut post);
v.append(&mut new);
v.append(&mut post);
return;
}
}
}
fn parse_events<'a>(&mut self,
p: &mut CMParser<'a>,
v: &mut Vec<Token>,
current_tag: Option<&Tag>)
-> Result<()> {
while let Some(event) = p.next() {
match event {
Event::Html(text) | Event::InlineHtml(text) => {
if self.html_as_text {
v.push(Token::Str(text.into_owned()));
} else {
debug!("{}", lformat!("ignoring HTML block '{}'", text));
}
},
Event::Text(text) => {
v.push(Token::Str(text.into_owned()));
}
Event::Start(tag) => self.parse_tag(p, v, tag)?,
Event::End(tag) => {
debug_assert!(format!("{:?}", Some(&tag)) == format!("{:?}", current_tag),
format!("Error: opening and closing tags mismatch!\n{:?}\
{:?}",
tag,
current_tag));
break;
}
Event::SoftBreak => v.push(Token::SoftBreak),
Event::HardBreak => v.push(Token::HardBreak),
Event::FootnoteReference(text) => {
v.push(Token::Footnote(vec![Token::Str(text.into_owned())]))
}
}
}
Ok(())
}
fn parse_tag<'a>(&mut self,
p: &mut CMParser<'a>,
v: &mut Vec<Token>,
tag: Tag<'a>)
-> Result<()> {
let mut res = vec![];
self.parse_events(p, &mut res, Some(&tag))?;
let token = match tag {
Tag::Paragraph => Token::Paragraph(res),
Tag::Emphasis => Token::Emphasis(res),
Tag::Strong => Token::Strong(res),
Tag::Code => Token::Code(res),
Tag::Header(x) => Token::Header(x, res),
Tag::Link(url, title) => {
self.features.url = true;
Token::Link(url.into_owned(), title.into_owned(), res)
},
Tag::Image(url, title) => {
self.features.image = true;
Token::Image(url.into_owned(), title.into_owned(), res)
},
Tag::Rule => Token::Rule,
Tag::List(opt) => {
if let Some(n) = opt {
self.features.ordered_list = true;
Token::OrderedList(n, res)
} else {
Token::List(res)
}
}
Tag::Item => Token::Item(res),
Tag::BlockQuote => {
self.features.blockquote = true;
Token::BlockQuote(res)
},
Tag::CodeBlock(language) => {
self.features.codeblock = true;
Token::CodeBlock(language.into_owned(), res)
},
Tag::Table(v) => {
self.features.table = true;
// TODO: actually use v's alignments
Token::Table(v.len() as i32, res)
},
Tag::TableHead => Token::TableHead(res),
Tag::TableRow => Token::TableRow(res),
Tag::TableCell => Token::TableCell(res),
Tag::FootnoteDefinition(reference) => {
if self.footnotes.contains_key(reference.as_ref()) {
warn!("{}", lformat!("in {file}, found footnote definition for \
note '{reference}' but previous \
definition already exist, overriding it",
file = self.source,
reference = reference));
}
self.footnotes.insert(reference.into_owned(), res);
Token::SoftBreak
}
};
v.push(token);
Ok(())
}
}
/// Look to a string and see if there is some superscript or subscript in it.
/// If there, returns a vec of tokens.
///
/// params: s: the string to parse, c, either b'^' for superscript or b'~' for subscript.
fn parse_super_sub(s: &str, c: u8) -> Option<Vec<Token>> {
let match_indices:Vec<_> = s.match_indices(c as char).collect();
if match_indices.is_empty() {
return None;
}
let to_escape = format!("\\{}", c as char);
let escaped = format!("{}", c as char);
let escape = |s: String| -> String {
s.replace(&to_escape, &escaped)
};
for (begin, _) in match_indices {
let bytes = s.as_bytes();
let len = bytes.len();
// Check if ^ was escaped
if begin > 0 && bytes[begin - 1] == b'\\' {
continue;
} else if begin + 1 >= len {
return None;
} else {
let mut i = begin + 1;
let mut sup = vec![];
let mut end = None;
while i < len {
match bytes[i] {
b'\\' => {
if i+1 < len && bytes[i+1] == b' ' {
sup.push(b' ');
i += 2;
} else if i + 1 < len && bytes[i+1] == c {
sup.push(c);
i += 2;
} else {
sup.push(b'\\');
i += 1;
}
},
b' ' => {
return None;
},
b if b == c => {
end = Some(i);
break;
},
b => {
sup.push(b);
i += 1;
},
}
}
if sup.is_empty() {
return None;
}
if let Some(end) = end {
let mut tokens = vec![];
if begin > 0 {
let pre_part = String::from_utf8((&bytes[0..begin])
.to_owned())
.unwrap();
tokens.push(Token::Str(escape(pre_part)));
}
let sup_part = String::from_utf8(sup).unwrap();
match c {
b'^' => tokens.push(Token::Superscript(vec![Token::Str(sup_part)])),
b'~' => tokens.push(Token::Subscript(vec![Token::Str(sup_part)])),
_ => unimplemented!(),
}
if end+1 < len {
let post_part = String::from_utf8((&bytes[end + 1..]).to_owned()).unwrap();
if let Some(mut v) = parse_super_sub(&post_part, c) {
tokens.append(&mut v);
} else {
tokens.push(Token::Str(escape(post_part)));
}
}
return Some(tokens);
} else {
return None;
}
}
}
return None;
}
/// Replace consecutives Strs by a Str of both, collapse soft breaks to previous std and so on
fn collapse(ast: &mut Vec<Token>) {
let mut i = 0;
while i < ast.len() {
if ast[i].is_str() && i + 1 < ast.len() {
if ast[i + 1].is_str() {
// Two consecutives Str, concatenate them
let token = ast.remove(i + 1);
if let (&mut Token::Str(ref mut dest), Token::Str(ref source)) = (&mut ast[i],
token) {
// dest.push(' ');
dest.push_str(source);
continue;
} else {
unreachable!();
}
} else if ast[i + 1] == Token::SoftBreak {
ast.remove(i + 1);
if let &mut Token::Str(ref mut dest) = &mut ast[i] {
dest.push(' ');
continue;
} else {
unreachable!();
}
}
}
// If token is containing others, recurse into them
if let Some(ref mut inner) = ast[i].inner_mut() {
collapse(inner);
}
i += 1;
}
}
/// Replace images which are alone in a paragraph by standalone images
fn find_standalone(ast: &mut Vec<Token>) {
for token in ast {
let res = if let &mut Token::Paragraph(ref mut inner) = token {
if inner.len() == 1 {
if inner[0].is_image() {
if let Token::Image(source, title, inner) = mem::replace(&mut inner[0],
Token::Rule) {
Token::StandaloneImage(source, title, inner)
} else {
unreachable!();
}
} else {
// If paragraph only contains a link only containing an image, ok too
// Fixme: messy code and unnecessary clone
if let Token::Link(ref url, ref alt, ref mut inner) = inner[0] {
if inner[0].is_image() {
if let Token::Image(source, title, inner) = mem::replace(&mut inner[0],
Token::Rule) {
Token::Link(url.clone(), alt.clone(), vec![Token::StandaloneImage(source, title, inner)])
} else {
unreachable!();
}
} else {
continue;
}
} else {
continue;
}
}
} else {
continue;
}
} else {
continue;
};
*token = res;
}
}
#[test]
fn test_parse_super_str() {
let c = b'^';
assert!(parse_super_sub("String without superscript", c).is_none());
assert!(parse_super_sub("String \\^without\\^ superscript", c).is_none());
assert!(parse_super_sub("String ^without superscript", c).is_none());
assert!(parse_super_sub("String ^without superscript^", c).is_none());
assert_eq!(parse_super_sub("^up^", c),
Some(vec!(Token::Superscript(vec!(Token::Str(String::from("up")))))));
assert_eq!(parse_super_sub("foo^up^ bar", c),
Some(vec!(Token::Str("foo".to_owned()),
Token::Superscript(vec!(Token::Str("up".to_owned()))),
Token::Str(" bar".to_owned()))));
assert_eq!(parse_super_sub("foo^up^ bar^up^baz", c),
Some(vec!(Token::Str("foo".to_owned()),
Token::Superscript(vec!(Token::Str("up".to_owned()))),
Token::Str(" bar".to_owned()),
Token::Superscript(vec!(Token::Str("up".to_owned()))),
Token::Str("baz".to_owned()))));
assert_eq!(parse_super_sub("foo^up^ bar^baz", c),
Some(vec!(Token::Str("foo".to_owned()),
Token::Superscript(vec!(Token::Str("up".to_owned()))),
Token::Str(" bar^baz".to_owned()))));
assert_eq!(parse_super_sub("foo\\^bar^up^", c),
Some(vec!(Token::Str("foo^bar".to_owned()),
Token::Superscript(vec!(Token::Str("up".to_owned()))))));
assert_eq!(parse_super_sub("foo^bar\\^up^", c),
Some(vec!(Token::Str("foo".to_owned()),
Token::Superscript(vec!(Token::Str("bar^up".to_owned()))))));
assert_eq!(parse_super_sub("foo^bar up^", c),
None);
assert_eq!(parse_super_sub("foo^bar\\ up^", c),
Some(vec!(Token::Str("foo".to_owned()),
Token::Superscript(vec!(Token::Str("bar up".to_owned()))))));
}
#[test]
fn test_parse_supb_str() {
let c = b'~';
assert!(parse_super_sub("String without subscript", c).is_none());
assert!(parse_super_sub("String \\~without\\~ subscript", c).is_none());
assert!(parse_super_sub("String ~without subscript", c).is_none());
assert!(parse_super_sub("String ~without\nsubscript", c).is_none());
assert!(parse_super_sub("String ~without subscript~", c).is_none());
assert_eq!(parse_super_sub("~down~", c),
Some(vec!(Token::Subscript(vec!(Token::Str(String::from("down")))))));
assert_eq!(parse_super_sub("foo~down~ bar", c),
Some(vec!(Token::Str("foo".to_owned()),
Token::Subscript(vec!(Token::Str("down".to_owned()))),
Token::Str(" bar".to_owned()))));
assert_eq!(parse_super_sub("foo~down~ bar~down~baz", c),
Some(vec!(Token::Str("foo".to_owned()),
Token::Subscript(vec!(Token::Str("down".to_owned()))),
Token::Str(" bar".to_owned()),
Token::Subscript(vec!(Token::Str("down".to_owned()))),
Token::Str("baz".to_owned()))));
assert_eq!(parse_super_sub("foo~down~ bar~baz", c),
Some(vec!(Token::Str("foo".to_owned()),
Token::Subscript(vec!(Token::Str("down".to_owned()))),
Token::Str(" bar~baz".to_owned()))));
assert_eq!(parse_super_sub("foo\\~bar~down~", c),
Some(vec!(Token::Str("foo~bar".to_owned()),
Token::Subscript(vec!(Token::Str("down".to_owned()))))));
assert_eq!(parse_super_sub("foo~bar\\~down~", c),
Some(vec!(Token::Str("foo".to_owned()),
Token::Subscript(vec!(Token::Str("bar~down".to_owned()))))));
assert_eq!(parse_super_sub("foo~bar down~", c),
None);
assert_eq!(parse_super_sub("foo~bar\\ down~", c),
Some(vec!(Token::Str("foo".to_owned()),
Token::Subscript(vec!(Token::Str("bar down".to_owned()))))));
}