diff --git a/.editorconfig b/.editorconfig index 473207c5..477a603e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,12 +3,22 @@ root = true # All files [*] indent_style = space -dotnet_diagnostic.CA1047.severity = error # Xml files [*.xml] indent_size = 2 +# Xml project files +[*.{csproj,fsproj,vbproj,proj,slnx}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +[*.json] +indent_size = 2 + # C# files [*.cs] @@ -19,7 +29,6 @@ indent_size = 4 tab_width = 4 # New line preferences -end_of_line = crlf insert_final_newline = false #### .NET Coding Conventions #### @@ -53,13 +62,16 @@ dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent dotnet_style_coalesce_expression = true:suggestion dotnet_style_collection_initializer = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_object_initializer = true:suggestion dotnet_style_operator_placement_when_wrapping = beginning_of_line dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion dotnet_style_prefer_compound_assignment = true:suggestion dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_return = false:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion @@ -79,15 +91,15 @@ dotnet_remove_unnecessary_suppression_exclusions = none [*.cs] # var preferences -csharp_style_var_elsewhere = false:silent +csharp_style_var_elsewhere = false:suggestion csharp_style_var_for_built_in_types = false:silent -csharp_style_var_when_type_is_apparent = false:silent +csharp_style_var_when_type_is_apparent = true:suggestion # Expression-bodied members csharp_style_expression_bodied_accessors = true:silent csharp_style_expression_bodied_constructors = false:silent csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_lambdas = when_on_single_line:error +csharp_style_expression_bodied_lambdas = true:suggestion csharp_style_expression_bodied_local_functions = false:silent csharp_style_expression_bodied_methods = false:silent csharp_style_expression_bodied_operators = false:silent @@ -96,6 +108,7 @@ csharp_style_expression_bodied_properties = true:silent # Pattern matching preferences csharp_style_pattern_matching_over_as_with_null_check = true:suggestion csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion csharp_style_prefer_not_pattern = true:suggestion csharp_style_prefer_pattern_matching = true:silent csharp_style_prefer_switch_expression = true:suggestion @@ -104,20 +117,31 @@ csharp_style_prefer_switch_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion # Modifier preferences +csharp_prefer_static_anonymous_function = true:suggestion csharp_prefer_static_local_function = true:warning -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent +csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion # Code-block preferences csharp_prefer_braces = true:silent csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_prefer_top_level_statements = true:silent # Expression-level preferences csharp_prefer_simple_default_expression = true:suggestion csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion -csharp_style_pattern_local_over_anonymous_function = true:suggestion csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion csharp_style_prefer_range_operator = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion csharp_style_throw_expression = true:suggestion csharp_style_unused_value_assignment_preference = discard_variable:suggestion csharp_style_unused_value_expression_statement_preference = discard_variable:silent @@ -171,32 +195,21 @@ csharp_space_between_square_brackets = false # Wrapping preferences csharp_preserve_single_line_blocks = true csharp_preserve_single_line_statements = true -csharp_style_namespace_declarations = file_scoped:error -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent -csharp_style_prefer_primary_constructors = false:suggestion -csharp_style_prefer_null_check_over_type_check = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion -csharp_style_prefer_utf8_string_literals = true:suggestion -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent -csharp_style_prefer_readonly_struct = true:suggestion -csharp_style_prefer_readonly_struct_member = true:suggestion -dotnet_diagnostic.CA1070.severity = error -dotnet_diagnostic.CA1032.severity = error -dotnet_diagnostic.CA1865.severity = error -dotnet_diagnostic.CA1866.severity = error -dotnet_diagnostic.CA1867.severity = error -dotnet_diagnostic.CA2014.severity = error csharp_prefer_system_threading_lock = true:suggestion -csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion -csharp_style_prefer_implicitly_typed_lambda_expression = true:suggestion csharp_style_prefer_simple_property_accessors = true:suggestion +csharp_style_prefer_implicitly_typed_lambda_expression = true:suggestion +csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +dotnet_diagnostic.CA1003.severity = error +dotnet_diagnostic.CA1008.severity = error +dotnet_diagnostic.CA1032.severity = error +dotnet_diagnostic.CA1034.severity = silent +dotnet_diagnostic.CA1036.severity = error +dotnet_diagnostic.CA1044.severity = error #### Naming styles #### [*.{cs,vb}] @@ -388,32 +401,17 @@ dotnet_naming_style.s_camelcase.required_prefix = s_ dotnet_naming_style.s_camelcase.required_suffix = dotnet_naming_style.s_camelcase.word_separator = dotnet_naming_style.s_camelcase.capitalization = camel_case -dotnet_style_prefer_collection_expression = when_types_exactly_match:suggestion -dotnet_style_namespace_match_folder = true:error tab_width = 4 indent_size = 4 end_of_line = crlf -dotnet_diagnostic.CA1000.severity = silent -dotnet_diagnostic.CA1001.severity = warning -dotnet_diagnostic.CA1002.severity = suggestion -dotnet_diagnostic.CA1008.severity = error -dotnet_diagnostic.CA1061.severity = error -dotnet_diagnostic.CA1063.severity = error -dotnet_diagnostic.CA2000.severity = error -dotnet_diagnostic.CA1816.severity = error -dotnet_diagnostic.CA2213.severity = error -dotnet_diagnostic.CA2215.severity = error -dotnet_diagnostic.CA1064.severity = error -dotnet_diagnostic.CA1069.severity = error -dotnet_diagnostic.CA1401.severity = error -dotnet_diagnostic.CA1501.severity = error -dotnet_diagnostic.CA1700.severity = error -dotnet_diagnostic.CA1821.severity = error -dotnet_diagnostic.CA1836.severity = error -dotnet_diagnostic.CA1843.severity = error -dotnet_diagnostic.CA1842.severity = error -dotnet_diagnostic.CA2011.severity = error dotnet_style_allow_multiple_blank_lines_experimental = true:silent dotnet_style_allow_statement_immediately_after_block_experimental = true:silent insert_final_newline = true +dotnet_diagnostic.CA1001.severity = error +dotnet_diagnostic.CA1002.severity = silent +dotnet_diagnostic.CA1005.severity = error +dotnet_diagnostic.CA1010.severity = error +dotnet_diagnostic.CA1041.severity = error +dotnet_diagnostic.CA2025.severity = error +dotnet_diagnostic.CA2213.severity = error diff --git a/Assets/BundledAssets.Source/bookgen.css b/Assets/BundledAssets.Source/bookgen.css index 05f47d9e..83284059 100644 --- a/Assets/BundledAssets.Source/bookgen.css +++ b/Assets/BundledAssets.Source/bookgen.css @@ -1,6 +1,6 @@ /* BookGen Css Not minified -Version: 2025-05-17 +Version: 2026-03-04 */ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Nunito:wght@700&family=Open+Sans&display=swap'); @@ -12,6 +12,10 @@ Version: 2025-05-17 --blue: #003e8a; --brightBlue: #2e6cba; --selection: #CACACA; + --box-info: #d1e7dd; + --box-waring: #fff3cd; + --box-error: #f8d7da; + --box-foreground: #3e3e3e; --max-width: 1200px; } @@ -22,6 +26,10 @@ body.dark-mode { --blue: #82aaff; --brightBlue: #d6acff; --selection: #5D5D5D; + --box-info: #B9E5D1; + --box-waring: #FFECB5; + --box-error: #F7BEC4; + --box-foreground: #212121; } h1, @@ -228,6 +236,48 @@ dialog #container #dialogBase { color: var(--foreground); } +/* Boxes */ + +.info { + background-color: var(--box-info); + color: var(--box-foreground); + margin: 1rem; + padding: 1rem; + border-radius: 0.5rem; +} + +.info::before { + content: "ℹ️"; + display: block; +} + +.warning { + background-color: var(--box-waring); + color: var(--box-foreground); + margin: 1rem; + padding: 1rem; + border-radius: 0.5rem; +} + +.warning::before { + content: "⚠️"; + display: block; +} + + +.error { + background-color: var(--box-error); + color: var(--box-foreground); + margin: 1rem; + padding: 1rem; + border-radius: 0.5rem; +} + +.error::before { + content: "🛑"; + display: block; +} + @media print { body { diff --git a/Assets/BundledAssets.Source/bookgen.epub.css b/Assets/BundledAssets.Source/bookgen.epub.css index 87a42d9c..124841fa 100644 --- a/Assets/BundledAssets.Source/bookgen.epub.css +++ b/Assets/BundledAssets.Source/bookgen.epub.css @@ -1,6 +1,6 @@ /* BookGen Css for epub files Not minified -Version: 2025-07-15 +Version: 2026-03-04 */ :root { @@ -193,6 +193,48 @@ figcaption { font-style: italic; } +/* Boxes */ + +.info { + background-color: var(--box-info); + color: var(--box-foreground); + margin: 1rem; + padding: 1rem; + border-radius: 0.5rem; +} + +.info::before { + content: "ℹ️"; + display: block; +} + +.warning { + background-color: var(--box-waring); + color: var(--box-foreground); + margin: 1rem; + padding: 1rem; + border-radius: 0.5rem; +} + +.warning::before { + content: "⚠️"; + display: block; +} + + +.error { + background-color: var(--box-error); + color: var(--box-foreground); + margin: 1rem; + padding: 1rem; + border-radius: 0.5rem; +} + +.error::before { + content: "🛑"; + display: block; +} + /* PrismJS 1.30.0 https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+abap+abnf+actionscript+ada+agda+al+antlr4+apacheconf+apex+apl+applescript+aql+arduino+arff+armasm+arturo+asciidoc+aspnet+asm6502+asmatmel+autohotkey+autoit+avisynth+avro-idl+awk+bash+basic+batch+bbcode+bbj+bicep+birb+bison+bnf+bqn+brainfuck+brightscript+bro+bsl+c+csharp+cpp+cfscript+chaiscript+cil+cilkc+cilkcpp+clojure+cmake+cobol+coffeescript+concurnas+csp+cooklang+coq+crystal+css-extras+csv+cue+cypher+d+dart+dataweave+dax+dhall+diff+django+dns-zone-file+docker+dot+ebnf+editorconfig+eiffel+ejs+elixir+elm+etlua+erb+erlang+excel-formula+fsharp+factor+false+firestore-security-rules+flow+fortran+ftl+gml+gap+gcode+gdscript+gedcom+gettext+gherkin+git+glsl+gn+linker-script+go+go-module+gradle+graphql+groovy+haml+handlebars+haskell+haxe+hcl+hlsl+hoon+http+hpkp+hsts+ichigojam+icon+icu-message-format+idris+ignore+inform7+ini+io+j+java+javadoc+javadoclike+javastacktrace+jexl+jolie+jq+jsdoc+js-extras+json+json5+jsonp+jsstacktrace+js-templates+julia+keepalived+keyman+kotlin+kumir+kusto+latex+latte+less+lilypond+liquid+lisp+livescript+llvm+log+lolcode+lua+magma+makefile+markdown+markup-templating+mata+matlab+maxscript+mel+mermaid+metafont+mizar+mongodb+monkey+moonscript+n1ql+n4js+nand2tetris-hdl+naniscript+nasm+neon+nevod+nginx+nim+nix+nsis+objectivec+ocaml+odin+opencl+openqasm+oz+parigp+parser+pascal+pascaligo+psl+pcaxis+peoplecode+perl+php+phpdoc+php-extras+plant-uml+plsql+powerquery+powershell+processing+prolog+promql+properties+protobuf+pug+puppet+pure+purebasic+purescript+python+qsharp+q+qml+qore+r+racket+cshtml+jsx+tsx+reason+regex+rego+renpy+rescript+rest+rip+roboconf+robotframework+ruby+rust+sas+sass+scss+scala+scheme+shell-session+smali+smalltalk+smarty+sml+solidity+solution-file+soy+sparql+splunk-spl+sqf+sql+squirrel+stan+stata+iecst+stylus+supercollider+swift+systemd+t4-templating+t4-cs+t4-vb+tap+tcl+tt2+textile+toml+tremor+turtle+twig+typescript+typoscript+unrealscript+uorazor+uri+v+vala+vbnet+velocity+verilog+vhdl+vim+visual-basic+warpscript+wasm+web-idl+wgsl+wiki+wolfram+wren+xeora+xml-doc+xojo+xquery+yaml+yang+zig */ code[class*=language-], diff --git a/Assets/BundledAssets.Source/test.html b/Assets/BundledAssets.Source/test.html index d621e548..c018ad35 100644 --- a/Assets/BundledAssets.Source/test.html +++ b/Assets/BundledAssets.Source/test.html @@ -122,6 +122,10 @@
H6
+
Info box
+
Warning box
+
Error box
+
diff --git a/Assets/BundledAssets/Md2Html.html b/Assets/BundledAssets/Md2Html.html index b78ec0a5..a5523a41 100644 --- a/Assets/BundledAssets/Md2Html.html +++ b/Assets/BundledAssets/Md2Html.html @@ -7,7 +7,7 @@ {{Title}} diff --git a/Assets/BundledAssets/Print.html b/Assets/BundledAssets/Print.html index c42dd2b5..427eeda0 100644 --- a/Assets/BundledAssets/Print.html +++ b/Assets/BundledAssets/Print.html @@ -7,7 +7,7 @@ {{Title}} diff --git a/Assets/BundledAssets/Static.html b/Assets/BundledAssets/Static.html index 4f679a25..db4d6bca 100644 --- a/Assets/BundledAssets/Static.html +++ b/Assets/BundledAssets/Static.html @@ -8,7 +8,7 @@ {{Title}} diff --git a/Assets/BundledAssets/bookgen.epub.min.css b/Assets/BundledAssets/bookgen.epub.min.css index 21e26696..9f2971af 100644 --- a/Assets/BundledAssets/bookgen.epub.min.css +++ b/Assets/BundledAssets/bookgen.epub.min.css @@ -1 +1 @@ -:root{--red:#970b16;--blue:#003e8a;--brightBlue:#2e6cba;--max-width:1200px}@font-face{font-family:"JetBrains Mono";src:url(JetBrainsMono-Regular.ttf);font-weight:400;font-style:normal}@font-face{font-family:Nunito;src:url(Nunito-Bold.ttf);font-optical-sizing:auto;font-weight:700;font-style:normal}@font-face{font-family:"Open Sans";src:url(OpenSans-Regular.ttf);font-optical-sizing:auto;font-weight:400;font-style:normal;font-variation-settings:"wdth" 100}body{font-family:"Open Sans",sans-serif;font-size:1.1em;line-height:1.6;margin:0;padding:1em;color:#1a1a1a;background:0 0;widows:2;orphans:2}p{margin:1em 0;text-align:justify;text-indent:1.5em}h1,h2,h3,h4,h5,h6{font-family:Nunito,sans-serif;font-weight:700;color:#000;margin:1.5em 0 .5em;page-break-after:avoid;page-break-inside:avoid}h1{font-size:1.8em}h2{font-size:1.5em}h3{font-size:1.3em}h4{font-size:1.2em}h5{font-size:1.1em}h6{font-size:1em}blockquote{margin:1em 2em;font-style:italic;color:#555}em{font-style:italic}strong{font-weight:700}a{color:#0645ad;text-decoration:underline}ol,ul{margin:1em 0 1em 2em;padding:0}li{margin-bottom:.5em}img{max-width:100%;height:auto;display:block;margin:1em auto}p code{color:var(--red);font-style:italic}pre{transition:box-shadow .3s ease,transform .3s ease;font-family:"JetBrains Mono",monospace;font-optical-sizing:auto;font-weight:400;font-style:normal;font-variant-ligatures:contextual;font-feature-settings:"liga","calt"}pre code{font-family:"JetBrains Mono",monospace;font-optical-sizing:auto;font-weight:400;font-style:normal;font-variant-ligatures:contextual;font-feature-settings:"liga","calt"}hr{border:0;height:1px;background:#ccc;margin:2em 0}blockquote,h1,h2,h3,img,pre,table{page-break-inside:avoid}table{width:100%;border-collapse:collapse;margin:1em 0}td,th{border:1px solid #ccc;padding:.5em;text-align:left}figure{margin:0}figcaption{text-align:center;font-style:italic}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} \ No newline at end of file +:root{--red:#970b16;--blue:#003e8a;--brightBlue:#2e6cba;--max-width:1200px}@font-face{font-family:"JetBrains Mono";src:url(JetBrainsMono-Regular.ttf);font-weight:400;font-style:normal}@font-face{font-family:Nunito;src:url(Nunito-Bold.ttf);font-optical-sizing:auto;font-weight:700;font-style:normal}@font-face{font-family:"Open Sans";src:url(OpenSans-Regular.ttf);font-optical-sizing:auto;font-weight:400;font-style:normal;font-variation-settings:"wdth" 100}body{font-family:"Open Sans",sans-serif;font-size:1.1em;line-height:1.6;margin:0;padding:1em;color:#1a1a1a;background:0 0;widows:2;orphans:2}p{margin:1em 0;text-align:justify;text-indent:1.5em}h1,h2,h3,h4,h5,h6{font-family:Nunito,sans-serif;font-weight:700;color:#000;margin:1.5em 0 .5em;page-break-after:avoid;page-break-inside:avoid}h1{font-size:1.8em}h2{font-size:1.5em}h3{font-size:1.3em}h4{font-size:1.2em}h5{font-size:1.1em}h6{font-size:1em}blockquote{margin:1em 2em;font-style:italic;color:#555}em{font-style:italic}strong{font-weight:700}a{color:#0645ad;text-decoration:underline}ol,ul{margin:1em 0 1em 2em;padding:0}li{margin-bottom:.5em}img{max-width:100%;height:auto;display:block;margin:1em auto}p code{color:var(--red);font-style:italic}pre{transition:box-shadow .3s ease,transform .3s ease;font-family:"JetBrains Mono",monospace;font-optical-sizing:auto;font-weight:400;font-style:normal;font-variant-ligatures:contextual;font-feature-settings:"liga","calt"}pre code{font-family:"JetBrains Mono",monospace;font-optical-sizing:auto;font-weight:400;font-style:normal;font-variant-ligatures:contextual;font-feature-settings:"liga","calt"}hr{border:0;height:1px;background:#ccc;margin:2em 0}blockquote,h1,h2,h3,img,pre,table{page-break-inside:avoid}table{width:100%;border-collapse:collapse;margin:1em 0}td,th{border:1px solid #ccc;padding:.5em;text-align:left}figure{margin:0}figcaption{text-align:center;font-style:italic}.info{background-color:var(--box-info);color:var(--box-foreground);margin:1rem;padding:1rem;border-radius:.5rem}.info::before{content:"ℹ️";display:block}.warning{background-color:var(--box-waring);color:var(--box-foreground);margin:1rem;padding:1rem;border-radius:.5rem}.warning::before{content:"⚠️";display:block}.error{background-color:var(--box-error);color:var(--box-foreground);margin:1rem;padding:1rem;border-radius:.5rem}.error::before{content:"🛑";display:block}code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} \ No newline at end of file diff --git a/Assets/BundledAssets/graphre.js b/Assets/BundledAssets/graphre.js new file mode 100644 index 00000000..89b1500d --- /dev/null +++ b/Assets/BundledAssets/graphre.js @@ -0,0 +1 @@ +!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports):"function"==typeof define&&define.amd?define(["exports"],r):r((e="undefined"!=typeof globalThis?globalThis:e||self).graphre={})}(this,(function(e){"use strict";class r{constructor(){var e={};e._next=e._prev=e,this._sentinel=e}dequeue(){var e=this._sentinel,r=e._prev;if(r!==e)return n(r),r}enqueue(e){var r=this._sentinel,t=e;t._prev&&t._next&&n(t),t._next=r._next,r._next._prev=t,r._next=t,t._prev=r}toString(){for(var e=[],r=this._sentinel,n=r._prev;n!==r;)e.push(JSON.stringify(n,t)),n=n._prev;return"["+e.join(", ")+"]"}}function n(e){e._prev._next=e._next,e._next._prev=e._prev,delete e._next,delete e._prev}function t(e,r){if("_next"!==e&&"_prev"!==e)return r}var o=Object.freeze({__proto__:null,List:r});const i={};function a(e){var r=[];for(var n of e)r.push(...n);return r}function s(e,r){return null!=e&&e.hasOwnProperty(r)}function d(e){const r=null==e?0:e.length;return r?e[r-1]:void 0}function u(e,r){e=Object(e);const n={};return Object.keys(e).forEach((t=>{n[t]=r(e[t],t)})),n}function f(e,r){var n=Number.POSITIVE_INFINITY,t=void 0;for(var o of e){var i=r(o);ir(e)-r(n)))}function v(e){i[e]||(i[e]=0);return`${e}${++i[e]}`}function l(e){return e?Object.keys(e).map((r=>e[r])):[]}function g(e,r){for(var n=[],t=0;t!e.children(r).length)),t=g(Math.max(...n.map((r=>e.node(r).rank)))+1,(()=>[]));return c(n,(r=>e.node(r).rank)).forEach((function n(o){if(!s(r,o)){r[o]=!0;var i=e.node(o);t[i.rank].push(o),e.successors(o).forEach(n)}})),t}function b(e,r){for(var n=0,t=1;te.pos))}))),s=1;s0)),f=0;return i.forEach((function(e){var r=e.pos+s;u[r]+=e.weight;for(var n=0;r>0;)r%2&&(n+=u[r+1]),u[r=r-1>>1]+=e.weight;f+=e.weight*n})),f}function k(e,r){return r?r.map((function(r){var n=e.inEdges(r);if(n.length){var t=n.reduce((function(r,n){var t=e.edge(n),o=e.node(n.v);return{sum:r.sum+t.weight*o.order,weight:r.weight+t.weight}}),{sum:0,weight:0});return{v:r,barycenter:t.sum/t.weight,weight:t.weight}}return{v:r}})):[]}function E(e,r){for(var n={},t=0;t=e.barycenter)&&function(e,r){var n=0,t=0;e.weight&&(n+=e.barycenter*e.weight,t+=e.weight);r.weight&&(n+=r.barycenter*r.weight,t+=r.weight);e.vs=r.vs.concat(e.vs),e.barycenter=n/t,e.weight=t,e.i=Math.min(r.i,e.i),r.merged=!0}(e,r)}}function t(r){return function(n){n.in.push(r),0==--n.indegree&&e.push(n)}}for(;e.length;){var o=e.pop();r.push(o),o.in.reverse().forEach(n(o)),o.out.forEach(t(o))}return r.filter((e=>!e.merged)).map((function(e){var r={vs:e.vs,i:e.i};return"barycenter"in e&&(r.barycenter=e.barycenter),"weight"in e&&(r.weight=e.weight),r}))}(l(n).filter((e=>!e.indegree)))}var N="\0";class x{constructor(e={}){this._label=void 0,this._nodeCount=0,this._edgeCount=0,this._isDirected=!s(e,"directed")||e.directed,this._isMultigraph=!!s(e,"multigraph")&&e.multigraph,this._isCompound=!!s(e,"compound")&&e.compound,this._defaultNodeLabelFn=()=>{},this._defaultEdgeLabelFn=()=>{},this._nodes={},this._isCompound&&(this._parent={},this._children={},this._children["\0"]={}),this._in={},this._preds={},this._out={},this._sucs={},this._edgeObjs={},this._edgeLabels={}}isDirected(){return this._isDirected}isMultigraph(){return this._isMultigraph}isCompound(){return this._isCompound}setGraph(e){return this._label=e,this}graph(){return this._label}setDefaultNodeLabel(e){var r;return r=e,this._defaultNodeLabelFn="function"!=typeof r?()=>e:e,this}nodeCount(){return this._nodeCount}nodes(){return Object.keys(this._nodes)}sources(){var e=this;return this.nodes().filter((function(r){return w(e._in[r])}))}sinks(){var e=this;return this.nodes().filter((r=>w(e._out[r])))}setNodes(e,r){for(var n of e)void 0!==r?this.setNode(n,r):this.setNode(n);return this}setNode(e,r){return s(this._nodes,e)?(arguments.length>1&&(this._nodes[e]=r),this):(this._nodes[e]=arguments.length>1?r:this._defaultNodeLabelFn(e),this._isCompound&&(this._parent[e]=N,this._children[e]={},this._children["\0"][e]=!0),this._in[e]={},this._preds[e]={},this._out[e]={},this._sucs[e]={},++this._nodeCount,this)}node(e){return this._nodes[e]}hasNode(e){return s(this._nodes,e)}removeNode(e){var r=this;if(s(this._nodes,e)){var n=e=>{r.removeEdge(this._edgeObjs[e])};if(delete this._nodes[e],this._isCompound){for(var t of(this._removeFromParentsChildList(e),delete this._parent[e],this.children(e)))r.setParent(t);delete this._children[e]}for(var o of Object.keys(this._in[e]))n(o);for(var o of(delete this._in[e],delete this._preds[e],Object.keys(this._out[e])))n(o);delete this._out[e],delete this._sucs[e],--this._nodeCount}return this}setParent(e,r){if(!this._isCompound)throw new Error("Cannot set parent in a non-compound graph");if(void 0===r)r=N;else{for(var n=r+="";!p(n);n=this.parent(n))if(n===e)throw new Error(`Setting ${r} as parent of ${e} would create a cycle`);this.setNode(r)}return this.setNode(e),this._removeFromParentsChildList(e),this._parent[e]=r,this._children[r][e]=!0,this}_removeFromParentsChildList(e){delete this._children[this._parent[e]][e]}parent(e){if(this._isCompound){var r=this._parent[e];if(r!==N)return r}}children(e){if(p(e)&&(e=N),this._isCompound){var r=this._children[e];return r?Object.keys(r):void 0}return e===N?this.nodes():this.hasNode(e)?[]:void 0}predecessors(e){var r=this._preds[e];if(r)return Object.keys(r)}successors(e){var r=this._sucs[e];if(r)return Object.keys(r)}neighbors(e){var r=this.predecessors(e);if(r)return function(e,r){var n=[...e];for(var t of r)-1===n.indexOf(t)&&n.push(t);return n}(r,this.successors(e))}isLeaf(e){return 0===(this.isDirected()?this.successors(e):this.neighbors(e)).length}filterNodes(e){var r=new x({directed:this._isDirected,multigraph:this._isMultigraph,compound:this._isCompound});r.setGraph(this.graph());var n=this;m(this._nodes,(function(n,t){e(t)&&r.setNode(t,n)})),m(this._edgeObjs,(function(e){r.hasNode(e.v)&&r.hasNode(e.w)&&r.setEdge(e,n.edge(e))}));var t={};function o(e){var i=n.parent(e);return void 0===i||r.hasNode(i)?(t[e]=i,i):i in t?t[i]:o(i)}if(this._isCompound)for(var i of r.nodes())r.setParent(i,o(i));return r}setDefaultEdgeLabel(e){var r;return r=e,this._defaultEdgeLabelFn="function"!=typeof r?()=>e:e,this}edgeCount(){return this._edgeCount}edges(){return Object.values(this._edgeObjs)}setPath(e,r){var n=this,t=arguments;return e.reduce((function(e,o){return t.length>1?n.setEdge(e,o,r):n.setEdge(e,o),o})),this}setEdge(e,r,n,t){var o=!1,i=e;"object"==typeof i&&null!==i&&"v"in i?(e=i.v,r=i.w,t=i.name,2===arguments.length&&(n=arguments[1],o=!0)):(e=i,r=arguments[1],t=arguments[3],arguments.length>2&&(n=arguments[2],o=!0)),e=""+e,r=""+r,p(t)||(t=""+t);var a=j(this._isDirected,e,r,t);if(s(this._edgeLabels,a))return o&&(this._edgeLabels[a]=n),this;if(!p(t)&&!this._isMultigraph)throw new Error("Cannot set a named edge when isMultigraph = false");this.setNode(e),this.setNode(r),this._edgeLabels[a]=o?n:this._defaultEdgeLabelFn(e,r,t);var d=function(e,r,n,t){var o=""+r,i=""+n;if(!e&&o>i){var a=o;o=i,i=a}var s={v:o,w:i};t&&(s.name=t);return s}(this._isDirected,e,r,t);return e=d.v,r=d.w,Object.freeze(d),this._edgeObjs[a]=d,C(this._preds[r],e),C(this._sucs[e],r),this._in[r][a]=d,this._out[e][a]=d,this._edgeCount++,this}edge(e,r,n){var t="object"==typeof e?M(this._isDirected,e):j(this._isDirected,e,r,n);return this._edgeLabels[t]}hasEdge(e,r,n){var t=1===arguments.length?M(this._isDirected,arguments[0]):j(this._isDirected,e,r,n);return s(this._edgeLabels,t)}removeEdge(e,r,n){var t="object"==typeof e?M(this._isDirected,e):j(this._isDirected,e,r,n),o=this._edgeObjs[t];return o&&(e=o.v,r=o.w,delete this._edgeLabels[t],delete this._edgeObjs[t],O(this._preds[r],e),O(this._sucs[e],r),delete this._in[r][t],delete this._out[e][t],this._edgeCount--),this}inEdges(e,r){var n=this._in[e];if(n){var t=Object.values(n);return r?t.filter((function(e){return e.v===r})):t}}outEdges(e,r){var n=this._out[e];if(n){var t=Object.values(n);return r?t.filter((function(e){return e.w===r})):t}}nodeEdges(e,r){var n=this.inEdges(e,r);if(n)return n.concat(this.outEdges(e,r))}}class I extends x{}function C(e,r){e[r]?e[r]++:e[r]=1}function O(e,r){--e[r]||delete e[r]}function j(e,r,n,t){var o=""+r,i=""+n;if(!e&&o>i){var a=o;o=i,i=a}return o+""+i+""+(p(t)?"\0":t)}function M(e,r){return j(e,r.v,r.w,r.name)}function L(e,r,n,t){var o;do{o=v(t)}while(e.hasNode(o));return n.dummy=r,e.setNode(o,n),o}function T(e){var r=(new x).setGraph(e.graph());for(var n of e.nodes())r.setNode(n,e.node(n));for(var t of e.edges()){var o=r.edge(t.v,t.w)||{weight:0,minlen:1},i=e.edge(t);r.setEdge(t.v,t.w,{weight:o.weight+i.weight,minlen:Math.max(o.minlen,i.minlen)})}return r}function S(e){var r=new x({multigraph:e.isMultigraph()}).setGraph(e.graph());for(var n of e.nodes())e.children(n).length||r.setNode(n,e.node(n));for(var t of e.edges())r.setEdge(t,e.edge(t));return r}function P(e,r){var n,t,o=e.x,i=e.y,a=r.x-o,s=r.y-i,d=e.width/2,u=e.height/2;if(!a&&!s)throw new Error("Not possible to find intersection inside of the rectangle");return Math.abs(s)*d>Math.abs(a)*u?(s<0&&(u=-u),n=u*a/s,t=u):(a<0&&(d=-d),n=d,t=d*s/a),{x:o+n,y:i+t}}function R(e){var r=g(G(e)+1,(()=>[]));for(var n of e.nodes()){var t=e.node(n),o=t.rank;void 0!==o&&(r[o][t.order]=n)}return r}function F(e){var r=Math.min(...e.nodes().map((r=>e.node(r).rank)).filter((e=>void 0!==e)));for(var n of e.nodes()){var t=e.node(n);s(t,"rank")&&(t.rank-=r)}}function D(e){var r=Math.min(...e.nodes().map((r=>e.node(r).rank)).filter((e=>void 0!==e))),n=[];for(var t of e.nodes()){var o=e.node(t).rank-r;n[o]||(n[o]=[]),n[o].push(t)}for(var i=0,a=e.graph().nodeRankFactor,s=0;s=4&&(o.rank=n,o.order=t),L(e,"border",o,r)}function G(e){var r=e.nodes().map((r=>e.node(r).rank)).filter((e=>void 0!==e));return Math.max(...r)}function V(e,r){var n=[],t=[];for(var o of e)r(o)?n.push(o):t.push(o);return{lhs:n,rhs:t}}function Y(e,r){var n=Date.now();try{return r()}finally{console.log(e+" time: "+(Date.now()-n)+"ms")}}function B(e,r){return r()}var A=Object.freeze({__proto__:null,addDummyNode:L,simplify:T,asNonCompoundGraph:S,successorWeights:function(e){var r={};for(var n of e.nodes()){var t={};for(var o of e.outEdges(n))t[o.w]=(t[o.w]||0)+e.edge(o).weight;r[n]=t}return r},predecessorWeights:function(e){var r={};for(var n of e.nodes()){var t={};for(var o of e.inEdges(n))t[o.v]=(t[o.v]||0)+e.edge(o).weight;r[n]=t}return r},intersectRect:P,buildLayerMatrix:R,normalizeRanks:F,removeEmptyRanks:D,addBorderNode:z,maxRank:G,partition:V,time:Y,notime:B});function q(e,r){var n,t=V(e,(function(e){return s(e,"barycenter")})),o=t.lhs,i=c(t.rhs,(e=>-e.i)),d=[],u=0,f=0,h=0;for(var v of(o.sort((n=!!r,function(e,r){return e.barycenterr.barycenter?1:n?r.i-e.i:e.i-r.i})),h=W(d,i,h),o))h+=v.vs.length,d.push(v.vs),u+=v.barycenter*v.weight,f+=v.weight,h=W(d,i,h);var l={vs:a(d)};return f&&(l.barycenter=u/f,l.weight=f),l}function W(e,r,n){for(var t;r.length&&(t=d(r)).i<=n;)r.pop(),e.push(t.vs),n++;return n}function $(e,r,n,t){var o=e.children(r),i=e.node(r),d=i?i.borderLeft:void 0,u=i?i.borderRight:void 0,f={};d&&(o=o.filter((e=>e!==d&&e!==u)));var h=k(e,o);for(var c of h)if(e.children(c.v).length){var v=$(e,c.v,n,t);f[c.v]=v,s(v,"barycenter")&&J(c,v)}var l=E(h,n);!function(e,r){for(var n of e)n.vs=a(n.vs.map((function(e){return r[e]?r[e].vs:[e]})))}(l,f);var g=q(l,t);if(d&&(g.vs=[d,...g.vs,u],e.predecessors(d).length)){var p=e.node(e.predecessors(d)[0]),m=e.node(e.predecessors(u)[0]);s(g,"barycenter")||(g.barycenter=0,g.weight=0),g.barycenter=(g.barycenter*g.weight+p.order+m.order)/(g.weight+2),g.weight+=2}return g}function J(e,r){void 0!==e.barycenter?(e.barycenter=(e.barycenter*e.weight+r.barycenter*r.weight)/(e.weight+r.weight),e.weight+=r.weight):(e.barycenter=r.barycenter,e.weight=r.weight)}function Q(e,r,n){var t=function(e){var r;for(;e.hasNode(r=v("_root")););return r}(e),o=new x({compound:!0}).setGraph({root:t}).setDefaultNodeLabel((r=>e.node(r)));for(var i of e.nodes()){var a=e.node(i),d=e.parent(i);if(a.rank===r||a.minRank<=r&&r<=a.maxRank){for(var u of(o.setNode(i),o.setParent(i,d||t),e[n](i))){var f=u.v===i?u.w:u.v,h=o.edge(f,i),c=void 0!==h?h.weight:0;o.setEdge(f,i,{weight:e.edge(u).weight+c})}s(a,"minRank")&&o.setNode(i,{borderLeft:a.borderLeft[r],borderRight:a.borderRight[r]})}}return o}function K(e,r,n){var t,o={};for(var i of n)!function(){for(var n,a=e.parent(i);a;){var s=e.parent(a);if(s?(n=o[s],o[s]=a):(n=t,t=a),n&&n!==a)return void r.setEdge(n,a);a=s}}()}function X(e){var r=G(e),n=H(e,h(1,r+1),"inEdges"),t=H(e,h(r-1,-1),"outEdges"),o=_(e);Z(e,o);for(var i,a=Number.POSITIVE_INFINITY,s=0,d=0;d<4;++s,++d){U(s%2?n:t,s%4>=2);var u=b(e,o=R(e));ue.slice(0))),a=u)}Z(e,i)}function H(e,r,n){return r.map((r=>Q(e,r,n)))}function U(e,r){var n=new x;for(var t of e){var o=t.graph().root,i=$(t,o,n,r);i.vs.map((function(e,r){t.node(e).order=r})),K(t,n,i.vs)}}function Z(e,r){for(var n of r)n.map((function(r,n){e.node(r).order=n}))}var ee=Object.freeze({__proto__:null,order:X,addSubgraphConstraints:K,barycenter:k,buildLayerGraph:Q,crossCount:b,initOrder:_,resolveConflicts:E,sortSubgraph:$,sort:q});function re(e,r){var n={};return r.reduce((function(r,t){for(var o=0,i=0,a=r.length,s=d(t),u=0;ua)&&oe(n,u,s)}}return r.reduce((function(r,n){for(var o,i=-1,a=0,s=0;sn){var t=r;r=n,n=t}var o=e[r];o||(e[r]=o={}),o[n]=!0}function ie(e,r,n){if(r>n){var t=r;r=n,n=t}return s(e[r],n)}function ae(e,r,n,t){var o={},i={},a={};for(var s of r)for(var d=0;da[e]))).length-1)/2,l=Math.floor(v),g=Math.ceil(v);l<=g;++l){var p=h[l];i[f]===f&&ua.predecessors(e))),d((function(r){var n=a.outEdges(r).reduce((function(e,r){return Math.min(e,i[r.w]-a.edge(r))}),Number.POSITIVE_INFINITY),t=e.node(r);n!==Number.POSITIVE_INFINITY&&t.borderType!==s&&(i[r]=Math.max(i[r],n))}),(e=>a.successors(e))),Object.keys(t))){var f=t[u];i[f]=i[n[f]]}return i}function de(e,r){return f(l(r),(function(r){var n=Number.NEGATIVE_INFINITY,t=Number.POSITIVE_INFINITY;for(var o in r){var i=r[o],a=ve(e,o)/2;n=Math.max(i+a,n),t=Math.min(i-a,t)}return n-t}))}function ue(e,r){var n=l(r),t=Math.min(...n),o=Math.max(...n);for(var i of["ul","ur","dl","dr"]){var a=i[1],s=e[i];if(s!==r){var d=l(s),f="l"===a?t-Math.min(...d):o-Math.max(...d);f&&(e[i]=u(s,(e=>e+f)))}}}function fe(e,r){return u(e.ul,(function(n,t){if(r)return e[r.toLowerCase()][t];var o=c([e.ul[t],e.ur[t],e.dl[t],e.dr[t]],(e=>e));return(o[1]+o[2])/2}))}function he(e){var r,n=R(e),t=Object.assign(Object.assign({},re(e,n)),ne(e,n)),o={ul:{},ur:{},dl:{},dr:{}};for(var i of["u","d"])for(var a of(r="u"===i?n:n.map((e=>e)).reverse(),["l","r"])){"r"===a&&(r=r.map((e=>e.map((e=>e)).reverse())));var s=ae(0,r,t,("u"===i?e.predecessors:e.successors).bind(e)),d=se(e,r,s.root,s.align,"r"===a);"r"===a&&(d=u(d,(e=>-e))),o[i+a]=d}return ue(o,de(e,o)),fe(o,e.graph().align)}function ce(e,r,n){return function(t,o,i){var a,d=t.node(o),u=t.node(i),f=0;if(f+=d.width/2,s(d,"labelpos"))switch(d.labelpos.toLowerCase()){case"l":a=-d.width/2;break;case"r":a=d.width/2}if(a&&(f+=n?a:-a),a=0,f+=(d.dummy?r:e)/2,f+=(u.dummy?r:e)/2,f+=u.width/2,s(u,"labelpos"))switch(u.labelpos.toLowerCase()){case"l":a=u.width/2;break;case"r":a=-u.width/2}return a&&(f+=n?a:-a),a=0,f}}function ve(e,r){return e.node(r).width}var le=Object.freeze({__proto__:null,findType1Conflicts:re,findType2Conflicts:ne,findOtherInnerSegmentNode:te,addConflict:oe,hasConflict:ie,verticalAlignment:ae,horizontalCompaction:se,findSmallestWidthAlignment:de,alignCoordinates:ue,balance:fe,positionX:he,sep:ce,width:ve});function ge(e){!function(e){var r=R(e),n=e.graph().ranksep,t=0;for(var o of r){var i=Math.max(...o.map((r=>e.node(r).height)));for(var a of o)e.node(a).y=t+i/2;t+=i+n}}(e=S(e));var r=he(e);for(var n in r)e.node(n).x=r[n]}var pe=Object.freeze({__proto__:null,bk:le,position:ge});function me(e){var r={};e.sources().forEach((function n(t){var o=e.node(t);if(s(r,t))return o.rank;r[t]=!0;var i=Math.min(...e.outEdges(t).map((r=>n(r.w)-e.edge(r).minlen)));return i!==Number.POSITIVE_INFINITY&&null!=i||(i=0),o.rank=i}))}function we(e,r){return e.node(r.w).rank-e.node(r.v).rank-e.edge(r).minlen}function _e(e){var r,n=new x({directed:!1}),t=e.nodes()[0],o=e.nodeCount();for(n.setNode(t,{});i(e)this._arr[n].priority)throw new Error("New priority is greater than current priority. Key: "+e+" Old: "+this._arr[n].priority+" New: "+r);this._arr[n].priority=r,this._decrease(n)}_heapify(e){var r=this._arr,n=2*e,t=n+1,o=e;n>1].priority1;function ke(e,r,n,t){return function(e,r,n,t){var o,i,a={},s=new be,d=function(e){var r=e.v!==o?e.v:e.w,t=a[r],d=n(e),u=i.distance+d;if(d<0)throw new Error("dijkstra does not allow negative edge weights. Bad edge: "+e+" Weight: "+d);u0&&(o=s.removeMin(),(i=a[o]).distance!==Number.POSITIVE_INFINITY);)t(o).forEach(d);return a}(e,String(r),n||ye,t||function(r){return e.outEdges(r)})}function Ee(e){var r=0,n=[],t={},o=[];function i(a){var s=t[a]={onStack:!0,lowlink:r,index:r++};if(n.push(a),e.successors(a).forEach((function(e){e in t?t[e].onStack&&(s.lowlink=Math.min(s.lowlink,t[e].index)):(i(e),s.lowlink=Math.min(s.lowlink,t[e].lowlink))})),s.lowlink===s.index){var d,u=[];do{d=n.pop(),t[d].onStack=!1,u.push(d)}while(a!==d);o.push(u)}}return e.nodes().forEach((function(e){e in t||i(e)})),o}var Ne=()=>1;class xe extends Error{}function Ie(e){var r={},n={},t=[];function o(i){if(i in n)throw new xe;if(!(i in r)){for(var a of(n[i]=!0,r[i]=!0,e.predecessors(i)))o(a);delete n[i],t.push(i)}}for(var i of e.sinks())o(i);if(Object.keys(r).length!==e.nodeCount())throw new xe;return t}function Ce(e,r,n){var t=Array.isArray(r)?r:[r],o=(e.isDirected()?e.successors:e.neighbors).bind(e),i=[],a={};for(var s of t){if(!e.hasNode(s))throw new Error("Graph does not have node: "+s);Oe(e,s,"post"===n,a,o,i)}return i}function Oe(e,r,n,t,o,i){if(!(r in t)){for(var a of(t[r]=!0,n||i.push(r),o(r)))Oe(e,a,n,t,o,i);n&&i.push(r)}}function je(e,r){return Ce(e,r,"post")}function Me(e,r){return Ce(e,r,"pre")}var Le=Object.freeze({__proto__:null,components:function(e){var r,n={},t=[];function o(t){if(!(t in n)){for(var i of(n[t]=!0,r.push(t),e.successors(t)))o(i);for(var a of e.predecessors(t))o(a)}}for(var i of e.nodes())r=[],o(i),r.length&&t.push(r);return t},dijkstra:ke,dijkstraAll:function(e,r,n){var t={};for(var o of e.nodes())t[o]=ke(e,o,r,n);return t},findCycles:function(e){return Ee(e).filter((function(r){return r.length>1||1===r.length&&e.hasEdge(r[0],r[0])}))},floydWarshall:function(e,r,n){return function(e,r,n){var t={},o=e.nodes();return o.forEach((function(e){t[e]={},t[e][e]={distance:0},o.forEach((function(r){e!==r&&(t[e][r]={distance:Number.POSITIVE_INFINITY})})),n(e).forEach((function(n){var o=n.v===e?n.w:n.v,i=r(n);t[e][o]={distance:i,predecessor:e}}))})),o.forEach((function(e){var r=t[e];o.forEach((function(n){var i=t[n];o.forEach((function(n){var t=i[e],o=r[n],a=i[n],s=t.distance+o.distance;s0;){if((n=i.removeMin())in o)t.setEdge(n,o[n]);else{if(s)throw new Error("Input graph is not connected: "+e);s=!0}e.nodeEdges(n).forEach(a)}return t},tarjan:Ee,topsort:Ie});function Te(e){me(e=T(e));var r,n=_e(e);for(Fe(n),Se(n,e);r=ze(n);)Ve(n,e,r,Ge(n,e,r))}function Se(e,r){var n=je(e,e.nodes());for(var t of n=n.slice(0,n.length-1))Pe(e,r,t)}function Pe(e,r,n){var t=e.node(n).parent;e.edge(n,t).cutvalue=Re(e,r,n)}function Re(e,r,n){var t,o,i=e.node(n).parent,a=!0,s=r.edge(n,i),d=0;for(var u of(s||(a=!1,s=r.edge(i,n)),d=s.weight,r.nodeEdges(n))){var f=u.v===n,h=f?u.w:u.v;if(h!==i){var c=f===a,v=r.edge(u).weight;if(d+=c?v:-v,t=n,o=h,e.hasEdge(t,o)){var l=e.edge(n,h).cutvalue;d+=c?-l:l}}}return d}function Fe(e,r){arguments.length<2&&(r=e.nodes()[0]),De(e,{},1,r)}function De(e,r,n,t,o){var i=n,a=e.node(t);for(var d of(r[t]=!0,e.neighbors(t)))s(r,d)||(n=De(e,r,n,d,t));return a.low=i,a.lim=n++,o?a.parent=o:delete a.parent,n}function ze(e){for(var r of e.edges())if(e.edge(r).cutvalue<0)return r}function Ge(e,r,n){var t=n.v,o=n.w;r.hasEdge(t,o)||(t=n.w,o=n.v);var i=e.node(t),a=e.node(o),s=i,d=!1;return i.lim>a.lim&&(s=a,d=!0),f(r.edges().filter((function(r){return d===Ye(e,e.node(r.v),s)&&d!==Ye(e,e.node(r.w),s)})),(e=>we(r,e)))}function Ve(e,r,n,t){var o=n.v,i=n.w;e.removeEdge(o,i),e.setEdge(t.v,t.w,{}),Fe(e),Se(e,r),function(e,r){var n=function(e,r){for(var n of e.nodes())if(!r.node(n).parent)return n;return}(e,r),t=Me(e,n);for(var o of t=t.slice(1)){var i=e.node(o).parent,a=r.edge(o,i),s=!1;a||(a=r.edge(i,o),s=!0),r.node(o).rank=r.node(i).rank+(s?a.minlen:-a.minlen)}}(e,r)}function Ye(e,r,n){return n.low<=r.lim&&r.lim<=n.lim}function Be(e){switch(e.graph().ranker){case"network-simplex":We(e);break;case"tight-tree":qe(e);break;case"longest-path":Ae(e);break;default:We(e)}}Te.initLowLimValues=Fe,Te.initCutValues=Se,Te.calcCutValue=Re,Te.leaveEdge=ze,Te.enterEdge=Ge,Te.exchangeEdges=Ve;var Ae=me;function qe(e){me(e),_e(e)}function We(e){Te(e)}var $e=Object.freeze({__proto__:null,rank:Be,tightTreeRanker:qe,networkSimplexRanker:We,networkSimplex:Te,feasibleTree:_e,longestPath:me}),Je=e=>1;function Qe(e,n){if(e.nodeCount()<=1)return[];var t=function(e,n){var t=new x,o=0,i=0;for(var a of e.nodes())t.setNode(a,{v:a,in:0,out:0});for(var s of e.edges()){var d=t.edge(s.v,s.w)||0,u=n(s),f=d+u;t.setEdge(s.v,s.w,f),i=Math.max(i,t.node(s.v).out+=u),o=Math.max(o,t.node(s.w).in+=u)}var h=g(i+o+3,(()=>new r)),c=o+1;for(var a of t.nodes())Xe(h,c,t.node(a));return{graph:t,buckets:h,zeroIdx:c}}(e,n||Je);return a(function(e,r,n){var t,o=[],i=r[r.length-1],a=r[0];for(;e.nodeCount();){for(;t=a.dequeue();)Ke(e,r,n,t);for(;t=i.dequeue();)Ke(e,r,n,t);if(e.nodeCount())for(var s=r.length-2;s>0;--s)if(t=r[s].dequeue()){o=o.concat(Ke(e,r,n,t,!0));break}}return o}(t.graph,t.buckets,t.zeroIdx).map((r=>e.outEdges(r.v,r.w))))}function Ke(e,r,n,t,o){var i=o?[]:void 0;for(var a of e.inEdges(t.v)){var s=e.edge(a),d=e.node(a.v);o&&i.push({v:a.v,w:a.w}),d.out-=s,Xe(r,n,d)}for(var a of e.outEdges(t.v)){s=e.edge(a);var u=a.w,f=e.node(u);f.in-=s,Xe(r,n,f)}return e.removeNode(t.v),i}function Xe(e,r,n){n.out?n.in?e[n.out-n.in+r].enqueue(n):e[e.length-1].enqueue(n):e[0].enqueue(n)}var He={run:function(e){var r="greedy"===e.graph().acyclicer?Qe(e,function(e){return function(r){return e.edge(r).weight}}(e)):function(e){var r=[],n={},t={};function o(i){if(!s(t,i)){for(var a of(t[i]=!0,n[i]=!0,e.outEdges(i)))s(n,a.w)?r.push(a):o(a.w);delete n[i]}}return e.nodes().forEach(o),r}(e);for(var n of r){var t=e.edge(n);e.removeEdge(n),t.forwardName=n.name,t.reversed=!0,e.setEdge(n.w,n.v,t,v("rev"))}},undo:function(e){for(var r of e.edges()){var n=e.edge(r);if(n.reversed){e.removeEdge(r);var t=n.forwardName;delete n.reversed,delete n.forwardName,e.setEdge(r.w,r.v,n,t)}}}};function Ue(e){e.children().forEach((function r(n){var t=e.children(n),o=e.node(n);if(t.length&&t.forEach(r),s(o,"minRank")){o.borderLeft=[],o.borderRight=[];for(var i=o.minRank,a=o.maxRank+1;id||u>r[o].lim));for(i=o,o=t;(o=e.parent(o))!==i;)s.push(o);return{path:a.concat(s.reverse()),lca:i}}var fr={run:function(e){var r=L(e,"root",{},"_root"),n=function(e){var r={};function n(t,o){var i=e.children(t);if(i&&i.length)for(var a of i)n(a,o+1);r[t]=o}for(var t of e.children())n(t,1);return r}(e),t=Math.max(...l(n))-1,o=2*t+1;for(var i of(e.graph().nestingRoot=r,e.edges()))e.edge(i).minlen*=o;var a=function(e){return e.edges().reduce(((r,n)=>r+e.edge(n).weight),0)}(e)+1;for(var s of e.children())hr(e,r,o,a,t,n,s);e.graph().nodeRankFactor=o},cleanup:function(e){var r=e.graph();for(var n of(e.removeNode(r.nestingRoot),delete r.nestingRoot,e.edges())){e.edge(n).nestingEdge&&e.removeEdge(n)}}};function hr(e,r,n,t,o,i,a){var s=e.children(a);if(s.length){var d=z(e,"_bt"),u=z(e,"_bb"),f=e.node(a);for(var h of(e.setParent(d,a),f.borderTop=d,e.setParent(u,a),f.borderBottom=u,s)){hr(e,r,n,t,o,i,h);var c=e.node(h),v=c.borderTop?c.borderTop:h,l=c.borderBottom?c.borderBottom:h,g=c.borderTop?t:2*t,p=v!==l?1:o-i[a]+1;e.setEdge(d,v,{weight:g,minlen:p,nestingEdge:!0}),e.setEdge(l,u,{weight:g,minlen:p,nestingEdge:!0})}e.parent(a)||e.setEdge(r,d,{weight:0,minlen:o+i[a]})}else a!==r&&e.setEdge(r,a,{weight:0,minlen:n})}function cr(e){return"edge-proxy"==e.dummy}function vr(e){return"selfedge"==e.dummy}var lr=50,gr=20,pr=50,mr="tb",wr=1,_r=1,br=0,yr=0,kr=10,Er="r";function Nr(e={}){var r={};for(var n of Object.keys(e))r[n.toLowerCase()]=e[n];return r}function xr(e){return e.nodes().map((function(r){var n=e.node(r),t=e.parent(r),o={v:r};return void 0!==n&&(o.value=n),void 0!==t&&(o.parent=t),o}))}function Ir(e){return e.edges().map((function(r){var n=e.edge(r),t={v:r.v,w:r.w};return void 0!==r.name&&(t.name=r.name),void 0!==n&&(t.value=n),t}))}var Cr=Object.freeze({__proto__:null,write:function(e){var r={options:{directed:e.isDirected(),multigraph:e.isMultigraph(),compound:e.isCompound()},nodes:xr(e),edges:Ir(e)};return void 0!==e.graph()&&(r.value=JSON.parse(JSON.stringify(e.graph()))),r},read:function(e){var r=new x(e.options).setGraph(e.value);for(var n of e.nodes)r.setNode(n.v,n.value),n.parent&&r.setParent(n.v,n.parent);for(var n of e.edges)r.setEdge({v:n.v,w:n.w,name:n.name},n.value);return r}}),Or={Graph:x,GraphLike:I,alg:Le,json:Cr,PriorityQueue:be};e.Graph=x,e.GraphLike=I,e.PriorityQueue=be,e.acyclic=He,e.addBorderSegments=Ue,e.alg=Le,e.coordinateSystem=er,e.data=o,e.debug=ir,e.graphlib=Or,e.greedyFAS=Qe,e.json=Cr,e.layout=function(e,r){var n=r&&r.debugTiming?Y:B;n("layout",(function(){var r=n(" buildLayoutGraph",(function(){return function(e){var r,n,t,o,i,a,s,d,u,f,h,c,v,l,g,p=new x({multigraph:!0,compound:!0}),m=Nr(e.graph()),w={nodesep:null!==(r=m.nodesep)&&void 0!==r?r:pr,edgesep:null!==(n=m.edgesep)&&void 0!==n?n:gr,ranksep:null!==(t=m.ranksep)&&void 0!==t?t:lr,marginx:+(null!==(o=m.marginx)&&void 0!==o?o:0),marginy:+(null!==(i=m.marginy)&&void 0!==i?i:0),acyclicer:m.acyclicer,ranker:null!==(a=m.ranker)&&void 0!==a?a:"network-simplex",rankdir:null!==(s=m.rankdir)&&void 0!==s?s:mr,align:m.align};for(var _ of(p.setGraph(w),e.nodes())){var b=Nr(e.node(_)),y={width:+(null!==(d=b&&b.width)&&void 0!==d?d:0),height:+(null!==(u=b&&b.height)&&void 0!==u?u:0)};p.setNode(_,y),p.setParent(_,e.parent(_))}for(var k of e.edges()){var E=Nr(e.edge(k)),N={minlen:null!==(f=E.minlen)&&void 0!==f?f:wr,weight:null!==(h=E.weight)&&void 0!==h?h:_r,width:null!==(c=E.width)&&void 0!==c?c:br,height:null!==(v=E.height)&&void 0!==v?v:yr,labeloffset:null!==(l=E.labeloffset)&&void 0!==l?l:kr,labelpos:null!==(g=E.labelpos)&&void 0!==g?g:Er};p.setEdge(k,N)}return p}(e)}));n(" runLayout",(function(){!function(e,r){r(" makeSpaceForEdgeLabels",(function(){!function(e){var r=e.graph();for(var n of(r.ranksep/=2,e.edges())){var t=e.edge(n);t.minlen*=2,"c"!==t.labelpos.toLowerCase()&&("TB"===r.rankdir||"BT"===r.rankdir?t.width+=t.labeloffset:t.height+=t.labeloffset)}}(e)})),r(" removeSelfEdges",(function(){!function(e){for(var r of e.edges())if(r.v===r.w){var n=e.node(r.v);n.selfEdges||(n.selfEdges=[]),n.selfEdges.push({e:r,label:e.edge(r)}),e.removeEdge(r)}}(e)})),r(" acyclic",(function(){He.run(e)})),r(" nestingGraph.run",(function(){fr.run(e)})),r(" rank",(function(){Be(S(e))})),r(" injectEdgeLabelProxies",(function(){!function(e){for(var r of e.edges()){var n=e.edge(r);if(n.width&&n.height){var t=e.node(r.v),o=e.node(r.w);L(e,"edge-proxy",{rank:(o.rank-t.rank)/2+t.rank,e:r},"_ep")}}}(e)})),r(" removeEmptyRanks",(function(){D(e)})),r(" nestingGraph.cleanup",(function(){fr.cleanup(e)})),r(" normalizeRanks",(function(){F(e)})),r(" assignRankMinMax",(function(){!function(e){var r=0;for(var n of e.nodes()){var t=e.node(n);t.borderTop&&(t.minRank=e.node(t.borderTop).rank,t.maxRank=e.node(t.borderBottom).rank,r=Math.max(r,t.maxRank))}e.graph().maxRank=r}(e)})),r(" removeEdgeLabelProxies",(function(){!function(e){for(var r of e.nodes()){var n=e.node(r);cr(n)&&(e.edge(n.e).labelRank=n.rank,e.removeNode(r))}}(e)})),r(" normalize.run",(function(){ar.run(e)})),r(" parentDummyChains",(function(){dr(e)})),r(" addBorderSegments",(function(){Ue(e)})),r(" order",(function(){X(e)})),r(" insertSelfEdges",(function(){!function(e){var r,n=R(e);for(var t of n)for(var o=0,i=0;i e.width ?? 0)); + clas.height = sum(clas.parts, (e) => e.height ?? 0 ?? 0); + clas.dividers = []; + let y = 0; + for (const comp of clas.parts) { + comp.x = 0 + offset.x; + comp.y = y + offset.y; + comp.width = clas.width; + y += comp.height ?? 0 ?? 0; + if (comp != last(clas.parts)) + clas.dividers.push([ + { x: 0, y: y }, + { x: clas.width, y: y }, + ]); + } + } + function box(config, clas) { + offsetBox(config, clas, { x: 0, y: 0 }); + } + function icon(config, clas) { + clas.dividers = []; + clas.parts = []; + clas.width = config.fontSize * 2.5; + clas.height = config.fontSize * 2.5; + } + function labelledIcon(config, clas) { + clas.width = config.fontSize * 1.5; + clas.height = config.fontSize * 1.5; + clas.dividers = []; + let y = config.direction == 'LR' ? clas.height - config.padding : -clas.height / 2; + for (const comp of clas.parts) { + if (config.direction == 'LR') { + comp.x = clas.width / 2 - (comp.width ?? 0) / 2; + comp.y = y; + } + else { + comp.x = clas.width / 2 + config.padding / 2; + comp.y = y; + } + y += comp.height ?? 0 ?? 0; + } + } + const layouters = { + actor: function (config, clas) { + clas.width = Math.max(config.padding * 2, ...clas.parts.map((e) => e.width ?? 0)); + clas.height = config.padding * 3 + sum(clas.parts, (e) => e.height ?? 0); + clas.dividers = []; + let y = config.padding * 3; + for (const comp of clas.parts) { + comp.x = 0; + comp.y = y; + comp.width = clas.width; + y += comp.height ?? 0; + if (comp != last(clas.parts)) + clas.dividers.push([ + { x: config.padding, y: y }, + { x: clas.width - config.padding, y: y }, + ]); + } + }, + class: box, + database: function (config, clas) { + clas.width = Math.max(...clas.parts.map((e) => e.width ?? 0)); + clas.height = sum(clas.parts, (e) => e.height ?? 0) + config.padding * 2; + clas.dividers = []; + let y = config.padding * 1.5; + for (const comp of clas.parts) { + comp.x = 0; + comp.y = y; + comp.width = clas.width; + y += comp.height ?? 0; + if (comp != last(clas.parts)) { + const path = range([0, Math.PI], 16).map((a) => ({ + x: clas.width * 0.5 * (1 - Math.cos(a)), + y: y + config.padding * (0.75 * Math.sin(a) - 0.5), + })); + clas.dividers.push(path); + } + } + }, + ellipse: function (config, clas) { + const width = Math.max(...clas.parts.map((e) => e.width ?? 0)); + const height = sum(clas.parts, (e) => e.height ?? 0); + clas.width = width * 1.25; + clas.height = height * 1.25; + clas.dividers = []; + let y = height * 0.125; + const sq = (x) => x * x; + const rimPos = (y) => Math.sqrt(sq(0.5) - sq(y / clas.height - 0.5)) * clas.width; + for (const comp of clas.parts) { + comp.x = width * 0.125; + comp.y = y; + comp.width = width; + y += comp.height ?? 0; + if (comp != last(clas.parts)) + clas.dividers.push([ + { x: clas.width / 2 + rimPos(y) - 1, y: y }, + { x: clas.width / 2 - rimPos(y) + 1, y: y }, + ]); + } + }, + end: icon, + frame: function (config, clas) { + const w = clas.parts[0].width ?? 0; + const h = clas.parts[0].height ?? 0; + clas.parts[0].width = h / 2 + (clas.parts[0].width ?? 0); + box(config, clas); + if (clas.dividers?.length) + clas.dividers.shift(); + clas.dividers?.unshift([ + { x: 0, y: h }, + { x: w - h / 4, y: h }, + { x: w + h / 4, y: h / 2 }, + { x: w + h / 4, y: 0 }, + ]); + }, + hidden: function (config, clas) { + clas.dividers = []; + clas.parts = []; + clas.width = 1; + clas.height = 1; + }, + input: box, + lollipop: labelledIcon, + none: box, + note: box, + package: box, + pipe: function box(config, clas) { + offsetBox(config, clas, { x: -config.padding / 2, y: 0 }); + }, + receiver: box, + rhomb: function (config, clas) { + const width = Math.max(...clas.parts.map((e) => e.width ?? 0)); + const height = sum(clas.parts, (e) => e.height ?? 0); + clas.width = width * 1.5; + clas.height = height * 1.5; + clas.dividers = []; + let y = height * 0.25; + for (const comp of clas.parts) { + comp.x = width * 0.25; + comp.y = y; + comp.width = width; + y += comp.height ?? 0; + const slope = clas.width / clas.height; + if (comp != last(clas.parts)) + clas.dividers.push([ + { + x: clas.width / 2 + (y < clas.height / 2 ? y * slope : (clas.height - y) * slope), + y: y, + }, + { + x: clas.width / 2 - (y < clas.height / 2 ? y * slope : (clas.height - y) * slope), + y: y, + }, + ]); + } + }, + roundrect: box, + sender: box, + socket: labelledIcon, + start: icon, + sync: function (config, clas) { + clas.dividers = []; + clas.parts = []; + if (config.direction == 'LR') { + clas.width = config.lineWidth * 3; + clas.height = config.fontSize * 5; + } + else { + clas.width = config.fontSize * 5; + clas.height = config.lineWidth * 3; + } + }, + table: function (config, clas) { + if (clas.parts.length == 1) { + box(config, clas); + return; + } + const gridcells = clas.parts.slice(1); + const rows = [[]]; + function isRowBreak(e) { + return !e.lines.length && !e.nodes.length && !e.assocs.length; + } + function isRowFull(e) { + const current = last(rows); + return rows[0] != current && rows[0].length == current.length; + } + function isEnd(e) { + return e == last(gridcells); + } + for (const comp of gridcells) { + if (!isEnd(comp) && isRowBreak(comp) && last(rows).length) { + rows.push([]); + } + else if (isRowFull()) { + rows.push([comp]); + } + else { + last(rows).push(comp); + } + } + const header = clas.parts[0]; + const cellW = Math.max((header.width ?? 0) / rows[0].length, ...gridcells.map((e) => e.width ?? 0)); + const cellH = Math.max(...gridcells.map((e) => e.height ?? 0)); + clas.width = cellW * rows[0].length; + clas.height = (header.height ?? 0) + cellH * rows.length; + const hh = header.height ?? 0; + clas.dividers = [ + [ + { x: 0, y: header.height ?? 0 }, + { x: 0, y: header.height ?? 0 }, + ], + ...rows.map((e, i) => [ + { x: 0, y: hh + i * cellH }, + { x: clas.width ?? 0, y: hh + i * cellH }, + ]), + ...rows[0].map((e, i) => [ + { x: (i + 1) * cellW, y: hh }, + { x: (i + 1) * cellW, y: clas.height }, + ]), + ]; + header.x = 0; + header.y = 0; + header.width = clas.width; + for (let i = 0; i < rows.length; i++) { + for (let j = 0; j < rows[i].length; j++) { + const cell = rows[i][j]; + cell.x = j * cellW; + cell.y = hh + i * cellH; + cell.width = cellW; + } + } + clas.parts = clas.parts.filter((e) => !isRowBreak(e)); + }, + transceiver: box, + }; + const visualizers = { + actor: function (node, x, y, config, g) { + const a = config.padding / 2; + const yp = y + a * 4; + const faceCenter = { x: node.x, y: yp - a }; + g.circle(faceCenter, a).fillAndStroke(); + g.path([ + { x: node.x, y: yp }, + { x: node.x, y: yp + 2 * a }, + ]).stroke(); + g.path([ + { x: node.x - a, y: yp + a }, + { x: node.x + a, y: yp + a }, + ]).stroke(); + g.path([ + { x: node.x - a, y: yp + a + config.padding }, + { x: node.x, y: yp + config.padding }, + { x: node.x + a, y: yp + a + config.padding }, + ]).stroke(); + }, + class: function (node, x, y, config, g) { + g.rect(x, y, node.width, node.height).fillAndStroke(); + }, + database: function (node, x, y, config, g) { + const pad = config.padding; + const cy = y - pad / 2; + const pi = 3.1416; + g.rect(x, y + pad, node.width, node.height - pad * 2).fill(); + g.path([ + { x: x, y: cy + pad * 1.5 }, + { x: x, y: cy - pad * 0.5 + node.height }, + ]).stroke(); + g.path([ + { x: x + node.width, y: cy + pad * 1.5 }, + { x: x + node.width, y: cy - pad * 0.5 + node.height }, + ]).stroke(); + g.ellipse({ x: node.x, y: cy + pad * 1.5 }, node.width, pad * 1.5).fillAndStroke(); + g.ellipse({ x: node.x, y: cy - pad * 0.5 + node.height }, node.width, pad * 1.5, 0, pi).fillAndStroke(); + }, + ellipse: function (node, x, y, config, g) { + g.ellipse({ x: node.x, y: node.y }, node.width, node.height).fillAndStroke(); + }, + end: function (node, x, y, config, g) { + g.circle({ x: node.x, y: y + node.height / 2 }, node.height / 3).fillAndStroke(); + g.fillStyle(config.stroke); + g.circle({ x: node.x, y: y + node.height / 2 }, node.height / 3 - config.padding / 2).fill(); + }, + frame: function (node, x, y, config, g) { + g.rect(x, y, node.width, node.height).fillAndStroke(); + }, + hidden: function (node, x, y, config, g) { }, + input: function (node, x, y, config, g) { + g.circuit([ + { x: x + config.padding, y: y }, + { x: x + node.width, y: y }, + { x: x + node.width - config.padding, y: y + node.height }, + { x: x, y: y + node.height }, + ]).fillAndStroke(); + }, + lollipop: function (node, x, y, config, g) { + g.circle({ x: node.x, y: y + node.height / 2 }, node.height / 2.5).fillAndStroke(); + }, + none: function (node, x, y, config, g) { }, + note: function (node, x, y, config, g) { + g.circuit([ + { x: x, y: y }, + { x: x + node.width - config.padding, y: y }, + { x: x + node.width, y: y + config.padding }, + { x: x + node.width, y: y + node.height }, + { x: x, y: y + node.height }, + { x: x, y: y }, + ]).fillAndStroke(); + g.path([ + { x: x + node.width - config.padding, y: y }, + { x: x + node.width - config.padding, y: y + config.padding }, + { x: x + node.width, y: y + config.padding }, + ]).stroke(); + }, + package: function (node, x, y, config, g) { + const headHeight = node.parts[0].height ?? 0; + g.rect(x, y + headHeight, node.width, node.height - headHeight).fillAndStroke(); + const w = g.measureText(node.parts[0].lines[0]).width + 2 * config.padding; + g.circuit([ + { x: x, y: y + headHeight }, + { x: x, y: y }, + { x: x + w, y: y }, + { x: x + w, y: y + headHeight }, + ]).fillAndStroke(); + }, + pipe: function (node, x, y, config, g) { + const pad = config.padding; + const pi = 3.1416; + g.rect(x, y, node.width, node.height).fill(); + g.path([ + { x: x, y: y }, + { x: x + node.width, y: y }, + ]).stroke(); + g.path([ + { x: x, y: y + node.height }, + { x: x + node.width, y: y + node.height }, + ]).stroke(); + g.ellipse({ x: x + node.width, y: node.y }, pad * 1.5, node.height).fillAndStroke(); + g.ellipse({ x: x, y: node.y }, pad * 1.5, node.height, pi / 2, (pi * 3) / 2).fillAndStroke(); + }, + receiver: function (node, x, y, config, g) { + g.circuit([ + { x: x - config.padding, y: y }, + { x: x + node.width, y: y }, + { x: x + node.width, y: y + node.height }, + { x: x - config.padding, y: y + node.height }, + { x: x, y: y + node.height / 2 }, + ]).fillAndStroke(); + }, + rhomb: function (node, x, y, config, g) { + g.circuit([ + { x: node.x, y: y }, + { x: x + node.width, y: node.y }, + { x: node.x, y: y + node.height }, + { x: x, y: node.y }, + ]).fillAndStroke(); + }, + roundrect: function (node, x, y, config, g) { + const r = Math.min(config.padding * 2 * config.leading, node.height / 2); + g.roundRect(x, y, node.width, node.height, r).fillAndStroke(); + }, + sender: function (node, x, y, config, g) { + g.circuit([ + { x: x, y: y }, + { x: x + node.width - config.padding, y: y }, + { x: x + node.width, y: y + node.height / 2 }, + { x: x + node.width - config.padding, y: y + node.height }, + { x: x, y: y + node.height }, + ]).fillAndStroke(); + }, + socket: function (node, x, y, config, g) { + const from = config.direction === 'TB' ? Math.PI : Math.PI / 2; + const to = config.direction === 'TB' ? 2 * Math.PI : -Math.PI / 2; + g.ellipse({ x: node.x, y: node.y }, node.width, node.height, from, to).stroke(); + }, + start: function (node, x, y, config, g) { + g.fillStyle(config.stroke); + g.circle({ x: node.x, y: y + node.height / 2 }, node.height / 2.5).fill(); + }, + sync: function (node, x, y, config, g) { + g.fillStyle(config.stroke); + g.rect(x, y, node.width, node.height).fillAndStroke(); + }, + table: function (node, x, y, config, g) { + g.rect(x, y, node.width, node.height).fillAndStroke(); + }, + transceiver: function (node, x, y, config, g) { + g.circuit([ + { x: x - config.padding, y: y }, + { x: x + node.width - config.padding, y: y }, + { x: x + node.width, y: y + node.height / 2 }, + { x: x + node.width - config.padding, y: y + node.height }, + { x: x - config.padding, y: y + node.height }, + { x: x, y: y + node.height / 2 }, + ]).fillAndStroke(); + }, + }; + + function layout(measurer, config, ast) { + function measureLines(lines, fontWeight) { + if (!lines.length) + return { width: 0, height: config.padding }; + measurer.setFont(config.font, config.fontSize, fontWeight, 'normal'); + return { + width: Math.round(Math.max(...lines.map(measurer.textWidth)) + 2 * config.padding), + height: Math.round(measurer.textHeight() * lines.length + 2 * config.padding), + }; + } + function layoutCompartment(c, compartmentIndex, style) { + const textSize = measureLines(c.lines, compartmentIndex ? 'normal' : 'bold'); + if (!c.nodes.length && !c.assocs.length) { + const layoutedPart = c; + layoutedPart.width = textSize.width; + layoutedPart.height = textSize.height; + layoutedPart.offset = { x: config.padding, y: config.padding }; + return; + } + const styledConfig = { + ...config, + direction: style.direction ?? config.direction, + }; + const layoutedNodes = c.nodes; + const layoutedAssoc = c.assocs; + for (let i = 0; i < layoutedAssoc.length; i++) + layoutedAssoc[i].id = `${i}`; + for (const e of layoutedNodes) + layoutNode(e, styledConfig); + const g = new graphre.graphlib.Graph({ + multigraph: true, + }); + g.setGraph({ + rankdir: style.direction || config.direction, + nodesep: config.spacing, + edgesep: config.spacing, + ranksep: config.spacing, + acyclicer: config.acyclicer, + ranker: config.ranker, + }); + for (const e of layoutedNodes) { + g.setNode(e.id, { width: e.layoutWidth, height: e.layoutHeight }); + } + for (const r of layoutedAssoc) { + if (r.type.indexOf('_') > -1) { + g.setEdge(r.start, r.end, { minlen: 0 }, r.id); + } + else if ((config.gravity ?? 1) != 1) { + g.setEdge(r.start, r.end, { minlen: config.gravity }, r.id); + } + else { + g.setEdge(r.start, r.end, {}, r.id); + } + } + graphre.layout(g); + const rels = indexBy(c.assocs, 'id'); + const nodes = indexBy(c.nodes, 'id'); + for (const name of g.nodes()) { + const node = g.node(name); + nodes[name].x = node.x; + nodes[name].y = node.y; + } + let left = 0; + let right = 0; + let top = 0; + let bottom = 0; + for (const edgeObj of g.edges()) { + const edge = g.edge(edgeObj); + const start = nodes[edgeObj.v]; + const end = nodes[edgeObj.w]; + const rel = rels[edgeObj.name]; + rel.path = [start, ...edge.points, end].map(toPoint); + const startP = rel.path[1]; + const endP = rel.path[rel.path.length - 2]; + layoutLabel(rel.startLabel, startP, adjustQuadrant(quadrant(startP, start) ?? 4, start, end)); + layoutLabel(rel.endLabel, endP, adjustQuadrant(quadrant(endP, end) ?? 2, end, start)); + left = Math.min(left, rel.startLabel.x, rel.endLabel.x, ...edge.points.map((e) => e.x), ...edge.points.map((e) => e.x)); + right = Math.max(right, rel.startLabel.x + rel.startLabel.width, rel.endLabel.x + rel.endLabel.width, ...edge.points.map((e) => e.x)); + top = Math.min(top, rel.startLabel.y, rel.endLabel.y, ...edge.points.map((e) => e.y)); + bottom = Math.max(bottom, rel.startLabel.y + rel.startLabel.height, rel.endLabel.y + rel.endLabel.height, ...edge.points.map((e) => e.y)); + } + const graph = g.graph(); + const width = Math.max(graph.width + (left < 0 ? -left : 0), right - left); + const height = Math.max(graph.height + (top < 0 ? -top : 0), bottom - top); + const graphHeight = height ? height + 2 * config.gutter : 0; + const graphWidth = width ? width + 2 * config.gutter : 0; + const part = c; + part.width = Math.max(textSize.width, graphWidth) + 2 * config.padding; + part.height = textSize.height + graphHeight + config.padding; + part.offset = { x: config.padding - left, y: config.padding - top }; + } + function toPoint(o) { + return { x: o.x, y: o.y }; + } + function layoutLabel(label, point, quadrant) { + if (!label.text) { + label.width = 0; + label.height = 0; + label.x = point.x; + label.y = point.y; + } + else { + const fontSize = config.fontSize; + const lines = label.text.split('`'); + label.width = Math.max(...lines.map((l) => measurer.textWidth(l))); + label.height = fontSize * lines.length; + label.x = + point.x + (quadrant == 1 || quadrant == 4 ? config.padding : -label.width - config.padding); + label.y = + point.y + (quadrant == 3 || quadrant == 4 ? config.padding : -label.height - config.padding); + } + } + function quadrant(point, node) { + if (point.x < node.x && point.y < node.y) + return 1; + if (point.x > node.x && point.y < node.y) + return 2; + if (point.x > node.x && point.y > node.y) + return 3; + if (point.x < node.x && point.y > node.y) + return 4; + return undefined; + } + function adjustQuadrant(quadrant, point, opposite) { + if (opposite.x == point.x || opposite.y == point.y) + return quadrant; + const flipHorizontally = [4, 3, 2, 1]; + const flipVertically = [2, 1, 4, 3]; + const oppositeQuadrant = opposite.y < point.y ? (opposite.x < point.x ? 2 : 1) : opposite.x < point.x ? 3 : 4; + if (oppositeQuadrant === quadrant) { + if (config.direction === 'LR') + return flipHorizontally[quadrant - 1]; + if (config.direction === 'TB') + return flipVertically[quadrant - 1]; + } + return quadrant; + } + function layoutNode(node, config) { + const style = config.styles[node.type] || styles.class; + for (let i = 0; i < node.parts.length; i++) { + layoutCompartment(node.parts[i], i, style); + } + const visual = layouters[style.visual] ?? layouters.class; + visual(config, node); + node.layoutWidth = (node.width ?? 0) + 2 * config.edgeMargin; + node.layoutHeight = (node.height ?? 0) + 2 * config.edgeMargin; + } + const root = ast; + layoutCompartment(root, 0, styles.class); + return root; + } + + function extractDirectives(source) { + const directives = []; + for (const line of source.split('\n')) { + if (line[0] === '#') { + const [key, ...values] = line.slice(1).split(':'); + directives.push({ key, value: values.join(':').trim() }); + } + } + return directives; + } + function linearParse(source) { + let line = 1; + let lineStartIndex = 0; + let index = 0; + const directives = extractDirectives(source); + source = source.replace(/^[ \t]*\/\/[^\n]*/gm, '').replace(/^#[^\n]*/gm, ''); + if (source.trim() === '') + return { + root: { nodes: [], assocs: [], lines: [] }, + directives, + }; + const part = parsePart(); + if (index < source.length) + error('end of file', source[index]); + return { root: part, directives }; + function advanceLineCounter() { + line++; + lineStartIndex = index; + } + function addNode(nodes, node) { + const i = nodes.findIndex((e) => e.id === node.id); + if (i === -1) + nodes.push(node); + else if (nodes[i].parts.length < node.parts.length) + nodes[i] = node; + } + function parsePart() { + const nodes = []; + const assocs = []; + const lines = []; + while (index < source.length) { + let lastIndex = index; + discard(/ /); + if (source[index] === '\n') { + pop(); + advanceLineCounter(); + } + else if (source[index] === ';') { + pop(); + } + else if (source[index] == '|' || source[index] == ']') { + return { nodes, assocs, lines }; + } + else if (source[index] == '[') { + const extracted = parseNodesAndAssocs(); + for (const node of extracted.nodes) + addNode(nodes, node); + for (const assoc of extracted.assocs) + assocs.push(assoc); + } + else { + const text = parseLine().trim(); + if (text) + lines.push(text); + } + if (index === lastIndex) + throw new Error('Infinite loop'); + } + return { nodes, assocs, lines }; + } + function parseNodesAndAssocs() { + const nodes = []; + const assocs = []; + let node = parseNode(); + addNode(nodes, node); + while (index < source.length) { + let lastIndex = index; + discard(/ /); + if (isOneOf('\n', ']', '|', ';')) { + return { nodes, assocs }; + } + else { + const { association, target } = parseAssociation(node); + assocs.push(association); + addNode(nodes, target); + node = target; + } + if (index === lastIndex) + throw new Error('Infinite loop'); + } + return { nodes, assocs }; + } + function transformEscapes(char) { + if (char === 'n') + return '\n'; + return char; + } + function parseAssociation(fromNode) { + let startLabel = ''; + while (index < source.length) { + let lastIndex = index; + if (isOneOf('\\')) { + pop(); + startLabel += transformEscapes(pop()); + } + if (isOneOf('(o-', '(-', 'o<-', 'o-', '+-', '<:-', '<-', '-')) + break; + else if (isOneOf('[', ']', '|', '<', '>', ';')) + error('label', source[index]); + else + startLabel += pop(); + if (index === lastIndex) + throw new Error('Infinite loop'); + } + const assoc1 = consumeOneOf('(o', '(', 'o<', 'o', '+', '<:', '<', ''); + const assoc2 = consumeOneOf('--', '-/-', '-'); + const assoc3 = consumeOneOf('o)', 'o', '>o', '>', ')', '+', ':>', ''); + const endLabel = consumeOptional(/[^\[]/); + const target = parseNode(); + return { + association: { + type: `${assoc1}${assoc2}${assoc3}`, + start: fromNode.id, + end: target.id, + startLabel: { text: startLabel.trim() }, + endLabel: { text: endLabel.trim() }, + }, + target: target, + }; + } + function parseNode() { + index++; + let attr = {}; + let type = 'class'; + if (source[index] == '<') { + const meta = parseMeta(); + attr = meta.attr; + type = meta.type ?? 'class'; + } + const parts = [parsePart()]; + while (source[index] == '|') { + let lastIndex = index; + pop(); + parts.push(parsePart()); + if (lastIndex === index) + throw new Error('Infinite loop'); + } + if (source[index] == ']') { + pop(); + discard(/ /); + return { parts: parts, attr, id: attr.id ?? parts[0].lines[0], type }; + } + error(']', source[index]); + } + function parseLine() { + const chars = []; + while (index < source.length) { + let lastIndex = index; + if (source[index] === '\\') { + pop(); + chars.push(transformEscapes(pop())); + } + else if (source[index].match(/[\[\]|;\n]/)) { + break; + } + else { + chars.push(pop()); + } + if (lastIndex === index) + throw new Error('Infinite loop'); + } + return chars.join(''); + } + function parseMeta() { + index++; + const type = consume(/[a-zA-Z0-9_]/); + const char = pop(); + if (char == '>') + return { type, attr: {} }; + if (char != ' ') + error([' ', '>'], char); + return { type, attr: parseAttrs() }; + } + function parseAttrs() { + const key = consume(/[a-zA-Z0-9_]/); + const separator = pop(); + if (separator != '=') + error('=', separator); + const value = consume(/[^> ]/); + const char = pop(); + if (char == '>') + return { [key]: value }; + if (char == ' ') + return { [key]: value, ...parseAttrs() }; + error([' ', '>'], char); + } + function pop() { + const char = source[index]; + index++; + return char; + } + function discard(regex) { + while (source[index]?.match(regex)) + index++; + } + function consume(regex, optional) { + const start = index; + while (source[index]?.match(regex)) + index++; + const end = index; + if (!optional && start == end) + error(regex, source[index]); + return source.slice(start, end); + } + function consumeOptional(regex) { + return consume(regex, 'optional'); + } + function isOneOf(...patterns) { + for (const pattern of patterns) { + const token = source.slice(index, index + pattern.length); + if (token == pattern) { + return true; + } + } + return false; + } + function consumeOneOf(...patterns) { + for (const pattern of patterns) { + const token = source.slice(index, index + pattern.length); + if (token == pattern) { + index += pattern.length; + return pattern; + } + } + const maxPatternLength = Math.max(...patterns.map((e) => e.length)); + if (index + 1 >= source.length) + error(patterns, undefined); + else + error(patterns, source.slice(index + 1, maxPatternLength)); + } + function error(expected, actual) { + throw new ParseError(expected, actual, line, index - lineStartIndex); + } + } + function serializeValue(value) { + if (value == null) + return 'end of file'; + if (value instanceof RegExp) + return value.toString().slice(1, -1); + if (Array.isArray(value)) + return value.map(serializeValue).join(' or '); + return JSON.stringify(value); + } + class ParseError extends Error { + constructor(expected, actual, line, column) { + const exp = serializeValue(expected); + const act = serializeValue(actual); + super(`Parse error at line ${line} column ${column}, expected ${exp} but got ${act}`); + this.expected = exp; + this.actual = act; + this.line = line; + this.column = column; + } + } + + function parse(source) { + const { root, directives } = linearParse(source); + return { root, directives, config: getConfig(directives) }; + function directionToDagre(word) { + if (word == 'down') + return 'TB'; + if (word == 'right') + return 'LR'; + else + return 'TB'; + } + function parseRanker(word) { + if (word == 'network-simplex' || word == 'tight-tree' || word == 'longest-path') { + return word; + } + return 'network-simplex'; + } + function parseCustomStyle(styleDef) { + const floatingKeywords = styleDef.replace(/[a-z]*=[^ ]+/g, ''); + const titleDef = last(styleDef.match('title=([^ ]*)') || ['']); + const bodyDef = last(styleDef.match('body=([^ ]*)') || ['']); + return { + title: { + bold: titleDef.includes('bold') || floatingKeywords.includes('bold'), + underline: titleDef.includes('underline') || floatingKeywords.includes('underline'), + italic: titleDef.includes('italic') || floatingKeywords.includes('italic'), + center: !(titleDef.includes('left') || styleDef.includes('align=left')), + }, + body: { + bold: bodyDef.includes('bold'), + underline: bodyDef.includes('underline'), + italic: bodyDef.includes('italic'), + center: bodyDef.includes('center'), + }, + dashed: styleDef.includes('dashed'), + fill: last(styleDef.match('fill=([^ ]*)') || []), + stroke: last(styleDef.match('stroke=([^ ]*)') || []), + visual: (last(styleDef.match('visual=([^ ]*)') || []) || 'class'), + direction: directionToDagre(last(styleDef.match('direction=([^ ]*)') || [])), + }; + } + function getConfig(directives) { + const d = Object.fromEntries(directives.map((e) => [e.key, e.value])); + const userStyles = {}; + for (const key in d) { + if (key[0] != '.') + continue; + const styleDef = d[key]; + userStyles[key.substring(1)] = parseCustomStyle(styleDef); + } + return { + arrowSize: +d.arrowSize || 1, + bendSize: +d.bendSize || 0.3, + direction: directionToDagre(d.direction), + gutter: +d.gutter || 20, + edgeMargin: +d.edgeMargin || 0, + gravity: Math.round(+(d.gravity ?? 1)), + edges: d.edges == 'hard' ? 'hard' : 'rounded', + fill: (d.fill || '#eee8d5;#fdf6e3;#eee8d5;#fdf6e3').split(';'), + background: d.background || 'transparent', + fillArrows: d.fillArrows === 'true', + font: d.font || 'Helvetica', + fontSize: +d.fontSize || 12, + leading: +d.leading || 1.35, + lineWidth: +d.lineWidth || 3, + padding: +d.padding || 8, + spacing: +d.spacing || 40, + stroke: d.stroke || '#33322E', + title: d.title || '', + zoom: +d.zoom || 1, + acyclicer: d.acyclicer === 'greedy' ? 'greedy' : undefined, + ranker: parseRanker(d.ranker), + styles: { ...styles, ...userStyles }, + }; + } + } + + function add(a, b) { + return { x: a.x + b.x, y: a.y + b.y }; + } + function diff(a, b) { + return { x: a.x - b.x, y: a.y - b.y }; + } + function mult(v, factor) { + return { x: factor * v.x, y: factor * v.y }; + } + function mag(v) { + return Math.sqrt(v.x * v.x + v.y * v.y); + } + function normalize(v) { + return mult(v, 1 / mag(v)); + } + function rot(a) { + return { x: a.y, y: -a.x }; + } + + const empty = false; + const filled = true; + function getPath(config, r) { + const path = r.path.slice(1, -1); + const endDir = normalize(diff(path[path.length - 2], last(path))); + const startDir = normalize(diff(path[1], path[0])); + const size = (config.spacing * config.arrowSize) / 30; + const head = 0; + const end = path.length - 1; + const copy = path.map((p) => ({ x: p.x, y: p.y })); + const tokens = r.type.split(/[-_]/); + copy[head] = add(copy[head], mult(startDir, size * terminatorSize(tokens[0]))); + copy[end] = add(copy[end], mult(endDir, size * terminatorSize(last(tokens)))); + return copy; + } + function terminatorSize(id) { + if (id === '>' || id === '<') + return 5; + if (id === ':>' || id === '<:') + return 10; + if (id === '+') + return 14; + if (id === 'o') + return 14; + if (id === '(' || id === ')') + return 11; + if (id === '(o' || id === 'o)') + return 11; + if (id === '>o' || id === 'o<') + return 15; + return 0; + } + function drawTerminators(g, config, r) { + const start = r.path[1]; + const end = r.path[r.path.length - 2]; + const path = r.path.slice(1, -1); + const tokens = r.type.split(/[-_]/); + drawArrowEnd(last(tokens), path, end); + drawArrowEnd(tokens[0], path.reverse(), start); + function drawArrowEnd(id, path, end) { + const dir = normalize(diff(path[path.length - 2], last(path))); + const size = (config.spacing * config.arrowSize) / 30; + if (id === '>' || id === '<') + drawArrow(dir, size, filled, end); + else if (id === ':>' || id === '<:') + drawArrow(dir, size, empty, end); + else if (id === '+') + drawDiamond(dir, size, filled, end); + else if (id === 'o') + drawDiamond(dir, size, empty, end); + else if (id === '(' || id === ')') { + drawSocket(dir, size, 11, end); + drawStem(dir, size, 5, end); + } + else if (id === '(o' || id === 'o)') { + drawSocket(dir, size, 11, end); + drawStem(dir, size, 5, end); + drawBall(dir, size, 11, end); + } + else if (id === '>o' || id === 'o<') { + drawArrow(dir, size * 0.75, empty, add(end, mult(dir, size * 10))); + drawStem(dir, size, 8, end); + drawBall(dir, size, 8, end); + } + } + function drawBall(nv, size, stem, end) { + const center = add(end, mult(nv, size * stem)); + g.fillStyle(config.fill[0]); + g.ellipse(center, size * 6, size * 6).fillAndStroke(); + } + function drawStem(nv, size, stem, end) { + const center = add(end, mult(nv, size * stem)); + g.path([center, end]).stroke(); + } + function drawSocket(nv, size, stem, end) { + const base = add(end, mult(nv, size * stem)); + const t = rot(nv); + const socket = range([-Math.PI / 2, Math.PI / 2], 12).map((a) => add(base, add(mult(nv, -6 * size * Math.cos(a)), mult(t, 6 * size * Math.sin(a))))); + g.path(socket).stroke(); + } + function drawArrow(nv, size, isOpen, end) { + const x = (s) => add(end, mult(nv, s * size)); + const y = (s) => mult(rot(nv), s * size); + const arrow = [ + add(x(10), y(4)), + x(isOpen && !config.fillArrows ? 5 : 10), + add(x(10), y(-4)), + end, + ]; + g.fillStyle(isOpen ? config.stroke : config.fill[0]); + g.circuit(arrow).fillAndStroke(); + } + function drawDiamond(nv, size, isOpen, end) { + const x = (s) => add(end, mult(nv, s * size)); + const y = (s) => mult(rot(nv), s * size); + const arrow = [add(x(7), y(4)), x(14), add(x(7), y(-4)), end]; + g.save(); + g.fillStyle(isOpen ? config.stroke : config.fill[0]); + g.circuit(arrow).fillAndStroke(); + g.restore(); + } + } + + function render(graphics, config, compartment) { + const g = graphics; + function renderCompartment(compartment, color, style, level) { + g.save(); + g.translate(compartment.offset.x, compartment.offset.y); + g.fillStyle(color || config.stroke); + for (let i = 0; i < compartment.lines.length; i++) { + const text = compartment.lines[i]; + g.textAlign(style.center ? 'center' : 'left'); + const x = style.center ? compartment.width / 2 - config.padding : 0; + let y = (0.5 + (i + 0.5) * config.leading) * config.fontSize; + if (text) { + g.fillText(text, x, y); + } + if (style.underline) { + const w = g.measureText(text).width; + y += Math.round(config.fontSize * 0.2) + 0.5; + if (style.center) { + g.path([ + { x: x - w / 2, y: y }, + { x: x + w / 2, y: y }, + ]).stroke(); + } + else { + g.path([ + { x: x, y: y }, + { x: x + w, y: y }, + ]).stroke(); + } + g.lineWidth(config.lineWidth); + } + } + g.save(); + g.translate(config.gutter, config.gutter); + for (const r of compartment.assocs) + renderRelation(r); + for (const n of compartment.nodes) + renderNode(n, level); + g.restore(); + g.restore(); + } + function renderNode(node, level) { + const x = node.x - node.width / 2; + const y = node.y - node.height / 2; + const style = config.styles[node.type] || styles.class; + g.save(); + g.setData('name', node.id); + g.setData('compartment', undefined); + g.save(); + g.fillStyle(style.fill || config.fill[level] || last(config.fill)); + g.strokeStyle(style.stroke || config.stroke); + if (style.dashed) { + const dash = Math.max(4, 2 * config.lineWidth); + g.setLineDash([dash, dash]); + } + const drawNode = visualizers[style.visual] || visualizers.class; + drawNode(node, x, y, config, g); + for (const divider of node.dividers) { + g.path(divider.map((e) => add(e, { x, y }))).stroke(); + } + g.restore(); + let partIndex = 0; + for (let part of node.parts) { + const textStyle = part === node.parts[0] ? style.title : style.body; + g.save(); + g.setData('compartment', String(partIndex)); + g.translate(x + part.x, y + part.y); + g.setFont(config.font, config.fontSize, textStyle.bold ? 'bold' : 'normal', textStyle.italic ? 'italic' : 'normal'); + renderCompartment(part, style.stroke, textStyle, level + 1); + partIndex++; + g.restore(); + } + g.restore(); + } + function strokePath(p) { + if (config.edges === 'rounded') { + const radius = config.spacing * config.bendSize; + g.beginPath(); + g.moveTo(p[0].x, p[0].y); + for (let i = 1; i < p.length - 1; i++) { + g.arcTo(p[i].x, p[i].y, p[i + 1].x, p[i + 1].y, radius); + } + g.lineTo(last(p).x, last(p).y); + g.stroke(); + } + else + g.path(p).stroke(); + } + function renderLabel(label) { + if (!label || !label.text) + return; + const fontSize = config.fontSize; + const lines = label.text.split('`'); + for (let i = 0; i < lines.length; i++) { + g.fillText(lines[i], label.x, label.y + fontSize * (i + 1)); + } + } + function renderRelation(r) { + const path = getPath(config, r); + g.fillStyle(config.stroke); + g.setFont(config.font, config.fontSize, 'normal', 'normal'); + renderLabel(r.startLabel); + renderLabel(r.endLabel); + if (r.type !== '-/-') { + if (r.type.includes('--')) { + const dash = Math.max(4, 2 * config.lineWidth); + g.save(); + g.setLineDash([dash, dash]); + strokePath(path); + g.restore(); + } + else + strokePath(path); + } + drawTerminators(g, config, r); + } + function setBackground() { + g.clear(); + g.save(); + g.strokeStyle('transparent'); + g.fillStyle(config.background); + g.rect(0, 0, compartment.width, compartment.height).fill(); + g.restore(); + } + g.save(); + g.scale(config.zoom, config.zoom); + setBackground(); + g.setFont(config.font, config.fontSize, 'bold', 'normal'); + g.lineWidth(config.lineWidth); + g.lineJoin('round'); + g.lineCap('round'); + g.strokeStyle(config.stroke); + renderCompartment(compartment, undefined, buildStyle({}, {}).title, 0); + g.restore(); + } + + function GraphicsCanvas(canvas) { + const ctx = canvas.getContext('2d'); + const twopi = 2 * 3.1416; + let mousePos = { x: 0, y: 0 }; + const chainable = { + stroke: function () { + ctx.stroke(); + return chainable; + }, + fill: function () { + ctx.fill(); + return chainable; + }, + fillAndStroke: function () { + ctx.fill(); + ctx.stroke(); + return chainable; + }, + }; + function tracePath(path, offset, s) { + s = s === undefined ? 1 : s; + offset = offset || { x: 0, y: 0 }; + ctx.beginPath(); + ctx.moveTo(offset.x + s * path[0].x, offset.y + s * path[0].y); + for (let i = 1, len = path.length; i < len; i++) + ctx.lineTo(offset.x + s * path[i].x, offset.y + s * path[i].y); + return chainable; + } + return { + mousePos: function () { + return mousePos; + }, + width: function () { + return canvas.width; + }, + height: function () { + return canvas.height; + }, + clear: function () { + ctx.clearRect(0, 0, canvas.width, canvas.height); + }, + circle: function (p, r) { + ctx.beginPath(); + ctx.arc(p.x, p.y, r, 0, twopi); + return chainable; + }, + ellipse: function (center, rx, ry, start, stop) { + if (start === undefined) + start = 0; + if (stop === undefined) + stop = twopi; + ctx.beginPath(); + ctx.save(); + ctx.translate(center.x, center.y); + ctx.scale(1, ry / rx); + ctx.arc(0, 0, rx / 2, start, stop); + ctx.restore(); + return chainable; + }, + arc: function (x, y, r, start, stop) { + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.arc(x, y, r, start, stop); + return chainable; + }, + roundRect: function (x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + r, r); + ctx.lineTo(x + w, y + h - r); + ctx.arcTo(x + w, y + h, x + w - r, y + h, r); + ctx.lineTo(x + r, y + h); + ctx.arcTo(x, y + h, x, y + h - r, r); + ctx.lineTo(x, y + r); + ctx.arcTo(x, y, x + r, y, r); + ctx.closePath(); + return chainable; + }, + rect: function (x, y, w, h) { + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + w, y); + ctx.lineTo(x + w, y + h); + ctx.lineTo(x, y + h); + ctx.closePath(); + return chainable; + }, + path: tracePath, + circuit: function (path, offset, s) { + tracePath(path, offset, s); + ctx.closePath(); + return chainable; + }, + setFont: function (family, size, weight, style) { + ctx.font = `${weight} ${style} ${size}pt ${family}, Helvetica, sans-serif`; + }, + fillStyle: function (s) { + ctx.fillStyle = s; + }, + strokeStyle: function (s) { + ctx.strokeStyle = s; + }, + textAlign: function (a) { + ctx.textAlign = a; + }, + lineCap: function (cap) { + ctx.lineCap = cap; + }, + lineJoin: function (join) { + ctx.lineJoin = join; + }, + lineWidth: function (w) { + ctx.lineWidth = w; + }, + arcTo: function () { + return ctx.arcTo.apply(ctx, arguments); + }, + beginPath: function () { + return ctx.beginPath.apply(ctx, arguments); + }, + fillText: function () { + return ctx.fillText.apply(ctx, arguments); + }, + lineTo: function () { + return ctx.lineTo.apply(ctx, arguments); + }, + measureText: function () { + return ctx.measureText.apply(ctx, arguments); + }, + moveTo: function () { + return ctx.moveTo.apply(ctx, arguments); + }, + restore: function () { + return ctx.restore.apply(ctx, arguments); + }, + setData: function (name, value) { }, + save: function () { + return ctx.save.apply(ctx, arguments); + }, + scale: function () { + return ctx.scale.apply(ctx, arguments); + }, + setLineDash: function () { + return ctx.setLineDash.apply(ctx, arguments); + }, + stroke: function () { + return ctx.stroke.apply(ctx, arguments); + }, + translate: function () { + return ctx.translate.apply(ctx, arguments); + }, + }; + } + + function toAttrString(obj) { + return Object.entries(obj) + .filter(([_, val]) => val !== undefined) + .map(([key, val]) => `${key}="${xmlEncode(val)}"`) + .join(' '); + } + function xmlEncode(str) { + if ('number' === typeof str) + return str.toFixed(1); + return (str ?? '') + .toString() + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + const charWidths = { "0": 10, "1": 10, "2": 10, "3": 10, "4": 10, "5": 10, "6": 10, "7": 10, "8": 10, "9": 10, " ": 5, "!": 5, "\"": 6, "#": 10, "$": 10, "%": 15, "&": 11, "'": 4, "(": 6, ")": 6, "*": 7, "+": 10, ",": 5, "-": 6, ".": 5, "/": 5, ":": 5, ";": 5, "<": 10, "=": 10, ">": 10, "?": 10, "@": 17, "A": 11, "B": 11, "C": 12, "D": 12, "E": 11, "F": 10, "G": 13, "H": 12, "I": 5, "J": 9, "K": 11, "L": 10, "M": 14, "N": 12, "O": 13, "P": 11, "Q": 13, "R": 12, "S": 11, "T": 10, "U": 12, "V": 11, "W": 16, "X": 11, "Y": 11, "Z": 10, "[": 5, "\\": 5, "]": 5, "^": 8, "_": 10, "`": 6, "a": 10, "b": 10, "c": 9, "d": 10, "e": 10, "f": 5, "g": 10, "h": 10, "i": 4, "j": 4, "k": 9, "l": 4, "m": 14, "n": 10, "o": 10, "p": 10, "q": 10, "r": 6, "s": 9, "t": 5, "u": 10, "v": 9, "w": 12, "x": 9, "y": 9, "z": 9, "{": 6, "|": 5, "}": 6, "~": 10 }; + function GraphicsSvg(document) { + const initialState = { + stroke: undefined, + 'stroke-width': 1, + 'stroke-dasharray': undefined, + 'stroke-linecap': undefined, + 'stroke-linejoin': undefined, + 'text-align': 'left', + font: '12pt Helvetica, Arial, sans-serif', + 'font-size': '12pt', + }; + const measurementCanvas = document + ? document.createElement('canvas') + : null; + const ctx = measurementCanvas ? measurementCanvas.getContext('2d') : null; + class Element { + constructor(name, attr, parent, text) { + this.elideEmpty = false; + this.name = name; + this.attr = attr; + this.parent = parent; + this.children = []; + this.text = text || undefined; + } + stroke() { + this.attr.fill = 'none'; + return this; + } + fill() { + this.attr.stroke = 'none'; + return this; + } + fillAndStroke() { + return this; + } + group() { + return this.parent; + } + serialize() { + const data = getAncestorData(this.group()) ?? {}; + const attrs = toAttrString({ ...this.attr, ...data }); + const content = this.children.map((o) => o.serialize()).join('\n'); + if (this.text && this.children.length === 0) + return `<${this.name} ${attrs}>${xmlEncode(this.text)}`; + else if (this.children.length === 0) + return this.elideEmpty ? '' : `<${this.name} ${attrs}>`; + else + return `<${this.name} ${attrs}> + ${content.replace(/\n/g, '\n\t')} +`; + } + } + function getAncestorData(group) { + if (!group) + return syntheticRoot.data; + return { ...getAncestorData(group.parent), ...group.data }; + } + function getDefined(group, getter) { + if (!group) + return getter(syntheticRoot); + return getter(group) ?? getDefined(group.parent, getter) ?? getter(syntheticRoot); + } + class GroupElement extends Element { + constructor(parent) { + super('g', {}, parent); + this.elideEmpty = true; + } + group() { + return this; + } + } + const syntheticRoot = new GroupElement({}); + syntheticRoot.attr = initialState; + const root = new Element('svg', { + version: '1.1', + baseProfile: 'full', + xmlns: 'http://www.w3.org/2000/svg', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'xmlns:ev': 'http://www.w3.org/2001/xml-events', + }, undefined); + let current = new GroupElement(root); + current.attr = initialState; + root.children.push(current); + let inPathBuilderMode = false; + function tracePath(path, offset = { x: 0, y: 0 }, s = 1) { + const d = path + .map((e, i) => (i ? 'L' : 'M') + (offset.x + s * e.x).toFixed(1) + ' ' + (offset.y + s * e.y).toFixed(1)) + .join(' '); + return el('path', { d: d }); + } + function el(type, attr, text) { + const element = new Element(type, attr, current, text); + current.children.push(element); + return element; + } + return { + width: function () { + return 0; + }, + height: function () { + return 0; + }, + clear: function () { }, + circle: function (p, r) { + return el('circle', { r: r, cx: p.x, cy: p.y }); + }, + ellipse: function (center, w, h, start = 0, stop = 0) { + if (start || stop) { + const path = range([start, stop], 64).map((a) => add(center, { x: (Math.cos(a) * w) / 2, y: (Math.sin(a) * h) / 2 })); + return tracePath(path); + } + else { + return el('ellipse', { cx: center.x, cy: center.y, rx: w / 2, ry: h / 2 }); + } + }, + arc: function (cx, cy, r) { + return el('ellipse', { cx, cy, rx: r, ry: r }); + }, + roundRect: function (x, y, width, height, r) { + return el('rect', { x, y, rx: r, ry: r, height, width }); + }, + rect: function (x, y, width, height) { + return el('rect', { x, y, height, width }); + }, + path: tracePath, + circuit: function (path, offset, s) { + const element = tracePath(path, offset, s); + element.attr.d += ' Z'; + return element; + }, + setFont: function (family, size, weight, style) { + current.attr['font-family'] = family; + current.attr['font-size'] = size + 'pt'; + current.attr['font-weight'] = weight; + current.attr['font-style'] = style; + }, + strokeStyle: function (stroke) { + current.attr.stroke = stroke; + }, + fillStyle: function (fill) { + current.attr.fill = fill; + }, + arcTo: function (x1, y1, x2, y2) { + if (inPathBuilderMode) + last(current.children).attr.d += 'L' + x1 + ' ' + y1 + ' L' + x2 + ' ' + y2 + ' '; + else + throw new Error('can only be called after .beginPath()'); + }, + beginPath: function () { + inPathBuilderMode = true; + return el('path', { d: '' }); + }, + fillText: function (text, x, y) { + return el('text', { + x, + y, + stroke: 'none', + font: undefined, + style: undefined, + 'text-anchor': getDefined(current, (e) => e.attr['text-align']) === 'center' ? 'middle' : undefined, + }, text); + }, + lineCap: function (cap) { + current.attr['stroke-linecap'] = cap; + }, + lineJoin: function (join) { + current.attr['stroke-linejoin'] = join; + }, + lineTo: function (x, y) { + if (inPathBuilderMode) + last(current.children).attr.d += 'L' + x.toFixed(1) + ' ' + y.toFixed(1) + ' '; + else + throw new Error('can only be called after .beginPath()'); + return current; + }, + lineWidth: function (w) { + current.attr['stroke-width'] = w; + }, + measureText: function (s) { + if (ctx) { + if (current) + ctx.font = `${getDefined(current, (e) => e.attr['font-weight'])} ${getDefined(current, (e) => e.attr['font-style'])} ${getDefined(current, (e) => e.attr['font-size'])} ${getDefined(current, (e) => e.attr['font-family'])}`; + else + ctx.font = `${initialState['font-weight']} ${initialState['font-style']} ${initialState['font-size']} ${initialState['font-family']}`; + return ctx.measureText(s); + } + else { + return { + width: sum(s, function (c) { + const size = getDefined(current, (e) => e.attr['font-size']) ?? 12; + const scale = parseInt(size.toString()) / 12; + return (charWidths[c] ?? 16) * scale; + }), + }; + } + }, + moveTo: function (x, y) { + if (inPathBuilderMode) + last(current.children).attr.d += 'M' + x.toFixed(1) + ' ' + y.toFixed(1) + ' '; + else + throw new Error('can only be called after .beginPath()'); + }, + restore: function () { + if (current.parent) + current = current.parent; + }, + save: function () { + const node = new GroupElement(current); + current.children.push(node); + current = node; + }, + setData: function (name, value) { + current.data = current.data ?? {}; + current.data['data-' + name] = value; + }, + scale: function () { }, + setLineDash: function (d) { + current.attr['stroke-dasharray'] = d.length === 0 ? 'none' : d[0] + ' ' + d[1]; + }, + stroke: function () { + inPathBuilderMode = false; + last(current.children).stroke(); + }, + textAlign: function (a) { + current.attr['text-align'] = a; + }, + translate: function (dx, dy) { + if (Number.isNaN(dx) || Number.isNaN(dy)) { + throw new Error('dx and dy must be real numbers'); + } + current.attr.transform = `translate(${dx}, ${dy})`; + }, + serialize: function (size, desc, title) { + if (desc) { + root.children.unshift(new Element('desc', {}, undefined, desc)); + } + if (title) { + root.children.unshift(new Element('title', {}, undefined, title)); + } + root.attr = { + version: '1.1', + baseProfile: 'full', + width: size.width, + height: size.height, + viewBox: '0 0 ' + size.width + ' ' + size.height, + xmlns: 'http://www.w3.org/2000/svg', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'xmlns:ev': 'http://www.w3.org/2001/xml-events', + }; + return root.serialize(); + }, + }; + } + + function fitCanvasSize(canvas, rect, zoom) { + canvas.width = rect.width * zoom; + canvas.height = rect.height * zoom; + } + function createMeasurer(config, graphics) { + return { + setFont(family, size, weight, style) { + graphics.setFont(family, size, weight, style); + }, + textWidth(s) { + return graphics.measureText(s).width; + }, + textHeight() { + return config.leading * config.fontSize; + }, + }; + } + function parseAndRender(code, graphics, canvas, scale) { + const parsedDiagram = parse(code); + const config = parsedDiagram.config; + const measurer = createMeasurer(config, graphics); + const graphLayout = layout(measurer, config, parsedDiagram.root); + if (canvas) { + fitCanvasSize(canvas, graphLayout, config.zoom * scale); + } + config.zoom *= scale; + render(graphics, config, graphLayout); + return { config: config, layout: graphLayout }; + } + function draw(canvas, code, scale) { + return parseAndRender(code, GraphicsCanvas(canvas), canvas, scale || 1); + } + function renderSvg(code, document) { + const skCanvas = GraphicsSvg(document); + const { config, layout } = parseAndRender(code, skCanvas, null, 1); + return skCanvas.serialize({ + width: layout.width, + height: layout.height, + }, code, config.title); + } + class ImportDepthError extends Error { + constructor() { + super('max_import_depth exceeded'); + } + } + async function processAsyncImports(source, loadFile, maxImportDepth = 10) { + if (maxImportDepth == -1) { + throw new ImportDepthError(); + } + async function lenientLoadFile(key) { + try { + return (await loadFile(key)) || ''; + } + catch (e) { + return ''; + } + } + const imports = []; + source.replace(/#import: *(.*)/g, (a, file) => { + const promise = lenientLoadFile(file).then((contents) => processAsyncImports(contents, loadFile, maxImportDepth - 1)); + imports.push({ file, promise }); + return ''; + }); + const imported = {}; + for (const imp of imports) { + imported[imp.file] = await imp.promise; + } + return source.replace(/#import: *(.*)/g, (a, file) => imported[file]); + } + function processImports(source, loadFile, maxImportDepth = 10) { + if (maxImportDepth == -1) { + throw new ImportDepthError(); + } + function lenientLoadFile(key) { + try { + return loadFile(key) || ''; + } + catch (e) { + return ''; + } + } + return source.replace(/#import: *(.*)/g, (a, file) => processImports(lenientLoadFile(file), loadFile, maxImportDepth - 1)); + } + function compileFile(filepath, maxImportDepth) { + const fs = require('fs'); + const path = require('path'); + const directory = path.dirname(filepath); + const rootFileName = path.basename(filepath); + function loadFile(filename) { + return fs.readFileSync(path.join(directory, filename), { encoding: 'utf8' }); + } + return processImports(loadFile(rootFileName), loadFile, maxImportDepth); + } + + const version = '1.7.0'; + + exports.ImportDepthError = ImportDepthError; + exports.ParseError = ParseError; + exports.compileFile = compileFile; + exports.draw = draw; + exports.layout = layout; + exports.parse = parse; + exports.processAsyncImports = processAsyncImports; + exports.processImports = processImports; + exports.renderSvg = renderSvg; + exports.skanaar = util; + exports.styles = styles; + exports.version = version; + exports.visualizers = visualizers; + +})); diff --git a/Assets/dictionaries/af_ZA.aff b/Assets/dictionaries/af_ZA.aff new file mode 100644 index 00000000..0759c265 --- /dev/null +++ b/Assets/dictionaries/af_ZA.aff @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/af_ZA/af_ZA.aff at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/af_ZA.dic b/Assets/dictionaries/af_ZA.dic new file mode 100644 index 00000000..c7c58134 --- /dev/null +++ b/Assets/dictionaries/af_ZA.dic @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/af_ZA/af_ZA.dic at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/de_DE_frami.aff b/Assets/dictionaries/de_DE_frami.aff new file mode 100644 index 00000000..528e2f7e --- /dev/null +++ b/Assets/dictionaries/de_DE_frami.aff @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/de/de_DE_frami.aff at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/de_DE_frami.dic b/Assets/dictionaries/de_DE_frami.dic new file mode 100644 index 00000000..6bc205d2 --- /dev/null +++ b/Assets/dictionaries/de_DE_frami.dic @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/de/de_DE_frami.dic at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/en_GB.aff b/Assets/dictionaries/en_GB.aff new file mode 100644 index 00000000..2e825980 --- /dev/null +++ b/Assets/dictionaries/en_GB.aff @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/en/en_GB.aff at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/en_GB.dic b/Assets/dictionaries/en_GB.dic new file mode 100644 index 00000000..f99aa9f3 --- /dev/null +++ b/Assets/dictionaries/en_GB.dic @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/en/en_GB.dic at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/en_US.aff b/Assets/dictionaries/en_US.aff new file mode 100644 index 00000000..f7d5f826 --- /dev/null +++ b/Assets/dictionaries/en_US.aff @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/en/en_US.aff at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/en_US.dic b/Assets/dictionaries/en_US.dic new file mode 100644 index 00000000..2a2d405d --- /dev/null +++ b/Assets/dictionaries/en_US.dic @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/en/en_US.dic at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/es_ES.aff b/Assets/dictionaries/es_ES.aff new file mode 100644 index 00000000..6eebee96 --- /dev/null +++ b/Assets/dictionaries/es_ES.aff @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/es/es_ES.aff at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/es_ES.dic b/Assets/dictionaries/es_ES.dic new file mode 100644 index 00000000..a1858be5 --- /dev/null +++ b/Assets/dictionaries/es_ES.dic @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/es/es_ES.dic at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/hr_HR.aff b/Assets/dictionaries/hr_HR.aff new file mode 100644 index 00000000..e3511130 --- /dev/null +++ b/Assets/dictionaries/hr_HR.aff @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/hr_HR/hr_HR.aff at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/hr_HR.dic b/Assets/dictionaries/hr_HR.dic new file mode 100644 index 00000000..797f4a63 --- /dev/null +++ b/Assets/dictionaries/hr_HR.dic @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/hr_HR/hr_HR.dic at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/hu_HU.aff b/Assets/dictionaries/hu_HU.aff new file mode 100644 index 00000000..cb831357 --- /dev/null +++ b/Assets/dictionaries/hu_HU.aff @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/hu_HU/hu_HU.aff at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/hu_HU.dic b/Assets/dictionaries/hu_HU.dic new file mode 100644 index 00000000..7692e93a --- /dev/null +++ b/Assets/dictionaries/hu_HU.dic @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/hu_HU/hu_HU.dic at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/it_IT.aff b/Assets/dictionaries/it_IT.aff new file mode 100644 index 00000000..c44a6359 --- /dev/null +++ b/Assets/dictionaries/it_IT.aff @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/it_IT/it_IT.aff at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/it_IT.dic b/Assets/dictionaries/it_IT.dic new file mode 100644 index 00000000..53d44c25 --- /dev/null +++ b/Assets/dictionaries/it_IT.dic @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/it_IT/it_IT.dic at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/nl_NL.aff b/Assets/dictionaries/nl_NL.aff new file mode 100644 index 00000000..e64fb30d --- /dev/null +++ b/Assets/dictionaries/nl_NL.aff @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/nl_NL/nl_NL.aff at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/dictionaries/nl_NL.dic b/Assets/dictionaries/nl_NL.dic new file mode 100644 index 00000000..6d5d5272 --- /dev/null +++ b/Assets/dictionaries/nl_NL.dic @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dictionaries/nl_NL/nl_NL.dic at master · LibreOffice/dictionaries · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + dictionaries + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Assets/download-dictionaries.ps1 b/Assets/download-dictionaries.ps1 new file mode 100644 index 00000000..e275c4a7 --- /dev/null +++ b/Assets/download-dictionaries.ps1 @@ -0,0 +1,24 @@ +$dictionariesPath = ".\dictionaries" +if (-not (Test-Path $dictionariesPath)) { + New-Item -ItemType Directory -Path $dictionariesPath | Out-Null +} + +cd $dictionariesPath +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/en/en_GB.aff" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/en/en_GB.dic" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/en/en_US.aff" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/en/en_US.dic" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/hu_HU/hu_HU.aff" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/hu_HU/hu_HU.dic" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/hr_HR/hr_HR.aff" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/hr_HR/hr_HR.dic" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/it_IT/it_IT.aff" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/it_IT/it_IT.dic" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/nl_NL/nl_NL.aff" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/nl_NL/nl_NL.dic" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/de/de_DE_frami.aff" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/de/de_DE_frami.dic" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/es/es_ES.aff" +curl.exe -L -O "https://github.com/LibreOffice/dictionaries/blob/master/es/es_ES.dic" +cd .. + diff --git a/BookGen.slnx b/BookGen.slnx index f5b834a6..1ac763dc 100644 --- a/BookGen.slnx +++ b/BookGen.slnx @@ -11,10 +11,10 @@ - - - + + + diff --git a/Changelog.md b/Changelog.md index 2f625c86..850712c2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,16 @@ +# 2026. 03. 04 (Prerelease) + +* Change: Updated dependencies +* Change: Better help rendering in terminal +* New: Support for config overlays, which can be used to have multiple configurations in the same project +* New: Added Spellcheck command +* New: Md2Terminal command, to render markdown in terminal +* New: support for rendering [nomnoml](https://www.nomnoml.com/) diagrams +* New: ISO image build for Windows reintroduced +* Fix: Base64 encoding of images was not working correctly +* Fix: Md2HTML was ignoring -ns argument +* Fix: ImgConvert command was not working due to resolution parsing issue + # 2025. 11. 16 (Prerelease) * Breaking: Template engine reworked diff --git a/Commands.md b/Commands.md index 72748c9f..da1ba6b2 100644 --- a/Commands.md +++ b/Commands.md @@ -2,8 +2,7 @@ BookGen - Markdown to Book tool. -For the tool to work in the work folder there must be a bookgen.json config file. This config file -can be created with the following command: +For the tool to work in the work folder there must be a bookgen.json config file. This config file can be created with the following command: `BookGen Newbook` @@ -17,565 +16,710 @@ To list available subcommands type: General arguments: -`-wd` -`--wait-debugger` - Waits for a debugger to be attached. Usefull for error reporting & error finding. +* `-wd` or `--wait-debugger` -`-ad` -`--attach-debugger` - Attaches a debugger. Usefull for error reporting & error finding. + Waits for a debugger to be attached. Usefull for error reporting & error finding. -`-js` -`--json-log` - Outputs log in JSON format. Usefull for interop purposes. +* `-ad` or `--attach-debugger` + + Attaches a debugger. Usefull for error reporting & error finding. + +* `-js` or `--json-log` + + Outputs log in JSON format. Usefull for interop purposes. # Addfrontmatter Add a basic YAML frontmatter information to all markdown files located in the current folder and it's subfolders. -`BookGen Addfrontmatter [-v] [-d [directory]]` -`BookGen Addfrontmatter [--verbose] [--dir [directory]]` +``` +BookGen Addfrontmatter [-v] [-d [directory]] +BookGen Addfrontmatter [--verbose] [--dir [directory]] +``` Arguments: --d, --dir: - Optional argument. Specifies work directory. If not specified, then - the current directory will be used as working directory. +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues +* `-v`, `--verbose`: + + Optional argument, turns on detailed logging. Usefull for locating issues # Assembly-document Generates a markdown file(s) from a given .NET assembly and it's XML documentation file. -`BookGen Assembly-document -i -o [-d] [-n]` -`BookGen Assembly-document --input --output [--dry] [--namespace-pages]` +``` +BookGen Assembly-document -i -o [-d] [-n] +BookGen Assembly-document --input --output [--dry] [--namespace-pages] +``` Arguments: --i, --input: - Required argument. Specifies the input assembly file path. The file must be a .NET assembly. +* `-i`, `--input`: + + Required argument. Specifies the input assembly file path. The file must be a .NET assembly. --o, --output: - Required argument. Specifies the output files path. +* `-o`, `--output`: --d, --dry: - Optional argument. If specified, the command will not write any files, - but will only print the output to console. + Required argument. Specifies the output files path. --n, --namespace-pages: - Optional argument. If specified, the command will create a separate markdown file for each - namespace in the assembly. +* `-d`, `--dry`: + + Optional argument. If specified, the command will not write any files, but will only print the output to console. + +* `-n`, `--namespace-pages`: + + Optional argument. If specified, the command will create a separate markdown file for each namespace in the assembly. # BuildEpub Build an epub3 file from the book. -`BookGen BuildEpub -o [-v] [-d [directory]]` -`BookGen BuildEpub --output [--verbose] [--dir [directory]]` +``` +BookGen BuildEpub -o [-v] [-d [directory]] +BookGen BuildEpub --output [--verbose] [--dir [directory]] +``` Arguments: --o, --output: - Required argument. Specifies the output directory name. +* `-o`, `--output`: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. + Required argument. Specifies the output directory name. --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. + +* `-v`, `--verbose`: + + Optional argument, turns on detailed logging. Usefull for locating issues # BuildExport Build a JSON file with schema for post processing of the book. -`BookGen BuildExport -o [-v] [-d [directory]]` -`BookGen BuildExport --output [--verbose] [--dir [directory]]` +``` +BookGen BuildExport -o [-v] [-d [directory]] +BookGen BuildExport --output [--verbose] [--dir [directory]] +``` Arguments: --o, --output: - Required argument. Specifies the output directory name. +* `-o`, `--output`: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. + Required argument. Specifies the output directory name. + +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. + +* `-v`, `--verbose`: + + Optional argument, turns on detailed logging. Usefull for locating issues --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues +* `-h`, `--host`: --h, --host: - Optional argument. If specified, the host name set in the config file will be ignored and the - host name will be set to the specified value. + Optional argument. If specified, the host name set in the config file will be ignored and the host name will be set to the specified value. # BuildFeed Build an RSS 2.0 and an Atom 1.0 feed from the book. -`BookGen BuildFeed -o [-v] [-d [directory]]` -`BookGen BuildFeed --output [--verbose] [--dir [directory]]` +``` +BookGen BuildFeed -o [-v] [-d [directory]] +BookGen BuildFeed --output [--verbose] [--dir [directory]] +``` Arguments: --o, --output: - Required argument. Specifies the output directory name. +* `-o`, `--output`: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. + Required argument. Specifies the output directory name. + +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. + +* `-v`, `--verbose`: --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues + Optional argument, turns on detailed logging. Usefull for locating issues # BuildPrint Build a printable html & xhtml file from the book -`BookGen BuildPrint -o [-v] [-d [directory]]` -`BookGen BuildPrint --output [--verbose] [--dir [directory]]` +``` +BookGen BuildPrint -o [-v] [-d [directory]] +BookGen BuildPrint --output [--verbose] [--dir [directory]] +``` Arguments: --o, --output: - Required argument. Specifies the output directory name. +* `-o`, `--output`: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. + Required argument. Specifies the output directory name. + +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. + +* `-v`, `--verbose`: --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues + Optional argument, turns on detailed logging. Usefull for locating issues --h, --host: - Optional argument. If specified, the host name set in the config file will be ignored and the - host name will be set to the specified value. +* `-h`, `--host`: + + Optional argument. If specified, the host name set in the config file will be ignored and the host name will be set to the specified value. # BuildWeb Build a static website from the book -`BookGen BuildWeb -o [-v] [-d [directory]]` -`BookGen BuildWeb --output [--verbose] [--dir [directory]]` +``` +BookGen BuildWeb -o [-v] [-d [directory]] +BookGen BuildWeb --output [--verbose] [--dir [directory]] +``` Arguments: --o, --output: - Required argument. Specifies the output directory name. +* `-o`, `--output`: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. + Required argument. Specifies the output directory name. + +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues +* `-v`, `--verbose`: --h, --host: - Optional argument. If specified, the host name set in the config file will be ignored and the - host name will be set to the specified value. + Optional argument, turns on detailed logging. Usefull for locating issues + +* `-h`, `--host`: + + Optional argument. If specified, the host name set in the config file will be ignored and the host name will be set to the specified value. # BuildWp Build a wordpress export file from the book. -`BookGen BuildWp -o [-v] [-d [directory]]` -`BookGen BuildWp --output [--verbose] [--dir [directory]]` +``` +BookGen BuildWp -o [-v] [-d [directory]] +BookGen BuildWp --output [--verbose] [--dir [directory]] +``` Arguments: --o, --output: - Required argument. Specifies the output directory name. +* `-o`, `--output`: + + Required argument. Specifies the output directory name. + +* `-d`, `--dir`: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues +* `-v`, `--verbose`: --h, --host: - Optional argument. If specified, the host name set in the config file will be ignored and the - host name will be set to the specified value. + Optional argument, turns on detailed logging. Usefull for locating issues + +* `-h`, `--host`: + + + Optional argument. If specified, the host name set in the config file will be ignored and the host name will be set to the specified value. # Config Get or set bookgen application specific settings -`BookGen Config` - List all currently supported application wide settings +* `BookGen Config` + + List all currently supported application wide settings + +* `BookGen Config ` -`BookGen Config ` - Gets a setting value, prints it to output and exits. + Gets a setting value, prints it to output and exits. -`BookGen Config ` - Sets a setting value and exits +* `BookGen Config ` + + Sets a setting value and exits # Edit Open a file for editing with configured editor. +`BookGen edit [filename]` + # Gui Starts the program with a command line gui interface - -`BookGen Gui [-v] [-d [directory]]` -`BookGen Gui [--verbose] [--dir [directory]]` +``` +BookGen Gui [-v] [-d [directory]] +BookGen Gui [--verbose] [--dir [directory]] +``` Arguments: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. +* `-d`, `--dir`: --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. -`BookGen edit [filename]` +* `-v`, `--verbose`: + + Optional argument, turns on detailed logging. Usefull for locating issues. # Html2Pdf Converts a HTML file to a png using edges or chromes headless mode. The tool will use chrome, if it's installed, otherwise it will use edge. This command is only supported on Windows OS. -`BookGen Html2Pdf -i -o ` -`BookGen Html2Pdf --input --output ` +``` +BookGen Html2Pdf -i -o +BookGen Html2Pdf --input --output +``` + +* `-i`, `--input`: --i, --input: - Input html file with extension of .htm or .html + Input html file with extension of .htm or .html --o, --output: - Output PDF file. +* `-o`, `--output`: + + Output PDF file. # Html2Png Converts a HTML file to a png using edges or chromes headless mode. The tool will use chrome, if it's installed, otherwise it will use edge. This command is only supported on Windows OS. -`BookGen Html2Png -i -o [-w [width]] [-h [height]]` -`BookGen Html2Png --input --output [--width [width]] [--height [height]]` +``` +BookGen Html2Png -i -o [-w [width]] [-h [height]] +BookGen Html2Png --input --output [--width [width]] [--height [height]] +``` + +* `-i`, `--input`: + + Input html file with extension of .htm or .html + +* `-o`, `--output`: --i, --input: - Input html file with extension of .htm or .html + Output PNG file. --o, --output: - Output PNG file. +* `-w`, `--width`: --w, --width: - Optional argument. Specifies the width of the output image in pixels. + Optional argument. Specifies the width of the output image in pixels. --h, --height: - Optional argument. Specifies the height of the output image in pixels. +* `-h`, `--height`: + + Optional argument. Specifies the height of the output image in pixels. # ImgConvert Converts an image file to a different format. The tool supports png, jpeg, webp and svg formats. -`BookGen ImgConvert -i -o -f [-q [quality]] [-r [resolution]]` +``` +BookGen ImgConvert -i -o -f [-q [quality]] [-r [resolution]] +``` Arguments: --i, --input: - Required argument. Specifies the input image file path. The file must be a valid image file or - a directory containing image files. +* `-i`, `--input`: + + Required argument. Specifies the input image file path. The file must be a valid image file or a directory containing image files. + +* `-o`, `--output`: + + Required argument. Specifies the output file path. The file must have a valid image file extension, like .png, .jpg, .jpeg, .webp. Can also be a directory, in which case the output files will be saved with the same name as the input files --o, --output: - Required argument. Specifies the output file path. The file must have a valid image file - extension, like .png, .jpg, .jpeg, .webp. Can also be a directory, in which case the output - files will be saved with the same name as the input files +* `-f`, `--format`: --f, --format: Required argument. Specifies the output image format. Supported formats are png, jpeg, webp. --q, --quality: - Optional argument. Specifies the quality of the output image. The value must be between - 0 and 100. Default is 90. If not specified, then the default value will be used. +* `-q`, `--quality`: --r, --resolution: - Optional argument. Specifies the resolution of the output image. The value must be a valid - resolution string, like 1920x1080 or 1280x720. If not specified, then the resolution will be - the same as the input image. + Optional argument. Specifies the quality of the output image. The value must be between 0 and 100. Default is 90. If not specified, then the default value will be used. + +* `-r`, `--resolution`: + + Optional argument. Specifies the resolution of the output image. The value must be a valid resolution string, like 1920x1080 or 1280x720. If not specified, then the resolution will be the same as the input image. # Install Windows only command that installs BookGen to the system PATH & optionally to the windows terminal. -`BookGen Install` +``` +BookGen Install +``` # JsonArgs Creates an empty json arguments template file for a given bookgen command. -`BookGen JsonArgs -c [-d [directory]]` -`BookGen JsonArgs --command [--dir [directory]]` +``` +BookGen JsonArgs -c [-d [directory]] +BookGen JsonArgs --command [--dir [directory]] +``` -A Json arguments template can be used to store command line arguments, so the bookgen command can -be invoked with the same arguments without having to type them in again. +A Json arguments template can be used to store command line arguments, so the bookgen command can be invoked with the same arguments without having to type them in again. Arguments: --c, --command: - Required argument. Specifies the command for which the json template will be created. +* `-c`, `--command`: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. + Required argument. Specifies the command for which the json template will be created. + +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. # Links Scans all markdown files in the current book and writes the lins to a markdown file, named links.md -`BookGen Links [-v] [-d [directory]]` -`BookGen Links [--verbose] [--dir [directory]]` +``` +BookGen Links [-vf] [-v] [-d [directory]] +BookGen Links [--verify] [--verbose] [--dir [directory]] +``` Arguments: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. + +* `-v`, `--verbose`: + + Optional argument, turns on detailed logging. Usefull for locating issues --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues +* `-vf`, `--verify`: + + Optional argument. When specified, the command will verify if the links are accessible and will print the result to the console. If not specified, the command will only write the links to the output file. # Math2Svg Renders a single markdown file containing Tex formulas to svg files -`BookGen Math2Svg -f -o [-s [scale]]` -`BookGen Math2Svg --formula --output [--scale [scale]]` +``` +BookGen Math2Svg -f -o [-s [scale]] +BookGen Math2Svg --formula --output [--scale [scale]] +``` Arguments: --f, --formula: - Formula to render to svg. The formula must be a valid Tex formula. +* `-f`, `--formula`: + + Formula to render to svg. The formula must be a valid Tex formula. + +* `-o`, `--output`: + + Output svg file. --o, --output: - Output svg file. +* `-s`, `--scale`: --s, --scale: - Optional argument. Specifies the scale of the output svg file. Default is 1.0. - If not specified, then the default value will be used. + Optional argument. Specifies the scale of the output svg file. Default is 1.0. If not specified, then the default value will be used. # Md2HTML Renders a single markdown file to an HTML file -`BookGen Md2HTML -i -o ` -`BookGen Md2HTML --input --output ` +``` +BookGen Md2HTML -i -o +BookGen Md2HTML --input --output +``` Arguments: --i, --input: - Input markdown file path. Multiple files can be set with multiple - -i arguments +* `-i`, `--input`: --o, --output: - Output html file path. If file name is "con", outputs to console. + Input markdown file path. Multiple files can be set with multiple `-i` arguments --tf, --template - Optional argument. If not specified, default template is used. If custom file provided, then - the file must contain the folloing tags: - `` - For document title - `` - For document content +* `-o`, `--output`: --ns, --no-syntax - Optional argument. Disables syntax highlighting. + Output html file path. If file name is "-", outputs to console. --ne, --no-embed - Optional argument. Disables embedding of media site links, - like youtube. +* `-tf`, `--template` --r, --raw - Optional argument. Disables full html generation, only outputs the html produced by - the markdown formatting. + Optional argument. If not specified, default template is used. If custom file provided, then the file must contain the folloing tags: --s, --svg - Enables SVG Passthrough. When enabled SVG files will be embedded in resulting html, instead - of being rendered to webp. + * `` - For document title + * `` - For document content --t, --title - Optional argument. Specifies the rendered HTML page title. Only has affect, when -r or --raw - is not specified. +* `-ns`, `--no-syntax` + + Optional argument. Disables syntax highlighting. + +* `-ne`, `--no-embed` + + Optional argument. Disables embedding of media site links, like youtube. + +* `-r`, `--raw` + + Optional argument. Disables full html generation, only outputs the html produced by the markdown formatting. + +* `-s`, `--svg` + + Enables SVG Passthrough. When enabled SVG files will be embedded in resulting html, instead of being rendered to webp. + +* `-t`, `--title` + + Optional argument. Specifies the rendered HTML page title. Only has affect, when `-r` or `--raw` is not specified. + +# Md2terminal + +Converts a markdown file to terminal formatted text. + +``` +BookGen Md2terminal -i -o +BookGen Md2terminal --input --output +``` + +* `-i`, `--input`: + + Input markdown file path. Multiple files can be set with multiple `-i` arguments + +* `-o`, `--output`: + + Output html file path. If file name is "-", outputs to console. # Migrate Migrate an old Bookgen book to the new format. -`BookGen Migrate [-v] [-d [directory]]` -`BookGen Migrate [--verbose] [--dir [directory]]` +``` +BookGen Migrate [-v] [-d [directory]] +BookGen Migrate [--verbose] [--dir [directory]] +``` Arguments: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. +* `-d`, `--dir`: --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. + +* `-v`, `--verbose`: + + Optional argument, turns on detailed logging. Usefull for locating issues # NewBook Creates a new book structure in the given folder -`BookGen NewBook [-v] [-d [directory]]` -`BookGen NewBook [--verbose] [-dir [directory]]` +``` +BookGen NewBook [-v] [-d [directory]] +BookGen NewBook [--verbose] [-dir [directory]] +``` Arguments: --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues +* `-v`, `--verbose`: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. + Optional argument, turns on detailed logging. Usefull for locating issues + +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. # NewPage Creates a new markdown page. -`BookGen NewPage -n [-v] [-d [directory]]` -`BookGen NewPage --name [--verbose] [-dir [directory]]` +``` +BookGen NewPage -n [-v] [-d [directory]] +BookGen NewPage --name [--verbose] [-dir [directory]] +``` Arguments: --n, --name: - File name. Specifies new file name +* `-n`, `--name`: --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues + File name. Specifies new file name --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. +* `-v`, `--verbose`: + + Optional argument, turns on detailed logging. Usefull for locating issues + +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. # QrCode Renders an url into a QRCode image -`BookGen QrCode -d -o [-c [color]]` -`BookGen QrCode --data --output [--color [color]]` +``` +BookGen QrCode -d -o [-c [color]] +BookGen QrCode --data --output [--color [color]] +``` Arguments: --d, --data: - Url data to encode. Minimum 1 byte, Maximum 900 bytes +* `-d`, `--data`: + + Url data to encode. Minimum 1 byte, Maximum 900 bytes + +* `-c`, `--color`: + + Optional argument. Specifies the color of the QRCode. The color must be a valid hex color code, like #FF0000 or #F00. --c, --color: - Optional argument. Specifies the color of the QRCode. The color must be a valid hex color code, - like #FF0000 or #F00. +* `-o`, `--output` --o, --output - Output file. Must have .png or .svg extension + Output file. Must have .png or .svg extension # Schemas Creates a schemas.md documentation file, describing the various config schemas used by bookgen. -`BookGen Schemas [-v] [-d [directory]]` -`BookGen Schemas [--verbose] [--dir [directory]]` +``` +BookGen Schemas [-v] [-d [directory]] +BookGen Schemas [--verbose] [--dir [directory]] +``` Arguments: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues +* `-v`, `--verbose`: + + Optional argument, turns on detailed logging. Usefull for locating issues # Serve Starts a local only http server that serves file from the given directory -`BookGen Serve [-d [directory]]` -`BookGen Serve [--dir [directory]]` +``` +BookGen Serve [-d [directory]] +BookGen Serve [--dir [directory]] +``` Arguments: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. # Shell Autocompleter command, that is used by Powershell -`BookGen Shell` +``` +BookGen Shell +``` # Shortcut -Create a cmd file in the current directory that can be used to start the bookgen Shell in the -current directory. +Create a cmd file in the current directory that can be used to start the bookgen Shell in the current directory. -`BookGen Shortcut` +``` +BookGen Shortcut +``` This command is only supported on Windows OS. +# Spellcheck + +Perform spell check on a given markdown file or text file. The command will print the misspelled words to the console. + +``` +BookGen Spellcheck -i [-l ] [-v] +BookGen Spellcheck --input [--language ] [--verbose] +``` + +Arguments: + +* `-i`, `--input`: + + Required argument. Specifies the input file path. The file must be a markdown file or a text file. + +* `-l`, `--language`: + + Optional argument. Specifies the language to use for spell checking. The value must be a valid language code, like en_US or hu_HU. If not specified, then en_US will be used as the default language. + +* `-v`, `--verbose`: + + Optional argument, turns on detailed logging. Usefull for locating issues + +* `-ld`, `--list-dictionaires` + + Optional argument. When specified, the command will list all available dictionaries and exit. + # Stats Displays various statistics about the bookgen project. -`BookGen Stats [-v] [-d [directory]]` -`BookGen Stats [--verbose] [--dir [directory]]` +``` +BookGen Stats [-v] [-d [directory]] +BookGen Stats [--verbose] [--dir [directory]] +``` Arguments: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. + +* `-v`, `--verbose`: --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues + Optional argument, turns on detailed logging. Usefull for locating issues # Subcommands Listst all available subcommands -`BookGen SubCommands` +``` +BookGen SubCommands +``` # Templates -`Bookgen Templates [-n [template name]]` -`Bookgen Templates [--name [template name]]` - Lists all available templates, or extracts a single template to the current directory. +``` +Bookgen Templates [-n [template name]] +Bookgen Templates [--name [template name]] +``` + Arguments: --n, --name: - Optional argument. If specified, only the template with the given name will be extracted. - If not specified, all available templates will be printed. +* `-n`, `--name`: + + Optional argument. If specified, only the template with the given name will be extracted. If not specified, all available templates will be printed. # Terminalinstall Installs a bookgen profile to the Windows Termninal. -This command is only supported on Windows OS. +This command is only supported on Windows OS. Without arguments, performs terminal profile install. -`BookGen Terminalinstall [-c] [-t]` -`BookGen Terminalinstall [--checkinstall] [--checkterminalinstall]` +``` +BookGen Terminalinstall [-c] [-t] +BookGen Terminalinstall [--checkinstall] [--checkterminalinstall] +``` Arguments: --c, --checkinstall: - Optional argument. When specified checks, if terminal profile installed or not. If exit code - is 0, profile is installed. +* `-c`, `--checkinstall`: + + Optional argument. When specified checks, if terminal profile installed or not. If exit code is 0, profile is installed. --t, --checkterminalinstall: - Optional argument. When specified checks, if windows terminal is installed or not. If exit code - is 0, terminal is installed. +* `-t`, `--checkterminalinstall`: -Without arguments, performs terminal profile install. + Optional argument. When specified checks, if windows terminal is installed or not. If exit code is 0, terminal is installed. # Tools -Display a list of downloadable tools that can be installed and used with BookGen shell. This -command is only supported on Windows OS. +Display a list of downloadable tools that can be installed and used with BookGen shell. + +This command is only supported on Windows OS. -`BookGen Tools` +``` +BookGen Tools +``` # Upgrade @@ -583,34 +727,40 @@ Upgrades the bookgen project to the latest version. This command will upgrade th config file to the latest version, and will also upgrade the bookgen.toc.json file to the latest version. -`BookGen Upgrade [-v] [-d [directory]]` -`BookGen Upgrade [--verbose] [--dir [directory]]` +``` +BookGen Upgrade [-v] [-d [directory]] +BookGen Upgrade [--verbose] [--dir [directory]] +``` Arguments: --d, --dir: - Optional argument. Specifies work directory. If not specified, then - the current directory will be used as working directory. +* `-d`, `--dir`: --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. + +* `-v`, `--verbose`: + + Optional argument, turns on detailed logging. Usefull for locating issues # Validate Validate the configuration files used by bookgen in the specified folder. -`BookGen Validate [-v] [-d [directory]]` -`BookGen Validate [--verbose] [--dir [directory]]` +``` +BookGen Validate [-v] [-d [directory]] +BookGen Validate [--verbose] [--dir [directory]] +``` Arguments: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory will - be used as working directory. +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues +* `-v`, `--verbose`: + + Optional argument, turns on detailed logging. Usefull for locating issues # Version @@ -618,19 +768,21 @@ Print the current program and config API version `BookGen Version` - # Vstasks Generates a Visual Studio Code tasks.json file for the bookgen project. -`BookGen Vstasks [-v] [-d [directory]]` -`BookGen Vstasks [--verbose] [--dir [directory]]` +``` +BookGen Vstasks [-v] [-d [directory]] +BookGen Vstasks [--verbose] [--dir [directory]] +``` Arguments: --d, --dir: - Optional argument. Specifies work directory. If not specified, then the current directory - will be used as working directory. +* `-d`, `--dir`: + + Optional argument. Specifies work directory. If not specified, then the current directory will be used as working directory. + +* `-v`, `--verbose`: --v, --verbose: - Optional argument, turns on detailed logging. Usefull for locating issues + Optional argument, turns on detailed logging. Usefull for locating issues diff --git a/Directory.Packages.props b/Directory.Packages.props index 0aff0ee3..909874e4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,29 +4,33 @@ true - - + + - - - - + + + + + + + - + - + - - - - + + + + - - + + + diff --git a/Installers/debian-info.txt b/Installers/debian-info.txt deleted file mode 100644 index 95c8a9ab..00000000 --- a/Installers/debian-info.txt +++ /dev/null @@ -1,2 +0,0 @@ -Debian user: user -Debian password: pass \ No newline at end of file diff --git a/Installers/install.sh b/Installers/install.sh deleted file mode 100644 index 3100d2ee..00000000 --- a/Installers/install.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -#check for admin rights -if [ "$EUID" -ne 0 ] - then echo "Please run as root" - exit -fi - -mkdir -p /opt/bookgen -cp bin/* /opt/bookgen -ln -s /opt/bookgen/BookGen /usr/bin/bookgen - -echo "BookGen installed successfully!" \ No newline at end of file diff --git a/Installers/prepare-debian-base.sh b/Installers/prepare-debian-base.sh deleted file mode 100644 index 8506a236..00000000 --- a/Installers/prepare-debian-base.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -#check for admin rights -if [ "$EUID" -ne 0 ] - then echo "Please run as root" - exit -fi - -apt update -apt dist-upgrade -y -apt install -y curl wget mc - -wget https://packages.microsoft.com/config/debian/13/packages-microsoft-prod.deb -O packages-microsoft-prod.deb -sudo dpkg -i packages-microsoft-prod.deb -rm packages-microsoft-prod.deb - -apt update -apt install -y dotnet-sdk-10.0 -apt autoremove --purge -apt clean \ No newline at end of file diff --git a/LICENCE b/LICENCE index c68cf738..e8971bb6 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2023 Ruzsinszki Gábor +Copyright (c) 2019-2026 Ruzsinszki Gábor Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including diff --git a/Installers/install.cmd b/PublishFiles/install.cmd similarity index 100% rename from Installers/install.cmd rename to PublishFiles/install.cmd diff --git a/PublishFiles/mkisofs.exe b/PublishFiles/mkisofs.exe new file mode 100644 index 00000000..e9787c72 Binary files /dev/null and b/PublishFiles/mkisofs.exe differ diff --git a/PublishFiles/start_bookgen_shell.cmd b/PublishFiles/start_bookgen_shell.cmd new file mode 100644 index 00000000..3c97a204 --- /dev/null +++ b/PublishFiles/start_bookgen_shell.cmd @@ -0,0 +1,8 @@ +@echo off +title "Bookgen shell start script" +where /q pwsh +if %errorlevel%==0 ( + pwsh -ExecutionPolicy Bypass -NoExit -File "%~dp0bin\BookGenShell.ps1" +) else ( + powershell -ExecutionPolicy Bypass -NoExit -File "%~dp0bin\BookGenShell.ps1" +) diff --git a/Source/BookGen.Cli/ArgumentParsing/ArgumentParser.cs b/Source/BookGen.Cli/ArgumentParsing/ArgumentParser.cs index ba07ef6c..f12746b1 100644 --- a/Source/BookGen.Cli/ArgumentParsing/ArgumentParser.cs +++ b/Source/BookGen.Cli/ArgumentParsing/ArgumentParser.cs @@ -30,10 +30,10 @@ public ArgumentParser(Type argumentType, ILogger log) private void OrganizeProperties(PropertyInfo[] propertyInfos) { - foreach (var propertyInfo in propertyInfos) + foreach (PropertyInfo propertyInfo in propertyInfos) { - var switchAttribute = propertyInfo.GetCustomAttribute(); - var argumentAttribute = propertyInfo.GetCustomAttribute(); + SwitchAttribute? switchAttribute = propertyInfo.GetCustomAttribute(); + ArgumentAttribute? argumentAttribute = propertyInfo.GetCustomAttribute(); if (switchAttribute != null && argumentAttribute != null) { @@ -70,9 +70,9 @@ public ArgumentsBase Fill(IReadOnlyList args) private void HandleSwitchProperties(object argumentsClass, ArgumentBag argBag) { - foreach (var property in _switchPropertyInfos) + foreach (PropertyInfo property in _switchPropertyInfos) { - var switchAttribute = property.GetCustomAttribute() + SwitchAttribute switchAttribute = property.GetCustomAttribute() ?? throw new System.Diagnostics.UnreachableException(); if (switchAttribute.ShortName.StartsWith('-')) @@ -88,7 +88,7 @@ private void HandleSwitchProperties(object argumentsClass, ArgumentBag argBag) else if (property.PropertyType.IsArray) { string[] values = argBag.GetSwitchValues(switchAttribute); - var elementType = property.PropertyType.GetElementType(); + Type? elementType = property.PropertyType.GetElementType(); if (elementType != null) { var finalValues = values @@ -114,9 +114,9 @@ private void HandleSwitchProperties(object argumentsClass, ArgumentBag argBag) private void HandleArgumentProperties(object argumentsClass, ArgumentBag argBag) { - foreach (var property in _argumentPropertyInfos) + foreach (PropertyInfo property in _argumentPropertyInfos) { - var argumentAttribute = property.GetCustomAttribute() + ArgumentAttribute argumentAttribute = property.GetCustomAttribute() ?? throw new System.Diagnostics.UnreachableException(); if (property.PropertyType.IsArray) diff --git a/Source/BookGen.Cli/ArgumentParsing/Autocomplete.cs b/Source/BookGen.Cli/ArgumentParsing/Autocomplete.cs index 463b4533..4a6ed1d5 100644 --- a/Source/BookGen.Cli/ArgumentParsing/Autocomplete.cs +++ b/Source/BookGen.Cli/ArgumentParsing/Autocomplete.cs @@ -13,11 +13,11 @@ internal static class Autocomplete { public static IEnumerable GetInfo(Type t) { - var properties = t.GetProperties(BindingFlags.Public | BindingFlags.Instance); + PropertyInfo[] properties = t.GetProperties(BindingFlags.Public | BindingFlags.Instance); - foreach (var property in properties) + foreach (PropertyInfo property in properties) { - var sw = property.GetCustomAttribute(); + SwitchAttribute? sw = property.GetCustomAttribute(); if (sw != null) { yield return $"-{sw.ShortName}"; diff --git a/Source/BookGen.Cli/BookGen.Cli.csproj b/Source/BookGen.Cli/BookGen.Cli.csproj index 1633e045..8e569b4b 100644 --- a/Source/BookGen.Cli/BookGen.Cli.csproj +++ b/Source/BookGen.Cli/BookGen.Cli.csproj @@ -6,6 +6,7 @@ ..\..\bin\$(Configuration)\ false true + True diff --git a/Source/BookGen.Cli/CommandRunner.cs b/Source/BookGen.Cli/CommandRunner.cs index 22ba41f5..dca581b3 100644 --- a/Source/BookGen.Cli/CommandRunner.cs +++ b/Source/BookGen.Cli/CommandRunner.cs @@ -28,7 +28,7 @@ public sealed class CommandRunner private static string GetCommandName(Type t) { - var nameAttribure = t.GetCustomAttribute(); + CommandNameAttribute? nameAttribure = t.GetCustomAttribute(); return nameAttribure?.Name ?? throw new InvalidOperationException($"Command {t.FullName} is missing a {nameof(CommandNameAttribute)}"); } @@ -76,13 +76,13 @@ private static SupportedOs GetCurrentOs() cmd = cmd.BaseType; } - var method = originalType.GetMethod(nameof(AsyncCommand.ExecuteAsync)) + MethodInfo? method = originalType.GetMethod(nameof(AsyncCommand.ExecuteAsync)) ?? originalType.GetMethod(nameof(Command.Execute)); if (method == null) throw new InvalidOperationException($"Command {originalType.FullName} is missing Exetutable method"); - var parameter = method + Type? parameter = method ?.GetParameters() .FirstOrDefault(p => p.ParameterType.IsAssignableTo(typeof(ArgumentsBase))) ?.ParameterType; @@ -101,18 +101,24 @@ private void DefaultExceptionHandler(Exception obj) private ICommand CreateCommand(string commandName) { - var constructor = _commands[commandName] + ConstructorInfo constructor = _commands[commandName] .GetConstructors(BindingFlags.Public | BindingFlags.Instance) .OrderByDescending(c => c.GetParameters().Length) .First(); - List contructorParameters = new(); - foreach (var param in constructor.GetParameters()) + List constructorParameters = new(); + foreach (ParameterInfo param in constructor.GetParameters()) { - contructorParameters.Add(_serviceProvider.GetRequiredService(param.ParameterType)); + FromKeyedServicesAttribute? keyAttribute = param.GetCustomAttribute(); + + object parameterInstance = keyAttribute != null + ? _serviceProvider.GetRequiredKeyedService(param.ParameterType, keyAttribute.Key) + : _serviceProvider.GetRequiredService(param.ParameterType); + + constructorParameters.Add(parameterInstance); } - var instance = Activator.CreateInstance(_commands[commandName], contructorParameters.ToArray()) + var instance = Activator.CreateInstance(_commands[commandName], constructorParameters.ToArray()) ?? throw new InvalidOperationException(); return (ICommand)instance; @@ -171,14 +177,14 @@ public CommandRunner AddDefaultCommand() where TCommand : ICommand public CommandRunner AddCommandsFrom(Assembly assembly) { - var commands = assembly + IEnumerable commands = assembly .GetTypes() .Where(t => t.IsAssignableTo(typeof(ICommand))) .Where(t => !t.IsAbstract && !t.IsInterface); - foreach (var command in commands) + foreach (Type? command in commands) { - var name = GetCommandName(command); + string name = GetCommandName(command); if (!_commands.ContainsKey(name)) { _commands.Add(name.ToLower(), command); @@ -195,9 +201,9 @@ public string[] GetAutoCompleteItems(string commandName) { if (_commands.TryGetValue(commandName, out Type? value)) { - var type = value; + Type type = value; - var args = GetArgumentType(type); + Type? args = GetArgumentType(type); if (args != null) { @@ -241,7 +247,7 @@ public async Task Run(IReadOnlyList args) private async Task LoadFromJsonFile(string jsonFile) { - await using var stream = File.OpenRead(jsonFile); + await using FileStream stream = File.OpenRead(jsonFile); return await JsonSerializer.DeserializeAsync(stream, _serializerOptions) ?? throw new InvalidOperationException("Failed to load arguments from json"); } @@ -254,7 +260,7 @@ public async Task RunCommand(string commandName, IReadOnlyList args return _settings.UnknownCommandCodeAndMessage.code; } - var argumentType = GetArgumentType(value); + Type? argumentType = GetArgumentType(value); ICommand command = CreateCommand(commandName); if (!command.SupportedOs.HasFlag(_currentOs)) @@ -274,24 +280,28 @@ public async Task RunCommand(string commandName, IReadOnlyList args && File.Exists(argsJson)) { _log.LogInformation("Loading arguments from {filename}...", jsonFileName); - var items = await LoadFromJsonFile(argsJson); + ArgumentJsonItem[] items = await LoadFromJsonFile(argsJson); - return await ExecuteMultiple(items, argumentType, command); + return await ExecuteMultiple(items, argumentType, command, commandName); } - return await ExecuteSingle(argsToParse, argumentType, command); + return await ExecuteSingle(argsToParse, argumentType, command, commandName); } - private async Task ExecuteSingle(IReadOnlyList argsToParse, Type argumentType, ICommand command) + private async Task ExecuteSingle(IReadOnlyList argsToParse, + Type argumentType, + ICommand command, + string commandName) { ArgumentsBase args = ArgumentsBase.Empty; ArgumentParser parser = new(argumentType, _log); args = parser.Fill(argsToParse); - var validationResult = args.Validate(ValidationContext); + ValidationResult validationResult = args.Validate(ValidationContext); if (!validationResult.IsOk) { _log.LogCritical(validationResult.ToString()); + _log.LogInformation("Use help {commandName} to get help on command", commandName); return _settings.BadParametersExitCode; } @@ -303,12 +313,15 @@ private async Task ExecuteSingle(IReadOnlyList argsToParse, Type ar return await command.ExecuteAsync(args, argsToParse); } - private async Task ExecuteMultiple(ArgumentJsonItem[] items, Type argumentType, ICommand command) + private async Task ExecuteMultiple(ArgumentJsonItem[] items, + Type argumentType, + ICommand command, + string commandName) { - foreach (var item in items) + foreach (ArgumentJsonItem item in items) { _log.LogInformation("Executing {name} from json file...", item.Name); - int exitcode = await ExecuteSingle(item.Arguments, argumentType, command); + int exitcode = await ExecuteSingle(item.Arguments, argumentType, command, commandName); if (exitcode != 0) { _log.LogCritical("Failed to execute {name}. Exit code: {exitcode}", item.Name, exitcode); diff --git a/Source/BookGen.Cli/packages.lock.json b/Source/BookGen.Cli/packages.lock.json index e1bba11c..f260753c 100644 --- a/Source/BookGen.Cli/packages.lock.json +++ b/Source/BookGen.Cli/packages.lock.json @@ -4,17 +4,17 @@ "net10.0": { "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Direct", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "oIy8fQxxbUsSrrOvgBqlVgOeCtDmrcynnTG+FQufcUWBrwyPfwlUkCDB2vaiBeYPyT+20u9/HeuHeBf+H4F/8g==" + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw==" }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Direct", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "YkmyiPIWAXVb+lPIrM0LE5bbtLOJkCiRTFiHpkVOvhI7uTvCfoOHLEN0LcsY56GpSD7NqX3gJNpsaDe87/B3zg==", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3" } }, "bookgen.vfs": { diff --git a/Source/BookGen.Contents/BookGen.Contents.csproj b/Source/BookGen.Contents/BookGen.Contents.csproj index 9227d78c..28bddcf7 100644 --- a/Source/BookGen.Contents/BookGen.Contents.csproj +++ b/Source/BookGen.Contents/BookGen.Contents.csproj @@ -5,6 +5,7 @@ enable ..\..\bin\$(Configuration)\ false + True @@ -31,7 +32,16 @@ $([System.IO.Path]::GetFullPath('$(OutputPath)\assets.zip')) - + + + + + + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..\Assets\dictionaries')) + $([System.IO.Path]::GetFullPath('$(OutputPath)\dictionaries.zip')) + + + @@ -42,5 +52,14 @@ + + + + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../../Assets/dictionaries')) + $([System.IO.Path]::GetFullPath('$(OutputPath)/dictionaries.zip')) + + + + diff --git a/Source/BookGen.Contents/BookGenShell.ps1 b/Source/BookGen.Contents/BookGenShell.ps1 index 66978089..af7ef4a3 100644 --- a/Source/BookGen.Contents/BookGenShell.ps1 +++ b/Source/BookGen.Contents/BookGenShell.ps1 @@ -1,7 +1,7 @@ # ----------------------------------------------------------------------------- # BookGen PowerShell Registration script -# Version 3.7.2 -# Last modified: 2025-10-05 +# Version 3.8.0 +# Last modified: 2025-11-16 # ----------------------------------------------------------------------------- # ----------------------------------------------------------------------------- @@ -173,6 +173,15 @@ function gh { GetTool "gh" "github-cli" @Args } +function glow { + param ( + [string[]] + [Parameter(ValueFromRemainingArguments = $true)] + $Args + ) + GetTool "glow" "glow" @Args +} + function copyparty { param ( [string[]] @@ -252,14 +261,14 @@ function weather { [string]$Location ) - $url = "https://wttr.in/" + $url = "https://wttr.in/?n" if ($PSBoundParameters.ContainsKey('Location')) { $url += $Location } Clear-Host - curl $url + (Invoke-WebRequest $url).Content } # intro message @@ -403,11 +412,14 @@ Register-ArgumentCompleter -Native -CommandName git -ScriptBlock { # set prompt function prompt { $git = $(BookGen.Shellprog.exe "prompt" $(Get-Location).Path) + $location = (Get-Location).Path + $topLine = "╭╴$location"+"`n" + if (-not [string]::IsNullOrWhiteSpace($git)) { - 'PS ' + $(Get-Location) + "`n" + $git + $(if ($NestedPromptLevel -ge 1) { '>>' }) + ' > ' + $topLine + '╰╴ PS ' + $git + $(if ($NestedPromptLevel -ge 1) { '>>' }) + ' > ' } else { - 'PS ' + $(Get-Location) + $(if ($NestedPromptLevel -ge 1) { '>>' }) + ' > ' + $topLine + '╰╴ PS ' + $(if ($NestedPromptLevel -ge 1) { '>>' }) + ' > ' } } diff --git a/Source/BookGen.Shell.Shared/BookGen.Shell.Shared.csproj b/Source/BookGen.Shell.Shared/BookGen.Shell.Shared.csproj index c07c7470..399331cc 100644 --- a/Source/BookGen.Shell.Shared/BookGen.Shell.Shared.csproj +++ b/Source/BookGen.Shell.Shared/BookGen.Shell.Shared.csproj @@ -5,11 +5,17 @@ enable enable true + True + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/BookGen.Shell.Shared/GitCommandProvider.cs b/Source/BookGen.Shell.Shared/GitCommandProvider.cs index 32380404..0fe6e658 100644 --- a/Source/BookGen.Shell.Shared/GitCommandProvider.cs +++ b/Source/BookGen.Shell.Shared/GitCommandProvider.cs @@ -14,7 +14,7 @@ public static class GitCommandProvider { private static IEnumerable GetBranches(string folder) { - var (exitcode, output, _) = ProcessRunner.RunProcess("git", ["branch", "-a"], 10, folder); + (int exitcode, string? output, string _) = ProcessRunner.RunProcess("git", ["branch", "-a"], 10, folder); if (exitcode != 0) return Enumerable.Empty(); @@ -24,7 +24,7 @@ private static IEnumerable GetBranches(string folder) public static IEnumerable GetGitCommands(string folder) { - var branches = GetBranches(folder); + IEnumerable branches = GetBranches(folder); yield return "git add ."; yield return "git add"; diff --git a/Source/BookGen/Infrastructure/Loging/ConsoleLogProvider.cs b/Source/BookGen.Shell.Shared/Loging/ConsoleLogProvider.cs similarity index 95% rename from Source/BookGen/Infrastructure/Loging/ConsoleLogProvider.cs rename to Source/BookGen.Shell.Shared/Loging/ConsoleLogProvider.cs index 40e128ab..02534223 100644 --- a/Source/BookGen/Infrastructure/Loging/ConsoleLogProvider.cs +++ b/Source/BookGen.Shell.Shared/Loging/ConsoleLogProvider.cs @@ -9,7 +9,7 @@ using Spectre.Console; -namespace BookGen.Infrastructure.Loging; +namespace BookGen.Shell.Shared.Loging; public sealed class ConsoleLogProvider : ILoggerProvider { @@ -36,7 +36,7 @@ public ILogger CreateLogger(string categoryName) public void Dispose() { - foreach (var logger in _loggers) + foreach (KeyValuePair logger in _loggers) { logger.Value.Dispose(); } @@ -54,7 +54,7 @@ public ConsoleLogger(string categoryName) } public IDisposable? BeginScope(TState state) where TState : notnull - => new DumyLogScope(); + => null; public void Dispose() { diff --git a/Source/BookGen.Shell.Shared/ProcessRunner.cs b/Source/BookGen.Shell.Shared/ProcessRunner.cs index 3a801b5c..8e724138 100644 --- a/Source/BookGen.Shell.Shared/ProcessRunner.cs +++ b/Source/BookGen.Shell.Shared/ProcessRunner.cs @@ -68,7 +68,7 @@ public static void RunCmdScript(string shellScript, ILogger log) public static void RunPowershellScript(string shellScript, ILogger log) { - var installResult = InstallDetector.GetInstallResult(); + InstallResult installResult = InstallDetector.GetInstallResult(); if (installResult.IsPsCoreInstalled) RunShell(InstallDetector.PowershellCoreExe, $"-ExecutionPolicy Bypass -File \"{shellScript}\"", log); else diff --git a/Source/BookGen.Shell.Shared/ShellAutoCompleteFilter.cs b/Source/BookGen.Shell.Shared/ShellAutoCompleteFilter.cs index dfe484db..3a228434 100644 --- a/Source/BookGen.Shell.Shared/ShellAutoCompleteFilter.cs +++ b/Source/BookGen.Shell.Shared/ShellAutoCompleteFilter.cs @@ -20,12 +20,12 @@ public static IEnumerable DoFilter(IReadOnlyList candidates, str string prefix = input[..cursorposition]; int prefixLength = cursorposition >= prefix.Length ? prefix.Length - 1 : cursorposition; - var filteredCommands = candidates + IEnumerable filteredCommands = candidates .Where(cmd => cmd.StartsWith(prefix)); foreach (var filtered in filteredCommands) { - var (start, _) = GetWordPositions(filtered).FirstOrDefault(p => cursorposition >= p.start && cursorposition <= p.end); + (int start, int _) = GetWordPositions(filtered).FirstOrDefault(p => cursorposition >= p.start && cursorposition <= p.end); yield return filtered[start..]; } } @@ -38,7 +38,7 @@ public static IEnumerable DoFilter(IReadOnlyList candidates, str { if (char.IsWhiteSpace(c)) { - var item = (start, pos); + (int start, int pos) item = (start, pos); start = pos + 1; yield return item; } diff --git a/Source/BookGen.Shell.Shared/packages.lock.json b/Source/BookGen.Shell.Shared/packages.lock.json index 012f2179..0bbd134f 100644 --- a/Source/BookGen.Shell.Shared/packages.lock.json +++ b/Source/BookGen.Shell.Shared/packages.lock.json @@ -4,13 +4,25 @@ "net10.0": { "Microsoft.Extensions.Logging.Abstractions": { "type": "Direct", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "YkmyiPIWAXVb+lPIrM0LE5bbtLOJkCiRTFiHpkVOvhI7uTvCfoOHLEN0LcsY56GpSD7NqX3gJNpsaDe87/B3zg==", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3" } }, + "Spectre.Console": { + "type": "Direct", + "requested": "[0.54.0, )", + "resolved": "0.54.0", + "contentHash": "StDXCFayfy0yB1xzUHT2tgEpV1/HFTiS4JgsAQS49EYTfMixSwwucaQs/bIOCwXjWwIQTMuxjUIxcB5XsJkFJA==" + }, + "Spectre.Console.Analyzer": { + "type": "Direct", + "requested": "[1.0.0, )", + "resolved": "1.0.0", + "contentHash": "TojbwWASRf3PMUsotXa8MQ9SNwEwWZZ5bAYf1eLv+jiDsxaD/n7TKTi3da6z4+m8uZHri2i+BVRuLj5w+6j64Q==" + }, "Webmaster442.WindowsTerminal": { "type": "Direct", "requested": "[4.1.1, )", @@ -19,9 +31,9 @@ }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "CentralTransitive", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "oIy8fQxxbUsSrrOvgBqlVgOeCtDmrcynnTG+FQufcUWBrwyPfwlUkCDB2vaiBeYPyT+20u9/HeuHeBf+H4F/8g==" + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw==" } } } diff --git a/Source/BookGen.Shellprog/BookGen.Shellprog.csproj b/Source/BookGen.Shellprog/BookGen.Shellprog.csproj index 9e0bc280..bf548b8a 100644 --- a/Source/BookGen.Shellprog/BookGen.Shellprog.csproj +++ b/Source/BookGen.Shellprog/BookGen.Shellprog.csproj @@ -9,11 +9,14 @@ false ..\..\Branding\icon-shell.ico true + True + + - + all diff --git a/Source/BookGen.Shellprog/CommandCode/Cdg/CdgSelector.cs b/Source/BookGen.Shellprog/CommandCode/Cdg/CdgSelector.cs index 2f98d322..bfa67f6a 100644 --- a/Source/BookGen.Shellprog/CommandCode/Cdg/CdgSelector.cs +++ b/Source/BookGen.Shellprog/CommandCode/Cdg/CdgSelector.cs @@ -117,7 +117,7 @@ public CdgSelector(string startDirectory, bool showHidden) Icon = ":eye: ", Action = () => { - var showHide = _menuItems?.First(m => m.Id == "ShowHide") ?? throw new InvalidOperationException(); + SelectionItemAction showHide = _menuItems?.First(m => m.Id == "ShowHide") ?? throw new InvalidOperationException(); showHide.DisplayString = _showHidden ? "Show hidden files" : "Hide hidden files"; _showHidden = !_showHidden; }, @@ -141,7 +141,7 @@ private bool CanAccess(string path, [NotNullWhen(true)] out string[]? subdirs) { try { - var items = new DirectoryInfo(path).GetDirectories(); + DirectoryInfo[] items = new DirectoryInfo(path).GetDirectories(); if (_showHidden) { items = items.Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden)).ToArray(); @@ -224,8 +224,8 @@ public async Task ShowMenu() try { AnsiConsole.Clear(); - var menu = CreateSelection(); - var selected = await menu.ShowAsync(AnsiConsole.Console, CancellationToken.None); + SelectionPrompt menu = CreateSelection(); + SelectionItemBase selected = await menu.ShowAsync(AnsiConsole.Console, CancellationToken.None); if (selected is SelectionItemDirectory directory) { if (CanAccess(directory.Path, out string[]? subdirs)) @@ -250,7 +250,7 @@ public async Task ShowMenu() AnsiConsole.WriteException(ex); #endif AnsiConsole.WriteLine(ex.Message); - var confirm = new ConfirmationPrompt("Press a key to continue").HideChoices(); + ConfirmationPrompt confirm = new ConfirmationPrompt("Press a key to continue").HideChoices(); await confirm.ShowAsync(AnsiConsole.Console, CancellationToken.None); } } diff --git a/Source/BookGen.Shellprog/CommandCode/Cdg/SelectionItemFactory.cs b/Source/BookGen.Shellprog/CommandCode/Cdg/SelectionItemFactory.cs index d7fa5b04..daac663a 100644 --- a/Source/BookGen.Shellprog/CommandCode/Cdg/SelectionItemFactory.cs +++ b/Source/BookGen.Shellprog/CommandCode/Cdg/SelectionItemFactory.cs @@ -24,8 +24,8 @@ static string GetVolumeLabel(DriveInfo drive) return string.Empty; } - var drives = DriveInfo.GetDrives(); - foreach (var drive in drives) + DriveInfo[] drives = DriveInfo.GetDrives(); + foreach (DriveInfo drive in drives) { yield return new SelectionItemDirectory { @@ -53,7 +53,7 @@ public static IEnumerable CreateFromDirectories(string[] public static IEnumerable GetSpecialFolders() { - foreach (var specialFolder in Enum.GetValues()) + foreach (Environment.SpecialFolder specialFolder in Enum.GetValues()) { var path = Environment.GetFolderPath(specialFolder); if (!string.IsNullOrEmpty(path)) diff --git a/Source/BookGen.Shellprog/CommandCode/Organize/RuleEngine.cs b/Source/BookGen.Shellprog/CommandCode/Organize/RuleEngine.cs index 1f5ef028..4d56706c 100644 --- a/Source/BookGen.Shellprog/CommandCode/Organize/RuleEngine.cs +++ b/Source/BookGen.Shellprog/CommandCode/Organize/RuleEngine.cs @@ -24,7 +24,7 @@ public void Run(string folder, bool simulate) { foreach (var file in Directory.GetFiles(folder)) { - var foundRule = _loadedRules.FirstOrDefault(rule => rule.Key.IsMatch(file)); + KeyValuePair foundRule = _loadedRules.FirstOrDefault(rule => rule.Key.IsMatch(file)); if (foundRule.Key == null) { diff --git a/Source/BookGen.Shellprog/CommandCode/Organize/RuleLoader.cs b/Source/BookGen.Shellprog/CommandCode/Organize/RuleLoader.cs index 9277681f..1e106b84 100644 --- a/Source/BookGen.Shellprog/CommandCode/Organize/RuleLoader.cs +++ b/Source/BookGen.Shellprog/CommandCode/Organize/RuleLoader.cs @@ -57,7 +57,7 @@ private void EditFile() private OrganizeRule[] Deserialize() { - var collection = JsonSerializer.Deserialize(File.ReadAllText(_ruleFile), _options); + OrganizeRule[]? collection = JsonSerializer.Deserialize(File.ReadAllText(_ruleFile), _options); return collection?.Length > 0 ? collection : []; } } diff --git a/Source/BookGen.Shellprog/GitAutoCompleteCommand.cs b/Source/BookGen.Shellprog/GitAutoCompleteCommand.cs index c66b7944..2134c669 100644 --- a/Source/BookGen.Shellprog/GitAutoCompleteCommand.cs +++ b/Source/BookGen.Shellprog/GitAutoCompleteCommand.cs @@ -30,7 +30,7 @@ public override int Execute(IReadOnlyList context) && int.TryParse(context[0], out int index) && !string.IsNullOrEmpty(context[1])) { - var candidates = ShellAutoCompleteFilter.DoFilter(items, context[1], index); + IEnumerable candidates = ShellAutoCompleteFilter.DoFilter(items, context[1], index); #if DEBUGGING var json = System.Text.Json.JsonSerializer.Serialize(new diff --git a/Source/BookGen.Shellprog/GitCommandBase.cs b/Source/BookGen.Shellprog/GitCommandBase.cs index da8f56d6..db759a8b 100644 --- a/Source/BookGen.Shellprog/GitCommandBase.cs +++ b/Source/BookGen.Shellprog/GitCommandBase.cs @@ -44,7 +44,7 @@ protected static GitDirectoryStatus TestIfGitDir(string workDir) { string[] arguments = ["rev-parse", "--is-inside-work-tree"]; - var (exitcode, result, error) = ProcessRunner.RunProcess("git", arguments, TimeOut, workDir); + (int exitcode, string? result, string? error) = ProcessRunner.RunProcess("git", arguments, TimeOut, workDir); if (exitcode == 128 && error.Contains("detected dubious ownership")) { @@ -66,7 +66,7 @@ protected static GitDirectoryStatus TestIfGitDir(string workDir) protected static string GetGitRemote(string workDirectory) { string[] gitArguments = ["config", "--get", "remote.origin.url"]; - var (exitcode, output, error) = ProcessRunner.RunProcess("git", gitArguments, TimeOut, workDirectory); + (int exitcode, string? output, string? error) = ProcessRunner.RunProcess("git", gitArguments, TimeOut, workDirectory); return exitcode == 0 && string.IsNullOrEmpty(error) ? output : string.Empty; } @@ -76,7 +76,7 @@ protected static string GetGitRemote(string workDirectory) { string[] gitArguments = ["status", "-b", "-s", "--porcelain=2"]; - var (exitcode, output, _) = ProcessRunner.RunProcess("git", gitArguments, TimeOut, workDirectory); + (int exitcode, string? output, string _) = ProcessRunner.RunProcess("git", gitArguments, TimeOut, workDirectory); if (exitcode == 0) { return GitParser.ParseStatus(output); @@ -92,7 +92,7 @@ protected static string GetGitRemote(string workDirectory) protected void PrintUntrusted() { - var builder = new TerminalOutputBuilder() + TerminalOutputBuilder builder = new TerminalOutputBuilder() .Append(TerminalOutputBuilder.ForegroundColor.Yellow, TerminalOutputBuilder.BackgroundColor.Black, ""); _console.WriteLine(builder.ToString()); @@ -105,7 +105,7 @@ protected void PrintStatus(GitStatus? status) return; } - var builder = new TerminalOutputBuilder() + TerminalOutputBuilder builder = new TerminalOutputBuilder() .Append(TerminalOutputBuilder.ForegroundColor.Default, TerminalOutputBuilder.BackgroundColor.Green, $"({status.BranchName}) "); if (status.IncommingCommits > 0) diff --git a/Source/BookGen.Shellprog/OrganizeCommand.cs b/Source/BookGen.Shellprog/OrganizeCommand.cs index 7a70e31b..a9e1346b 100644 --- a/Source/BookGen.Shellprog/OrganizeCommand.cs +++ b/Source/BookGen.Shellprog/OrganizeCommand.cs @@ -26,7 +26,7 @@ public override int Execute(OrganizeArguments arguments, IReadOnlyList c try { var ruleLoader = new RuleLoader(arguments.Folder); - var rules = ruleLoader.LoadRules(); + IReadOnlyList rules = ruleLoader.LoadRules(); var engine = new RuleEngine(rules, _log); engine.Run(arguments.Folder, arguments.Simulate); return 0; diff --git a/Source/BookGen.Shellprog/Program.cs b/Source/BookGen.Shellprog/Program.cs index 5902f151..c42798a4 100644 --- a/Source/BookGen.Shellprog/Program.cs +++ b/Source/BookGen.Shellprog/Program.cs @@ -4,6 +4,7 @@ //----------------------------------------------------------------------------- using BookGen.Cli; +using BookGen.Shell.Shared.Loging; using BookGen.Shellprog; using Microsoft.Extensions.DependencyInjection; @@ -11,7 +12,12 @@ using Spectre.Console; -using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => +{ + builder.ClearProviders(); + builder.AddFilter(level => level >= LogLevel.Information); + builder.AddProvider(new ConsoleLogProvider()); +}); ILogger logger = loggerFactory.CreateLogger("BookGen.Shell"); @@ -22,7 +28,7 @@ ioc.AddSingleton(runnerProxy); ioc.AddSingleton(logger); -using var provider = ioc.BuildServiceProvider(); +using ServiceProvider provider = ioc.BuildServiceProvider(); CommandRunner runner = new(provider, logger, new CommandRunnerSettings { diff --git a/Source/BookGen.Shellprog/PromptCommand.cs b/Source/BookGen.Shellprog/PromptCommand.cs index c7044e9c..af0f57e3 100644 --- a/Source/BookGen.Shellprog/PromptCommand.cs +++ b/Source/BookGen.Shellprog/PromptCommand.cs @@ -4,6 +4,7 @@ //----------------------------------------------------------------------------- using BookGen.Cli.Annotations; +using BookGen.Shell.Shared; using Spectre.Console; @@ -20,10 +21,10 @@ public override int Execute(GitArguments arguments, IReadOnlyList contex { if (!string.IsNullOrEmpty(arguments.WorkDirectory)) { - var result = TestIfGitDir(arguments.WorkDirectory); + GitDirectoryStatus result = TestIfGitDir(arguments.WorkDirectory); if (result == GitDirectoryStatus.GitDirectory) { - var status = GetGitStatus(arguments.WorkDirectory); + GitStatus? status = GetGitStatus(arguments.WorkDirectory); PrintStatus(status); } else if (result == GitDirectoryStatus.UntrustedGitDirectory) diff --git a/Source/BookGen.Shellprog/packages.lock.json b/Source/BookGen.Shellprog/packages.lock.json index 5cdb23de..3fb26f16 100644 --- a/Source/BookGen.Shellprog/packages.lock.json +++ b/Source/BookGen.Shellprog/packages.lock.json @@ -2,28 +2,39 @@ "version": 2, "dependencies": { "net10.0": { + "Microsoft.Extensions.DependencyInjection": { + "type": "Direct", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Direct", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw==" + }, "Microsoft.Extensions.Logging": { "type": "Direct", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "9ItMpMLFZFJFqCuHLLbR3LiA4ahA8dMtYuXpXl2YamSDWZhYS9BruPprkftY0tYi2bQ0slNrixdFm+4kpz1g5w==", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.1", - "Microsoft.Extensions.Logging.Abstractions": "10.0.1", - "Microsoft.Extensions.Options": "10.0.1" + "Microsoft.Extensions.DependencyInjection": "10.0.3", + "Microsoft.Extensions.Logging.Abstractions": "10.0.3", + "Microsoft.Extensions.Options": "10.0.3" } }, - "Microsoft.Extensions.Logging.Console": { + "Microsoft.Extensions.Logging.Abstractions": { "type": "Direct", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "38Q8sEHwQ/+wVO/mwQBa0fcdHbezFpusHE+vBw/dSr6Fq/kzZm3H/NQX511Jki/R3FHd64IY559gdlHZQtYeEA==", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", - "Microsoft.Extensions.Logging": "10.0.1", - "Microsoft.Extensions.Logging.Abstractions": "10.0.1", - "Microsoft.Extensions.Logging.Configuration": "10.0.1", - "Microsoft.Extensions.Options": "10.0.1" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3" } }, "Spectre.Console": { @@ -38,87 +49,26 @@ "resolved": "1.0.0", "contentHash": "TojbwWASRf3PMUsotXa8MQ9SNwEwWZZ5bAYf1eLv+jiDsxaD/n7TKTi3da6z4+m8uZHri2i+BVRuLj5w+6j64Q==" }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "njoRekyMIK+smav8B6KL2YgIfUtlsRNuT7wvurpLW+m/hoRKVnoELk2YxnUnWRGScCd1rukLMxShwLqEOKowDg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", - "Microsoft.Extensions.Primitives": "10.0.1" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "kPlU11hql+L9RjrN2N9/0GcRcRcZrNFlLLjadasFWeBORT6pL6OE+RYRk90GGCyVGSxTK+e1/f3dsMj5zpFFiQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.1" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "Lp4CZIuTVXtlvkAnTq6QvMSW7+H62gX2cU2vdFxHQUxvrWTpi7LwYI3X+YAyIS0r12/p7gaosco7efIxL4yFNw==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.1", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.1" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "zerXV0GAR9LCSXoSIApbWn+Dq1/T+6vbXMHGduq1LoVQRHT0BXsGQEau0jeLUBUcsoF/NaUT8ADPu8b+eNcIyg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" - } - }, - "Microsoft.Extensions.Logging.Configuration": { - "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "Zg8LLnfZs5o2RCHD/+9NfDtJ40swauemwCa7sI8gQoAye/UJHRZNpCtC7a5XE7l9Z7mdI8iMWnLZ6m7Q6S3jLg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.1", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", - "Microsoft.Extensions.Configuration.Binder": "10.0.1", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", - "Microsoft.Extensions.Logging": "10.0.1", - "Microsoft.Extensions.Logging.Abstractions": "10.0.1", - "Microsoft.Extensions.Options": "10.0.1", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.1" - } - }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "G6VVwywpJI4XIobetGHwg7wDOYC2L2XBYdtskxLaKF/Ynb5QBwLl7Q//wxAR2aVCLkMpoQrjSP9VoORkyddsNQ==", + "resolved": "10.0.3", + "contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", - "Microsoft.Extensions.Primitives": "10.0.1" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "pL78/Im7O3WmxHzlKUsWTYchKL881udU7E26gCD3T0+/tPhWVfjPwMzfN/MRKU7aoFYcOiqcG2k1QTlH5woWow==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", - "Microsoft.Extensions.Configuration.Binder": "10.0.1", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", - "Microsoft.Extensions.Options": "10.0.1", - "Microsoft.Extensions.Primitives": "10.0.1" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3", + "Microsoft.Extensions.Primitives": "10.0.3" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "DO8XrJkp5x4PddDuc/CH37yDBCs9BYN6ijlKyR3vMb55BP1Vwh90vOX8bNfnKxr5B2qEI3D8bvbY1fFbDveDHQ==" + "resolved": "10.0.3", + "contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg==" }, "bookgen.cli": { "type": "Project", "dependencies": { "BookGen.Vfs": "[1.0.0, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.1, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.1, )" + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )" } }, "bookgen.contents": { @@ -127,28 +77,14 @@ "bookgen.shell.shared": { "type": "Project", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.1, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )", + "Spectre.Console": "[0.54.0, )", "Webmaster442.WindowsTerminal": "[4.1.1, )" } }, "bookgen.vfs": { "type": "Project" }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "oIy8fQxxbUsSrrOvgBqlVgOeCtDmrcynnTG+FQufcUWBrwyPfwlUkCDB2vaiBeYPyT+20u9/HeuHeBf+H4F/8g==" - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "YkmyiPIWAXVb+lPIrM0LE5bbtLOJkCiRTFiHpkVOvhI7uTvCfoOHLEN0LcsY56GpSD7NqX3gJNpsaDe87/B3zg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" - } - }, "Webmaster442.WindowsTerminal": { "type": "CentralTransitive", "requested": "[4.1.1, )", diff --git a/Source/BookGen.Vfs/ApiClient.cs b/Source/BookGen.Vfs/ApiClient.cs index 5abf2f8c..90466a36 100644 --- a/Source/BookGen.Vfs/ApiClient.cs +++ b/Source/BookGen.Vfs/ApiClient.cs @@ -46,7 +46,7 @@ public async Task DownloadJsonAsync(Uri url, JsonSerializerOptions? option if (response.IsSuccessStatusCode) { - await using var stream = await response.Content.ReadAsStreamAsync(); + await using Stream stream = await response.Content.ReadAsStreamAsync(); return await JsonSerializer.DeserializeAsync(stream, options) ?? throw new InvalidOperationException($"Coudln't deserialize response as {typeof(T)}"); } @@ -62,7 +62,7 @@ public async Task DownloadFileTo(Uri url, Stream target, IProgress progres if (response.IsSuccessStatusCode) { - await using var source = await response.Content.ReadAsStreamAsync(); + await using Stream source = await response.Content.ReadAsStreamAsync(); byte[] buffer = new byte[8192]; int read = 0; diff --git a/Source/BookGen.Vfs/BookGen.Vfs.csproj b/Source/BookGen.Vfs/BookGen.Vfs.csproj index 69971531..e4ae09ae 100644 --- a/Source/BookGen.Vfs/BookGen.Vfs.csproj +++ b/Source/BookGen.Vfs/BookGen.Vfs.csproj @@ -7,6 +7,7 @@ ..\..\bin\$(Configuration)\ false preview + True diff --git a/Source/BookGen.Vfs/EmptyAssetSource.cs b/Source/BookGen.Vfs/EmptyAssetSource.cs new file mode 100644 index 00000000..4badaba0 --- /dev/null +++ b/Source/BookGen.Vfs/EmptyAssetSource.cs @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2025 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Diagnostics.CodeAnalysis; + +namespace BookGen.Vfs; + +public sealed class EmptyAssetSource : IAssetSource +{ + public IReadOnlyList AssetNames + => Array.Empty(); + + public Stream GetBinaryAssetStream(string name) + { + return Stream.Null; + } + + public bool TryGetAsset(string name, [NotNullWhen(true)] out string? content) + { + content = null; + return false; + } +} diff --git a/Source/BookGen.Vfs/Extensions.cs b/Source/BookGen.Vfs/Extensions.cs index f9ff6546..4824c723 100644 --- a/Source/BookGen.Vfs/Extensions.cs +++ b/Source/BookGen.Vfs/Extensions.cs @@ -3,6 +3,7 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Schema; @@ -12,60 +13,86 @@ namespace BookGen.Vfs; public static class Extensions { - public static XmlWriter CreateXmlWriter(this IWritableFileSystem fs, string path) + extension(IReadOnlyFileSystem fs) { - return XmlWriter.Create(fs.CreateTextWriter(path)); - } - - public static async Task DeserializeAsync(this IReadOnlyFileSystem fs, string path) - { - await using var stream = fs.OpenReadStream(path); - T? result = await JsonSerializer.DeserializeAsync(stream, JsonOptions.SerializerOptions); - return result; - } - - public static async Task SerializeAsync(this IWritableFileSystem fs, string path, T value, bool writeSchema) - { - await using var stream = fs.CreateWriteStream(path); - await JsonSerializer.SerializeAsync(stream, value, JsonOptions.SerializerOptions); - if (writeSchema) + public async Task DeserializeAsync(string path) { - var newName = Path.ChangeExtension(path, ".schema.json"); - await fs.WriteSchema(newName); + await using Stream stream = fs.OpenReadStream(path); + T? result = await JsonSerializer.DeserializeAsync(stream, JsonOptions.SerializerOptions); + return result; } - } - public static async Task WriteJsonAsync(this IWritableFileSystem fs, string path, JsonObject json) - { - await fs.WriteAllTextAsync(path, json.ToJsonString(JsonOptions.SerializerOptions)); - } + public async Task ReadJsonAsync(string path) + { + string content = await fs.ReadAllTextAsync(path); + JsonNode? parsed = JsonNode.Parse(content); + if (parsed is not JsonObject jsonObject) + { + throw new InvalidOperationException($"Failed to parse JSON from {path}"); + } + return jsonObject; + } - public static async Task ReadJsonAsync(this IReadOnlyFileSystem fs, string path) - { - string content = await fs.ReadAllTextAsync(path); - var parsed = JsonObject.Parse(content); - if (parsed is not JsonObject jsonObject) + public string GetFileNameInTargetFolder(IReadOnlyFileSystem targetFolder, string file, string newExtension) { - throw new InvalidOperationException($"Failed to parse JSON from {path}"); + ArgumentException.ThrowIfNullOrWhiteSpace(fs.Scope); + ArgumentException.ThrowIfNullOrWhiteSpace(targetFolder.Scope); + + var fullPath = Path.GetFullPath(file, fs.Scope); + + var relativePart = Path.GetRelativePath(fs.Scope, fullPath); + + return Path.ChangeExtension(Path.GetFullPath(relativePart, targetFolder.Scope), newExtension); } - return jsonObject; - } - public static async Task WriteSchema(this IWritableFileSystem fs, string path) - { - var node = JsonOptions.SerializerOptions.GetJsonSchemaAsNode(typeof(T), JsonOptions.ExporterOptions); - await fs.WriteAllTextAsync(path, node.ToJsonString(JsonOptions.SerializerOptions)); + public (string content, DateTime lastmodified) ReadInputFiles(string[] inputFiles) + { + StringBuilder md = new(inputFiles.Length * 1024); + DateTime lastmodified = DateTime.MinValue; + foreach (var inputFile in inputFiles) + { + string content = fs.ReadAllText(inputFile); + DateTime date = fs.GetLastModifiedUtc(inputFile); + + if (date > lastmodified) + lastmodified = date; + + md.Append(content); + + if (!content.EndsWith('\n')) + md.Append(System.Environment.NewLine); + } + return (md.ToString(), lastmodified); + } } - public static string GetFileNameInTargetFolder(this IReadOnlyFileSystem sourceFolder, IReadOnlyFileSystem targetFolder, string file, string newExtension) + extension(IWritableFileSystem fs) { - ArgumentException.ThrowIfNullOrWhiteSpace(sourceFolder.Scope); - ArgumentException.ThrowIfNullOrWhiteSpace(targetFolder.Scope); + public XmlWriter CreateXmlWriter(string path) + { + return XmlWriter.Create(fs.CreateTextWriter(path)); + } - var fullPath = Path.GetFullPath(file, sourceFolder.Scope); + public async Task WriteJsonAsync(string path, JsonObject json) + { + await fs.WriteAllTextAsync(path, json.ToJsonString(JsonOptions.SerializerOptions)); + } - var relativePart = Path.GetRelativePath(sourceFolder.Scope, fullPath); + public async Task WriteSchema(string path) + { + JsonNode node = JsonOptions.SerializerOptions.GetJsonSchemaAsNode(typeof(T), JsonOptions.ExporterOptions); + await fs.WriteAllTextAsync(path, node.ToJsonString(JsonOptions.SerializerOptions)); + } - return Path.ChangeExtension(Path.GetFullPath(relativePart, targetFolder.Scope), newExtension); + public async Task SerializeAsync(string path, T value, bool writeSchema) + { + await using Stream stream = fs.CreateWriteStream(path); + await JsonSerializer.SerializeAsync(stream, value, JsonOptions.SerializerOptions); + if (writeSchema) + { + var newName = Path.ChangeExtension(path, ".schema.json"); + await fs.WriteSchema(newName); + } + } } } diff --git a/Source/BookGen.Vfs/FileSystem.cs b/Source/BookGen.Vfs/FileSystem.cs index 614e6c60..af25fe12 100644 --- a/Source/BookGen.Vfs/FileSystem.cs +++ b/Source/BookGen.Vfs/FileSystem.cs @@ -29,15 +29,15 @@ public async Task CopyToAsync(string path, string destination) { static async Task CopySingleFile(string source, string destination) { - await using var sourceStream = File.OpenRead(source); - await using var destinationStream = File.Create(destination); + await using FileStream sourceStream = File.OpenRead(source); + await using FileStream destinationStream = File.Create(destination); await sourceStream.CopyToAsync(destinationStream); } var actualPath = GetAndValidateFullNameInScope(path); if (Directory.Exists(actualPath)) { - var files = Directory.EnumerateFiles(actualPath, "*.*", SearchOption.AllDirectories); + IEnumerable files = Directory.EnumerateFiles(actualPath, "*.*", SearchOption.AllDirectories); foreach (var file in files) { CreatePathIfNeeded(file); diff --git a/Source/BookGen.Vfs/FileSystemFactory.cs b/Source/BookGen.Vfs/FileSystemFactory.cs new file mode 100644 index 00000000..89610458 --- /dev/null +++ b/Source/BookGen.Vfs/FileSystemFactory.cs @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +namespace BookGen.Vfs; + +public sealed class FileSystemFactory : IFileSystemFactory +{ + public IReadOnlyFileSystem CreateMultiReadScopeFileSystem(params IEnumerable scopes) + => new MultiReadScopeFileSystem(scopes); + + public IReadOnlyFileSystem CreateReadOnlyFileSystem(string scope = "") + { + return new ReadOnlyFileSystem + { + Scope = scope + }; + } + + public IWritableFileSystem CreateWritableFileSystem(string scope = "") + { + return new FileSystem + { + Scope = scope + }; + } +} diff --git a/Source/BookGen.Vfs/IAssetSource.cs b/Source/BookGen.Vfs/IAssetSource.cs index aef66f88..7b6c0601 100644 --- a/Source/BookGen.Vfs/IAssetSource.cs +++ b/Source/BookGen.Vfs/IAssetSource.cs @@ -13,7 +13,7 @@ public interface IAssetSource IReadOnlyList AssetNames { get; } - byte[] GetBinaryAsset(string name); + Stream GetBinaryAssetStream(string name); string GetAsset(string name) { diff --git a/Source/BookGen.Vfs/IFileSystemFactory.cs b/Source/BookGen.Vfs/IFileSystemFactory.cs new file mode 100644 index 00000000..66868c00 --- /dev/null +++ b/Source/BookGen.Vfs/IFileSystemFactory.cs @@ -0,0 +1,13 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +namespace BookGen.Vfs; + +public interface IFileSystemFactory +{ + IReadOnlyFileSystem CreateMultiReadScopeFileSystem(params IEnumerable scopes); + IWritableFileSystem CreateWritableFileSystem(string scope = ""); + IReadOnlyFileSystem CreateReadOnlyFileSystem(string scope = ""); +} diff --git a/Source/BookGen.Vfs/Internals/JsonSchemaTransformer.cs b/Source/BookGen.Vfs/Internals/JsonSchemaTransformer.cs index 0239b769..ddb7c69c 100644 --- a/Source/BookGen.Vfs/Internals/JsonSchemaTransformer.cs +++ b/Source/BookGen.Vfs/Internals/JsonSchemaTransformer.cs @@ -36,11 +36,12 @@ public static JsonNode TransformSchemaNode(JsonSchemaExporterContext context, Js && propertiesNode is JsonObject properties) { var requiredArray = new JsonArray(); - foreach (var property in properties) + foreach (KeyValuePair property in properties) { requiredArray.Add(property.Key); } obj["required"] = requiredArray; + obj["additionalProperties"] = false; } AddDecription(node, GetAttribute(attributeProvider)); @@ -106,7 +107,7 @@ static bool IsNumeric(Type? propertyType) }; } - if (rangeAttribute == null || node is not JsonObject jObj) + if (rangeAttribute is null || node is not JsonObject jObj) return; if (IsNumeric(propertyInfo?.PropertyType)) @@ -118,7 +119,7 @@ static bool IsNumeric(Type? propertyType) private static void AddRegex(JsonNode node, string? regexPattern, JsonPropertyInfo? propertyInfo) { - if (regexPattern == null || node is not JsonObject jObj) + if (regexPattern is null || node is not JsonObject jObj) return; if (propertyInfo?.PropertyType == typeof(string)) @@ -135,7 +136,7 @@ static bool IsCollection(Type? propertyType) || propertyType?.IsAssignableTo(typeof(System.Collections.IEnumerable)) == true; } - if (length == null || node is not JsonObject jObj) + if (length is null || node is not JsonObject jObj) return; if (propertyInfo?.PropertyType == typeof(string)) diff --git a/Source/BookGen.Vfs/JsonOptions.cs b/Source/BookGen.Vfs/JsonOptions.cs index 9c0618d9..d6ae0798 100644 --- a/Source/BookGen.Vfs/JsonOptions.cs +++ b/Source/BookGen.Vfs/JsonOptions.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -13,7 +13,7 @@ namespace BookGen.Vfs; -internal static class JsonOptions +public static class JsonOptions { public readonly static JsonSerializerOptions SerializerOptions = new(JsonSerializerOptions.Default) { diff --git a/Source/BookGen.Vfs/MultiReadScopeFileSystem.cs b/Source/BookGen.Vfs/MultiReadScopeFileSystem.cs new file mode 100644 index 00000000..d472c648 --- /dev/null +++ b/Source/BookGen.Vfs/MultiReadScopeFileSystem.cs @@ -0,0 +1,122 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +namespace BookGen.Vfs; + +public sealed class MultiReadScopeFileSystem : IReadOnlyFileSystem +{ + private readonly HashSet _scopes; + + private string Resolve(string path) + { + foreach (var scope in _scopes) + { + var fullPath = Path.GetFullPath(Path.Combine(scope, path)); + if (File.Exists(fullPath)) + return fullPath; + } + throw new InvalidOperationException($"{path} can't be found"); + } + + public MultiReadScopeFileSystem(params IEnumerable scopes) + { + _scopes = new HashSet(scopes); + } + + public string Scope + { + get => string.Join(Environment.NewLine, _scopes); + set => throw new NotSupportedException(); + } + + public bool DirectoryExists(string path) + { + foreach (var scope in _scopes) + { + var fullPath = Path.GetFullPath(Path.Combine(scope, path)); + + if (Directory.Exists(fullPath)) + return true; + } + return false; + } + + public bool FileExists(string path) + { + foreach (var scope in _scopes) + { + var fullPath = Path.GetFullPath(Path.Combine(scope, path)); + + if (File.Exists(fullPath)) + return true; + } + return false; + } + + public IEnumerable GetDirectories(string path, bool recursive) + { + foreach (var scope in _scopes) + { + string fullPath = Path.GetFullPath(Path.Combine(scope, path)); + IEnumerable directories = Directory.EnumerateDirectories(fullPath, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + foreach (var directory in directories) + { + yield return directory; + } + } + } + + public IEnumerable GetFiles(string path, string filter, bool recursive) + { + foreach (var scope in _scopes) + { + string fullPath = Path.GetFullPath(Path.Combine(scope, path)); + IEnumerable files = Directory.EnumerateFiles(fullPath, filter, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + foreach (var file in files) + { + yield return file; + } + } + } + + public long GetFileSize(string path) + { + var actualPath = Resolve(path); + FileInfo fileInfo = new FileInfo(actualPath); + return fileInfo.Length; + } + + public DateTime GetLastModifiedUtc(string path) + { + var actualPath = Resolve(path); + return Directory.Exists(actualPath) + ? Directory.GetLastWriteTimeUtc(actualPath) + : File.GetLastWriteTimeUtc(actualPath); + } + + public Stream OpenReadStream(string path) + { + var actualPath = Resolve(path); + return File.OpenRead(actualPath); + } + + public TextReader OpenTextReader(string path) + { + var actualPath = Resolve(path); + return File.OpenText(actualPath); + } + + public string ReadAllText(string path) + { + var actualPath = Resolve(path); + return File.ReadAllText(actualPath); + } + + public async Task ReadAllTextAsync(string path) + { + var actualPath = Resolve(path); + return await File.ReadAllTextAsync(actualPath); + } +} diff --git a/Source/BookGen.Vfs/ZipAssetSoruce.cs b/Source/BookGen.Vfs/ZipAssetSoruce.cs index 18897aa1..094e7bfb 100644 --- a/Source/BookGen.Vfs/ZipAssetSoruce.cs +++ b/Source/BookGen.Vfs/ZipAssetSoruce.cs @@ -70,25 +70,13 @@ public bool TryGetAsset(string name, [NotNullWhen(true)] out string? content) return true; } - public byte[] GetBinaryAsset(string name) + public Stream GetBinaryAssetStream(string name) { ObjectDisposedException.ThrowIf(_disposed, nameof(_zip)); lock (_lock) { ZipArchiveEntry? entry = _zip.GetEntry(name) ?? throw new InvalidOperationException($"{name} was not found in assets"); - byte[] data = new byte[entry.Length]; - using Stream dataStream = entry.Open(); - byte[] buffer = ArrayPool.Shared.Rent(4096); - int read = 0; - int offset = 0; - while ((read = dataStream.Read(buffer, 0, buffer.Length)) > 0) - { - Array.Copy(buffer, 0, data, offset, read); - offset += read; - } - ArrayPool.Shared.Return(buffer, true); - - return data; + return entry.Open(); } } } diff --git a/Source/BookGen.Vfs/ZipBuilder.cs b/Source/BookGen.Vfs/ZipBuilder.cs index e48418a8..b0cd48f3 100644 --- a/Source/BookGen.Vfs/ZipBuilder.cs +++ b/Source/BookGen.Vfs/ZipBuilder.cs @@ -28,8 +28,8 @@ public async Task AddAsync(string entryName, Encoding encoding, CompressionLevel compressionLevel = CompressionLevel.SmallestSize) { - var entry = _archive.CreateEntry(entryName, compressionLevel); - await using var entryStream = entry.Open(); + ZipArchiveEntry entry = _archive.CreateEntry(entryName, compressionLevel); + await using Stream entryStream = entry.Open(); await using var writer = new StreamWriter(entryStream, encoding); await writer.WriteAsync(entryValue); } @@ -38,8 +38,8 @@ public async Task AddAsync(string entryName, Stream entryValue, CompressionLevel compressionLevel = CompressionLevel.SmallestSize) { - var entry = _archive.CreateEntry(entryName, compressionLevel); - await using var entryStream = entry.Open(); + ZipArchiveEntry entry = _archive.CreateEntry(entryName, compressionLevel); + await using Stream entryStream = entry.Open(); await entryValue.CopyToAsync(entryStream); } @@ -47,8 +47,8 @@ public async Task AddAsync(string entryName, byte[] entryValue, CompressionLevel compressionLevel = CompressionLevel.NoCompression) { - var entry = _archive.CreateEntry(entryName, compressionLevel); - await using var entryStream = entry.Open(); + ZipArchiveEntry entry = _archive.CreateEntry(entryName, compressionLevel); + await using Stream entryStream = entry.Open(); await entryStream.WriteAsync(entryValue, 0, entryValue.Length); } @@ -68,8 +68,8 @@ public void AddXml(string entryName, } } - var entry = _archive.CreateEntry(entryName, CompressionLevel.SmallestSize); - using var entryStream = entry.Open(); + ZipArchiveEntry entry = _archive.CreateEntry(entryName, CompressionLevel.SmallestSize); + using Stream entryStream = entry.Open(); serializer.Serialize(entryStream, instance, xnames); } diff --git a/Source/BookGen/BookGen.csproj b/Source/BookGen/BookGen.csproj index 0bcf8def..6cf6b380 100644 --- a/Source/BookGen/BookGen.csproj +++ b/Source/BookGen/BookGen.csproj @@ -11,6 +11,7 @@ ..\..\Branding\icon-bookgen.ico $([System.DateTime]::UtcNow.ToString("yyyy")).$([System.DateTime]::UtcNow.ToString("MM")).$([System.DateTime]::UtcNow.ToString("dd")).0 true + True @@ -22,15 +23,19 @@ + + + + - all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/BookGen/BookGenArgumentBase.cs b/Source/BookGen/BookGenArgumentBase.cs index 83126586..0c902746 100644 --- a/Source/BookGen/BookGenArgumentBase.cs +++ b/Source/BookGen/BookGenArgumentBase.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -17,8 +17,27 @@ public class BookGenArgumentBase : ArgumentsBase, IVerbosablityToggle [Switch("d", "dir")] public string Directory { get; set; } + [Switch("co", "configoverlay")] + public string ConfigOverlay { get; set; } = string.Empty; + public BookGenArgumentBase() { Directory = Environment.CurrentDirectory; } + + override public ValidationResult Validate(IValidationContext context) + { + if (!context.FileSystem.DirectoryExists(Directory)) + { + return ValidationResult.Error($"Directory '{Directory}' does not exist."); + } + + if (!string.IsNullOrEmpty(ConfigOverlay) + && !context.FileSystem.FileExists(ConfigOverlay)) + { + return ValidationResult.Error($"Config overlay file '{ConfigOverlay}' does not exist."); + } + + return ValidationResult.Ok(); + } } diff --git a/Source/BookGen/BuildArguments.cs b/Source/BookGen/BuildArguments.cs index aac18e9b..41f09d79 100644 --- a/Source/BookGen/BuildArguments.cs +++ b/Source/BookGen/BuildArguments.cs @@ -18,7 +18,7 @@ public sealed class BuildArguments : BookGenArgumentBase public override ValidationResult Validate(IValidationContext context) { - var originalResult = base.Validate(context); + ValidationResult originalResult = base.Validate(context); if (originalResult.IsOk && !string.IsNullOrEmpty(HostOverride) && !HostOverride.EndsWith('/')) diff --git a/Source/BookGen/BuildCommandBase.cs b/Source/BookGen/BuildCommandBase.cs index b4e798f2..df36b1c7 100644 --- a/Source/BookGen/BuildCommandBase.cs +++ b/Source/BookGen/BuildCommandBase.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -10,6 +10,7 @@ using BookGen.Infrastructure.Loging; using BookGen.Vfs; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace BookGen; @@ -19,17 +20,20 @@ internal abstract class BuildCommandBase : AsyncCommand protected readonly IWritableFileSystem _soruce; protected readonly IWritableFileSystem _target; protected readonly ILogger _logger; - private readonly IAssetSource _assetSource; + protected readonly IAssetSource _assetSource; + protected readonly IMemoryCache _memoryCache; public BuildCommandBase(IWritableFileSystem soruce, IWritableFileSystem target, ILogger logger, - IAssetSource assetSource) + IAssetSource assetSource, + IMemoryCache memoryCache) { _soruce = soruce; _target = target; _logger = logger; _assetSource = assetSource; + _memoryCache = memoryCache; } public abstract Pipeline GetPipeLine(); @@ -47,7 +51,7 @@ public override async Task ExecuteAsync(BuildArguments arguments, IReadOnly _target.Scope = arguments.OutputDirectory; using var env = new BookEnvironment(_soruce, _target, _assetSource); - EnvironmentStatus status = await env.Initialize(); + EnvironmentStatus status = await env.Initialize(arguments.ConfigOverlay); if (!status.IsOk) { diff --git a/Source/BookGen/Commands/AddFrontMatterCommand.cs b/Source/BookGen/Commands/AddFrontMatterCommand.cs index adca171e..38072267 100644 --- a/Source/BookGen/Commands/AddFrontMatterCommand.cs +++ b/Source/BookGen/Commands/AddFrontMatterCommand.cs @@ -17,6 +17,8 @@ using Microsoft.Extensions.Logging; +using YamlDotNet.Serialization; + namespace BookGen.Commands; [CommandName("addfrontmatter")] @@ -38,7 +40,7 @@ public override async Task ExecuteAsync(BookGenArgumentBase arguments, IRea var files = _writableFileSystem.GetFiles(arguments.Directory, "*.md", true).ToArray(); _logger.LogInformation("Found {count} markdown files in {directory}", files.Length, arguments.Directory); - var serializer = YamlSerializerFactory.CreateSerializer(); + ISerializer serializer = YamlSerializerFactory.CreateSerializer(); foreach (var file in files) { @@ -51,7 +53,7 @@ public override async Task ExecuteAsync(BookGenArgumentBase arguments, IRea _logger.LogDebug("Adding front matter to: {file}...", file); - var firstHedding = Markdown.Parse(content).OfType().FirstOrDefault(); + HeadingBlock? firstHedding = Markdown.Parse(content).OfType().FirstOrDefault(); string title = firstHedding?.Inline != null ? string.Join("", firstHedding.Inline) diff --git a/Source/BookGen/Commands/AssemblyDocument.cs b/Source/BookGen/Commands/AssemblyDocument.cs index dcfb80f0..35fc02d8 100644 --- a/Source/BookGen/Commands/AssemblyDocument.cs +++ b/Source/BookGen/Commands/AssemblyDocument.cs @@ -33,7 +33,7 @@ public class Arguments : InputOutputArguments public override int Execute(Arguments arguments, IReadOnlyList context) { - var result = XmlDocMarkdownGenerator.Generate(arguments.InputFile, arguments.OutputFile, new XmlDocMarkdownSettings + XmlDocMarkdownResult result = XmlDocMarkdownGenerator.Generate(arguments.InputFile, arguments.OutputFile, new XmlDocMarkdownSettings { IsDryRun = arguments.DryRun, IncludeObsolete = true, diff --git a/Source/BookGen/Commands/BuildEpub.cs b/Source/BookGen/Commands/BuildEpub.cs index cf0a93ce..d6ed8c41 100644 --- a/Source/BookGen/Commands/BuildEpub.cs +++ b/Source/BookGen/Commands/BuildEpub.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -8,6 +8,7 @@ using BookGen.Cli.Annotations; using BookGen.Vfs; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace BookGen.Commands; @@ -18,11 +19,12 @@ internal sealed class BuildEpub : BuildCommandBase public BuildEpub(IWritableFileSystem soruce, IWritableFileSystem target, ILogger logger, - IAssetSource assetSource) - : base(soruce, target, logger, assetSource) + IAssetSource assetSource, + IMemoryCache memoryCache) + : base(soruce, target, logger, assetSource, memoryCache) { } public override Pipeline GetPipeLine() - => Pipeline.CreateEpubPileLine(); + => Pipeline.CreateEpubPileLine(_memoryCache); } diff --git a/Source/BookGen/Commands/BuildExportCommand.cs b/Source/BookGen/Commands/BuildExportCommand.cs index ccaf89a5..1a71f562 100644 --- a/Source/BookGen/Commands/BuildExportCommand.cs +++ b/Source/BookGen/Commands/BuildExportCommand.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -8,6 +8,7 @@ using BookGen.Cli.Annotations; using BookGen.Vfs; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace BookGen.Commands; @@ -16,13 +17,14 @@ namespace BookGen.Commands; internal sealed class BuildExportCommand : BuildCommandBase { public BuildExportCommand(IWritableFileSystem soruce, - IWritableFileSystem target, - ILogger logger, - IAssetSource assetSource) - : base(soruce, target, logger, assetSource) + IWritableFileSystem target, + ILogger logger, + IAssetSource assetSource, + IMemoryCache memoryCache) + : base(soruce, target, logger, assetSource, memoryCache) { } public override Pipeline GetPipeLine() - => Pipeline.CreatePostProcessPipeLine(); + => Pipeline.CreatePostProcessPipeLine(_memoryCache); } diff --git a/Source/BookGen/Commands/BuildFeedCommand.cs b/Source/BookGen/Commands/BuildFeedCommand.cs index b817deca..77941138 100644 --- a/Source/BookGen/Commands/BuildFeedCommand.cs +++ b/Source/BookGen/Commands/BuildFeedCommand.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -8,6 +8,7 @@ using BookGen.Cli.Annotations; using BookGen.Vfs; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace BookGen.Commands; @@ -18,11 +19,12 @@ internal sealed class BuildFeedCommand : BuildCommandBase public BuildFeedCommand(IWritableFileSystem soruce, IWritableFileSystem target, ILogger logger, - IAssetSource assetSource) - : base(soruce, target, logger, assetSource) + IAssetSource assetSource, + IMemoryCache memoryCache) + : base(soruce, target, logger, assetSource, memoryCache) { } public override Pipeline GetPipeLine() - => Pipeline.CreateFeedPipeline(); + => Pipeline.CreateFeedPipeline(_memoryCache); } diff --git a/Source/BookGen/Commands/BuildPrintCommand.cs b/Source/BookGen/Commands/BuildPrintCommand.cs index 3b1a2f13..fb1aec67 100644 --- a/Source/BookGen/Commands/BuildPrintCommand.cs +++ b/Source/BookGen/Commands/BuildPrintCommand.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -8,6 +8,7 @@ using BookGen.Cli.Annotations; using BookGen.Vfs; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace BookGen.Commands; @@ -18,11 +19,12 @@ internal sealed class BuildPrintCommand : BuildCommandBase public BuildPrintCommand(IWritableFileSystem soruce, IWritableFileSystem target, ILogger logger, - IAssetSource assetSource) - : base(soruce, target, logger, assetSource) + IAssetSource assetSource, + IMemoryCache memoryCache) + : base(soruce, target, logger, assetSource, memoryCache) { } public override Pipeline GetPipeLine() - => Pipeline.CratePrintPipeLine(); + => Pipeline.CratePrintPipeLine(_memoryCache); } diff --git a/Source/BookGen/Commands/BuildWebCommand.cs b/Source/BookGen/Commands/BuildWebCommand.cs index c2465d88..ef3cc040 100644 --- a/Source/BookGen/Commands/BuildWebCommand.cs +++ b/Source/BookGen/Commands/BuildWebCommand.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -8,6 +8,7 @@ using BookGen.Cli.Annotations; using BookGen.Vfs; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace BookGen.Commands; @@ -18,11 +19,12 @@ internal sealed class BuildWebCommand : BuildCommandBase public BuildWebCommand(IWritableFileSystem soruce, IWritableFileSystem target, ILogger logger, - IAssetSource assetSource) - : base(soruce, target, logger, assetSource) + IAssetSource assetSource, + IMemoryCache memoryCache) + : base(soruce, target, logger, assetSource, memoryCache) { } public override Pipeline GetPipeLine() - => Pipeline.CreateWebPipeLine(); + => Pipeline.CreateWebPipeLine(_memoryCache); } diff --git a/Source/BookGen/Commands/BuildWordpressCommand.cs b/Source/BookGen/Commands/BuildWordpressCommand.cs index a01b575b..c8f2ec59 100644 --- a/Source/BookGen/Commands/BuildWordpressCommand.cs +++ b/Source/BookGen/Commands/BuildWordpressCommand.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -8,6 +8,7 @@ using BookGen.Cli.Annotations; using BookGen.Vfs; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace BookGen.Commands; @@ -18,11 +19,12 @@ internal sealed class BuildWordpressCommand : BuildCommandBase public BuildWordpressCommand(IWritableFileSystem soruce, IWritableFileSystem target, ILogger logger, - IAssetSource assetSource) - : base(soruce, target, logger, assetSource) + IAssetSource assetSource, + IMemoryCache memoryCache) + : base(soruce, target, logger, assetSource, memoryCache) { } public override Pipeline GetPipeLine() - => Pipeline.CreateWordpressPipeLine(); + => Pipeline.CreateWordpressPipeLine(_memoryCache); } diff --git a/Source/BookGen/Commands/ConfigCommand.cs b/Source/BookGen/Commands/ConfigCommand.cs index 1e36ed65..f62c1f30 100644 --- a/Source/BookGen/Commands/ConfigCommand.cs +++ b/Source/BookGen/Commands/ConfigCommand.cs @@ -29,7 +29,7 @@ public override int Execute(IReadOnlyList context) { BookGenAppSettings appSettings = new(); - var data = typeof(BookGenAppSettings).GetProperties(BindingFlags.Public | BindingFlags.Instance); + PropertyInfo[] data = typeof(BookGenAppSettings).GetProperties(BindingFlags.Public | BindingFlags.Instance); if (context.Count == 0) return DisplayConfig(data, appSettings); @@ -46,7 +46,7 @@ public override int Execute(IReadOnlyList context) private int DisplaySpecific(PropertyInfo[] data, BookGenAppSettings appSettings, string property) { string[] headers = ["Name", "Type", "Value"]; - var tableData = data.Where(x => x.Name == property).ToArray(); + PropertyInfo[] tableData = data.Where(x => x.Name == property).ToArray(); if (tableData.Length != 0) return DisplayConfig(tableData, appSettings); @@ -58,7 +58,7 @@ private int DisplaySpecific(PropertyInfo[] data, BookGenAppSettings appSettings, private static int DisplayConfig(PropertyInfo[] data, BookGenAppSettings appSettings) { string[] headers = ["Name", "Type", "Value"]; - var tableData = data.Select(x => new string[] + IEnumerable tableData = data.Select(x => new string[] { x.Name, x.PropertyType.ToString(), diff --git a/Source/BookGen/Commands/GuiCommand.cs b/Source/BookGen/Commands/GuiCommand.cs index c970380f..a26e2a68 100644 --- a/Source/BookGen/Commands/GuiCommand.cs +++ b/Source/BookGen/Commands/GuiCommand.cs @@ -45,7 +45,7 @@ public override async Task ExecuteAsync(BookGenArgumentBase arguments, IRea figlet.Justification = Justify.Center; AnsiConsole.Write(figlet); - var path = new TextPath(_fileSystem.Scope) + TextPath path = new TextPath(_fileSystem.Scope) .RootColor(Color.Red) .SeparatorColor(Color.Green) .StemColor(Color.Blue) @@ -58,7 +58,7 @@ public override async Task ExecuteAsync(BookGenArgumentBase arguments, IRea AnsiConsole.WriteLine(); AnsiConsole.WriteLine(); - var selector = new SelectionPrompt() + SelectionPrompt selector = new SelectionPrompt() .Title("Select an action:") .PageSize(20) .UseConverter(mi => mi.ToString()) @@ -83,7 +83,7 @@ public override async Task ExecuteAsync(BookGenArgumentBase arguments, IRea new(Emoji.Known.Door, "Exit", OnExit) ]); - var selected = AnsiConsole.Prompt(selector); + MenuItem selected = AnsiConsole.Prompt(selector); return await selected.ExecuteAsync(); } diff --git a/Source/BookGen/Commands/HelpCommand.cs b/Source/BookGen/Commands/HelpCommand.cs index 4f28c9fe..bc52e1c0 100644 --- a/Source/BookGen/Commands/HelpCommand.cs +++ b/Source/BookGen/Commands/HelpCommand.cs @@ -16,6 +16,7 @@ internal sealed class HelpCommand : Command { private readonly IHelpProvider _helpProvider; private readonly HashSet _commandNames; + private readonly HelpRenderer _renderer = new(); public HelpCommand(IHelpProvider helpProvider, ICommandRunnerProxy runnerProxy) { @@ -27,7 +28,7 @@ public override int Execute(IReadOnlyList context) { if (context.Count == 0) { - HelpRenderer.RenderHelp(_helpProvider.GetCommandHelp("help")); + _renderer.RenderHelp(_helpProvider.GetCommandHelp("help")); return ExitCodes.Success; } @@ -38,7 +39,7 @@ public override int Execute(IReadOnlyList context) return ExitCodes.GeneralError; } - HelpRenderer.RenderHelp(_helpProvider.GetCommandHelp(command)); + _renderer.RenderHelp(_helpProvider.GetCommandHelp(command)); return ExitCodes.Success; } diff --git a/Source/BookGen/Commands/ImgConvert.cs b/Source/BookGen/Commands/ImgConvert.cs index e76fac8d..ce488b19 100644 --- a/Source/BookGen/Commands/ImgConvert.cs +++ b/Source/BookGen/Commands/ImgConvert.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -95,32 +95,39 @@ public override int Execute(ImgConvertArgs arguments, IReadOnlyList cont ".jpg", ".jpeg", ".png", ".webp" }; - if (!Resolution.TryParse(arguments.Resolution, CultureInfo.InvariantCulture, out var resolution)) + var maxResolution = new Resolution + { + Width = int.MaxValue, + Height = int.MaxValue + }; + + if (!string.IsNullOrEmpty(arguments.Resolution) + && !Resolution.TryParse(arguments.Resolution, CultureInfo.InvariantCulture, out maxResolution)) { Console.Error.WriteLine($"Invalid resolution format: '{arguments.Resolution}'. Expected format is 'WidthxHeight'."); return ExitCodes.ArgumentsError; } - var format = Enum.Parse(arguments.Format, ignoreCase: true); + ImageFormat format = Enum.Parse(arguments.Format, ignoreCase: true); if (_fileSystem.DirectoryExists(arguments.Input)) { - var files = _fileSystem.GetFiles(arguments.Input, "*.*", false).Where(f => supportedExtensions.Contains(Path.GetExtension(f))); + IEnumerable files = _fileSystem.GetFiles(arguments.Input, "*.*", false).Where(f => supportedExtensions.Contains(Path.GetExtension(f))); Parallel.ForEach(files, file => { var outputFile = Path.Combine(arguments.Output, Path.GetFileNameWithoutExtension(file) + "." + arguments.Format); - ConvertImage(file, outputFile, format, arguments.Quality, resolution); + ConvertImage(file, outputFile, format, arguments.Quality, maxResolution); }); } else { - ConvertImage(arguments.Input, arguments.Output, format, arguments.Quality, resolution); + ConvertImage(arguments.Input, arguments.Output, format, arguments.Quality, maxResolution); } return ExitCodes.Success; } - private static void ConvertImage(string file, string outputFile, ImageFormat format, int quality, Resolution resolution) + private static void ConvertImage(string file, string outputFile, ImageFormat format, int quality, Resolution maxResolution) { ImageConverter.Encode(file, outputFile, format switch { @@ -128,6 +135,6 @@ private static void ConvertImage(string file, string outputFile, ImageFormat for ImageFormat.Png => ImageType.Png, ImageFormat.Webp => ImageType.Webp, _ => throw new NotSupportedException($"Image format {format} is not supported.") - }, resolution.Width, resolution.Height, quality); + }, maxResolution.Width, maxResolution.Height, quality); } } diff --git a/Source/BookGen/Commands/InstallCommand.cs b/Source/BookGen/Commands/InstallCommand.cs index f35245dd..8411688b 100644 --- a/Source/BookGen/Commands/InstallCommand.cs +++ b/Source/BookGen/Commands/InstallCommand.cs @@ -42,9 +42,9 @@ public override async Task ExecuteAsync(IReadOnlyList context) Action = InstallTerminalProfile() } }; - var selction = Terminal.SelectionMenu(menu, "Bookgen installer", "Select install options", f => f.DisplayText); + List selction = Terminal.SelectionMenu(menu, "Bookgen installer", "Select install options", f => f.DisplayText); - foreach (var item in selction) + foreach (InstallOption item in selction) { await item.Action; } @@ -66,7 +66,7 @@ private Task AddToPath() { var currentFolder = Environment.CurrentDirectory; var path = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty; - var paths = path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); + List paths = path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); if (paths.Contains(currentFolder, StringComparer.OrdinalIgnoreCase)) { diff --git a/Source/BookGen/Commands/LinksCommand.cs b/Source/BookGen/Commands/LinksCommand.cs index d29212b2..8e66e0e0 100644 --- a/Source/BookGen/Commands/LinksCommand.cs +++ b/Source/BookGen/Commands/LinksCommand.cs @@ -1,11 +1,14 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using System.Collections.Concurrent; +using System.Net; using System.Text.RegularExpressions; using Bookgen.Lib; +using Bookgen.Lib.Domain.IO; using BookGen.Cli; using BookGen.Cli.Annotations; @@ -17,8 +20,15 @@ namespace BookGen.Commands; [CommandName("links")] -internal sealed partial class LinksCommand : AsyncCommand +internal sealed partial class LinksCommand : AsyncCommand { + public sealed class LinkArguments : BookGenArgumentBase + { + [Switch("vf", "verify")] + public bool Verify { get; set; } + } + + private readonly IWritableFileSystem _soruce; private readonly ILogger _logger; @@ -28,12 +38,12 @@ public LinksCommand(IWritableFileSystem soruce, ILogger logger) _logger = logger; } - public override async Task ExecuteAsync(BookGenArgumentBase arguments, IReadOnlyList context) + public override async Task ExecuteAsync(LinkArguments arguments, IReadOnlyList context) { _soruce.Scope = arguments.Directory; using var env = new BookEnvironment(_soruce, _soruce); - EnvironmentStatus status = await env.Initialize(); + EnvironmentStatus status = await env.Initialize(arguments.ConfigOverlay); if (!status.IsOk) { @@ -42,8 +52,9 @@ public override async Task ExecuteAsync(BookGenArgumentBase arguments, IRea } Dictionary allLinks = new(); + Dictionary badLinks = new(); - foreach (var chapter in env.TableOfContents.Chapters) + foreach (TocChapter chapter in env.TableOfContents.Chapters) { _logger.LogInformation("Scanning {chapter} for links...", chapter.Title); @@ -54,32 +65,117 @@ public override async Task ExecuteAsync(BookGenArgumentBase arguments, IRea var text = await _soruce.ReadAllTextAsync(file); chapterLinks.UnionWith(GetLinks(text)); } + + if (arguments.Verify) + { + _logger.LogInformation("Verifying links in {chapter}...", chapter.Title); + ConcurrentDictionary linksWithIssues = await VerifyLinks(chapterLinks); + if (linksWithIssues.Count > 0) + { + _logger.LogWarning("Found {count} links with issues in {chapter}:", linksWithIssues.Count, chapter.Title); + badLinks.Add(chapter.Title, linksWithIssues.Select(kvp => $"{kvp.Key} - {kvp.Value}").ToArray()); + } + } + allLinks.Add(chapter.Title, chapterLinks.ToArray()); chapterLinks.Clear(); } + await WriteMarkdown(allLinks, "links.md"); + + if (arguments.Verify) + { + await WriteMarkdown(badLinks, "links.issues.md"); + } + + return ExitCodes.Success; + } + + private async Task WriteMarkdown(Dictionary dataSet, string fileName) + { MarkdownBuilder markdown = new(); - foreach (var linkData in allLinks) + foreach (KeyValuePair linkData in dataSet) { markdown.Heading(2, linkData.Key); markdown.UnorderedList(linkData.Value); } - _logger.LogInformation("Writing links.md..."); - await _soruce.WriteAllTextAsync("links.md", markdown.ToString()); - - return ExitCodes.Success; + _logger.LogInformation("Writing {file}...", fileName); + await _soruce.WriteAllTextAsync(fileName, markdown.ToString()); } [GeneratedRegex(@"https?://[^\s\)\]\}""'<>]+")] - private partial Regex Links { get; } + private static partial Regex Links { get; } - private IEnumerable GetLinks(string text) + private static IEnumerable GetLinks(string text) { - var links = Links.Matches(text); + MatchCollection links = Links.Matches(text); foreach (Match link in links) { yield return link.Value; } } + + private async Task> VerifyLinks(HashSet chapterLinks) + { + ConcurrentDictionary linksWithIssues = new(); + + await Parallel.ForEachAsync(chapterLinks, async (link, cancellationToken) => + { + using HttpClient client = CreateHttpClient(); + + try + { + _logger.LogDebug("Verifying {link}...", link); + using var request = new HttpRequestMessage(HttpMethod.Head, link); + + using HttpResponseMessage response = await client.SendAsync(request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken); + + // If HEAD is not allowed, fall back to a minimal GET + if (response.StatusCode == HttpStatusCode.MethodNotAllowed || + response.StatusCode == HttpStatusCode.NotImplemented) + { + using var getRequest = new HttpRequestMessage(HttpMethod.Get, link); + using HttpResponseMessage getResponse = await client.SendAsync(getRequest, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken); + + if (!getResponse.IsSuccessStatusCode) + { + linksWithIssues.TryAdd(link, getResponse.StatusCode); + } + + return; + } + + if (!response.IsSuccessStatusCode) + { + linksWithIssues.TryAdd(link, response.StatusCode); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error verifying link: {link}", link); + linksWithIssues.TryAdd(link, HttpStatusCode.ServiceUnavailable); + } + }); + + return linksWithIssues; + } + + private static HttpClient CreateHttpClient() + { + var client = new HttpClient + { + Timeout = TimeSpan.FromSeconds(5), + }; + + client.DefaultRequestHeaders.UserAgent.Add( + new System.Net.Http.Headers.ProductInfoHeaderValue("BookGenLinkChecker", "1.0")); + + return client; + } + } diff --git a/Source/BookGen/Commands/Md2HtmlCommand.cs b/Source/BookGen/Commands/Md2HtmlCommand.cs index 4415f93a..6cbef01c 100644 --- a/Source/BookGen/Commands/Md2HtmlCommand.cs +++ b/Source/BookGen/Commands/Md2HtmlCommand.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -71,7 +71,7 @@ public override ValidationResult Validate(IValidationContext context) if (string.IsNullOrEmpty(OutputFile)) result.AddIssue("Output file must be specified"); - if (!InputFiles.Any()) + if (InputFiles.Length == 0) result.AddIssue("An Input file must be specified"); foreach (var inputfile in InputFiles) @@ -88,6 +88,7 @@ public override ValidationResult Validate(IValidationContext context) } private readonly ILogger _log; + private readonly IFileSystemFactory _fileSystemFactory; private readonly IWritableFileSystem _fileSystem; private readonly IAssetSource _assetSource; private const string TitleTag = "{{Title}}"; @@ -95,47 +96,55 @@ public override ValidationResult Validate(IValidationContext context) private readonly TemplateEngine _templateEngine; - public Md2HtmlCommand(ILogger log, IWritableFileSystem fileSystem, IAssetSource assetSource) + public Md2HtmlCommand(ILogger log, IFileSystemFactory fileSystemFactory, IAssetSource assetSource) { _log = log; - _fileSystem = fileSystem; + _fileSystemFactory = fileSystemFactory; + _fileSystem = fileSystemFactory.CreateWritableFileSystem(); _assetSource = assetSource; _templateEngine = new TemplateEngine(log, assetSource); } public override int Execute(Md2HtmlArguments arguments, IReadOnlyList context) { - (string md, DateTime lastmodified) = ReadInputFiles(arguments.InputFiles); + IEnumerable inputFolders = arguments.InputFiles.Select(i => Path.GetDirectoryName(i)); + + IReadOnlyFileSystem inputFilesScope = _fileSystemFactory.CreateMultiReadScopeFileSystem(inputFolders!); + + (string md, DateTime lastmodified) = inputFilesScope.ReadInputFiles(arguments.InputFiles); string? pageTemplate = string.Empty; if (string.IsNullOrEmpty(arguments.Template)) pageTemplate = _assetSource.GetAsset(BundledAssets.TemplateSinglePage); else - pageTemplate = _fileSystem.ReadAllText(arguments.Template); + pageTemplate = inputFilesScope.ReadAllText(arguments.Template); if (!ValidateTemplate(pageTemplate)) return ExitCodes.GeneralError; - var imgService = new ImgService(_fileSystem, _log, new ImageConfig + var imgConfig = new ImageConfig { SvgRecode = arguments.SvgPassthrough ? SvgRecodeOption.Passtrough : SvgRecodeOption.AsWebp, ImageQualityOnResize = 90, - }); + }; - using var settings = new RenderSettings(imgService) + var imgService = new ImgService(inputFilesScope, _log, imgConfig); + + using var settings = new MarkdownRenderSettings(imgService) { HostUrl = string.Empty, DeleteFirstH1 = false, CssClasses = new CssClasses(), OffsetHeadingsBy = 0, AutoEmbedSupportedLinks = !arguments.NoEmbed, - PrismJsInterop = new PrismJsInterop(_assetSource) + PrismJsInterop = arguments.NoSyntax ? null : new SyntaxRenderJsInterop(_assetSource), + ImageRenderJsInterop = new ImageRenderJsInterop(_assetSource, imgConfig) }; - using var mdToHtml = new MarkdownConverter(settings); + using var markdownConverter = new MarkdownConverter(settings); - string? mdcontent = mdToHtml.RenderMarkdownToHtml(md); + string? mdcontent = markdownConverter.RenderMarkdownToHtml(md); string rendered; if (arguments.RawHtml) @@ -163,26 +172,6 @@ public override int Execute(Md2HtmlArguments arguments, IReadOnlyList co return ExitCodes.Success; } - private (string content, DateTime lastmodified) ReadInputFiles(string[] inputFiles) - { - StringBuilder md = new(inputFiles.Length * 1024); - DateTime lastmodified = DateTime.MinValue; - foreach (var inputFile in inputFiles) - { - string content = _fileSystem.ReadAllText(inputFile); - DateTime date = _fileSystem.GetLastModifiedUtc(inputFile); - - if (date > lastmodified) - lastmodified = date; - - md.Append(content); - - if (!content.EndsWith('\n')) - md.Append(System.Environment.NewLine); - } - return (md.ToString(), lastmodified); - } - private bool ValidateTemplate(string pageTemplate) { bool returnValue = true; diff --git a/Source/BookGen/Commands/Md2TerminalCommand.cs b/Source/BookGen/Commands/Md2TerminalCommand.cs new file mode 100644 index 00000000..77f90523 --- /dev/null +++ b/Source/BookGen/Commands/Md2TerminalCommand.cs @@ -0,0 +1,96 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2025 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Text; + +using Bookgen.Lib.Domain.IO.Configuration; +using Bookgen.Lib.Markdown; + +using BookGen.Cli; +using BookGen.Cli.Annotations; +using BookGen.Vfs; + +using Microsoft.Extensions.Logging; + +namespace BookGen.Commands; + +[CommandName("md2terminal")] +internal sealed class Md2TerminalCommand : Command +{ + private readonly ILogger _log; + private readonly IWritableFileSystem _fileSystem; + + internal sealed class Arguments : ArgumentsBase + { + [Switch("i", "input")] + public string[] InputFiles { get; set; } + + [Switch("o", "output")] + public string OutputFile { get; set; } + + public Arguments() + { + InputFiles = []; + OutputFile = string.Empty; + } + + public override ValidationResult Validate(IValidationContext context) + { + ValidationResult result = new(); + + if (string.IsNullOrEmpty(OutputFile)) + result.AddIssue("Output file must be specified"); + + if (InputFiles.Length == 0) + result.AddIssue("An Input file must be specified"); + + foreach (var inputfile in InputFiles) + { + if (!context.FileSystem.FileExists(inputfile)) + result.AddIssue($"Input file: {inputfile} doesn't exist"); + } + + return base.Validate(context); + } + } + + public Md2TerminalCommand(ILogger log, IWritableFileSystem fileSystem) + { + _log = log; + _fileSystem = fileSystem; + } + + private static void WriteToStdout(string rendered) + { + Console.OutputEncoding = Encoding.UTF8; + Spectre.Console.AnsiConsole.WriteLine(rendered); + } + + public override int Execute(Arguments arguments, IReadOnlyList context) + { + (string md, _) = _fileSystem.ReadInputFiles(arguments.InputFiles); + + using var settings = new MarkdownRenderSettings(null!) + { + DeleteFirstH1 = false, + AutoEmbedSupportedLinks = false, + CssClasses = new CssClasses(), + HostUrl = string.Empty, + PrismJsInterop = null, + ImageRenderJsInterop = null! + }; + + using var markdonwConverter = new MarkdownConverter(settings); + + var rendered = markdonwConverter.RenderMarkdownToTerminal(md); + + if (arguments.OutputFile == "-") + WriteToStdout(rendered); + else + _fileSystem.WriteAllText(arguments.OutputFile, rendered); + + return ExitCodes.Success; + } +} diff --git a/Source/BookGen/Commands/NewPageCommand.cs b/Source/BookGen/Commands/NewPageCommand.cs index 7a39025e..3ed533ce 100644 --- a/Source/BookGen/Commands/NewPageCommand.cs +++ b/Source/BookGen/Commands/NewPageCommand.cs @@ -12,6 +12,8 @@ using Microsoft.Extensions.Logging; +using YamlDotNet.Serialization; + namespace BookGen.Commands; [CommandName("newpage")] @@ -54,7 +56,7 @@ public override int Execute(Arguments arguments, IReadOnlyList context) Tags = "", }; - var serializer = YamlSerializerFactory.CreateSerializer(); + ISerializer serializer = YamlSerializerFactory.CreateSerializer(); var yaml = serializer.Serialize(frontMatter); diff --git a/Source/BookGen/Commands/SchemasCommand.cs b/Source/BookGen/Commands/SchemasCommand.cs index 4733ee61..8ec9557d 100644 --- a/Source/BookGen/Commands/SchemasCommand.cs +++ b/Source/BookGen/Commands/SchemasCommand.cs @@ -1,15 +1,10 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- -using System.ComponentModel; -using System.Diagnostics; -using System.Reflection; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Schema; -using System.Text.Json.Serialization; using Bookgen.Lib; using Bookgen.Lib.Domain.IO; @@ -28,73 +23,29 @@ internal sealed class SchemasCommand : Command { private readonly IWritableFileSystem _writableFileSystem; private readonly ILogger _logger; - private readonly JsonSerializerOptions _options; - private readonly JsonSchemaExporterOptions _exporterOptions; public SchemasCommand(IWritableFileSystem writableFileSystem, ILogger logger) { _writableFileSystem = writableFileSystem; _logger = logger; - _options = new JsonSerializerOptions(JsonSerializerOptions.Default) - { - WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Converters = - { - new JsonStringEnumConverter() - }, - }; - _exporterOptions = new() - { - TransformSchemaNode = (context, schema) => - { - // Determine if a type or property and extract the relevant attribute provider. - ICustomAttributeProvider? attributeProvider = context.PropertyInfo is not null - ? context.PropertyInfo.AttributeProvider - : context.TypeInfo.Type; - - // Look up any description attributes. - DescriptionAttribute? descriptionAttr = attributeProvider? - .GetCustomAttributes(inherit: true) - .Select(attr => attr as DescriptionAttribute) - .FirstOrDefault(attr => attr is not null); - - // Apply description attribute to the generated schema. - if (descriptionAttr != null) - { - if (schema is not JsonObject jObj) - { - // Handle the case where the schema is a Boolean. - JsonValueKind valueKind = schema.GetValueKind(); - Debug.Assert(valueKind is JsonValueKind.True or JsonValueKind.False); - schema = jObj = new JsonObject(); - if (valueKind is JsonValueKind.False) - { - jObj.Add("not", true); - } - } - - jObj.Insert(0, "description", descriptionAttr.Description); - } - - return schema; - } - }; } public override int Execute(BookGenArgumentBase arguments, IReadOnlyList context) { + JsonSerializerOptions options = JsonOptions.SerializerOptions; + JsonSchemaExporterOptions exporterOptions = JsonOptions.ExporterOptions; + MarkdownBuilder markdownBuilder = new(); markdownBuilder.Heading(1, "Bookgen Schemas") .Paragraph("This document contains the schemas used by Bookgen.") .Heading(2, "Bookgen.json") - .CodeBlock(_options.GetJsonSchemaAsNode(typeof(Config), _exporterOptions).ToString(), "json") + .CodeBlock(options.GetJsonSchemaAsNode(typeof(Config), exporterOptions).ToString(), "json") .Heading(2, "Table of contents file") - .CodeBlock(_options.GetJsonSchemaAsNode(typeof(TableOfContents), _exporterOptions).ToString(), "json") + .CodeBlock(options.GetJsonSchemaAsNode(typeof(TableOfContents), exporterOptions).ToString(), "json") .Heading(3, "Page frontmatter") .Paragraph("Each page in the table of contents must have a YAML front matter.") - .CodeBlock(_options.GetJsonSchemaAsNode(typeof(FrontMatter), _exporterOptions).ToString(), "json"); + .CodeBlock(options.GetJsonSchemaAsNode(typeof(FrontMatter), exporterOptions).ToString(), "json"); _logger.LogInformation("Writing schemas.md..."); _writableFileSystem.Scope = arguments.Directory; diff --git a/Source/BookGen/Commands/ShellCommand.cs b/Source/BookGen/Commands/ShellCommand.cs index 97dda178..6eb34d48 100644 --- a/Source/BookGen/Commands/ShellCommand.cs +++ b/Source/BookGen/Commands/ShellCommand.cs @@ -68,7 +68,7 @@ internal IEnumerable DoComplete(IReadOnlyList args) if (words.Length <= 1) return items; - var candidate = items.Where(arg => arg.StartsWith(words.Last(), StringComparison.OrdinalIgnoreCase)); + IEnumerable candidate = items.Where(arg => arg.StartsWith(words.Last(), StringComparison.OrdinalIgnoreCase)); if (candidate.Any()) return candidate; diff --git a/Source/BookGen/Commands/SpellCheckCommand.cs b/Source/BookGen/Commands/SpellCheckCommand.cs new file mode 100644 index 00000000..af7af400 --- /dev/null +++ b/Source/BookGen/Commands/SpellCheckCommand.cs @@ -0,0 +1,199 @@ +using System.Text; + +using BookGen.Cli; +using BookGen.Cli.Annotations; +using BookGen.Vfs; + +using Markdig.Syntax; +using Markdig.Syntax.Inlines; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using WeCantSpell.Hunspell; + +namespace BookGen.Commands; + +[CommandName("spellcheck")] +internal sealed class SpellCheckCommand : AsyncCommand +{ + public sealed class SpellCheckArguments : ArgumentsBase, IVerbosablityToggle + { + [Switch("i", "input")] + public string InputFile { get; set; } = string.Empty; + + [Switch("v", "verbose")] + public bool Verbose { get; set; } + + [Switch("l", "language")] + public string Language { get; set; } = "en_US"; + + [Switch("-ld", "--list-dictionaires")] + public bool DictionariesDisplay { get; set; } = false; + + public override ValidationResult Validate(IValidationContext context) + { + if (!context.FileSystem.FileExists(InputFile)) + return ValidationResult.Error($"Input file '{InputFile}' does not exist."); + + var extension = Path.GetExtension(InputFile); + + if (!string.Equals(extension, ".md", StringComparison.OrdinalIgnoreCase) + && !string.Equals(extension, ".txt", StringComparison.OrdinalIgnoreCase)) + { + return ValidationResult.Error($"Input file '{InputFile}' is not a markdown file."); + } + + return ValidationResult.Ok(); + } + } + + private readonly IAssetSource _dictionaries; + private readonly ILogger _logger; + private readonly IReadOnlyFileSystem _fileSystem; + + public SpellCheckCommand([FromKeyedServices("dictionaries")] IAssetSource dictionaries, + ILogger logger, + IReadOnlyFileSystem fileSystem) + { + _dictionaries = dictionaries; + _logger = logger; + _fileSystem = fileSystem; + } + + public override async Task ExecuteAsync(SpellCheckArguments arguments, IReadOnlyList context) + { + if (arguments.DictionariesDisplay) + { + _logger.LogInformation("Available dictionaries:"); + foreach (string assetName in _dictionaries.AssetNames) + { + if (assetName.EndsWith(".dic", StringComparison.OrdinalIgnoreCase)) + { + string language = Path.GetFileNameWithoutExtension(assetName); + _logger.LogInformation("- {Language}", language); + } + } + return ExitCodes.Success; + } + + WordList? dictionaries = await LoadDictionaries(arguments.Language); + if (dictionaries == null) + { + _logger.LogError("Dictionaries for language '{Language}' not found.", arguments.Language); + return ExitCodes.GeneralError; + } + + var extension = Path.GetExtension(arguments.InputFile); + + int count = 0; + + if (string.Equals(extension, ".txt", StringComparison.OrdinalIgnoreCase)) + { + string text = await _fileSystem.ReadAllTextAsync(arguments.InputFile); + foreach (var word in SplitIntoWords(text)) + { + if (!string.IsNullOrWhiteSpace(word) && !dictionaries.Check(word)) + { + IEnumerable suggestions = dictionaries.Suggest(word); + _logger.LogWarning("Misspelled word: '{Word}'. Suggestions: {Suggestions}", word, string.Join(", ", suggestions)); + ++count; + } + } + } + + if (string.Equals(extension, ".md", StringComparison.OrdinalIgnoreCase)) + { + string markdown = await _fileSystem.ReadAllTextAsync(arguments.InputFile); + var document = Markdig.Markdown.Parse(markdown); + CheckBlock(document, dictionaries, ref count); + } + + return count == 0 ? ExitCodes.Success : ExitCodes.GeneralError; + + } + + private async Task LoadDictionaries(string language) + { + string dicFileName = $"{language}.dic"; + string affFileName = $"{language}.aff"; + + if (!_dictionaries.AssetNames.Contains(dicFileName) + && !_dictionaries.AssetNames.Contains(affFileName)) + { + return null; + } + + await using Stream dicStream = _dictionaries.GetBinaryAssetStream(dicFileName); + await using Stream affStream = _dictionaries.GetBinaryAssetStream(affFileName); + return await WordList.CreateFromStreamsAsync(dicStream, affStream); + } + + private void CheckBlock(Block block, WordList spellChecker, ref int count) + { + if (block is LeafBlock leafBlock) + { + if (leafBlock.Inline != null) + { + foreach (Inline inline in leafBlock.Inline) + { + CheckInline(inline, spellChecker, ref count); + } + } + } + else if (block is ContainerBlock containerBlock) + { + foreach (Block child in containerBlock) + { + CheckBlock(child, spellChecker, ref count); + } + } + } + + private void CheckInline(Inline inline, WordList spellChecker, ref int count) + { + if (inline is LiteralInline literal) + { + IEnumerable words = SplitIntoWords(literal.Content.ToString()); + foreach (var word in words) + { + if (!string.IsNullOrWhiteSpace(word) && !spellChecker.Check(word)) + { + IEnumerable suggestions = spellChecker.Suggest(word); + _logger.LogWarning("Misspelled word: '{Word}'. Suggestions: {Suggestions}", word, string.Join(", ", suggestions)); + } + } + } + else if (inline is ContainerInline container) + { + foreach (Inline child in container) + { + CheckInline(child, spellChecker, ref count); + } + } + } + + private static IEnumerable SplitIntoWords(string text) + { + var currentWord = new StringBuilder(); + + foreach (char c in text) + { + if (char.IsLetter(c) || c == '\'') + { + currentWord.Append(c); + } + else if (currentWord.Length > 0) + { + string result = currentWord.ToString(); + currentWord.Clear(); + yield return result; + } + } + + if (currentWord.Length > 0) + { + yield return currentWord.ToString(); + } + } +} diff --git a/Source/BookGen/Commands/StatsCommand.cs b/Source/BookGen/Commands/StatsCommand.cs index 9ed62e59..5ac5bcfd 100644 --- a/Source/BookGen/Commands/StatsCommand.cs +++ b/Source/BookGen/Commands/StatsCommand.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -35,7 +35,7 @@ public override async Task ExecuteAsync(BookGenArgumentBase arguments, IRea _soruce.Scope = arguments.Directory; using var env = new BookEnvironment(_soruce, _soruce); - var status = await env.Initialize(); + EnvironmentStatus status = await env.Initialize(arguments.ConfigOverlay); if (!status.IsOk) { diff --git a/Source/BookGen/Commands/SubCommandsCommand.cs b/Source/BookGen/Commands/SubCommandsCommand.cs index 7066e0d7..f48fbb1c 100644 --- a/Source/BookGen/Commands/SubCommandsCommand.cs +++ b/Source/BookGen/Commands/SubCommandsCommand.cs @@ -27,7 +27,7 @@ public SubCommandsCommand(ICommandRunnerProxy runnerProxy) public override int Execute(IReadOnlyList context) { Terminal.Header("Available sub commands:"); - foreach (var commandGroup in _commands) + foreach (IGrouping commandGroup in _commands) { AnsiConsole.WriteLine(commandGroup.Key); foreach (var command in commandGroup) diff --git a/Source/BookGen/Commands/TemplatesCommand.cs b/Source/BookGen/Commands/TemplatesCommand.cs index 5fbdb75e..225e483c 100644 --- a/Source/BookGen/Commands/TemplatesCommand.cs +++ b/Source/BookGen/Commands/TemplatesCommand.cs @@ -59,7 +59,7 @@ private void ListTempates() Terminal.List(_defaultTemplates); Terminal.Header("Single page templates:", blankLineBefore: 1); - var singlePage = _assetSource.AssetNames.Where(n => n.EndsWith(".template", StringComparison.OrdinalIgnoreCase)).Order(); + IOrderedEnumerable singlePage = _assetSource.AssetNames.Where(n => n.EndsWith(".template", StringComparison.OrdinalIgnoreCase)).Order(); Terminal.List(singlePage); } } diff --git a/Source/BookGen/Commands/TerminalInstallCommand.cs b/Source/BookGen/Commands/TerminalInstallCommand.cs index 902ac4af..7291a66a 100644 --- a/Source/BookGen/Commands/TerminalInstallCommand.cs +++ b/Source/BookGen/Commands/TerminalInstallCommand.cs @@ -39,7 +39,7 @@ public override async Task ExecuteAsync(TerminalInstallArguments arguments, if (arguments.CheckTerminalInstall) { - var installReult = InstallDetector.GetInstallResult(); + InstallResult installReult = InstallDetector.GetInstallResult(); return installReult.IsWindowsTerminalInstalled ? ExitCodes.Success : ExitCodes.GeneralError; } diff --git a/Source/BookGen/Commands/ToolsCommand.cs b/Source/BookGen/Commands/ToolsCommand.cs index c4760aa5..7f65ddbe 100644 --- a/Source/BookGen/Commands/ToolsCommand.cs +++ b/Source/BookGen/Commands/ToolsCommand.cs @@ -36,6 +36,7 @@ public ToolsCommand(IApiClient apiClient, ILogger logger) new ChromaDownloader(apiClient, _memoryStreamManager, _logger), new CopyPartyDownloader(apiClient, _memoryStreamManager, _logger), new GithubDownloader(apiClient, _memoryStreamManager, _logger), + new GlowDownloader(apiClient, _memoryStreamManager, _logger), new MicrosoftEditToolDownloader(apiClient, _memoryStreamManager, _logger), new PandocTooldownloader(apiClient, _memoryStreamManager, _logger), ]; @@ -51,13 +52,13 @@ public override async Task ExecuteAsync(IReadOnlyList context) AnsiConsole.Clear(); AnsiConsole.Write(new FigletText("Tool installer")); - var selectedItems = Terminal.SelectionMenu(items: _tooldownloaders, + List selectedItems = Terminal.SelectionMenu(items: _tooldownloaders, title: "Select tools to download", instructions: "[grey](Press [blue][/] to toggle a tool for download, [green][/] to accept)[/]", displaySelector: ToSDisplayString); - foreach (var selected in selectedItems) + foreach (TooldownloaderBase selected in selectedItems) { _logger.LogInformation("Installing {tool} ...", selected.ToolInfo.Name); var ui = new ToolDownloadUi(); diff --git a/Source/BookGen/Commands/ValidateCommand.cs b/Source/BookGen/Commands/ValidateCommand.cs index 568a9918..5fd031c3 100644 --- a/Source/BookGen/Commands/ValidateCommand.cs +++ b/Source/BookGen/Commands/ValidateCommand.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -31,7 +31,7 @@ public override async Task ExecuteAsync(BookGenArgumentBase arguments, IRea using var environment = new BookEnvironment(_writableFileSystem, _writableFileSystem); - EnvironmentStatus status = await environment.Initialize(); + EnvironmentStatus status = await environment.Initialize(arguments.ConfigOverlay); if (!status.IsOk) { diff --git a/Source/BookGen/Extensions.cs b/Source/BookGen/Extensions.cs index 89031430..504c974b 100644 --- a/Source/BookGen/Extensions.cs +++ b/Source/BookGen/Extensions.cs @@ -12,7 +12,7 @@ public bool IsValidTemplateFile(string templateFile) if (string.IsNullOrEmpty(templateFile)) return true; - var source = context.Resolve(); + IAssetSource source = context.Resolve(); if (source.AssetNames.Contains(templateFile)) return true; diff --git a/Source/BookGen/Infrastructure/HelpProvider.cs b/Source/BookGen/Infrastructure/HelpProvider.cs index ddeee6bc..36dd5981 100644 --- a/Source/BookGen/Infrastructure/HelpProvider.cs +++ b/Source/BookGen/Infrastructure/HelpProvider.cs @@ -32,7 +32,7 @@ public HelpProvider(ILogger log, ICommandRunnerProxy nameProvider) private void LoadHelpData() { - var lines = + IReadOnlyList lines = ResourceHandler.GetResourceFileLines("Resources/Commands.md"); List chapterData = new(50); diff --git a/Source/BookGen/Infrastructure/HelpRenderer.cs b/Source/BookGen/Infrastructure/HelpRenderer.cs index e64b9e0c..1546d030 100644 --- a/Source/BookGen/Infrastructure/HelpRenderer.cs +++ b/Source/BookGen/Infrastructure/HelpRenderer.cs @@ -3,113 +3,38 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- -using Spectre.Console; +using Bookgen.Lib.Markdown.Renderers.Terminal; + +using Markdig; +using Markdig.Parsers; +using Markdig.Syntax; namespace BookGen.Infrastructure; -public static class HelpRenderer +internal sealed class HelpRenderer { - public static string[][] GetPages(IEnumerable article) - { - int pageSize = Console.WindowHeight - 3; - IReadOnlyList reWraped = DoReWrap(article, pageSize, Console.WindowWidth); - return reWraped.Chunk(pageSize).ToArray(); - } + private readonly MarkdownPipeline _terminalPipeLine; - public static void RenderPage(string[] pageContent) + public HelpRenderer() { - foreach (var line in pageContent) - { - if (line.StartsWith("# ")) - AnsiConsole.MarkupInterpolated($"[green bold]{line}[/]{Environment.NewLine}"); - else if (line.StartsWith('`') || line.EndsWith('`')) - AnsiConsole.MarkupInterpolated($"[aqua]{line}[/]{Environment.NewLine}"); - else - AnsiConsole.MarkupInterpolated($"[italic]{line}[/]{Environment.NewLine}"); - } - } - - public static void RenderHelp(IEnumerable article) - { - var pages = GetPages(article); - Console.Clear(); - - int currentPage = -1; - int nextPage = 0; - bool run = pages.Length > 1; - do - { - if (currentPage != nextPage) - { - currentPage = nextPage; - Console.Clear(); - RenderPage(pages[currentPage]); - RenderUsage(currentPage, pages.Length); - } - - if (!run) continue; - - var key = Console.ReadKey(); - switch (key.Key) - { - case ConsoleKey.LeftArrow: - case ConsoleKey.UpArrow: - nextPage = CalculatePage(currentPage, pages.Length, -1); - break; - case ConsoleKey.DownArrow: - case ConsoleKey.RightArrow: - nextPage = CalculatePage(currentPage, pages.Length, +1); - break; - case ConsoleKey.Escape: - case ConsoleKey.Q: - run = false; - Console.Clear(); - break; - } - } - while (run); + _terminalPipeLine = new MarkdownPipelineBuilder().Build(); } - private static void RenderUsage(int currentPage, int pages) + public void RenderHelp(IEnumerable article) { - if (pages < 2) - return; + string md = string.Join(Environment.NewLine, article); + MarkdownDocument document = MarkdownParser.Parse(md, _terminalPipeLine); - AnsiConsole.WriteLine(); - AnsiConsole.MarkupInterpolated($"[teal]{currentPage + 1} of {pages}[/]"); - AnsiConsole.MarkupInterpolated($" [silver]ESC or Q: Exit, <- Prev, Next ->[/]{Environment.NewLine}"); - } + using var writer = new StringWriter(); + var renderer = new TerminalRenderer(writer, new RenderOptions()); - private static int CalculatePage(int currentPage, int pages, int offset) - { - int newIndex = currentPage + offset; + renderer.Render(document); + renderer.Writer.Flush(); - if (newIndex < 0) - newIndex = 0; + using var reader = new StringReader(writer.ToString()); - if (newIndex > pages - 1) - newIndex = pages - 1; + Webmaster442.WindowsTerminal.Wigets.Pager pager = new(reader); - return newIndex; - } - - private static List DoReWrap(IEnumerable article, int pageSize, int windowWidth) - { - List result = new(pageSize); - foreach (string line in article) - { - if (line.Length > windowWidth) - { - var newLines = line - .Chunk(windowWidth) - .Select(chrs => new string(chrs)); - result.AddRange(newLines); - } - else - { - result.Add(line); - } - } - return result; + pager.Show(false); } } diff --git a/Source/BookGen/Infrastructure/Loging/DumyLogScope.cs b/Source/BookGen/Infrastructure/Loging/DumyLogScope.cs deleted file mode 100644 index 4124d928..00000000 --- a/Source/BookGen/Infrastructure/Loging/DumyLogScope.cs +++ /dev/null @@ -1,14 +0,0 @@ -//----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor -// This code is licensed under MIT license (see LICENSE for details) -//----------------------------------------------------------------------------- - -namespace BookGen.Infrastructure.Loging; - -internal sealed class DumyLogScope : IDisposable -{ - public void Dispose() - { - //empty - } -} diff --git a/Source/BookGen/Infrastructure/Terminal/Terminal.cs b/Source/BookGen/Infrastructure/Terminal/Terminal.cs index 9776b8e5..3d983ec7 100644 --- a/Source/BookGen/Infrastructure/Terminal/Terminal.cs +++ b/Source/BookGen/Infrastructure/Terminal/Terminal.cs @@ -29,14 +29,14 @@ public static void Table(string[] headers, IEnumerable rows) public static void BarChart(IDictionary items, string title = "") { - var chart = new BarChart() + BarChart chart = new BarChart() .Width(Console.WindowWidth) .Label(title) .CenterLabel(); _palette.Reset(); - foreach (var item in items) + foreach (KeyValuePair item in items) { chart.AddItem(item.Key, item.Value, _palette.GetNextColor()); } @@ -46,10 +46,10 @@ public static void BarChart(IDictionary items, string title = "" public static void BreakDownChart(IDictionary items, string title, bool descendingOrder = true) { - var rule = new Rule(title).Centered(); + Rule rule = new Rule(title).Centered(); AnsiConsole.Write(rule); - var chart = new BreakdownChart() + BreakdownChart chart = new BreakdownChart() .Width(Console.WindowWidth); _palette.Reset(); @@ -58,7 +58,7 @@ public static void BreakDownChart(IDictionary items, string titl ? items.OrderByDescending(x => x.Value) : items; - foreach (var item in data) + foreach (KeyValuePair item in data) { chart.AddItem(item.Key, item.Value, _palette.GetNextColor()); } @@ -84,7 +84,7 @@ public static bool Confirm(string message) public static List SelectionMenu(IEnumerable items, string title, string instructions, Func displaySelector) where T : notnull { - var prompt = new MultiSelectionPrompt() + MultiSelectionPrompt prompt = new MultiSelectionPrompt() .Title(title) .PageSize(15) .InstructionsText(instructions) diff --git a/Source/BookGen/Infrastructure/Tools/Digest.cs b/Source/BookGen/Infrastructure/Tools/Digest.cs index e84a7ff5..37eda317 100644 --- a/Source/BookGen/Infrastructure/Tools/Digest.cs +++ b/Source/BookGen/Infrastructure/Tools/Digest.cs @@ -3,6 +3,7 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; using System.Security.Cryptography; @@ -35,35 +36,35 @@ private static unsafe bool Compare(byte[] result, byte[] bytes) if (length == 32) // 256-bit { - var va = Avx.LoadVector256(pA); - var vb = Avx.LoadVector256(pB); + Vector256 va = Avx.LoadVector256(pA); + Vector256 vb = Avx.LoadVector256(pB); - var cmp = Avx2.CompareEqual(va, vb); + Vector256 cmp = Avx2.CompareEqual(va, vb); return Avx2.MoveMask(cmp) == -1; } else if (length == 48) // 384-bit { - var va1 = Avx.LoadVector256(pA); // first 32 bytes - var vb1 = Avx.LoadVector256(pB); + Vector256 va1 = Avx.LoadVector256(pA); // first 32 bytes + Vector256 vb1 = Avx.LoadVector256(pB); - var va2 = Sse2.LoadVector128(pA + 32); // remaining 16 bytes - var vb2 = Sse2.LoadVector128(pB + 32); + Vector128 va2 = Sse2.LoadVector128(pA + 32); // remaining 16 bytes + Vector128 vb2 = Sse2.LoadVector128(pB + 32); - var cmp1 = Avx2.CompareEqual(va1, vb1); - var cmp2 = Sse2.CompareEqual(va2, vb2); + Vector256 cmp1 = Avx2.CompareEqual(va1, vb1); + Vector128 cmp2 = Sse2.CompareEqual(va2, vb2); return Avx2.MoveMask(cmp1) == -1 && Sse2.MoveMask(cmp2) == 0xFFFF; } else if (length == 64) // 512-bit { - var va1 = Avx.LoadVector256(pA); - var vb1 = Avx.LoadVector256(pB); + Vector256 va1 = Avx.LoadVector256(pA); + Vector256 vb1 = Avx.LoadVector256(pB); - var va2 = Avx.LoadVector256(pA + 32); - var vb2 = Avx.LoadVector256(pB + 32); + Vector256 va2 = Avx.LoadVector256(pA + 32); + Vector256 vb2 = Avx.LoadVector256(pB + 32); - var cmp1 = Avx2.CompareEqual(va1, vb1); - var cmp2 = Avx2.CompareEqual(va2, vb2); + Vector256 cmp1 = Avx2.CompareEqual(va1, vb1); + Vector256 cmp2 = Avx2.CompareEqual(va2, vb2); return Avx2.MoveMask(cmp1) == -1 && Avx2.MoveMask(cmp2) == -1; } diff --git a/Source/BookGen/Infrastructure/Tools/Extractor.cs b/Source/BookGen/Infrastructure/Tools/Extractor.cs index b03fc0f4..b2bcbe86 100644 --- a/Source/BookGen/Infrastructure/Tools/Extractor.cs +++ b/Source/BookGen/Infrastructure/Tools/Extractor.cs @@ -22,7 +22,7 @@ public static async Task Copy(IDownloadUi ui, Stream stream, string folderName, if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); - await using var targetStream = File.Create(outputPath); + await using FileStream targetStream = File.Create(outputPath); await stream.CopyToAsync(targetStream, CancellationToken.None); ui.Report(stream.Length); } @@ -54,7 +54,7 @@ static string GetEntryOutputPath(string targetFolder, TarEntry entry) Directory.CreateDirectory(directory); } - await using var targetStream = File.Create(outputPath); + await using FileStream targetStream = File.Create(outputPath); if (entry.DataStream != null) { await entry.DataStream.CopyToAsync(targetStream, CancellationToken.None); @@ -73,7 +73,7 @@ static string GetEntryOutputPath(string targetFolder, ZipArchiveEntry entry) ui.BeginNew("Extracting...", archive.Entries.Sum(e => e.Length)); - foreach (var entry in archive.Entries) + foreach (ZipArchiveEntry entry in archive.Entries) { if (string.IsNullOrEmpty(entry.Name)) { @@ -88,8 +88,8 @@ static string GetEntryOutputPath(string targetFolder, ZipArchiveEntry entry) Directory.CreateDirectory(directory); } - await using var targetStream = File.Create(outputPath); - await using var source = entry.Open(); + await using FileStream targetStream = File.Create(outputPath); + await using Stream source = entry.Open(); await source.CopyToAsync(targetStream, CancellationToken.None); ui.Report(entry.Length); diff --git a/Source/BookGen/Program.cs b/Source/BookGen/Program.cs index 28fcbb5e..a7c28fd0 100644 --- a/Source/BookGen/Program.cs +++ b/Source/BookGen/Program.cs @@ -10,6 +10,7 @@ using BookGen.Commands; using BookGen.Infrastructure; using BookGen.Infrastructure.Loging; +using BookGen.Shell.Shared.Loging; using BookGen.Vfs; using Microsoft.Extensions.DependencyInjection; @@ -19,7 +20,7 @@ ProgramInfo info = new(); -var argumentList = ProgramConfigurator.ParseGeneralArgs(args, info); +List argumentList = ProgramConfigurator.ParseGeneralArgs(args, info); using ILoggerFactory factory = LoggerFactory .Create(builder => @@ -44,16 +45,27 @@ CommandRunnerProxy runnerProxy = new(); var ioc = new ServiceCollection(); +ioc.AddMemoryCache(); ioc.AddSingleton(logger); ioc.AddSingleton(info); ioc.AddSingleton(runnerProxy); ioc.AddSingleton(ZipAssetSoruce.DefaultAssets()); +ioc.AddSingleton(); ioc.AddSingleton(new HelpProvider(logger, runnerProxy)); ioc.AddTransient(); ioc.AddTransient(); ioc.AddTransient(); +ioc.AddKeyedSingleton("dictionaries", (provider, key) => +{ + var dictionaries = Path.Combine(AppContext.BaseDirectory, "dictionaries.zip"); + if (File.Exists(dictionaries)) + { + return new ZipAssetSoruce(dictionaries); + } + return new EmptyAssetSource(); +}); -using var provider = ioc.BuildServiceProvider(); +using ServiceProvider provider = ioc.BuildServiceProvider(); CommandRunner runner = new(provider, logger, new CommandRunnerSettings { @@ -62,10 +74,11 @@ ExcptionExitCode = -1, PlatformNotSupportedExitCode = 4, EnableUtf8Output = true, -}); - -runner.ExceptionHandlerDelegate = OnException; -runner.BeforeRunHook = OnBeforeRun; +}) +{ + ExceptionHandlerDelegate = OnException, + BeforeRunHook = OnBeforeRun +}; runner .AddDefaultCommand() diff --git a/Source/BookGen/Properties/launchSettings.json b/Source/BookGen/Properties/launchSettings.json index d3340b37..ab444ce0 100644 --- a/Source/BookGen/Properties/launchSettings.json +++ b/Source/BookGen/Properties/launchSettings.json @@ -44,6 +44,15 @@ "Templates": { "commandName": "Project", "commandLineArgs": "templates" + }, + "Help": { + "commandName": "Project", + "commandLineArgs": "help" + }, + "Links --verify": { + "commandName": "Project", + "commandLineArgs": "links --verify", + "workingDirectory": "G:\\Konyv\\hellocsharp" } } } \ No newline at end of file diff --git a/Source/BookGen/Tooldownloaders/GlowDownloader.cs b/Source/BookGen/Tooldownloaders/GlowDownloader.cs new file mode 100644 index 00000000..985b11f1 --- /dev/null +++ b/Source/BookGen/Tooldownloaders/GlowDownloader.cs @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2025 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using Bookgen.Lib.Domain.Github; + +using BookGen.Infrastructure.Tools; +using BookGen.Vfs; + +using Microsoft.Extensions.Logging; +using Microsoft.IO; + +namespace BookGen.Tooldownloaders; + +internal sealed class GlowDownloader : TooldownloaderBase +{ + public GlowDownloader(IApiClient apiClient, + RecyclableMemoryStreamManager memoryStreamManager, + ILogger log) + : base(apiClient, memoryStreamManager, log) + { + } + + protected override ToolInfo CreateToolInfo() + { + return new ToolInfo + { + Name = "Glow", + ApproximateSize = "18 MiB", + RepoOwner = "charmbracelet", + RepoName = "glow", + FolderName = "glow", + }; + } + + protected override ReleaseAsset? GetReleaseAsset(IEnumerable releaseAssets) + { + return releaseAssets + .Where(r => r.Name.EndsWith("Windows_x86_64.zip")) + .OrderByDescending(r => r.CreatedAt) + .FirstOrDefault(); + } +} diff --git a/Source/BookGen/Tooldownloaders/ToolDownloadUi.cs b/Source/BookGen/Tooldownloaders/ToolDownloadUi.cs index b5f6908f..0bd06bd2 100644 --- a/Source/BookGen/Tooldownloaders/ToolDownloadUi.cs +++ b/Source/BookGen/Tooldownloaders/ToolDownloadUi.cs @@ -11,7 +11,7 @@ namespace BookGen.Tooldownloaders; -internal class ToolDownloadUi : IDownloadUi +internal sealed class ToolDownloadUi : IDownloadUi { private class ExtendedProgresBar : Progressbar { diff --git a/Source/BookGen/Tooldownloaders/TooldownloaderBase.cs b/Source/BookGen/Tooldownloaders/TooldownloaderBase.cs index 4672f62b..ae8355a8 100644 --- a/Source/BookGen/Tooldownloaders/TooldownloaderBase.cs +++ b/Source/BookGen/Tooldownloaders/TooldownloaderBase.cs @@ -41,9 +41,9 @@ protected virtual Task Extract(IDownloadUi ui, Stream stream) public async Task DownloadToolAsync(IDownloadUi ui) { var downloadUrl = new Uri($"https://api.github.com/repos/{ToolInfo.RepoOwner}/{ToolInfo.RepoName}/releases"); - var releases = await _apiClient.DownloadJsonAsync(downloadUrl); + Release[] releases = await _apiClient.DownloadJsonAsync(downloadUrl); - var latestRelease = GetReleaseAsset(releases.SelectMany(a => a.Assets)); + ReleaseAsset? latestRelease = GetReleaseAsset(releases.SelectMany(a => a.Assets)); if (latestRelease == null) { @@ -53,7 +53,7 @@ public async Task DownloadToolAsync(IDownloadUi ui) ui.BeginNew($"Downloading {latestRelease.Name}...", latestRelease.Size); - await using var stream = _memoryStreamManager.GetStream(); + await using RecyclableMemoryStream stream = _memoryStreamManager.GetStream(); try { diff --git a/Source/BookGen/packages.lock.json b/Source/BookGen/packages.lock.json index 1329cd17..6cfcf66a 100644 --- a/Source/BookGen/packages.lock.json +++ b/Source/BookGen/packages.lock.json @@ -4,41 +4,65 @@ "net10.0": { "Markdig": { "type": "Direct", - "requested": "[0.44.0, )", - "resolved": "0.44.0", - "contentHash": "X+CYMjcUnh/yO24wOSQxVFLiGqWrrtXJ5M7toHiM1Zk4Fg9UMLN5fkaq6FSOWH+mIprsHHgDMlq3MJhmrXalhg==" + "requested": "[1.1.0, )", + "resolved": "1.1.0", + "contentHash": "ivaowI69dGxiyaKLy6+qo9Xm4DHwXDKB3orGIFOvA8k+eUBAV5UaW8GPIMc3h5jl7gZalyecR3x2t+nuLYdmFg==" }, - "Microsoft.Extensions.Logging": { + "Microsoft.Extensions.Caching.Abstractions": { "type": "Direct", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "9ItMpMLFZFJFqCuHLLbR3LiA4ahA8dMtYuXpXl2YamSDWZhYS9BruPprkftY0tYi2bQ0slNrixdFm+4kpz1g5w==", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "5dtXBvI8t3z8pF4tB38JYgi/enCL/DwSXxpqShgFz3SHJ7IzqFIMs6Gu5ik8sNZzcO9qQs3xIDpB3vDamkYG+Q==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.1", - "Microsoft.Extensions.Logging.Abstractions": "10.0.1", - "Microsoft.Extensions.Options": "10.0.1" + "Microsoft.Extensions.Primitives": "10.0.3" } }, - "Microsoft.Extensions.Logging.Abstractions": { + "Microsoft.Extensions.Caching.Memory": { + "type": "Direct", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "No4fVh0z30SWqiWFRoA4PNdrEco6OjXvCqRFvlmRgDQqqks2bRDdeavUgWEiAX153ZAwW9loUgbxcvuP4NKQLg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.3", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3", + "Microsoft.Extensions.Logging.Abstractions": "10.0.3", + "Microsoft.Extensions.Options": "10.0.3", + "Microsoft.Extensions.Primitives": "10.0.3" + } + }, + "Microsoft.Extensions.DependencyInjection": { "type": "Direct", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "YkmyiPIWAXVb+lPIrM0LE5bbtLOJkCiRTFiHpkVOvhI7uTvCfoOHLEN0LcsY56GpSD7NqX3gJNpsaDe87/B3zg==", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3" } }, - "Microsoft.Extensions.Logging.Console": { + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Direct", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw==" + }, + "Microsoft.Extensions.Logging": { "type": "Direct", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "38Q8sEHwQ/+wVO/mwQBa0fcdHbezFpusHE+vBw/dSr6Fq/kzZm3H/NQX511Jki/R3FHd64IY559gdlHZQtYeEA==", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", - "Microsoft.Extensions.Logging": "10.0.1", - "Microsoft.Extensions.Logging.Abstractions": "10.0.1", - "Microsoft.Extensions.Logging.Configuration": "10.0.1", - "Microsoft.Extensions.Options": "10.0.1" + "Microsoft.Extensions.DependencyInjection": "10.0.3", + "Microsoft.Extensions.Logging.Abstractions": "10.0.3", + "Microsoft.Extensions.Options": "10.0.3" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Direct", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3" } }, "Microsoft.IO.RecyclableMemoryStream": { @@ -59,6 +83,12 @@ "resolved": "1.0.0", "contentHash": "TojbwWASRf3PMUsotXa8MQ9SNwEwWZZ5bAYf1eLv+jiDsxaD/n7TKTi3da6z4+m8uZHri2i+BVRuLj5w+6j64Q==" }, + "WeCantSpell.Hunspell": { + "type": "Direct", + "requested": "[7.0.1, )", + "resolved": "7.0.1", + "contentHash": "Ha0x66QWA+IPj2kJYoweA8MSnB1tymBW6kAIS6Kkj/U3SlMLCBPjDlUXzrrY1zJAxVItrGpFIXjkl3eEIYIE9A==" + }, "XmlDocMarkdown.Core": { "type": "Direct", "requested": "[2.9.0, )", @@ -111,80 +141,19 @@ "Microsoft.ClearScript.Core": "7.5.0" } }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "njoRekyMIK+smav8B6KL2YgIfUtlsRNuT7wvurpLW+m/hoRKVnoELk2YxnUnWRGScCd1rukLMxShwLqEOKowDg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", - "Microsoft.Extensions.Primitives": "10.0.1" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "kPlU11hql+L9RjrN2N9/0GcRcRcZrNFlLLjadasFWeBORT6pL6OE+RYRk90GGCyVGSxTK+e1/f3dsMj5zpFFiQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.1" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "Lp4CZIuTVXtlvkAnTq6QvMSW7+H62gX2cU2vdFxHQUxvrWTpi7LwYI3X+YAyIS0r12/p7gaosco7efIxL4yFNw==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.1", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.1" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "zerXV0GAR9LCSXoSIApbWn+Dq1/T+6vbXMHGduq1LoVQRHT0BXsGQEau0jeLUBUcsoF/NaUT8ADPu8b+eNcIyg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" - } - }, - "Microsoft.Extensions.Logging.Configuration": { - "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "Zg8LLnfZs5o2RCHD/+9NfDtJ40swauemwCa7sI8gQoAye/UJHRZNpCtC7a5XE7l9Z7mdI8iMWnLZ6m7Q6S3jLg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.1", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", - "Microsoft.Extensions.Configuration.Binder": "10.0.1", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", - "Microsoft.Extensions.Logging": "10.0.1", - "Microsoft.Extensions.Logging.Abstractions": "10.0.1", - "Microsoft.Extensions.Options": "10.0.1", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.1" - } - }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "G6VVwywpJI4XIobetGHwg7wDOYC2L2XBYdtskxLaKF/Ynb5QBwLl7Q//wxAR2aVCLkMpoQrjSP9VoORkyddsNQ==", + "resolved": "10.0.3", + "contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", - "Microsoft.Extensions.Primitives": "10.0.1" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "pL78/Im7O3WmxHzlKUsWTYchKL881udU7E26gCD3T0+/tPhWVfjPwMzfN/MRKU7aoFYcOiqcG2k1QTlH5woWow==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", - "Microsoft.Extensions.Configuration.Binder": "10.0.1", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", - "Microsoft.Extensions.Options": "10.0.1", - "Microsoft.Extensions.Primitives": "10.0.1" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3", + "Microsoft.Extensions.Primitives": "10.0.3" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "DO8XrJkp5x4PddDuc/CH37yDBCs9BYN6ijlKyR3vMb55BP1Vwh90vOX8bNfnKxr5B2qEI3D8bvbY1fFbDveDHQ==" + "resolved": "10.0.3", + "contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg==" }, "Newtonsoft.Json": { "type": "Transitive", @@ -193,37 +162,37 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "3.2.1", - "contentHash": "WyMf+0aj5IJcHkG/t89WApGFg756oXB30ehu9kYu9hjdfS0snkXHoAiTJZqVlqyLOvzC6eA+nOQ2hI84g//5Pg==" + "resolved": "3.4.1", + "contentHash": "ab3J5OGdwYLyXnom90OghS9NyRPH9dtsRtW5B3mL+F43YTlxido3nUtiR9NjB/sI3jvG/J744C29bsSBQ28AfQ==" }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "3.119.1", - "contentHash": "6hR3BdLhApjDxR1bFrJ7/lMydPfI01s3K+3WjIXFUlfC0MFCFCwRzv+JtzIkW9bDXs7XUVQS+6EVf0uzCasnGQ==" + "resolved": "3.119.2", + "contentHash": "I2jMGQ/26KOnc6iAoR+Mxh9vSJJ2vioJyj9aJ9OL5yEZyXothXJxf4vBMqnSaiXMqiiU1scG7KqtT0CLkmMmWA==" }, "Svg.Custom": { "type": "Transitive", - "resolved": "3.2.1", - "contentHash": "Wu0+Il+SZfY5+AT25b2PMlSbBrzGIo/6vMMMdOK8jd25I9tdJJbxOFJNG87gFgo9UY9OmpIFzVobGOHzFdJYeQ==", + "resolved": "3.4.1", + "contentHash": "ksHa7Zv8X1InxSWAeM0sSSvZM2W6FWgneXjU0bdYrQ20Q6q0nl3g0uaSWe53i/CC1OmR+/JAxsp8E3/wigtcng==", "dependencies": { "ExCSS": "4.3.1" } }, "Svg.Model": { "type": "Transitive", - "resolved": "3.2.1", - "contentHash": "N4yWlvqv1XWZ+VvI29lT4HkEgtiP3rS/poMfWiVzz6Ao/3IjQCfmo2qrKphq77IqTdMYyh7wGDknMId7YrQY5Q==", + "resolved": "3.4.1", + "contentHash": "Zn04CWWIwV+qlJ4x4vg8FdCYNew5dRcsvkcCpzZggFKHGExIn4WTBtJMuLH8htuiLnbbgF6woIYpUll+seHfjw==", "dependencies": { - "ShimSkiaSharp": "3.2.1", - "Svg.Custom": "3.2.1" + "ShimSkiaSharp": "3.4.1", + "Svg.Custom": "3.4.1" } }, "bookgen.cli": { "type": "Project", "dependencies": { "BookGen.Vfs": "[1.0.0, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.1, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.1, )" + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )" } }, "bookgen.contents": { @@ -234,22 +203,23 @@ "dependencies": { "BookGen.Shell.Shared": "[1.0.0, )", "BookGen.Vfs": "[1.0.0, )", - "Markdig": "[0.44.0, )", + "Markdig": "[1.1.0, )", "Microsoft.ClearScript": "[7.5.0, )", "Microsoft.ClearScript.V8.Native.linux-x64": "[7.5.0, )", "Microsoft.ClearScript.V8.Native.win-x64": "[7.5.0, )", - "SkiaSharp": "[3.119.1, )", - "SkiaSharp.NativeAssets.Linux": "[3.119.1, )", - "SkiaSharp.NativeAssets.Win32": "[3.119.1, )", - "Svg.Skia": "[3.2.1, )", - "System.ServiceModel.Syndication": "[10.0.1, )", + "SkiaSharp": "[3.119.2, )", + "SkiaSharp.NativeAssets.Linux": "[3.119.2, )", + "SkiaSharp.NativeAssets.Win32": "[3.119.2, )", + "Svg.Skia": "[3.4.1, )", + "System.ServiceModel.Syndication": "[10.0.3, )", "YamlDotNet": "[16.3.0, )" } }, "bookgen.shell.shared": { "type": "Project", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.1, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )", + "Spectre.Console": "[0.54.0, )", "Webmaster442.WindowsTerminal": "[4.1.1, )" } }, @@ -281,50 +251,44 @@ "resolved": "7.5.0", "contentHash": "DKMxDLboTNflYkwDQ/ELrSf1vXTpew5UZ8xzrXSVKYFBU570VA6NKh1etEGhufuCuDyU7Je5L2g6H+19Dbl+tA==" }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "oIy8fQxxbUsSrrOvgBqlVgOeCtDmrcynnTG+FQufcUWBrwyPfwlUkCDB2vaiBeYPyT+20u9/HeuHeBf+H4F/8g==" - }, "SkiaSharp": { "type": "CentralTransitive", - "requested": "[3.119.1, )", - "resolved": "3.119.1", - "contentHash": "+Ru1BTSZQne3Vp+vbSb50Ke3Nlc3ZnItxx4+751J9WZ8YzLKAV/n+9DAo4zFTyeCI//ueT63c+VybmTTpYBEiw==", + "requested": "[3.119.2, )", + "resolved": "3.119.2", + "contentHash": "nmy2dOFWPvQKMglfpjz8+/xQQcSrL9jzul3cUyzCJVSwrmSAw+6B1sEgU7jt6NZBptwGq2k/V0kjyu2GizMFtg==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "3.119.1", - "SkiaSharp.NativeAssets.macOS": "3.119.1" + "SkiaSharp.NativeAssets.Win32": "3.119.2", + "SkiaSharp.NativeAssets.macOS": "3.119.2" } }, "SkiaSharp.NativeAssets.Linux": { "type": "CentralTransitive", - "requested": "[3.119.1, )", - "resolved": "3.119.1", - "contentHash": "9YNoc4SeKvQhrwiqwT4ezkNfMywPdPSK+UFvo/CaoXqLixcnYOTsQKm5BF9mc4+q3vKgDtEgMt0d1ygZhJTEHg==" + "requested": "[3.119.2, )", + "resolved": "3.119.2", + "contentHash": "9WzxSyG/s9Id506j0Ht+Bi5ucOpWKPzd1XXr9TD4fuCafHHy2swRSlbZtC3IDQAsvCH63OerkDJajj43uSv5og==" }, "SkiaSharp.NativeAssets.Win32": { "type": "CentralTransitive", - "requested": "[3.119.1, )", - "resolved": "3.119.1", - "contentHash": "8C4GSXVJqSr0y3Tyyv5jz6MJSTVUyYkMjeKrzK+VyZPGLo89MNoUEclVuYahzOCDdtbfXrd2HtxXfDuvoSXrUw==" + "requested": "[3.119.2, )", + "resolved": "3.119.2", + "contentHash": "uYe+da6+GXVgPKkCopzvIZ83DmC8SXXKeUAPrNcztJNsg0SjPQAxfKMOPZqmVjbzznrq/QUIjLUlJSZV/e0IPA==" }, "Svg.Skia": { "type": "CentralTransitive", - "requested": "[3.2.1, )", - "resolved": "3.2.1", - "contentHash": "pRJtMOc2hTcnxSu7cx6lf806S+2VrQ0eqUpGMBUQQ/pIIvzrtn0+lUjdABTvjr5rsdQF9OxE+r9npq2+3c6T4A==", + "requested": "[3.4.1, )", + "resolved": "3.4.1", + "contentHash": "fNGHIeIUtEDo41P4MYfVqjNL902t8EP5/tBY9vYDg8VbKSOD84ZlBfCL4KSxjf//HcJi1Uz4uZCUXuEAKJirCw==", "dependencies": { "SkiaSharp": "2.88.9", - "Svg.Custom": "3.2.1", - "Svg.Model": "3.2.1" + "Svg.Custom": "3.4.1", + "Svg.Model": "3.4.1" } }, "System.ServiceModel.Syndication": { "type": "CentralTransitive", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "Z3+s66hOp7JVDOhkVk0gH/sjsshn99NUMiILcXLaA+3qe2OkLr3nB+3bRzXdfF6arlJBxGfkkki9GP6R9kvdFg==" + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "mloJBxfbjYXgfcfMvH40UWwzekATlUzHLMKPQpg4qab+gpHRgQkhgo4DLz3v7Kacn6000sqoNkxiyk0pJ5x/ig==" }, "Webmaster442.WindowsTerminal": { "type": "CentralTransitive", diff --git a/Source/Bookgen.Lib/AppSettings/AppSettingsBase.cs b/Source/Bookgen.Lib/AppSettings/AppSettingsBase.cs index fd721f24..50a58d03 100644 --- a/Source/Bookgen.Lib/AppSettings/AppSettingsBase.cs +++ b/Source/Bookgen.Lib/AppSettings/AppSettingsBase.cs @@ -85,7 +85,7 @@ private void LoadFromFile() if (loaded == null) return; - foreach (var item in loaded) + foreach (KeyValuePair item in loaded) { if (_storage.ContainsKey(item.Key)) _storage[item.Key] = item.Value; diff --git a/Source/Bookgen.Lib/BookEnvironment.cs b/Source/Bookgen.Lib/BookEnvironment.cs index 96f99460..b1234776 100644 --- a/Source/Bookgen.Lib/BookEnvironment.cs +++ b/Source/Bookgen.Lib/BookEnvironment.cs @@ -1,9 +1,10 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Nodes; using Bookgen.Lib.Confighandling; using Bookgen.Lib.Domain.IO; @@ -57,7 +58,7 @@ public void Dispose() } } - public async Task Initialize() + public async Task Initialize(string configOverlay) { if (_isInitialized) { @@ -87,7 +88,17 @@ public async Task Initialize() return status; } - Config? config = await _source.DeserializeAsync(FileNameConstants.ConfigFile); + JsonObject baseConfig = await _source.ReadJsonAsync(FileNameConstants.ConfigFile); + + JsonMerger configMerger = new JsonMerger(baseConfig); + + if (!string.IsNullOrEmpty(configOverlay)) + { + JsonObject overlayConfig = await _source.ReadJsonAsync(configOverlay); + configMerger.Merge(overlayConfig); + } + + Config? config = configMerger.Deserialize(); if (config == null) { @@ -123,7 +134,7 @@ public async Task Initialize() public bool TryGetAsset(string name, [NotNullWhen(true)] out string? content) { - foreach (var assetsource in _assets) + foreach (IAssetSource assetsource in _assets) { if (assetsource.TryGetAsset(name, out content)) { @@ -135,13 +146,13 @@ public bool TryGetAsset(string name, [NotNullWhen(true)] out string? content) return false; } - public byte[] GetBinaryAsset(string name) + public Stream GetBinaryAssetStream(string name) { - foreach (var assetsource in _assets) + foreach (IAssetSource assetsource in _assets) { try { - return assetsource.GetBinaryAsset(name); + return assetsource.GetBinaryAssetStream(name); } catch (InvalidOperationException) { diff --git a/Source/Bookgen.Lib/BookStatFactory.cs b/Source/Bookgen.Lib/BookStatFactory.cs index 7689d227..f4d16904 100644 --- a/Source/Bookgen.Lib/BookStatFactory.cs +++ b/Source/Bookgen.Lib/BookStatFactory.cs @@ -4,6 +4,7 @@ //----------------------------------------------------------------------------- using Bookgen.Lib.Domain; +using Bookgen.Lib.Domain.IO; using Bookgen.Lib.Internals; using Bookgen.Lib.Pipeline; @@ -17,18 +18,18 @@ public static async Task CreateBookStat(IBookEnvironment environment, { BookStat stat = new(); - foreach (var chapter in environment.TableOfContents.Chapters) + foreach (TocChapter chapter in environment.TableOfContents.Chapters) { stat.ChapterSizes[chapter.Title] = 0; foreach (var file in chapter.Files) { - var sourceFile = await environment.Source.GetSourceFile(file, logger); + SourceFile sourceFile = await environment.Source.GetSourceFile(file, logger); long size = environment.Source.GetFileSize(file); stat.ChapterSizes[chapter.Title] += size; ProcessCodeBlocks(stat, sourceFile); - var (lineCount, wordCount, characterCount) = GetFileStats(sourceFile); + (long lineCount, long wordCount, long characterCount) = GetFileStats(sourceFile); stat.LineCount += lineCount; stat.WordCount += wordCount; stat.TotalSize += size; @@ -75,7 +76,7 @@ private static (long lineCount, long wordCount, long characterCount) GetFileStat string? line; while ((line = reader.ReadLine()) != null) { - var lineStats = GetLineStats(line.Where(c => !IsMarkdownChar(c))); + (int length, int words) lineStats = GetLineStats(line.Where(c => !IsMarkdownChar(c))); lineCount += (lineStats.length / 80) + (lineStats.length % 80) > 0 ? 1 : 0; wordCount += lineStats.words; characterCount += lineStats.length; diff --git a/Source/Bookgen.Lib/Bookgen.Lib.csproj b/Source/Bookgen.Lib/Bookgen.Lib.csproj index e8c9dd53..69f1c965 100644 --- a/Source/Bookgen.Lib/Bookgen.Lib.csproj +++ b/Source/Bookgen.Lib/Bookgen.Lib.csproj @@ -8,6 +8,7 @@ ..\..\bin\$(Configuration)\ false true + True @@ -25,7 +26,7 @@ - + diff --git a/Source/Bookgen.Lib/BundledAssets.cs b/Source/Bookgen.Lib/BundledAssets.cs index 9b4e5a89..56fea27c 100644 --- a/Source/Bookgen.Lib/BundledAssets.cs +++ b/Source/Bookgen.Lib/BundledAssets.cs @@ -17,5 +17,7 @@ public static class BundledAssets public const string MathJax = "mathjax-bundled.js"; public const string QrCodeJs = "qrcode.min.js"; public const string JsPageToc = "PageToc.js"; + public const string NomnomlJs = "nomnoml.js"; + public const string GraphreJs = "graphre.js"; } diff --git a/Source/Bookgen.Lib/Confighandling/ConfigUpgrader.cs b/Source/Bookgen.Lib/Confighandling/ConfigUpgrader.cs index 7d9418d8..e2f3f9ef 100644 --- a/Source/Bookgen.Lib/Confighandling/ConfigUpgrader.cs +++ b/Source/Bookgen.Lib/Confighandling/ConfigUpgrader.cs @@ -40,7 +40,7 @@ public async Task Init(IReadOnlyFileSystem sourceFolder) _tocJson = await sourceFolder.ReadJsonAsync(FileNameConstants.TableOfContents); _configJson = await sourceFolder.ReadJsonAsync(FileNameConstants.ConfigFile); - var version = _configJson["VersionTag"] + JsonNode version = _configJson["VersionTag"] ?? throw new InvalidOperationException("Failed to determine version"); _sourceVersion = 0; @@ -60,9 +60,9 @@ public async Task Init(IReadOnlyFileSystem sourceFolder) public static async Task IsUpgradeNeeded(IReadOnlyFileSystem sourceFolder) { - var configJson = await sourceFolder.ReadJsonAsync(FileNameConstants.ConfigFile); + JsonObject configJson = await sourceFolder.ReadJsonAsync(FileNameConstants.ConfigFile); - var version = configJson["VersionTag"] + JsonNode version = configJson["VersionTag"] ?? throw new InvalidOperationException("Failed to determine version"); if (version is not JsonValue jsonValue @@ -94,7 +94,7 @@ public static async Task IsUpgradeNeeded(IReadOnlyFileSystem sourceFolder) bool tocModifed = false; bool configModified = false; - foreach (var upgrader in upgraders) + foreach (UpgradeBase upgrader in upgraders) { _logger.LogInformation("Upgrading from version {from} to {to}", upgrader.VersionTagInfo.From, upgrader.VersionTagInfo.To); tocModifed |= upgrader.UpgradeToc(_tocJson); @@ -121,7 +121,7 @@ public static async Task IsUpgradeNeeded(IReadOnlyFileSystem sourceFolder) private List SelectUpgrades(int from, int to) { List selectedUpgrades = new List(); - foreach (var upgrader in _upgrades) + foreach (UpgradeBase upgrader in _upgrades) { if (upgrader.VersionTagInfo.From >= from && upgrader.VersionTagInfo.To <= to) { diff --git a/Source/Bookgen.Lib/Confighandling/JsonMerger.cs b/Source/Bookgen.Lib/Confighandling/JsonMerger.cs new file mode 100644 index 00000000..f4e9c649 --- /dev/null +++ b/Source/Bookgen.Lib/Confighandling/JsonMerger.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using BookGen.Vfs; + +namespace Bookgen.Lib.Confighandling; + +internal sealed class JsonMerger +{ + private JsonObject _baseObject; + + public JsonMerger(JsonObject baseObject) + { + _baseObject = baseObject; + } + + private static JsonNode? Merge(JsonNode jsonBase, + JsonNode jsonMerge, + bool mergeIfAlreadyExists = true) + { + if (jsonBase == null || jsonMerge == null) + return jsonBase; + + switch (jsonBase) + { + case JsonObject jsonBaseObj when jsonMerge is JsonObject jsonMergeObj: + { + //NOTE: We must materialize the set (e.g. to an Array), and then clear the merge array so the node can then be + // re-assigned to the target/base Json; clearing the Object seems to be the most efficient approach... + KeyValuePair[] mergeNodesArray = jsonMergeObj.ToArray(); + jsonMergeObj.Clear(); + + foreach (KeyValuePair prop in mergeNodesArray) + { + if (mergeIfAlreadyExists || !jsonBaseObj.ContainsKey(prop.Key)) + jsonBaseObj[prop.Key] = jsonBaseObj[prop.Key] switch + { + JsonObject jsonBaseChildObj when prop.Value is JsonObject jsonMergeChildObj + => Merge(jsonBaseChildObj, jsonMergeChildObj), + JsonArray jsonBaseChildArray when prop.Value is JsonArray jsonMergeChildArray + => Merge(jsonBaseChildArray, jsonMergeChildArray), + _ => prop.Value + }; + } + break; + } + case JsonArray jsonBaseArray when jsonMerge is JsonArray jsonMergeArray: + { + //NOTE: We must materialize the set (e.g. to an Array), and then clear the merge array, + // so they can then be re-assigned to the target/base Json... + JsonNode?[] mergeNodesArray = jsonMergeArray.ToArray(); + jsonMergeArray.Clear(); + foreach (JsonNode? mergeNode in mergeNodesArray) jsonBaseArray.Add(mergeNode); + break; + } + default: + throw new ArgumentException($"The JsonNode type [{jsonBase.GetType().Name}] is incompatible for merging with the target/base " + + $"type [{jsonMerge.GetType().Name}]; merge requires the types to be the same."); + + } + + return jsonBase; + } + + + public void Merge(JsonObject overlay) + { + JsonNode? result = Merge(_baseObject, overlay); + if (result is JsonObject mergedObj) + { + _baseObject = mergedObj; + return; + } + throw new InvalidOperationException("Merging resulted in invalid object"); + } + + public T? Deserialize() + => JsonSerializer.Deserialize(_baseObject, JsonOptions.SerializerOptions); +} diff --git a/Source/Bookgen.Lib/Confighandling/JsonObjectExtensions.cs b/Source/Bookgen.Lib/Confighandling/JsonObjectExtensions.cs index c5bb294f..e979ef34 100644 --- a/Source/Bookgen.Lib/Confighandling/JsonObjectExtensions.cs +++ b/Source/Bookgen.Lib/Confighandling/JsonObjectExtensions.cs @@ -12,7 +12,7 @@ internal static class JsonObjectExtensions { public static JsonObject GetSubObjectOrThrow(this JsonObject jsonObject, string propertyName) { - if (jsonObject.TryGetPropertyValue(propertyName, out var subObjectNode) && + if (jsonObject.TryGetPropertyValue(propertyName, out JsonNode? subObjectNode) && subObjectNode is JsonObject subObject) { return subObject; @@ -22,7 +22,7 @@ public static JsonObject GetSubObjectOrThrow(this JsonObject jsonObject, string public static JsonArray GetSubArrayOrThrow(this JsonObject jsonObject, string propertyName) { - if (jsonObject.TryGetPropertyValue(propertyName, out var subArrayNode) && + if (jsonObject.TryGetPropertyValue(propertyName, out JsonNode? subArrayNode) && subArrayNode is JsonArray subArray) { return subArray; diff --git a/Source/Bookgen.Lib/Confighandling/LegacyMigration/LoadLegacyConfig.cs b/Source/Bookgen.Lib/Confighandling/LegacyMigration/LoadLegacyConfig.cs index 99711d54..d3ed2ce8 100644 --- a/Source/Bookgen.Lib/Confighandling/LegacyMigration/LoadLegacyConfig.cs +++ b/Source/Bookgen.Lib/Confighandling/LegacyMigration/LoadLegacyConfig.cs @@ -3,6 +3,8 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using Bookgen.Lib.Domain.IO.Legacy; + using BookGen.Vfs; using Microsoft.Extensions.Logging; @@ -21,7 +23,7 @@ public async Task ExecuteAsync(IWritableFileSystem foler, MigrationState s return false; } - var config = await foler.DeserializeAsync(file); + Config? config = await foler.DeserializeAsync(file); if (config == null) { logger.LogError("Failed to load legacy config file"); diff --git a/Source/Bookgen.Lib/Confighandling/LegacyMigration/LoadLegacyTags.cs b/Source/Bookgen.Lib/Confighandling/LegacyMigration/LoadLegacyTags.cs index 837be98d..985d977f 100644 --- a/Source/Bookgen.Lib/Confighandling/LegacyMigration/LoadLegacyTags.cs +++ b/Source/Bookgen.Lib/Confighandling/LegacyMigration/LoadLegacyTags.cs @@ -20,7 +20,7 @@ public async Task ExecuteAsync(IWritableFileSystem foler, MigrationState s return false; } - var tags = await foler.DeserializeAsync>(file); + Dictionary? tags = await foler.DeserializeAsync>(file); if (tags == null) { diff --git a/Source/Bookgen.Lib/Confighandling/LegacyMigration/LoadLegacyToc.cs b/Source/Bookgen.Lib/Confighandling/LegacyMigration/LoadLegacyToc.cs index 6be33ec9..50ee4f8f 100644 --- a/Source/Bookgen.Lib/Confighandling/LegacyMigration/LoadLegacyToc.cs +++ b/Source/Bookgen.Lib/Confighandling/LegacyMigration/LoadLegacyToc.cs @@ -33,7 +33,7 @@ static void InsertChapter(ToC toc, ref string? currentchapter, ref List? c var parsed = new ToC(); parsed.RawMarkdown = content; MarkdownPipeline? pipeline = new MarkdownPipelineBuilder().UseAutoIdentifiers(AutoIdentifierOptions.GitHub).Build(); - var doc = Markdig.Markdown.Parse(content, pipeline); + MarkdownDocument doc = Markdig.Markdown.Parse(content, pipeline); string? chapterTitle = string.Empty; var chapterLinks = new List(); diff --git a/Source/Bookgen.Lib/Confighandling/LegacyMigration/MigrateFiles.cs b/Source/Bookgen.Lib/Confighandling/LegacyMigration/MigrateFiles.cs index 525f6a18..dcbf0c97 100644 --- a/Source/Bookgen.Lib/Confighandling/LegacyMigration/MigrateFiles.cs +++ b/Source/Bookgen.Lib/Confighandling/LegacyMigration/MigrateFiles.cs @@ -6,11 +6,14 @@ using System.Text; using Bookgen.Lib.Domain.IO; +using Bookgen.Lib.Domain.IO.Legacy; using BookGen.Vfs; using Microsoft.Extensions.Logging; +using YamlDotNet.Serialization; + namespace Bookgen.Lib.Confighandling.LegacyMigration; internal class MigrateFiles : IMigrationStep @@ -18,11 +21,11 @@ internal class MigrateFiles : IMigrationStep public async Task ExecuteAsync(IWritableFileSystem foler, MigrationState state, ILogger logger) { logger.LogInformation("Migrating files to add front matter..."); - var serializer = YamlSerializerFactory.CreateSerializer(); + ISerializer serializer = YamlSerializerFactory.CreateSerializer(); foreach (var chapter in state.LegacyToc.Chapters) { - foreach (var link in state.LegacyToc.GetLinksForChapter(chapter)) + foreach (Link link in state.LegacyToc.GetLinksForChapter(chapter)) { logger.LogDebug("Migrating file: {file}", link.Url); diff --git a/Source/Bookgen.Lib/Confighandling/LegacyMigration/Migrator.cs b/Source/Bookgen.Lib/Confighandling/LegacyMigration/Migrator.cs index aebe23a3..d6ca3981 100644 --- a/Source/Bookgen.Lib/Confighandling/LegacyMigration/Migrator.cs +++ b/Source/Bookgen.Lib/Confighandling/LegacyMigration/Migrator.cs @@ -28,7 +28,7 @@ public static async Task Migrate(IWritableFileSystem folder, ILogger logge try { - foreach (var step in steps) + foreach (IMigrationStep step in steps) { bool result = await step.ExecuteAsync(folder, state, logger); if (!result) diff --git a/Source/Bookgen.Lib/Confighandling/UpgradeSteps/FromVersion2003To2004.cs b/Source/Bookgen.Lib/Confighandling/UpgradeSteps/FromVersion2003To2004.cs index b05cab18..c01fc7bc 100644 --- a/Source/Bookgen.Lib/Confighandling/UpgradeSteps/FromVersion2003To2004.cs +++ b/Source/Bookgen.Lib/Confighandling/UpgradeSteps/FromVersion2003To2004.cs @@ -25,7 +25,7 @@ public override bool UpgradeToc(JsonObject tocFile) { JsonArray chaptersArray = tocFile.GetSubArrayOrThrow("Chapters"); - foreach (var chapterNode in chaptersArray) + foreach (JsonNode? chapterNode in chaptersArray) { if (chapterNode is JsonObject chapter) { diff --git a/Source/Bookgen.Lib/Confighandling/UpgradeSteps/FromVersion2004To2005.cs b/Source/Bookgen.Lib/Confighandling/UpgradeSteps/FromVersion2004To2005.cs index 37164a2d..5aaa91bf 100644 --- a/Source/Bookgen.Lib/Confighandling/UpgradeSteps/FromVersion2004To2005.cs +++ b/Source/Bookgen.Lib/Confighandling/UpgradeSteps/FromVersion2004To2005.cs @@ -24,7 +24,7 @@ public void UpdateImgeObject(JsonObject buildConfig) { JsonObject imgObject = buildConfig.GetSubObjectOrThrow("Images"); - bool oldvalue = imgObject.TryGetPropertyValue("ResizeAndRecodeImagesToWebp", out var webpValue) + bool oldvalue = imgObject.TryGetPropertyValue("ResizeAndRecodeImagesToWebp", out JsonNode? webpValue) && webpValue is JsonValue webpJsonValue && webpJsonValue.GetValue(); diff --git a/Source/Bookgen.Lib/Confighandling/UpgradeSteps/FromVersion2005To2006.cs b/Source/Bookgen.Lib/Confighandling/UpgradeSteps/FromVersion2005To2006.cs index f1f4f70d..7c4057d7 100644 --- a/Source/Bookgen.Lib/Confighandling/UpgradeSteps/FromVersion2005To2006.cs +++ b/Source/Bookgen.Lib/Confighandling/UpgradeSteps/FromVersion2005To2006.cs @@ -24,7 +24,7 @@ public void UpdateImgeObject(JsonObject buildConfig) { JsonObject imgObject = buildConfig.GetSubObjectOrThrow("Images"); - int oldvalue = imgObject.TryGetPropertyValue("WebpQuality", out var qualityValue) + int oldvalue = imgObject.TryGetPropertyValue("WebpQuality", out JsonNode? qualityValue) && qualityValue is JsonValue qualityJsonValue ? qualityJsonValue.GetValue() : throw new InvalidOperationException("WebpQuality property not found or is not an integer."); diff --git a/Source/Bookgen.Lib/Domain/IO/TableOfContents.cs b/Source/Bookgen.Lib/Domain/IO/TableOfContents.cs index c61eb924..9c962247 100644 --- a/Source/Bookgen.Lib/Domain/IO/TableOfContents.cs +++ b/Source/Bookgen.Lib/Domain/IO/TableOfContents.cs @@ -39,7 +39,7 @@ public IEnumerable GetFiles(bool withIndex = false) yield return IndexFile; } - foreach (var chapter in Chapters) + foreach (TocChapter chapter in Chapters) { if (chapter.Files != null) { diff --git a/Source/Bookgen.Lib/Domain/Validation/FileExistsAttribute.cs b/Source/Bookgen.Lib/Domain/Validation/FileExistsAttribute.cs index 133783a6..12633d8f 100644 --- a/Source/Bookgen.Lib/Domain/Validation/FileExistsAttribute.cs +++ b/Source/Bookgen.Lib/Domain/Validation/FileExistsAttribute.cs @@ -16,7 +16,7 @@ internal sealed class FileExistsAttribute : ValidationAttribute { protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { - var folder = validationContext.GetRequiredService(); + IReadOnlyFileSystem folder = validationContext.GetRequiredService(); if (value is IEnumerable files) { diff --git a/Source/Bookgen.Lib/Domain/Validation/WhenNotEmptyFileMustExistAttribute.cs b/Source/Bookgen.Lib/Domain/Validation/WhenNotEmptyFileMustExistAttribute.cs index dcc77bcd..221806e9 100644 --- a/Source/Bookgen.Lib/Domain/Validation/WhenNotEmptyFileMustExistAttribute.cs +++ b/Source/Bookgen.Lib/Domain/Validation/WhenNotEmptyFileMustExistAttribute.cs @@ -16,7 +16,7 @@ internal sealed class WhenNotEmptyFileMustExistAttribute : ValidationAttribute { protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { - var folder = validationContext.GetRequiredService(); + IReadOnlyFileSystem folder = validationContext.GetRequiredService(); if (value is not string @string) throw new InvalidOperationException($"{nameof(NotNullOrWhiteSpaceAttribute)} works with {typeof(string)} properties"); diff --git a/Source/Bookgen.Lib/Http/HttpServer.cs b/Source/Bookgen.Lib/Http/HttpServer.cs index 845ac984..260a17d5 100644 --- a/Source/Bookgen.Lib/Http/HttpServer.cs +++ b/Source/Bookgen.Lib/Http/HttpServer.cs @@ -139,7 +139,7 @@ public void AddRoute(ApiMetaData metaData, RequestDelegate handler) public void AddRoutes(IReadOnlyDictionary routes) { - foreach (var route in routes) + foreach (KeyValuePair route in routes) { AddRoute(route.Key, route.Value); } @@ -147,7 +147,7 @@ public void AddRoutes(IReadOnlyDictionary routes) public IEnumerable GetListenUrls() { - foreach (var (adress, _) in GetIpAdresses()) + foreach ((IPAddress? adress, IPAddress _) in GetIpAdresses()) { yield return $"http://{adress}:{Port}"; } @@ -159,12 +159,12 @@ public IEnumerable GetListenUrls() .Where(i => i.AddressFamily == AddressFamily.InterNetwork) .ToHashSet(); - var ifaceAddrs = NetworkInterface.GetAllNetworkInterfaces() + IEnumerable ifaceAddrs = NetworkInterface.GetAllNetworkInterfaces() .Where(i => i.OperationalStatus == OperationalStatus.Up) .SelectMany(x => x.GetIPProperties().UnicastAddresses) .Where(x => ipAdresses.Contains(x.Address)); - foreach (var adress in ifaceAddrs) + foreach (UnicastIPAddressInformation? adress in ifaceAddrs) { yield return (adress.Address, adress.IPv4Mask); } diff --git a/Source/Bookgen.Lib/Http/PageFactory.cs b/Source/Bookgen.Lib/Http/PageFactory.cs index c95500d6..0ab31d56 100644 --- a/Source/Bookgen.Lib/Http/PageFactory.cs +++ b/Source/Bookgen.Lib/Http/PageFactory.cs @@ -13,7 +13,7 @@ internal static class PageFactory { private static string GetResource(string resoruceName) { - using var stream = typeof(PageFactory).Assembly.GetManifestResourceStream(resoruceName) + using Stream stream = typeof(PageFactory).Assembly.GetManifestResourceStream(resoruceName) ?? throw new UnreachableException("Error page template was null"); using var reader = new StreamReader(stream); diff --git a/Source/Bookgen.Lib/Http/ServerFactory.cs b/Source/Bookgen.Lib/Http/ServerFactory.cs index bb8d73cc..fb2890b4 100644 --- a/Source/Bookgen.Lib/Http/ServerFactory.cs +++ b/Source/Bookgen.Lib/Http/ServerFactory.cs @@ -19,7 +19,7 @@ private static int ChoosePort(int @default = HostingPort) { IPGlobalProperties ipProps = IPGlobalProperties.GetIPGlobalProperties(); - var props = ipProps.GetActiveTcpConnections(); + TcpConnectionInformation[] props = ipProps.GetActiveTcpConnections(); IEnumerable tcpConnections = ipProps.GetActiveTcpConnections() .Where(c => c.State == TcpState.Listen) diff --git a/Source/Bookgen.Lib/ImageService/CachedImageService.cs b/Source/Bookgen.Lib/ImageService/CachedImageService.cs index 19330d8e..726da337 100644 --- a/Source/Bookgen.Lib/ImageService/CachedImageService.cs +++ b/Source/Bookgen.Lib/ImageService/CachedImageService.cs @@ -1,32 +1,52 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- using System.Collections.Concurrent; +using System.IO; using BookGen.Vfs; +using Microsoft.Extensions.Caching.Memory; + namespace Bookgen.Lib.ImageService; public sealed class CachedImageService : IImgService { private readonly IImgService _service; - private readonly ConcurrentDictionary _cache; + private readonly IMemoryCache _memoryCache; - public CachedImageService(IImgService service) + public CachedImageService(IImgService service, IMemoryCache memoryCache) { _service = service; - _cache = new ConcurrentDictionary(); + _memoryCache = memoryCache; + } + + private static ulong GetCacheKey(string str) + { + const ulong ofset = 0xcbf29ce484222325; + const ulong prime = 0x00000100000001b3; + + ulong hash = ofset; + + foreach (char c in str) + { + hash ^= c; + hash *= prime; + } + + return hash; } public ImageResult GetImageEmbedData(string path) { - if (_cache.TryGetValue(path, out var data)) - return data; + ulong cacheKey = GetCacheKey(path); + return _memoryCache.GetOrCreate(cacheKey, entry => + { - var result = _service.GetImageEmbedData(path); - _cache.TryAdd(path, result); - return result; + entry.SetAbsoluteExpiration(TimeSpan.FromSeconds(180)); + return _service.GetImageEmbedData(path); + })!; } } diff --git a/Source/Bookgen.Lib/ImageService/IImgService.cs b/Source/Bookgen.Lib/ImageService/IImgService.cs index 0d39ee6a..8d686632 100644 --- a/Source/Bookgen.Lib/ImageService/IImgService.cs +++ b/Source/Bookgen.Lib/ImageService/IImgService.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -7,5 +7,5 @@ namespace Bookgen.Lib.ImageService; public interface IImgService { - ImageResult GetImageEmbedData(string path); + ImageResult GetImageEmbedData(string filePath); } diff --git a/Source/Bookgen.Lib/ImageService/ImageConverter.cs b/Source/Bookgen.Lib/ImageService/ImageConverter.cs index d53f4240..77c19c09 100644 --- a/Source/Bookgen.Lib/ImageService/ImageConverter.cs +++ b/Source/Bookgen.Lib/ImageService/ImageConverter.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -11,19 +11,20 @@ namespace Bookgen.Lib.ImageService; public static class ImageConverter { - public static void Encode(string source, string output, ImageType imageType, int width, int height, int quality) + public static void Encode(string source, string output, ImageType imageType, int maxWidth, int maxHeight, int quality) { - using var srcStream = File.OpenRead(source); - using var destStream = File.Create(output); + using FileStream srcStream = File.OpenRead(source); + using FileStream destStream = File.Create(output); - if (Path.GetExtension("soruce").Equals(".svg", StringComparison.OrdinalIgnoreCase)) + if (Path.GetExtension(source).Equals(".svg", StringComparison.OrdinalIgnoreCase)) { - SKData img = Utils.RenderSvg(srcStream, width, height, GetRecodeOption(imageType)); + SKData img = ImageUtils.RenderSvg(srcStream, maxWidth, maxHeight, GetRecodeOption(imageType)); img.SaveTo(destStream); + return; } using SKBitmap loaded = SKBitmap.Decode(srcStream); - using SKBitmap result = Utils.ResizeIfBigger(loaded, width, height); + using SKBitmap result = ImageUtils.ResizeIfBigger(loaded, maxWidth, maxHeight); result.Encode(destStream, imageType switch { @@ -32,8 +33,6 @@ public static void Encode(string source, string output, ImageType imageType, int ImageType.Webp => SKEncodedImageFormat.Webp, _ => throw new NotSupportedException($"Image type {imageType} is not supported for encoding.") }, quality); - - } private static SvgRecodeOption GetRecodeOption(ImageType imageType) diff --git a/Source/Bookgen.Lib/ImageService/ImageResult.cs b/Source/Bookgen.Lib/ImageService/ImageResult.cs index 4eb634b1..76564a6f 100644 --- a/Source/Bookgen.Lib/ImageService/ImageResult.cs +++ b/Source/Bookgen.Lib/ImageService/ImageResult.cs @@ -3,6 +3,7 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- + namespace Bookgen.Lib.ImageService; public sealed record class ImageResult diff --git a/Source/Bookgen.Lib/ImageService/Utils.cs b/Source/Bookgen.Lib/ImageService/ImageUtils.cs similarity index 59% rename from Source/Bookgen.Lib/ImageService/Utils.cs rename to Source/Bookgen.Lib/ImageService/ImageUtils.cs index 637e1197..b8f0de05 100644 --- a/Source/Bookgen.Lib/ImageService/Utils.cs +++ b/Source/Bookgen.Lib/ImageService/ImageUtils.cs @@ -1,8 +1,9 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using System.Buffers; using System.Diagnostics; using System.Text; @@ -14,13 +15,13 @@ namespace Bookgen.Lib.ImageService; -internal static class Utils +internal static class ImageUtils { public static byte[] ConvertToPng(string file, int maxwidth, int maxHeight) { if (Path.GetExtension(file).Equals(".svg", StringComparison.OrdinalIgnoreCase)) { - using var stream = File.OpenRead(file); + using FileStream stream = File.OpenRead(file); return RenderSvg(stream, maxwidth, maxHeight, SvgRecodeOption.AsPng).ToArray(); } using SKBitmap bitmap = SKBitmap.Decode(file); @@ -74,11 +75,23 @@ public static SKBitmap ResizeIfBigger(SKBitmap input, int width, int height) return input.Resize(new SKImageInfo(renderWidth, renderHeight), SKSamplingOptions.Default); } - public static SKData RenderSvg(Stream stream, int maxWidth, int maxHeight, SvgRecodeOption svgRecode) + private static SKSvg LoadSvg(Stream stream) { - using var svg = new SKSvg(); + var svg = new SKSvg(); svg.Load(stream); + return svg; + } + + private static SKSvg LoadSvg(string svgData) + { + var svg = new SKSvg(); + using var xmlReader = System.Xml.XmlReader.Create(new StringReader(svgData)); + svg.Load(xmlReader); + return svg; + } + private static SKData RenderSvg(SKSvg svg, int maxWidth, int maxHeight, SvgRecodeOption svgRecode) + { if (svg.Picture == null) return SKData.Empty; @@ -107,7 +120,18 @@ public static SKData RenderSvg(Stream stream, int maxWidth, int maxHeight, SvgRe }; } } + } + + public static SKData RenderSvg(Stream svgsource, int maxWidth, int maxHeight, SvgRecodeOption svgRecode) + { + using var svg = LoadSvg(svgsource); + return RenderSvg(svg, maxWidth, maxHeight, svgRecode); + } + public static SKData RenderSvg(string svgData, int maxWidth, int maxHeight, SvgRecodeOption svgRecode) + { + using var svg = LoadSvg(svgData); + return RenderSvg(svg, maxWidth, maxHeight, svgRecode); } public static SKData Encode(Stream fileData, int resizeWith, int resizeHeight, int quality, ImgRecodeOption imgRecodeOption) @@ -124,18 +148,57 @@ public static SKData Encode(Stream fileData, int resizeWith, int resizeHeight, i }, quality); } - public static string Base64Encode(Stream fileData) { - StringBuilder builder = new StringBuilder(); - byte[] buffer = new byte[4096]; - int read = 0; - do + const int readBufferSize = 4096; + + int size = (int)Math.Ceiling(4 * (fileData.Length / 3.0)); + + var stringBuilder = new StringBuilder(size); + + byte[] remainder = new byte[2]; + int remainderCount = 0; + + byte[] readBuffer = ArrayPool.Shared.Rent(readBufferSize); + int bytesRead; + + try + { + while ((bytesRead = fileData.Read(readBuffer, 0, readBuffer.Length)) > 0) + { + int totalBytesToProcess = remainderCount + bytesRead; + + // Determine how many bytes can form complete 3-byte triplets. + var bytesToTakeForEncoding = (totalBytesToProcess / 3) * 3; + + if (bytesToTakeForEncoding > 0) + { + var chunk = ArrayPool.Shared.Rent(bytesToTakeForEncoding); + remainder.AsSpan(0, remainderCount).CopyTo(chunk); + readBuffer.AsSpan(0, bytesToTakeForEncoding - remainderCount).CopyTo(chunk.AsSpan(remainderCount)); + stringBuilder.Append(Convert.ToBase64String(chunk, 0, bytesToTakeForEncoding)); + ArrayPool.Shared.Return(chunk); + + remainderCount = bytesRead - (bytesToTakeForEncoding - remainderCount); + readBuffer.AsSpan(bytesRead - remainderCount, remainderCount).CopyTo(remainder); + } + else + { + readBuffer.AsSpan(0, bytesRead).CopyTo(remainder.AsSpan(remainderCount)); + remainderCount += bytesRead; + } + } + + if (remainderCount > 0) + { + stringBuilder.Append(Convert.ToBase64String(remainder.AsSpan(0, remainderCount).ToArray())); + } + } + finally { - read = fileData.Read(buffer, 0, buffer.Length); - builder.Append(Convert.ToBase64String(buffer, 0, read)); + ArrayPool.Shared.Return(readBuffer); } - while (read > 0); - return builder.ToString(); + + return stringBuilder.ToString(); } } diff --git a/Source/Bookgen.Lib/ImageService/ImgService.cs b/Source/Bookgen.Lib/ImageService/ImgService.cs index 56ba90e7..3da993a2 100644 --- a/Source/Bookgen.Lib/ImageService/ImgService.cs +++ b/Source/Bookgen.Lib/ImageService/ImgService.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -50,50 +50,50 @@ private static ImageType GetImageType(string file) }; } - public ImageResult GetImageEmbedData(string path) + private static ImageType GetImateType(SvgRecodeOption recodeOption) { - static ImageType GetImateType(SvgRecodeOption recodeOption) + return recodeOption switch { - return recodeOption switch - { - SvgRecodeOption.AsPng => ImageType.Png, - SvgRecodeOption.AsWebp => ImageType.Webp, - SvgRecodeOption.Passtrough => ImageType.Svg, - _ => throw new UnreachableException(), - }; - } + SvgRecodeOption.AsPng => ImageType.Png, + SvgRecodeOption.AsWebp => ImageType.Webp, + SvgRecodeOption.Passtrough => ImageType.Svg, + _ => throw new UnreachableException(), + }; + } - path = path.Replace("../", ""); + public ImageResult GetImageEmbedData(string filePath) + { + filePath = filePath.Replace("../", ""); - if (!IsImage(path)) - throw new InvalidOperationException($"{path} is not an image"); + if (!IsImage(filePath)) + throw new InvalidOperationException($"{filePath} is not an image"); - if (!_sourceFolder.FileExists(path)) + if (!_sourceFolder.FileExists(filePath)) { - _logger.LogWarning("Image {Path} does not exist in source folder", path); + _logger.LogWarning("Image {Path} does not exist in source folder", filePath); return new ImageResult { Data = string.Empty, ImageType = ImageType.Png, - OriginalName = path, + OriginalName = filePath, }; } - using Stream fileData = _sourceFolder.OpenReadStream(path); + using Stream fileData = _sourceFolder.OpenReadStream(filePath); - if (string.Equals(Path.GetExtension(path), ".svg", StringComparison.CurrentCultureIgnoreCase)) + if (string.Equals(Path.GetExtension(filePath), ".svg", StringComparison.CurrentCultureIgnoreCase)) { if (_imageConfig.SvgRecode == SvgRecodeOption.Passtrough) { return new ImageResult { - Data = _sourceFolder.ReadAllText(path), + Data = _sourceFolder.ReadAllText(filePath), ImageType = ImageType.Svg, - OriginalName = path, + OriginalName = filePath, }; } - using SKData rendered = Utils.RenderSvg(fileData, + using SKData rendered = ImageUtils.RenderSvg(fileData, _imageConfig.ResizeWith, _imageConfig.ResizeHeight, _imageConfig.SvgRecode); @@ -102,20 +102,20 @@ static ImageType GetImateType(SvgRecodeOption recodeOption) { Data = Convert.ToBase64String(rendered.AsSpan()), ImageType = GetImateType(_imageConfig.SvgRecode), - OriginalName = path, + OriginalName = filePath, }; } if (_imageConfig.ResizeAndRecodeImages != ImgRecodeOption.Passtrough) { - using SKData rendered = Utils.Encode(fileData, + using SKData rendered = ImageUtils.Encode(fileData, _imageConfig.ResizeWith, _imageConfig.ResizeHeight, _imageConfig.ImageQualityOnResize, _imageConfig.ResizeAndRecodeImages); - var type = _imageConfig.ResizeAndRecodeImages switch + ImageType type = _imageConfig.ResizeAndRecodeImages switch { ImgRecodeOption.AsPng => ImageType.Png, ImgRecodeOption.AsWebp => ImageType.Webp, @@ -126,16 +126,16 @@ static ImageType GetImateType(SvgRecodeOption recodeOption) { Data = Convert.ToBase64String(rendered.AsSpan()), ImageType = type, - OriginalName = path, + OriginalName = filePath, }; } return new ImageResult { - Data = Utils.Base64Encode(fileData), - ImageType = GetImageType(path), - OriginalName = path, + Data = ImageUtils.Base64Encode(fileData), + ImageType = GetImageType(filePath), + OriginalName = filePath, }; } } diff --git a/Source/Bookgen.Lib/Internals/Extensions.cs b/Source/Bookgen.Lib/Internals/Extensions.cs index 94c1ecad..f95d6046 100644 --- a/Source/Bookgen.Lib/Internals/Extensions.cs +++ b/Source/Bookgen.Lib/Internals/Extensions.cs @@ -41,7 +41,7 @@ static FrontMatter CreateDefaultFrontMatter(string diskPath, ILogger log) StringBuilder content = new StringBuilder(); StringBuilder yaml = new StringBuilder(); - using var reader = folder.OpenTextReader(file); + using TextReader reader = folder.OpenTextReader(file); string? line; bool inYaml = false; @@ -98,11 +98,11 @@ public async Task GetSourceFile(string file, ILogger logger) public async Task GetCoverFileName(TableOfContents tableOfContents, ILogger logger) { var contents = await folder.ReadAllTextAsync(tableOfContents.IndexFile); - foreach (var block in Markdig.Markdown.Parse(contents)) + foreach (Block block in Markdig.Markdown.Parse(contents)) { if (block is ParagraphBlock paragraph && paragraph.Inline != null) { - foreach (var inline in paragraph.Inline) + foreach (Inline inline in paragraph.Inline) { if (inline is LinkInline link && link.IsImage) { diff --git a/Source/Bookgen.Lib/Internals/SerializedObjectValidator.cs b/Source/Bookgen.Lib/Internals/SerializedObjectValidator.cs index f8bb42de..aba275d0 100644 --- a/Source/Bookgen.Lib/Internals/SerializedObjectValidator.cs +++ b/Source/Bookgen.Lib/Internals/SerializedObjectValidator.cs @@ -4,6 +4,7 @@ //----------------------------------------------------------------------------- using System.ComponentModel.DataAnnotations; +using System.Reflection; using System.Security.AccessControl; using BookGen.Vfs; @@ -44,7 +45,7 @@ public bool Validate(T @object, ICollection issues) where T : class { static void AddIssues(ICollection target, string prefix, IEnumerable results) { - foreach (var validationResult in results) + foreach (ValidationResult validationResult in results) { var names = string.Join(',', validationResult.MemberNames); @@ -74,7 +75,7 @@ static void AddIssues(ICollection target, string prefix, IEnumerable properties = @object .GetType() .GetProperties() .Where(p => p.CanRead @@ -84,7 +85,7 @@ static void AddIssues(ICollection target, string prefix, IEnumerable ImageType.Png, + SvgRecodeOption.AsWebp => ImageType.Webp, + SvgRecodeOption.Passtrough => ImageType.Svg, + _ => throw new UnreachableException(), + }; + } + + private ImageResult EncodeSvg(string svgData) + { + if (_imageConfig.SvgRecode == SvgRecodeOption.Passtrough) + { + return new ImageResult + { + Data = svgData, + ImageType = ImageType.Svg, + OriginalName = string.Empty, + }; + } + + + + using SKData rendered = ImageUtils.RenderSvg(svgData, + _imageConfig.ResizeWith, + _imageConfig.ResizeHeight, + _imageConfig.SvgRecode); + + return new ImageResult + { + Data = Convert.ToBase64String(rendered.AsSpan()), + ImageType = GetImateType(_imageConfig.SvgRecode), + OriginalName = string.Empty + }; + } + + public ImageResult RenderNomnoml(string nomnomlCode) + { + if (!_nomnomlLoaded) + { + Execute(_assetSource.GetAsset(BundledAssets.GraphreJs)); + Execute(_assetSource.GetAsset(BundledAssets.NomnomlJs)); + _nomnomlLoaded = true; + } + + _engine.Script.nomnomlCode = nomnomlCode; + string svg = ExecuteAndGetResult("nomnoml.renderSvg(nomnomlCode)"); + return EncodeSvg(svg); + } +} diff --git a/Source/Bookgen.Lib/JsInterop/PrismJsInterop.cs b/Source/Bookgen.Lib/JsInterop/SyntaxRenderJsInterop.cs similarity index 53% rename from Source/Bookgen.Lib/JsInterop/PrismJsInterop.cs rename to Source/Bookgen.Lib/JsInterop/SyntaxRenderJsInterop.cs index cf32285f..bbb985f5 100644 --- a/Source/Bookgen.Lib/JsInterop/PrismJsInterop.cs +++ b/Source/Bookgen.Lib/JsInterop/SyntaxRenderJsInterop.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -7,16 +7,24 @@ namespace Bookgen.Lib.JsInterop; -public sealed class PrismJsInterop : JavascriptInterop +public sealed class SyntaxRenderJsInterop : JavascriptInterop { - public PrismJsInterop(IAssetSource assetSource) + private readonly IAssetSource _assetSource; + private bool _prismLoaded; + + public SyntaxRenderJsInterop(IAssetSource assetSource) { - string prismjs = assetSource.GetAsset(BundledAssets.PrismJs); - Execute(prismjs); + _assetSource = assetSource; } public string PrismSyntaxHighlight(string code, string language) { + if (!_prismLoaded) + { + string prismjs = _assetSource.GetAsset(BundledAssets.PrismJs); + Execute(prismjs); + _prismLoaded = true; + } _engine.Script.code = code; return ExecuteAndGetResult($"Prism.highlight(code, Prism.languages.{language}, '{language}');"); } diff --git a/Source/Bookgen.Lib/Markdown/BookGenExtension.cs b/Source/Bookgen.Lib/Markdown/BookGenExtension.cs index 67662cab..20ac271c 100644 --- a/Source/Bookgen.Lib/Markdown/BookGenExtension.cs +++ b/Source/Bookgen.Lib/Markdown/BookGenExtension.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -20,13 +20,13 @@ namespace Bookgen.Lib.Markdown; internal sealed partial class BookGenExtension : IMarkdownExtension, IDisposable { - private RenderSettings? _settings; + private MarkdownRenderSettings? _settings; private MarkdownPipelineBuilder? _pipeline; [GeneratedRegex("^(\\w)+://")] private static partial Regex ProtocollRegex(); - public void Inject(RenderSettings settings) + public void Inject(MarkdownRenderSettings settings) { _settings = settings; } @@ -54,7 +54,7 @@ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) if (renderer is TextRendererBase htmlRenderer) { - var linkInLineRenderer = htmlRenderer.ObjectRenderers.FindExact(); + LinkInlineRenderer? linkInLineRenderer = htmlRenderer.ObjectRenderers.FindExact(); if (linkInLineRenderer != null) { htmlRenderer.ObjectRenderers.Remove(linkInLineRenderer); @@ -64,7 +64,7 @@ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) if (codeBlockRenderer != null) { htmlRenderer.ObjectRenderers.Remove(codeBlockRenderer); - htmlRenderer.ObjectRenderers.AddIfNotAlready(new SyntaxRenderer(codeBlockRenderer, _settings.PrismJsInterop)); + htmlRenderer.ObjectRenderers.AddIfNotAlready(new SyntaxRenderer(codeBlockRenderer, _settings.PrismJsInterop, _settings.ImageRenderJsInterop)); } } diff --git a/Source/Bookgen.Lib/Markdown/MarkdownConverter.cs b/Source/Bookgen.Lib/Markdown/MarkdownConverter.cs index c979d9a2..2033e891 100644 --- a/Source/Bookgen.Lib/Markdown/MarkdownConverter.cs +++ b/Source/Bookgen.Lib/Markdown/MarkdownConverter.cs @@ -5,11 +5,10 @@ using Bookgen.Lib.Markdown.Renderers.Terminal; using Bookgen.Lib.Markdown.TableOfContents; -using Bookgen.Lib.Pipeline; using Markdig; - -using Microsoft.AspNetCore.Components; +using Markdig.Parsers; +using Markdig.Syntax; namespace Bookgen.Lib.Markdown; @@ -18,16 +17,16 @@ public sealed class MarkdownConverter : IDisposable private readonly MarkdownPipeline _htmlPipeLine; private readonly MarkdownPipeline _terminalPipeLine; - public MarkdownConverter(RenderSettings settings) + public MarkdownConverter(MarkdownRenderSettings settings) { - var configuration = new MarkdownPipelineBuilder() + MarkdownPipelineBuilder configuration = new MarkdownPipelineBuilder() .UseAdvancedExtensions() .UseTableOfContents() .UseMathematics() .UseYamlFrontMatter() .Use(); - foreach (var extension in configuration.Extensions) + foreach (IMarkdownExtension extension in configuration.Extensions) { if (extension is BookGenExtension bookGenExtension) { @@ -39,6 +38,7 @@ public MarkdownConverter(RenderSettings settings) _terminalPipeLine = new MarkdownPipelineBuilder() .UseYamlFrontMatter() + .UseAutoLinks() .Build(); } @@ -56,13 +56,19 @@ public void Dispose() public string RenderMarkdownToHtml(string markdown) => Markdig.Markdown.ToHtml(markdown, _htmlPipeLine); - public string RenderMarkdownToTerminal(string markdown) + public string RenderMarkdownToTerminal(string markdown, RenderOptions? renderOptions = null) { - PSMarkdownOptionInfo optionInfo = new(); + MarkdownDocument document = MarkdownParser.Parse(markdown, _terminalPipeLine); using var writer = new StringWriter(); - var renderer = new VT100Renderer(writer, optionInfo); - return Markdig.Markdown.Convert(markdown, renderer, _terminalPipeLine).ToString() ?? ""; + renderOptions ??= new RenderOptions(); + + TerminalRenderer renderer = new TerminalRenderer(writer, renderOptions); + + renderer.Render(document); + renderer.Writer.Flush(); + + return renderer.Writer.ToString() ?? string.Empty; } } diff --git a/Source/Bookgen.Lib/Markdown/RenderSettings.cs b/Source/Bookgen.Lib/Markdown/MarkdownRenderSettings.cs similarity index 75% rename from Source/Bookgen.Lib/Markdown/RenderSettings.cs rename to Source/Bookgen.Lib/Markdown/MarkdownRenderSettings.cs index 3a4f9a43..a1c4320d 100644 --- a/Source/Bookgen.Lib/Markdown/RenderSettings.cs +++ b/Source/Bookgen.Lib/Markdown/MarkdownRenderSettings.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -9,12 +9,13 @@ namespace Bookgen.Lib.Markdown; -public sealed class RenderSettings : IDisposable +public sealed class MarkdownRenderSettings : IDisposable { private readonly IImgService _imgService; public required string? HostUrl { get; init; } - public required PrismJsInterop? PrismJsInterop { get; init; } + public required SyntaxRenderJsInterop? PrismJsInterop { get; init; } + public required ImageRenderJsInterop ImageRenderJsInterop { get; init; } public required CssClasses CssClasses { get; init; } public required bool DeleteFirstH1 { get; init; } public int OffsetHeadingsBy { get; init; } = 0; @@ -23,7 +24,7 @@ public sealed class RenderSettings : IDisposable public string RequestImage(string url) { - var img = _imgService.GetImageEmbedData(url); + ImageResult img = _imgService.GetImageEmbedData(url); return ImageUrlRewriter(img); } @@ -36,7 +37,7 @@ private string EmbedImage(ImageResult arg) : $"data:{arg.ImageType.GetMimeType()};base64,{arg.Data}"; } - public RenderSettings(IImgService imgService) + public MarkdownRenderSettings(IImgService imgService) { ImageUrlRewriter = EmbedImage; _imgService = imgService; @@ -45,5 +46,6 @@ public RenderSettings(IImgService imgService) public void Dispose() { PrismJsInterop?.Dispose(); + ImageRenderJsInterop.Dispose(); } } diff --git a/Source/Bookgen.Lib/Markdown/Renderers/ExtendedLinkInlineRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/ExtendedLinkInlineRenderer.cs index 1337af1f..76dcc238 100644 --- a/Source/Bookgen.Lib/Markdown/Renderers/ExtendedLinkInlineRenderer.cs +++ b/Source/Bookgen.Lib/Markdown/Renderers/ExtendedLinkInlineRenderer.cs @@ -3,6 +3,7 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using Markdig.Renderers; @@ -65,7 +66,7 @@ private static bool TryGetQueryParam(Uri uri, string queryparam, [NotNullWhen(tr value = null; return false; } - var queryParams = System.Web.HttpUtility.ParseQueryString(query); + NameValueCollection queryParams = System.Web.HttpUtility.ParseQueryString(query); value = queryParams[queryparam]; return value != null; diff --git a/Source/Bookgen.Lib/Markdown/Renderers/SyntaxRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/SyntaxRenderer.cs index ea4dbbf0..b59bc5b0 100644 --- a/Source/Bookgen.Lib/Markdown/Renderers/SyntaxRenderer.cs +++ b/Source/Bookgen.Lib/Markdown/Renderers/SyntaxRenderer.cs @@ -1,13 +1,15 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- using System.Text; using System.Web; +using Bookgen.Lib.ImageService; using Bookgen.Lib.JsInterop; +using Markdig.Helpers; using Markdig.Parsers; using Markdig.Renderers; using Markdig.Renderers.Html; @@ -18,9 +20,11 @@ namespace Bookgen.Lib.Markdown.Renderers; internal sealed class SyntaxRenderer : HtmlObjectRenderer, IDisposable { private readonly CodeBlockRenderer _originalRenderer; - private readonly PrismJsInterop? _prism; + private readonly SyntaxRenderJsInterop? _prism; + private readonly ImageRenderJsInterop _imageRenderJsInterop; private readonly HashSet _supportedLanguages; - public const string Terminallanguage = "terminal"; + public const string TerminalLanguage = "terminal"; + public const string NomnomlLanguage = "nomnoml"; public const string TerminalHtml = """
@@ -31,10 +35,13 @@ internal sealed class SyntaxRenderer : HtmlObjectRenderer, IDisposabl public bool PreRender => _prism != null; - public SyntaxRenderer(CodeBlockRenderer underlyingRenderer, PrismJsInterop? prism) + public SyntaxRenderer(CodeBlockRenderer underlyingRenderer, + SyntaxRenderJsInterop? prism, + ImageRenderJsInterop imageRenderJsInterop) { _originalRenderer = underlyingRenderer ?? new CodeBlockRenderer(); _prism = prism; + _imageRenderJsInterop = imageRenderJsInterop; _supportedLanguages = new HashSet { "markup", "css", "clike", "javascript", "abap", "actionscript", @@ -57,7 +64,7 @@ public SyntaxRenderer(CodeBlockRenderer underlyingRenderer, PrismJsInterop? pris "sass", "scss", "scala", "scheme", "smalltalk", "smarty", "sql", "soy", "stylus", "swift", "tap", "tcl", "textile", "tt2", "twig", "typescript", "vbnet", "velocity", "verilog", "vhdl", "vim", "visual-basic", "wasm", "wiki", - "xeora", "xojo", "xquery", "yaml", Terminallanguage + "xeora", "xojo", "xquery", "yaml", TerminalLanguage, NomnomlLanguage }; } @@ -68,8 +75,8 @@ public static string GetCode(LeafBlock node) int totalLines = lines.Length; for (int i = 0; i < totalLines; i++) { - var line = lines[i]; - var slice = line.Slice; + StringLine line = lines[i]; + StringSlice slice = line.Slice; if (slice.Text == null) { continue; @@ -110,22 +117,35 @@ protected override void Write(HtmlRenderer renderer, CodeBlock obj) string code = GetCode(obj); - if (languageMoniker == Terminallanguage) + switch (languageMoniker) { - string terminalOutput = RenderTerminalString(code); - renderer.Write(terminalOutput); - } - else if (PreRender) - { - string rendered = RenderWithPrism(code, languageMoniker); - renderer.Write(rendered); - } - else - { - _originalRenderer.Write(renderer, obj); + case TerminalLanguage: + renderer.Write(RenderTerminalString(code)); + break; + case NomnomlLanguage: + ImageResult img = _imageRenderJsInterop.RenderNomnoml(code); + renderer.Write(RendererImgage(img)); + break; + default: + if (PreRender) + { + renderer.Write(RenderWithPrism(code, languageMoniker)); + } + else + { + _originalRenderer.Write(renderer, obj); + } + break; } } + private static string RendererImgage(ImageResult img) + { + return img.ImageType == ImageType.Svg + ? img.Data + : $""; + } + public static string RenderTerminalString(string code) { const string codeTag = ""; diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/AutolinkInlineRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/AutolinkInlineRenderer.cs new file mode 100644 index 00000000..cae7de68 --- /dev/null +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/AutolinkInlineRenderer.cs @@ -0,0 +1,25 @@ +using System.Web; + +using Markdig.Syntax.Inlines; + +namespace Bookgen.Lib.Markdown.Renderers.Terminal; + +internal sealed class AutolinkInlineRenderer : TerminalObjectRenderer +{ + protected override void Write(TerminalRenderer renderer, AutolinkInline obj) + { + string url = obj.IsEmail + ? HttpUtility.UrlEncode($"mailto:{obj.Url}") + : HttpUtility.UrlEncode(obj.Url); + + var text = renderer + .Builder + .New() + .WithForegroundColor(renderer.RenderOptions.LinkColor) + .AppendLink(url, obj.Url) + .ResetFormat() + .ToString(); + + renderer.Write(text); + } +} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/CodeBlockRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/CodeBlockRenderer.cs new file mode 100644 index 00000000..87559381 --- /dev/null +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/CodeBlockRenderer.cs @@ -0,0 +1,40 @@ +using Markdig.Helpers; +using Markdig.Syntax; + +namespace Bookgen.Lib.Markdown.Renderers.Terminal; + +internal sealed class CodeBlockRenderer : TerminalObjectRenderer +{ + protected override void Write(TerminalRenderer renderer, CodeBlock obj) + { + if (obj?.Lines.Lines != null) + { + string begin = renderer.Builder + .New() + .WithBackgroundColor(renderer.RenderOptions.CodeBlockBackground) + .WithForegroundColor(renderer.RenderOptions.CodeBlockColor) + .ToString(); + + renderer.Write(begin); + + for (int i = 0; i < obj.Lines.Count; i++) + { + StringLine codeLine = obj.Lines.Lines[i]; + if (!string.IsNullOrWhiteSpace(codeLine.ToString())) + { + if (i == obj.Lines.Count - 1) + { + renderer.Write(codeLine.ToString()).WriteReset().WriteLine(); + } + else + { + renderer.WriteLine(codeLine.ToString()); + } + + } + } + + renderer.WriteLine(); + } + } +} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/CodeInlineRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/CodeInlineRenderer.cs index 168158ce..aa9658bd 100644 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/CodeInlineRenderer.cs +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/CodeInlineRenderer.cs @@ -1,17 +1,21 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Syntax.Inlines; +using Markdig.Syntax.Inlines; namespace Bookgen.Lib.Markdown.Renderers.Terminal; -/// -/// Renderer for adding VT100 escape sequences for inline code elements. -/// -internal class CodeInlineRenderer : VT100ObjectRenderer +internal sealed class CodeInlineRenderer : TerminalObjectRenderer { - protected override void Write(VT100Renderer renderer, CodeInline obj) + protected override void Write(TerminalRenderer renderer, CodeInline obj) { - renderer.Write(renderer.EscapeSequences.FormatCode(obj.Content, isInline: true)); + var begin = renderer.Builder + .New() + .WithForegroundColor(renderer.RenderOptions.CodeInlineColor) + .WithItalic() + .ToString(); + + renderer.Write(begin); + + renderer.Write(obj.ContentSpan); + + renderer.WriteReset(); } } diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/DelimiterInlineRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/DelimiterInlineRenderer.cs new file mode 100644 index 00000000..713c6776 --- /dev/null +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/DelimiterInlineRenderer.cs @@ -0,0 +1,12 @@ +using Markdig.Syntax.Inlines; + +namespace Bookgen.Lib.Markdown.Renderers.Terminal; + +internal sealed class DelimiterInlineRenderer : TerminalObjectRenderer +{ + protected override void Write(TerminalRenderer renderer, DelimiterInline obj) + { + renderer.Write(obj.ToLiteral()); + renderer.WriteChildren(obj); + } +} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/EmphasisInlineRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/EmphasisInlineRenderer.cs index 8e585e71..f5452f56 100644 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/EmphasisInlineRenderer.cs +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/EmphasisInlineRenderer.cs @@ -1,17 +1,47 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +using System.Diagnostics; using Markdig.Syntax.Inlines; +using Webmaster442.WindowsTerminal; + namespace Bookgen.Lib.Markdown.Renderers.Terminal; -/// -/// Renderer for adding VT100 escape sequences for bold and italics elements. -/// -internal class EmphasisInlineRenderer : VT100ObjectRenderer +internal sealed class EmphasisInlineRenderer : TerminalObjectRenderer { - protected override void Write(VT100Renderer renderer, EmphasisInline obj) + private enum RenderAs + { + Regular = 0, + Bold, + Italic, + } + + private static RenderAs GetRenderOption(EmphasisInline obj) + { + if (obj.DelimiterChar is '*' or '_') + { + Debug.Assert(obj.DelimiterCount <= 2); + return obj.DelimiterCount == 2 ? RenderAs.Bold : RenderAs.Italic; + } + return RenderAs.Regular; + } + + protected override void Write(TerminalRenderer renderer, EmphasisInline obj) { - renderer.Write(renderer.EscapeSequences.FormatEmphasis(obj.FirstChild?.ToString() ?? "", isBold: obj.DelimiterCount == 2)); + RenderAs option = GetRenderOption(obj); + if (option == RenderAs.Regular) + { + renderer.WriteChildren(obj); + return; + } + + TerminalFormattedStringBuilder preformat = renderer.Builder.New(); + + if (option == RenderAs.Bold) + preformat.WithBold(); + else + preformat.WithItalic(); + + renderer.Write(preformat.ToString()).WriteChildren(obj); + renderer.WriteReset(); } } diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/FencedCodeBlockRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/FencedCodeBlockRenderer.cs deleted file mode 100644 index b41df71a..00000000 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/FencedCodeBlockRenderer.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Helpers; -using Markdig.Syntax; - -namespace Bookgen.Lib.Markdown.Renderers.Terminal; - -/// -/// Renderer for adding VT100 escape sequences for code blocks with language type. -/// -internal class FencedCodeBlockRenderer : VT100ObjectRenderer -{ - protected override void Write(VT100Renderer renderer, FencedCodeBlock obj) - { - if (obj?.Lines.Lines != null) - { - foreach (StringLine codeLine in obj.Lines.Lines) - { - if (!string.IsNullOrWhiteSpace(codeLine.ToString())) - { - // If the code block is of type YAML, then tab to right to improve readability. - // This specifically helps for parameters help content. - if (string.Equals(obj.Info, "yaml", StringComparison.OrdinalIgnoreCase)) - { - renderer.Write("\t").WriteLine(codeLine.ToString()); - } - else - { - renderer.WriteLine(renderer.EscapeSequences.FormatCode(codeLine.ToString(), isInline: false)); - } - } - } - - // Add a blank line after the code block for better readability. - renderer.WriteLine(); - } - } -} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/HeaderBlockRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/HeaderBlockRenderer.cs deleted file mode 100644 index 342f2837..00000000 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/HeaderBlockRenderer.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Syntax; - -namespace Bookgen.Lib.Markdown.Renderers.Terminal; - -/// -/// Renderer for adding VT100 escape sequences for headings. -/// -internal class HeaderBlockRenderer : VT100ObjectRenderer -{ - protected override void Write(VT100Renderer renderer, HeadingBlock obj) - { - string? headerText = obj.Inline?.FirstChild?.ToString(); - - if (!string.IsNullOrEmpty(headerText)) - { - // Format header and then add blank line to improve readability. - switch (obj.Level) - { - case 1: - renderer.WriteLine(renderer.EscapeSequences.FormatHeader1(headerText)); - renderer.WriteLine(); - break; - - case 2: - renderer.WriteLine(renderer.EscapeSequences.FormatHeader2(headerText)); - renderer.WriteLine(); - break; - - case 3: - renderer.WriteLine(renderer.EscapeSequences.FormatHeader3(headerText)); - renderer.WriteLine(); - break; - - case 4: - renderer.WriteLine(renderer.EscapeSequences.FormatHeader4(headerText)); - renderer.WriteLine(); - break; - - case 5: - renderer.WriteLine(renderer.EscapeSequences.FormatHeader5(headerText)); - renderer.WriteLine(); - break; - - case 6: - renderer.WriteLine(renderer.EscapeSequences.FormatHeader6(headerText)); - renderer.WriteLine(); - break; - } - } - } -} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/HeadingRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/HeadingRenderer.cs new file mode 100644 index 00000000..dd275d5b --- /dev/null +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/HeadingRenderer.cs @@ -0,0 +1,27 @@ +using Markdig.Syntax; + +namespace Bookgen.Lib.Markdown.Renderers.Terminal; + +internal sealed class HeadingRenderer : TerminalObjectRenderer +{ + protected override void Write(TerminalRenderer renderer, HeadingBlock obj) + { + string prefix = new string('#', obj.Level); + + var beginText = renderer + .Builder + .New() + .WithForegroundColor(renderer.RenderOptions.HeadingColor) + .WithBold() + .Append(prefix) + .Append(' ') + .ToString(); + + renderer + .Write(beginText) + .WriteLeafInline(obj) + .WriteReset() + .EnsureLine() + .WriteLine(); + } +} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LeafInlineRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LeafInlineRenderer.cs deleted file mode 100644 index 44a3333d..00000000 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LeafInlineRenderer.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Syntax.Inlines; - -namespace Bookgen.Lib.Markdown.Renderers.Terminal; - -/// -/// Renderer for adding VT100 escape sequences for leaf elements like plain text in paragraphs. -/// -internal class LeafInlineRenderer : VT100ObjectRenderer -{ - protected override void Write(VT100Renderer renderer, LeafInline obj) - { - // If the next sibling is null, then this is the last line in the paragraph. - // Add new line character at the end. - // Else just write without newline at the end. - if (obj.NextSibling == null) - { - renderer.WriteLine(obj.ToString() ?? ""); - } - else - { - renderer.Write(obj.ToString()); - } - } -} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LineBreakInlineRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LineBreakInlineRenderer.cs new file mode 100644 index 00000000..928779d1 --- /dev/null +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LineBreakInlineRenderer.cs @@ -0,0 +1,15 @@ +using Markdig.Syntax.Inlines; + +namespace Bookgen.Lib.Markdown.Renderers.Terminal; + +internal sealed class LineBreakInlineRenderer : TerminalObjectRenderer +{ + protected override void Write(TerminalRenderer renderer, LineBreakInline obj) + { + if (obj.IsHard) + { + renderer.WriteLine(); + } + renderer.EnsureLine(); + } +} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LineBreakRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LineBreakRenderer.cs deleted file mode 100644 index 2d7054b2..00000000 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LineBreakRenderer.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Syntax.Inlines; - -namespace Bookgen.Lib.Markdown.Renderers.Terminal; - -/// -/// Renderer for adding VT100 escape sequences for line breaks. -/// -internal class LineBreakRenderer : VT100ObjectRenderer -{ - protected override void Write(VT100Renderer renderer, LineBreakInline obj) - { - // If it is a hard line break add new line at the end. - // Else, add a space for after the last character to improve readability. - if (obj.IsHard) - { - renderer.WriteLine(); - } - else - { - renderer.Write(" "); - } - } -} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LinkInlineRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LinkInlineRenderer.cs index a27491eb..9fc5dde2 100644 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LinkInlineRenderer.cs +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LinkInlineRenderer.cs @@ -1,27 +1,34 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Syntax.Inlines; +using Markdig.Syntax.Inlines; namespace Bookgen.Lib.Markdown.Renderers.Terminal; -/// -/// Renderer for adding VT100 escape sequences for links. -/// -internal class LinkInlineRenderer : VT100ObjectRenderer +internal sealed class LinkInlineRenderer : TerminalObjectRenderer { - protected override void Write(VT100Renderer renderer, LinkInline obj) + protected override void Write(TerminalRenderer renderer, LinkInline obj) { - string? text = obj.FirstChild?.ToString(); - - // Format link as image or link. if (obj.IsImage) { - renderer.Write(renderer.EscapeSequences.FormatImage(text)); + // TODO + return; } - else + + string? linkText = obj.FirstChild?.ToString(); + + if (obj.Url is null + || linkText is null) { - renderer.Write(renderer.EscapeSequences.FormatLink(text ?? "", obj.Url ?? "")); + return; } + + string text = renderer + .Builder + .New() + .WithForegroundColor(renderer.RenderOptions.LinkColor) + .AppendLink(obj.Url, linkText) + .ResetFormat() + .ToString(); + + renderer.Write(text); + } } diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ListBlockRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ListBlockRenderer.cs deleted file mode 100644 index b3a1f850..00000000 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ListBlockRenderer.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Syntax; - -namespace Bookgen.Lib.Markdown.Renderers.Terminal; - -/// -/// Renderer for adding VT100 escape sequences for list blocks. -/// -internal class ListBlockRenderer : VT100ObjectRenderer -{ - protected override void Write(VT100Renderer renderer, ListBlock obj) - { - // start index of a numbered block. - int index = 1; - - foreach (var item in obj) - { - if (item is ListItemBlock listItem) - { - if (obj.IsOrdered) - { - RenderNumberedList(renderer, listItem, index++); - } - else - { - renderer.Write(listItem); - } - } - } - - renderer.WriteLine(); - } - - private static void RenderNumberedList(VT100Renderer renderer, ListItemBlock block, int index) - { - // For a numbered list, we need to make sure the index is incremented. - foreach (var line in block) - { - if (line is ParagraphBlock paragraphBlock && paragraphBlock.Inline != null) - { - renderer.Write(index.ToString()).Write(". ").Write(paragraphBlock.Inline); - } - } - } -} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ListItemBlockRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ListItemBlockRenderer.cs deleted file mode 100644 index 7d4b60b0..00000000 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ListItemBlockRenderer.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Syntax; - -namespace Bookgen.Lib.Markdown.Renderers.Terminal; - -/// -/// Renderer for adding VT100 escape sequences for items in a list block. -/// -internal class ListItemBlockRenderer : VT100ObjectRenderer -{ - protected override void Write(VT100Renderer renderer, ListItemBlock obj) - { - if (obj.Parent is ListBlock parent) - { - if (!parent.IsOrdered) - { - foreach (var line in obj) - { - RenderWithIndent(renderer, line, parent.BulletType, 0); - } - } - } - } - - private static void RenderWithIndent(VT100Renderer renderer, MarkdownObject block, char listBullet, int indentLevel) - { - // Indent left by 2 for each level on list. - string indent = Padding(indentLevel * 2); - - if (block is ParagraphBlock paragraphBlock && paragraphBlock.Inline != null) - { - renderer.Write(indent).Write(listBullet).Write(" ").Write(paragraphBlock.Inline); - } - else - { - // If there is a sublist, the block is a ListBlock instead of ParagraphBlock. - if (block is ListBlock subList) - { - foreach (var subListItem in subList) - { - if (subListItem is ListItemBlock subListItemBlock) - { - foreach (var line in subListItemBlock) - { - // Increment indent level for sub list. - RenderWithIndent(renderer, line, listBullet, indentLevel + 1); - } - } - } - } - } - } - - // Typical padding is at most a screen's width, any more than that and we won't bother caching. - private const int IndentCacheMax = 120; - - private static readonly string[] IndentCache = new string[IndentCacheMax]; - - internal static string Padding(int countOfSpaces) - { - if (countOfSpaces >= IndentCacheMax) - { - return new string(' ', countOfSpaces); - } - - var result = IndentCache[countOfSpaces]; - - if (result == null) - { - Interlocked.CompareExchange(ref IndentCache[countOfSpaces], new string(' ', countOfSpaces), comparand: null); - result = IndentCache[countOfSpaces]; - } - - return result; - } -} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ListRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ListRenderer.cs new file mode 100644 index 00000000..0ae9bb82 --- /dev/null +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ListRenderer.cs @@ -0,0 +1,66 @@ +using Markdig.Syntax; + +namespace Bookgen.Lib.Markdown.Renderers.Terminal; + +internal sealed class ListRenderer : TerminalObjectRenderer +{ + protected override void Write(TerminalRenderer renderer, ListBlock obj) + { + int indent = 0; + string sub = ""; + Write(renderer, obj, sub, ref indent); + } + + private static void Write(TerminalRenderer renderer, ListBlock obj, string sub, ref int indent) + { + const int indentSize = 2; + if (!obj.IsOrdered) + { + foreach (ListItemBlock item in obj.Cast()) + { + renderer.Write(new string(' ', indent * indentSize)); + renderer.Write("* "); + for (int i = 0; i < item.Count; i++) + { + Block subBlock = item[i]; + if (subBlock is ListBlock subListBlock) + { + indent++; + Write(renderer, subListBlock, "", ref indent); + } + else + { + renderer.Render(subBlock); + } + } + } + } + else + { + int number = 1; + foreach (ListItemBlock item in obj.Cast()) + { + renderer.Write(new string(' ', indent * indentSize)) + .Write(sub) + .Write(number.ToString()) + .Write(". "); + + for (int i = 0; i < item.Count; i++) + { + Block subBlock = item[i]; + if (subBlock is ListBlock subListBlock) + { + indent++; + var nsub = $"{sub} {number}."; + Write(renderer, subListBlock, nsub, ref indent); + } + else + { + renderer.Render(subBlock); + } + } + ++number; + } + } + } +} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LiteralInlineRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LiteralInlineRenderer.cs new file mode 100644 index 00000000..56fadc82 --- /dev/null +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/LiteralInlineRenderer.cs @@ -0,0 +1,12 @@ +using Markdig.Syntax.Inlines; + +namespace Bookgen.Lib.Markdown.Renderers.Terminal; + +internal sealed class LiteralInlineRenderer : TerminalObjectRenderer +{ + protected override void Write(TerminalRenderer renderer, LiteralInline obj) + { + string content = obj.Content.ToString(); + renderer.Write(content); + } +} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/MarkdownOptionInfoProperty.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/MarkdownOptionInfoProperty.cs deleted file mode 100644 index 64406509..00000000 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/MarkdownOptionInfoProperty.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Bookgen.Lib.Markdown.Renderers.Terminal; - -/// -/// Enum to name all the properties of PSMarkdownOptionInfo. -/// -public enum MarkdownOptionInfoProperty -{ - /// - /// Property name Header1. - /// - Header1, - - /// - /// Property name Header2. - /// - Header2, - - /// - /// Property name Header3. - /// - Header3, - - /// - /// Property name Header4. - /// - Header4, - - /// - /// Property name Header5. - /// - Header5, - - /// - /// Property name Header6. - /// - Header6, - - /// - /// Property name Code. - /// - Code, - - /// - /// Property name Link. - /// - Link, - - /// - /// Property name Image. - /// - Image, - - /// - /// Property name EmphasisBold. - /// - EmphasisBold, - - /// - /// Property name EmphasisItalics. - /// - EmphasisItalics -} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/PSMarkdownOptionInfo.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/PSMarkdownOptionInfo.cs deleted file mode 100644 index dbb230cc..00000000 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/PSMarkdownOptionInfo.cs +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -namespace Bookgen.Lib.Markdown.Renderers.Terminal; - -/// -/// Class to represent color preference options for various Markdown elements. -/// -public sealed class PSMarkdownOptionInfo -{ - private const char Esc = (char)0x1b; - private const string EndSequence = "[0m"; - - /// - /// Gets or sets current VT100 escape sequence for header 1. - /// - public string Header1 { get; set; } - - /// - /// Gets or sets current VT100 escape sequence for header 2. - /// - public string Header2 { get; set; } - - /// - /// Gets or sets current VT100 escape sequence for header 3. - /// - public string Header3 { get; set; } - - /// - /// Gets or sets current VT100 escape sequence for header 4. - /// - public string Header4 { get; set; } - - /// - /// Gets or sets current VT100 escape sequence for header 5. - /// - public string Header5 { get; set; } - - /// - /// Gets or sets current VT100 escape sequence for header 6. - /// - public string Header6 { get; set; } - - /// - /// Gets or sets current VT100 escape sequence for code inline and code blocks. - /// - public string Code { get; set; } - - /// - /// Gets or sets current VT100 escape sequence for links. - /// - public string Link { get; set; } - - /// - /// Gets or sets current VT100 escape sequence for images. - /// - public string Image { get; set; } - - /// - /// Gets or sets current VT100 escape sequence for bold text. - /// - public string EmphasisBold { get; set; } - - /// - /// Gets or sets current VT100 escape sequence for italics text. - /// - public string EmphasisItalics { get; set; } - - /// - /// Gets or sets a value indicating whether VT100 escape sequences should be added. Default it true. - /// - public bool EnableVT100Encoding { get; set; } - - /// - /// Get the property as an rendered escape sequence. - /// This is used by formatting system for displaying. - /// - /// Name of the property to get as escape sequence. - /// Specified property name as escape sequence. - public string AsEscapeSequence(MarkdownOptionInfoProperty propertyName) - { - return propertyName switch - { - MarkdownOptionInfoProperty.Header1 => string.Concat(Esc, Header1, Header1, Esc, EndSequence), - MarkdownOptionInfoProperty.Header2 => string.Concat(Esc, Header2, Header2, Esc, EndSequence), - MarkdownOptionInfoProperty.Header3 => string.Concat(Esc, Header3, Header3, Esc, EndSequence), - MarkdownOptionInfoProperty.Header4 => string.Concat(Esc, Header4, Header4, Esc, EndSequence), - MarkdownOptionInfoProperty.Header5 => string.Concat(Esc, Header5, Header5, Esc, EndSequence), - MarkdownOptionInfoProperty.Header6 => string.Concat(Esc, Header6, Header6, Esc, EndSequence), - MarkdownOptionInfoProperty.Code => string.Concat(Esc, Code, Code, Esc, EndSequence), - MarkdownOptionInfoProperty.Link => string.Concat(Esc, Link, Link, Esc, EndSequence), - MarkdownOptionInfoProperty.Image => string.Concat(Esc, Image, Image, Esc, EndSequence), - MarkdownOptionInfoProperty.EmphasisBold => string.Concat(Esc, EmphasisBold, EmphasisBold, Esc, EndSequence), - MarkdownOptionInfoProperty.EmphasisItalics => string.Concat(Esc, EmphasisItalics, EmphasisItalics, Esc, EndSequence), - _ => throw new InvalidOperationException($"Unknown value: {propertyName}"), - }; - } - - /// - /// Initializes a new instance of the class and sets dark as the default theme. - /// - public PSMarkdownOptionInfo() - { - SetDarkTheme(); - EnableVT100Encoding = true; - } - - private const string Header1Dark = "[7m"; - private const string Header2Dark = "[4;93m"; - private const string Header3Dark = "[4;94m"; - private const string Header4Dark = "[4;95m"; - private const string Header5Dark = "[4;96m"; - private const string Header6Dark = "[4;97m"; - private const string CodeDark = "[48;2;155;155;155;38;2;30;30;30m"; - private const string CodeMacOS = "[107;95m"; - private const string LinkDark = "[4;38;5;117m"; - private const string ImageDark = "[33m"; - private const string EmphasisBoldDark = "[1m"; - private const string EmphasisItalicsDark = "[36m"; - - private const string Header1Light = "[7m"; - private const string Header2Light = "[4;33m"; - private const string Header3Light = "[4;34m"; - private const string Header4Light = "[4;35m"; - private const string Header5Light = "[4;36m"; - private const string Header6Light = "[4;30m"; - private const string CodeLight = "[48;2;155;155;155;38;2;30;30;30m"; - private const string LinkLight = "[4;38;5;117m"; - private const string ImageLight = "[33m"; - private const string EmphasisBoldLight = "[1m"; - private const string EmphasisItalicsLight = "[36m"; - - /// - /// Set all preference for dark theme. - /// - [MemberNotNull(nameof(Header1), - nameof(Header2), - nameof(Header3), - nameof(Header4), - nameof(Header5), - nameof(Header6), - nameof(Link), - nameof(Image), - nameof(EmphasisBold), - nameof(EmphasisItalics), - nameof(Code))] - public void SetDarkTheme() - { - Header1 = Header1Dark; - Header2 = Header2Dark; - Header3 = Header3Dark; - Header4 = Header4Dark; - Header5 = Header5Dark; - Header6 = Header6Dark; - Link = LinkDark; - Image = ImageDark; - EmphasisBold = EmphasisBoldDark; - EmphasisItalics = EmphasisItalicsDark; - SetCodeColor(isDarkTheme: true); - } - - /// - /// Set all preference for light theme. - /// - [MemberNotNull(nameof(Header1), - nameof(Header2), - nameof(Header3), - nameof(Header4), - nameof(Header5), - nameof(Header6), - nameof(Link), - nameof(Image), - nameof(EmphasisBold), - nameof(EmphasisItalics), - nameof(Code))] - public void SetLightTheme() - { - Header1 = Header1Light; - Header2 = Header2Light; - Header3 = Header3Light; - Header4 = Header4Light; - Header5 = Header5Light; - Header6 = Header6Light; - Link = LinkLight; - Image = ImageLight; - EmphasisBold = EmphasisBoldLight; - EmphasisItalics = EmphasisItalicsLight; - SetCodeColor(isDarkTheme: false); - } - - [MemberNotNull(nameof(Code))] - private void SetCodeColor(bool isDarkTheme) - { - // MacOS terminal app does not support extended colors for VT100, so we special case for it. - Code = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? CodeMacOS : isDarkTheme ? CodeDark : CodeLight; - } -} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ParagraphBlockRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ParagraphBlockRenderer.cs deleted file mode 100644 index e063ccb0..00000000 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ParagraphBlockRenderer.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Syntax; - -namespace Bookgen.Lib.Markdown.Renderers.Terminal; - -/// -/// Renderer for adding VT100 escape sequences for paragraphs. -/// -internal class ParagraphBlockRenderer : VT100ObjectRenderer -{ - protected override void Write(VT100Renderer renderer, ParagraphBlock obj) - { - if (obj.Inline == null) - return; - - // Call the renderer for children, leaf inline or line breaks. - renderer.WriteChildren(obj.Inline); - - // Add new line at the end of the paragraph. - renderer.WriteLine(); - } -} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ParagraphRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ParagraphRenderer.cs new file mode 100644 index 00000000..b566102c --- /dev/null +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ParagraphRenderer.cs @@ -0,0 +1,14 @@ +using Markdig.Syntax; + +namespace Bookgen.Lib.Markdown.Renderers.Terminal; + +internal sealed class ParagraphRenderer : TerminalObjectRenderer +{ + protected override void Write(TerminalRenderer renderer, ParagraphBlock obj) + { + renderer + .WriteLeafInline(obj) + .WriteLine() + .WriteLine(); + } +} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/QuoteBlockRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/QuoteBlockRenderer.cs index ae2639d0..8debad31 100644 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/QuoteBlockRenderer.cs +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/QuoteBlockRenderer.cs @@ -1,24 +1,22 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Syntax; +using Markdig.Syntax; namespace Bookgen.Lib.Markdown.Renderers.Terminal; -/// -/// Renderer for adding VT100 escape sequences for quote blocks. -/// -internal class QuoteBlockRenderer : VT100ObjectRenderer +internal sealed class QuoteBlockRenderer : TerminalObjectRenderer { - protected override void Write(VT100Renderer renderer, QuoteBlock obj) + protected override void Write(TerminalRenderer renderer, QuoteBlock obj) { - // Iterate through each item and add the quote character before the content. - foreach (var item in obj) - { - renderer.Write(obj.QuoteChar).Write(" ").Write(item); - } + var begin = renderer.Builder.New() + .WithForegroundColor(renderer.RenderOptions.QuoteBlockColor) + .WithItalic() + .ToString(); + + renderer + .Write(begin) + .WriteChildren(obj); + + renderer + .WriteReset(); - // Add blank line after the quote block. - renderer.WriteLine(); } } diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/RenderOptions.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/RenderOptions.cs new file mode 100644 index 00000000..a0953288 --- /dev/null +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/RenderOptions.cs @@ -0,0 +1,41 @@ +using Webmaster442.WindowsTerminal; + +namespace Bookgen.Lib.Markdown.Renderers.Terminal; + +public sealed class RenderOptions +{ + public TerminalColor HeadingColor { get; set; } + + public TerminalColor LinkColor { get; set; } + + public TerminalColor CodeInlineColor { get; set; } + + public TerminalColor QuoteBlockColor { get; set; } + + public TerminalColor CodeBlockBackground { get; set; } + + public TerminalColor CodeBlockColor { get; set; } + + public int Width + { + get => field; + set + { + if (value < 1) + throw new ArgumentOutOfRangeException(nameof(Width), "Width must be greater than 0."); + + field = value; + } + } + + public RenderOptions() + { + HeadingColor = TerminalColor.Green; + LinkColor = TerminalColor.BrightBlue; + CodeInlineColor = TerminalColor.BrightRed; + QuoteBlockColor = TerminalColor.BrightWhite; + CodeBlockColor = TerminalColor.Black; + CodeBlockBackground = TerminalColor.White; + Width = 120; + } +} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/TerminalObjectRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/TerminalObjectRenderer.cs new file mode 100644 index 00000000..a97ebc24 --- /dev/null +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/TerminalObjectRenderer.cs @@ -0,0 +1,8 @@ +using Markdig.Renderers; +using Markdig.Syntax; + +namespace Bookgen.Lib.Markdown.Renderers.Terminal; + +public abstract class TerminalObjectRenderer : MarkdownObjectRenderer where TObject : MarkdownObject +{ +} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/TerminalRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/TerminalRenderer.cs new file mode 100644 index 00000000..a587a8ca --- /dev/null +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/TerminalRenderer.cs @@ -0,0 +1,42 @@ +using Markdig.Renderers; + +using Webmaster442.WindowsTerminal; + +namespace Bookgen.Lib.Markdown.Renderers.Terminal; + +public sealed class TerminalRenderer : TextRendererBase +{ + + public TerminalRenderer(TextWriter writer, RenderOptions renderOptions) : base(writer) + { + RenderOptions = renderOptions; + Builder = new TerminalFormattedStringBuilder(); + + ObjectRenderers.Add(new CodeBlockRenderer()); + ObjectRenderers.Add(new ListRenderer()); + ObjectRenderers.Add(new HeadingRenderer()); + ObjectRenderers.Add(new ParagraphRenderer()); + ObjectRenderers.Add(new QuoteBlockRenderer()); + ObjectRenderers.Add(new ThematicBreakRenderer()); + + ObjectRenderers.Add(new AutolinkInlineRenderer()); + ObjectRenderers.Add(new CodeInlineRenderer()); + ObjectRenderers.Add(new DelimiterInlineRenderer()); + ObjectRenderers.Add(new EmphasisInlineRenderer()); + ObjectRenderers.Add(new LineBreakInlineRenderer()); + ObjectRenderers.Add(new LinkInlineRenderer()); + ObjectRenderers.Add(new LiteralInlineRenderer()); + } + + public RenderOptions RenderOptions { get; } + + internal TerminalFormattedStringBuilder Builder { get; } + + private const string ResetCode = "\u001b[0m"; + + public TerminalRenderer WriteReset() + { + Write(ResetCode); + return this; + } +} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ThematicBreakRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ThematicBreakRenderer.cs new file mode 100644 index 00000000..18b01324 --- /dev/null +++ b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/ThematicBreakRenderer.cs @@ -0,0 +1,12 @@ +using Markdig.Syntax; + +namespace Bookgen.Lib.Markdown.Renderers.Terminal; + +internal sealed class ThematicBreakRenderer : TerminalObjectRenderer +{ + protected override void Write(TerminalRenderer renderer, ThematicBreakBlock obj) + { + renderer.WriteLine(new string('-', renderer.RenderOptions.Width)); + renderer.WriteLine(); + } +} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/VT100EscapeSequences.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/VT100EscapeSequences.cs deleted file mode 100644 index 6c174bd1..00000000 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/VT100EscapeSequences.cs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Bookgen.Lib.Markdown.Renderers.Terminal; - -/// -/// Class to represent default VT100 escape sequences. -/// -public class VT100EscapeSequences -{ - private const char Esc = (char)0x1B; - - private readonly string _endSequence = Esc + "[0m"; - - // For code blocks, [500@ make sure that the whole line has background color. - private const string LongBackgroundCodeBlock = "[500@"; - - private readonly PSMarkdownOptionInfo _options; - - /// - /// Initializes a new instance of the class. - /// - /// PSMarkdownOptionInfo object to initialize with. - public VT100EscapeSequences(PSMarkdownOptionInfo optionInfo) - { - _options = optionInfo ?? throw new ArgumentNullException(nameof(optionInfo)); - } - - /// - /// Class to represent default VT100 escape sequences. - /// - /// Text of the header to format. - /// Formatted Header 1 string. - public string FormatHeader1(string headerText) - { - return FormatHeader(headerText, _options.Header1); - } - - /// - /// Class to represent default VT100 escape sequences. - /// - /// Text of the header to format. - /// Formatted Header 2 string. - public string FormatHeader2(string headerText) - { - return FormatHeader(headerText, _options.Header2); - } - - /// - /// Class to represent default VT100 escape sequences. - /// - /// Text of the header to format. - /// Formatted Header 3 string. - public string FormatHeader3(string headerText) - { - return FormatHeader(headerText, _options.Header3); - } - - /// - /// Class to represent default VT100 escape sequences. - /// - /// Text of the header to format. - /// Formatted Header 4 string. - public string FormatHeader4(string headerText) - { - return FormatHeader(headerText, _options.Header4); - } - - /// - /// Class to represent default VT100 escape sequences. - /// - /// Text of the header to format. - /// Formatted Header 5 string. - public string FormatHeader5(string headerText) - { - return FormatHeader(headerText, _options.Header5); - } - - /// - /// Class to represent default VT100 escape sequences. - /// - /// Text of the header to format. - /// Formatted Header 6 string. - public string FormatHeader6(string headerText) - { - return FormatHeader(headerText, _options.Header6); - } - - /// - /// Class to represent default VT100 escape sequences. - /// - /// Text of the code block to format. - /// True if it is a inline code block, false otherwise. - /// Formatted code block string. - public string FormatCode(string codeText, bool isInline) - { - bool isVT100Enabled = _options.EnableVT100Encoding; - - if (isInline) - { - if (isVT100Enabled) - { - return string.Concat(Esc, _options.Code, codeText, _endSequence); - } - else - { - return codeText; - } - } - else - { - if (isVT100Enabled) - { - return string.Concat(Esc, _options.Code, codeText, Esc, LongBackgroundCodeBlock, _endSequence); - } - else - { - return codeText; - } - } - } - - /// - /// Class to represent default VT100 escape sequences. - /// - /// Text of the link to format. - /// URL of the link. - /// True url should be hidden, false otherwise. Default is true. - /// Formatted link string. - public string FormatLink(string linkText, string url, bool hideUrl = true) - { - bool isVT100Enabled = _options.EnableVT100Encoding; - - if (hideUrl) - { - if (isVT100Enabled) - { - return string.Concat(Esc, _options.Link, "\"", linkText, "\"", _endSequence); - } - else - { - return string.Concat("\"", linkText, "\""); - } - } - else - { - if (isVT100Enabled) - { - return string.Concat("\"", linkText, "\" (", Esc, _options.Link, url, _endSequence, ")"); - } - else - { - return string.Concat("\"", linkText, "\" (", url, ")"); - } - } - } - - /// - /// Class to represent default VT100 escape sequences. - /// - /// Text to format as emphasis. - /// True if it is to be formatted as bold, false to format it as italics. - /// Formatted emphasis string. - public string FormatEmphasis(string emphasisText, bool isBold) - { - var sequence = isBold ? _options.EmphasisBold : _options.EmphasisItalics; - - if (_options.EnableVT100Encoding) - { - return string.Concat(Esc, sequence, emphasisText, _endSequence); - } - else - { - return emphasisText; - } - } - - /// - /// Class to represent default VT100 escape sequences. - /// - /// Text of the image to format. - /// Formatted image string. - public string FormatImage(string? altText) - { - var text = altText; - - if (string.IsNullOrEmpty(altText)) - { - text = "Image"; - } - - if (_options.EnableVT100Encoding) - { - return string.Concat(Esc, _options.Image, "[", text, "]", _endSequence); - } - else - { - return string.Concat("[", text, "]"); - } - } - - private string FormatHeader(string headerText, string headerEscapeSequence) - { - if (_options.EnableVT100Encoding) - { - return string.Concat(Esc, headerEscapeSequence, headerText, _endSequence); - } - else - { - return headerText; - } - } -} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/VT100ObjectRenderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/VT100ObjectRenderer.cs deleted file mode 100644 index 04f144c1..00000000 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/VT100ObjectRenderer.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Renderers; -using Markdig.Syntax; - -namespace Bookgen.Lib.Markdown.Renderers.Terminal; - -/// -/// Implement the MarkdownObjectRenderer with VT100Renderer. -/// -/// The element type of the renderer. -public abstract class VT100ObjectRenderer : MarkdownObjectRenderer where T : MarkdownObject -{ -} diff --git a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/VT100Renderer.cs b/Source/Bookgen.Lib/Markdown/Renderers/Terminal/VT100Renderer.cs deleted file mode 100644 index ff25494a..00000000 --- a/Source/Bookgen.Lib/Markdown/Renderers/Terminal/VT100Renderer.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Markdig.Renderers; - -namespace Bookgen.Lib.Markdown.Renderers.Terminal; - -/// -/// Initializes an instance of the VT100 renderer. -/// -public sealed class VT100Renderer : TextRendererBase -{ - /// - /// Initializes a new instance of the class. - /// - /// TextWriter to write to. - /// PSMarkdownOptionInfo object with options. - public VT100Renderer(TextWriter writer, PSMarkdownOptionInfo optionInfo) : base(writer) - { - EscapeSequences = new VT100EscapeSequences(optionInfo); - - // Add the various element renderers. - ObjectRenderers.Add(new HeaderBlockRenderer()); - ObjectRenderers.Add(new LineBreakRenderer()); - ObjectRenderers.Add(new CodeInlineRenderer()); - ObjectRenderers.Add(new FencedCodeBlockRenderer()); - ObjectRenderers.Add(new EmphasisInlineRenderer()); - ObjectRenderers.Add(new ParagraphBlockRenderer()); - ObjectRenderers.Add(new LeafInlineRenderer()); - ObjectRenderers.Add(new LinkInlineRenderer()); - ObjectRenderers.Add(new ListBlockRenderer()); - ObjectRenderers.Add(new ListItemBlockRenderer()); - ObjectRenderers.Add(new QuoteBlockRenderer()); - } - - /// - /// Gets the current escape sequences. - /// - public VT100EscapeSequences EscapeSequences { get; } -} diff --git a/Source/Bookgen.Lib/Markdown/TableOfContents/CustomAutoIdExtension.cs b/Source/Bookgen.Lib/Markdown/TableOfContents/CustomAutoIdExtension.cs index 0ccb1632..b601176f 100644 --- a/Source/Bookgen.Lib/Markdown/TableOfContents/CustomAutoIdExtension.cs +++ b/Source/Bookgen.Lib/Markdown/TableOfContents/CustomAutoIdExtension.cs @@ -3,6 +3,8 @@ // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- +using System.Text; + using ExCSS; using Markdig; @@ -39,14 +41,14 @@ public CustomAutoIdExtension(CustomAutoIdOptions options) public void Setup(MarkdownPipelineBuilder pipeline) { - var headingBlockParser = pipeline.BlockParsers.Find(); + HeadingBlockParser? headingBlockParser = pipeline.BlockParsers.Find(); if (headingBlockParser is not null) { // Install a hook on the HeadingBlockParser when a HeadingBlock is actually processed headingBlockParser.Closed -= HeadingBlockParser_Closed; headingBlockParser.Closed += HeadingBlockParser_Closed; } - var paragraphBlockParser = pipeline.BlockParsers.FindExact(); + ParagraphBlockParser? paragraphBlockParser = pipeline.BlockParsers.FindExact(); if (paragraphBlockParser is not null) { // Install a hook on the ParagraphBlockParser when a HeadingBlock is actually processed as a Setex heading @@ -75,7 +77,7 @@ private void HeadingBlockParser_Closed(BlockProcessor processor, Block block) // If the AutoLink options is set, we register a LinkReferenceDefinition at the document level if (options.HeadingIdGenerator == null && (options.Options & AutoIdentifierOptions.AutoLink) != 0) { - var headingLine = headingBlock.Lines.Lines[0]; + StringLine headingLine = headingBlock.Lines.Lines[0]; var text = headingLine.ToString(); @@ -84,7 +86,7 @@ private void HeadingBlockParser_Closed(BlockProcessor processor, Block block) CreateLinkInline = CreateLinkInlineForHeading }; - var doc = processor.Document; + MarkdownDocument doc = processor.Document; var dictionary = doc.GetData(this) as Dictionary; if (dictionary is null) { @@ -101,15 +103,15 @@ private void HeadingBlockParser_Closed(BlockProcessor processor, Block block) private void DocumentOnProcessInlinesBegin(InlineProcessor processor, Inline? inline) { - var doc = processor.Document; + MarkdownDocument doc = processor.Document; doc.ProcessInlinesBegin -= DocumentOnProcessInlinesBegin; var dictionary = (Dictionary)doc.GetData(this)!; - foreach (var keyPair in dictionary) + foreach (KeyValuePair keyPair in dictionary) { // Here we make sure that auto-identifiers will not override an existing link definition // defined in the document // If it is the case, we skip the auto identifier for the Heading - if (!doc.TryGetLinkReferenceDefinition(keyPair.Key, out var linkDef)) + if (!doc.TryGetLinkReferenceDefinition(keyPair.Key, out LinkReferenceDefinition? linkDef)) { doc.SetLinkReferenceDefinition(keyPair.Key, keyPair.Value, true); } @@ -155,13 +157,13 @@ private void HeadingBlock_ProcessInlinesEnd(InlineProcessor processor, Inline? i } // Use internally a HtmlRenderer to strip links from a heading - var stripRenderer = rendererCache.Get(); + CacheHtmlRenderer stripRenderer = rendererCache.Get(); stripRenderer.Render(headingBlock.Inline); var headingText = stripRenderer.Writer.ToString()!; rendererCache.Release(stripRenderer); // If id is already set, don't try to modify it - var attributes = processor.Block!.GetAttributes(); + HtmlAttributes attributes = processor.Block!.GetAttributes(); try { if (options.HeadingIdGenerator is not null) @@ -184,7 +186,7 @@ private void HeadingBlock_ProcessInlinesEnd(InlineProcessor processor, Inline? i // Add a trailing -1, -2, -3...etc. in case of collision int index = 0; var headingId = baseHeadingId; - var headingBuffer = StringBuilderCache.Local(); + StringBuilder headingBuffer = StringBuilderCache.Local(); while (!identifiers.Add(headingId)) { index++; diff --git a/Source/Bookgen.Lib/Markdown/TableOfContents/HeadingInfos.cs b/Source/Bookgen.Lib/Markdown/TableOfContents/HeadingInfos.cs index afe46da2..e25b6060 100644 --- a/Source/Bookgen.Lib/Markdown/TableOfContents/HeadingInfos.cs +++ b/Source/Bookgen.Lib/Markdown/TableOfContents/HeadingInfos.cs @@ -34,7 +34,7 @@ void RenderHtmlLint(HtmlRenderer renderer, TocState options) renderer.WriteLine("
    "); - foreach (var item in Children) + foreach (HeadingInfos item in Children) { if (item.Level > options.MaxLevel) continue; diff --git a/Source/Bookgen.Lib/Markdown/TableOfContents/HtmlTocRenderer.cs b/Source/Bookgen.Lib/Markdown/TableOfContents/HtmlTocRenderer.cs index ed6dc58e..f6170921 100644 --- a/Source/Bookgen.Lib/Markdown/TableOfContents/HtmlTocRenderer.cs +++ b/Source/Bookgen.Lib/Markdown/TableOfContents/HtmlTocRenderer.cs @@ -23,7 +23,7 @@ protected override void Write(HtmlRenderer renderer, TocBlock obj) return; renderer.EnsureLine(); - var attr = obj.GetAttributes(); + HtmlAttributes attr = obj.GetAttributes(); if (attr.Id is null) attr.Id = "toc"; diff --git a/Source/Bookgen.Lib/Markdown/TableOfContents/LevelList.cs b/Source/Bookgen.Lib/Markdown/TableOfContents/LevelList.cs index 97d9cf29..23b5f76f 100644 --- a/Source/Bookgen.Lib/Markdown/TableOfContents/LevelList.cs +++ b/Source/Bookgen.Lib/Markdown/TableOfContents/LevelList.cs @@ -104,7 +104,7 @@ public void Append(T item) if (item.Level > _current.Level) { //try to find last child that child.Level > item.Level - var found = LevelList.FindChildLevelLessThan(item.Level, _current); + T found = LevelList.FindChildLevelLessThan(item.Level, _current); var offset = item.Level - found.Level; if (offset > 1) { @@ -131,7 +131,7 @@ public void Append(T item) } //not find a right item, move to previous one - var parent = LevelList.FindParentLevelLessThan(item.Level, _current); + T? parent = LevelList.FindParentLevelLessThan(item.Level, _current); if (parent is not null) { //marge siblings which @@ -159,7 +159,7 @@ protected virtual void MargeSiblings(T parent, T add) for (int i = 0; i < parent.Count; i++) { - var t = parent[i]; + T t = parent[i]; if (t.Level > add.Level) { if (startAt == -1) @@ -193,7 +193,7 @@ protected virtual void MargeSiblings(T parent, T add) }; for (int k = start; k < end; k++) { - var item = parent[k]; + T item = parent[k]; item.Parent = emtpy; emtpy._data.Add(item); if (start == end - 1) diff --git a/Source/Bookgen.Lib/Markdown/TableOfContents/TocBlockParser.cs b/Source/Bookgen.Lib/Markdown/TableOfContents/TocBlockParser.cs index b76f4e79..fd7df598 100644 --- a/Source/Bookgen.Lib/Markdown/TableOfContents/TocBlockParser.cs +++ b/Source/Bookgen.Lib/Markdown/TableOfContents/TocBlockParser.cs @@ -39,14 +39,14 @@ public override BlockState TryOpen(BlockProcessor processor) int column = processor.Column; int sourcePosition = line.Start; - var matches = TocTagMatcher().Matches(line.ToString()); + MatchCollection matches = TocTagMatcher().Matches(line.ToString()); if (matches.Count < 1) return BlockState.None; int tagLength = matches.Select(x => x.Value.Length).Sum(); - var maxLevelMathes = MaxLevelMatcher().Matches(line.ToString()); + MatchCollection maxLevelMathes = MaxLevelMatcher().Matches(line.ToString()); if (maxLevelMathes.Count > 0) { if (int.TryParse(maxLevelMathes[0].Groups.Values.Last().Value, out int maxLevel)) diff --git a/Source/Bookgen.Lib/Markdown/TableOfContents/TocExtension.cs b/Source/Bookgen.Lib/Markdown/TableOfContents/TocExtension.cs index 427751be..c9621407 100644 --- a/Source/Bookgen.Lib/Markdown/TableOfContents/TocExtension.cs +++ b/Source/Bookgen.Lib/Markdown/TableOfContents/TocExtension.cs @@ -36,7 +36,7 @@ public TocExtension(TocState state) //register parsers public void Setup(MarkdownPipelineBuilder pipeline) { - var autoIdExtension = pipeline.Extensions.Find(); + CustomAutoIdExtension? autoIdExtension = pipeline.Extensions.Find(); if (autoIdExtension == null) throw new InvalidOperationException("CustomAutoIdExtension is null"); diff --git a/Source/Bookgen.Lib/Pipeline/Epub/CreateContentOpf.cs b/Source/Bookgen.Lib/Pipeline/Epub/CreateContentOpf.cs index 5203ff47..60d7d100 100644 --- a/Source/Bookgen.Lib/Pipeline/Epub/CreateContentOpf.cs +++ b/Source/Bookgen.Lib/Pipeline/Epub/CreateContentOpf.cs @@ -75,7 +75,7 @@ public override Task ExecuteAsync(IBookEnvironment environment, ILog } }; - var coverItem = State.PackageItems.FirstOrDefault(item => item.Properties == "cover-image"); + PackageItem? coverItem = State.PackageItems.FirstOrDefault(item => item.Properties == "cover-image"); if (coverItem != null) { opf.Metadata.Meta.Add(new PackageMetadataMeta diff --git a/Source/Bookgen.Lib/Pipeline/Epub/CreateEpubCoverAndStyle.cs b/Source/Bookgen.Lib/Pipeline/Epub/CreateEpubCoverAndStyle.cs index d7d0a780..a40914a3 100644 --- a/Source/Bookgen.Lib/Pipeline/Epub/CreateEpubCoverAndStyle.cs +++ b/Source/Bookgen.Lib/Pipeline/Epub/CreateEpubCoverAndStyle.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -24,7 +24,7 @@ public override async Task ExecuteAsync(IBookEnvironment environment var coverfile = await environment.Source.GetCoverFileName(environment.TableOfContents, logger); if (coverfile != null) { - byte[] coverdata = Utils.ConvertToPng(coverfile, 1200, 1200); + byte[] coverdata = ImageUtils.ConvertToPng(coverfile, 1200, 1200); await State.EpubFile.AddAsync("EPUB/cover.png", coverdata); State.PackageItems.Add(new PackageItem { diff --git a/Source/Bookgen.Lib/Pipeline/Epub/CreateFontFiles.cs b/Source/Bookgen.Lib/Pipeline/Epub/CreateFontFiles.cs index a052862f..e48cfd4a 100644 --- a/Source/Bookgen.Lib/Pipeline/Epub/CreateFontFiles.cs +++ b/Source/Bookgen.Lib/Pipeline/Epub/CreateFontFiles.cs @@ -20,9 +20,9 @@ public CreateFontFiles(EpubState state) : base(state) public override async Task ExecuteAsync(IBookEnvironment environment, ILogger logger) { logger.LogInformation("Creating EPUB font files..."); - await State.EpubFile.AddAsync("JetBrainsMono-Regular.ttf", environment.GetBinaryAsset("JetBrainsMono-Regular.ttf"), CompressionLevel.Optimal); - await State.EpubFile.AddAsync("OpenSans-Regular.ttf", environment.GetBinaryAsset("OpenSans-Regular.ttf"), CompressionLevel.Optimal); - await State.EpubFile.AddAsync("Nunito-Bold.ttf", environment.GetBinaryAsset("Nunito-Bold.ttf"), CompressionLevel.Optimal); + await State.EpubFile.AddAsync("JetBrainsMono-Regular.ttf", environment.GetBinaryAssetStream("JetBrainsMono-Regular.ttf"), CompressionLevel.Optimal); + await State.EpubFile.AddAsync("OpenSans-Regular.ttf", environment.GetBinaryAssetStream("OpenSans-Regular.ttf"), CompressionLevel.Optimal); + await State.EpubFile.AddAsync("Nunito-Bold.ttf", environment.GetBinaryAssetStream("Nunito-Bold.ttf"), CompressionLevel.Optimal); State.PackageItems.AddRange([ new PackageItem diff --git a/Source/Bookgen.Lib/Pipeline/Epub/CreateHtmlPages.cs b/Source/Bookgen.Lib/Pipeline/Epub/CreateHtmlPages.cs index 51049ee7..e5261332 100644 --- a/Source/Bookgen.Lib/Pipeline/Epub/CreateHtmlPages.cs +++ b/Source/Bookgen.Lib/Pipeline/Epub/CreateHtmlPages.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -7,6 +7,7 @@ using Bookgen.Lib.Domain; using Bookgen.Lib.Domain.Epub; +using Bookgen.Lib.Domain.IO; using Bookgen.Lib.Domain.IO.Configuration; using Bookgen.Lib.ImageService; using Bookgen.Lib.Internals; @@ -14,6 +15,7 @@ using Bookgen.Lib.Markdown; using Bookgen.Lib.Templates; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using static Bookgen.Lib.Pipeline.Epub.EpubState; @@ -22,8 +24,11 @@ namespace Bookgen.Lib.Pipeline.Epub; internal class CreateHtmlPages : PipeLineStep { - public CreateHtmlPages(EpubState state) : base(state) + private readonly IMemoryCache _memoryCache; + + public CreateHtmlPages(EpubState state, IMemoryCache memoryCache) : base(state) { + _memoryCache = memoryCache; } private string EpubImageRewrite(ImageResult result) @@ -35,24 +40,27 @@ private string EpubImageRewrite(ImageResult result) public override async Task ExecuteAsync(IBookEnvironment environment, ILogger logger) { - var imgService = new ImgService(environment.Source, logger, new ImageConfig + var imgConfig = new ImageConfig { SvgRecode = SvgRecodeOption.AsWebp, ResizeAndRecodeImages = ImgRecodeOption.AsPng, ImageQualityOnResize = 90, ResizeWith = 1600, ResizeHeight = 1600, - }); - var cached = new CachedImageService(imgService); + }; + + var imgService = new ImgService(environment.Source, logger, imgConfig); + var cached = new CachedImageService(imgService, _memoryCache); - using var settings = new RenderSettings(cached) + using var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.PrintConfig.CssClasses, DeleteFirstH1 = false, HostUrl = string.Empty, - PrismJsInterop = new PrismJsInterop(environment), + PrismJsInterop = new SyntaxRenderJsInterop(environment), OffsetHeadingsBy = 0, AutoEmbedSupportedLinks = false, + ImageRenderJsInterop = new ImageRenderJsInterop(environment, imgConfig), ImageUrlRewriter = EpubImageRewrite }; @@ -69,7 +77,7 @@ public override async Task ExecuteAsync(IBookEnvironment environment await RenderIndex(environment, logger, markdown, renderer, template); - foreach (var chapter in environment.TableOfContents.Chapters) + foreach (TocChapter chapter in environment.TableOfContents.Chapters) { logger.LogInformation("Rendering chapter {chapter}...", chapter.Title); fileId = 1; @@ -116,7 +124,7 @@ public override async Task ExecuteAsync(IBookEnvironment environment private async Task RenderIndex(IBookEnvironment environment, ILogger logger, MarkdownConverter markdown, TemplateEngine renderer, string template) { - var index = await environment.Source.GetSourceFile(environment.TableOfContents.IndexFile, logger); + SourceFile index = await environment.Source.GetSourceFile(environment.TableOfContents.IndexFile, logger); var indexView = new ViewData { diff --git a/Source/Bookgen.Lib/Pipeline/Epub/CreateImageFiles.cs b/Source/Bookgen.Lib/Pipeline/Epub/CreateImageFiles.cs index 1f69d73e..688321b4 100644 --- a/Source/Bookgen.Lib/Pipeline/Epub/CreateImageFiles.cs +++ b/Source/Bookgen.Lib/Pipeline/Epub/CreateImageFiles.cs @@ -21,7 +21,7 @@ public override async Task ExecuteAsync(IBookEnvironment environment { logger.LogInformation("Writing {count} images to epub...", State.ImagesData.Count); - foreach (var image in State.ImagesData) + foreach (KeyValuePair image in State.ImagesData) { logger.LogDebug("Writing {image}...", image.Key); await State.EpubFile.AddAsync($"EPUB/content/{image.Key}", Convert.FromBase64String(image.Value)); diff --git a/Source/Bookgen.Lib/Pipeline/Epub/CreateNav.cs b/Source/Bookgen.Lib/Pipeline/Epub/CreateNav.cs index db19b19d..dfe8814b 100644 --- a/Source/Bookgen.Lib/Pipeline/Epub/CreateNav.cs +++ b/Source/Bookgen.Lib/Pipeline/Epub/CreateNav.cs @@ -21,17 +21,17 @@ public CreateNav(EpubState state) : base(state) public override async Task ExecuteAsync(IBookEnvironment environment, ILogger logger) { - var ncx = CreateNcx(environment); + Ncx ncx = CreateNcx(environment); var tocHtml = new EpubTocRenderer(); tocHtml.BeginNav(); tocHtml.AddTitle(environment.Configuration.BookTitle); tocHtml.BeginOl(display: false); - foreach (var chapter in State.TocData) + foreach (KeyValuePair> chapter in State.TocData) { tocHtml.BeginChapter(chapter.Key); tocHtml.BeginOl(); - foreach (var item in chapter.Value) + foreach (EpubState.ChapterItem item in chapter.Value) { tocHtml.AddItem(item.Title, item.FileName.Replace("content/", "")); string id = IdGenerator.Generate32BitDeterministicId(item.FileName); diff --git a/Source/Bookgen.Lib/Pipeline/Feed/CreateItems.cs b/Source/Bookgen.Lib/Pipeline/Feed/CreateItems.cs index 9d30d96a..5471cc5c 100644 --- a/Source/Bookgen.Lib/Pipeline/Feed/CreateItems.cs +++ b/Source/Bookgen.Lib/Pipeline/Feed/CreateItems.cs @@ -1,40 +1,46 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- using System.ServiceModel.Syndication; using Bookgen.Lib.Domain; +using Bookgen.Lib.Domain.IO; using Bookgen.Lib.ImageService; using Bookgen.Lib.Internals; using Bookgen.Lib.JsInterop; using Bookgen.Lib.Markdown; using Bookgen.Lib.Templates; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Bookgen.Lib.Pipeline.Feed; internal sealed class CreateItems : PipeLineStep { - public CreateItems(SyndicationFeedState state) : base(state) + private readonly IMemoryCache _memoryCache; + + public CreateItems(SyndicationFeedState state, IMemoryCache memoryCache) : base(state) { + _memoryCache = memoryCache; } public override async Task ExecuteAsync(IBookEnvironment environment, ILogger logger) { var imgService = new ImgService(environment.Source, logger, environment.Configuration.FeedConfig.Images); - var cached = new CachedImageService(imgService); + var cached = new CachedImageService(imgService, _memoryCache); - using var settings = new RenderSettings(cached) + using var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.FeedConfig.CssClasses, DeleteFirstH1 = false, HostUrl = string.Empty, - PrismJsInterop = environment.Configuration.FeedConfig.PreRenderCode ? new PrismJsInterop(environment) : null, + PrismJsInterop = environment.Configuration.FeedConfig.PreRenderCode ? new SyntaxRenderJsInterop(environment) : null, OffsetHeadingsBy = 0, - AutoEmbedSupportedLinks = false + AutoEmbedSupportedLinks = false, + ImageRenderJsInterop = new ImageRenderJsInterop(environment, environment.Configuration.FeedConfig.Images) }; using var markdown = new MarkdownConverter(settings); @@ -43,7 +49,7 @@ public override async Task ExecuteAsync(IBookEnvironment environment List items = new(); - foreach (var chapter in environment.TableOfContents.Chapters) + foreach (TocChapter chapter in environment.TableOfContents.Chapters) { logger.LogInformation("Rendering chapter {chapter}...", chapter.Title); diff --git a/Source/Bookgen.Lib/Pipeline/Feed/WriteFeeds.cs b/Source/Bookgen.Lib/Pipeline/Feed/WriteFeeds.cs index 5bccd77b..72f4b520 100644 --- a/Source/Bookgen.Lib/Pipeline/Feed/WriteFeeds.cs +++ b/Source/Bookgen.Lib/Pipeline/Feed/WriteFeeds.cs @@ -4,6 +4,7 @@ //----------------------------------------------------------------------------- using System.ServiceModel.Syndication; +using System.Xml; using BookGen.Vfs; @@ -20,14 +21,14 @@ public WriteFeeds(SyndicationFeedState state) : base(state) public override Task ExecuteAsync(IBookEnvironment environment, ILogger logger) { logger.LogInformation("Writing RSS feed..."); - using (var rss = environment.Output.CreateXmlWriter("rss.xml")) + using (XmlWriter rss = environment.Output.CreateXmlWriter("rss.xml")) { Rss20FeedFormatter rssFormatter = new Rss20FeedFormatter(State.Feed); rssFormatter.WriteTo(rss); } logger.LogInformation("Writing Atom feed..."); - using (var atom = environment.Output.CreateXmlWriter("atom.xml")) + using (XmlWriter atom = environment.Output.CreateXmlWriter("atom.xml")) { Atom10FeedFormatter atomFormatter = new Atom10FeedFormatter(State.Feed); atomFormatter.WriteTo(atom); diff --git a/Source/Bookgen.Lib/Pipeline/Pipeline.cs b/Source/Bookgen.Lib/Pipeline/Pipeline.cs index af578c0f..fcfad91b 100644 --- a/Source/Bookgen.Lib/Pipeline/Pipeline.cs +++ b/Source/Bookgen.Lib/Pipeline/Pipeline.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -10,6 +10,7 @@ using Bookgen.Lib.Pipeline.StaticWebsite; using Bookgen.Lib.Pipeline.Wordpress; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Bookgen.Lib.Pipeline; @@ -25,7 +26,7 @@ public Pipeline(params IPipeLineStep[] steps) public async Task ExecuteAsync(IBookEnvironment environment, ILogger logger, CancellationToken cancellationToken) { - foreach (var step in Steps) + foreach (IPipeLineStep step in Steps) { if (cancellationToken.IsCancellationRequested) { @@ -33,7 +34,7 @@ public async Task ExecuteAsync(IBookEnvironment environment, ILogger logge return false; } - var result = await step.ExecuteAsync(environment, logger); + StepResult result = await step.ExecuteAsync(environment, logger); if (result == StepResult.Failure) { return false; @@ -42,28 +43,28 @@ public async Task ExecuteAsync(IBookEnvironment environment, ILogger logge return true; } - public static Pipeline CratePrintPipeLine() + public static Pipeline CratePrintPipeLine(IMemoryCache memoryCache) { var state = new PrintState(); return new Pipeline( - new RenderPages(state), + new RenderPages(state, memoryCache), new WriteHtml(state), new WriteXHtml(state) ); } - public static Pipeline CreatePostProcessPipeLine() + public static Pipeline CreatePostProcessPipeLine(IMemoryCache memoryCache) { var state = new PostProcessState(); return new Pipeline( - new RenderPagesForPostProcess(state), + new RenderPagesForPostProcess(state, memoryCache), new WriteFile(state) ); } - public static Pipeline CreateWebPipeLine() + public static Pipeline CreateWebPipeLine(IMemoryCache memoryCache) { var state = new StaticWebState(); @@ -72,26 +73,26 @@ public static Pipeline CreateWebPipeLine() new ExtractTemplateAssets(state), new ReadInFiles(state), new RenderTableOfContents(state), - new RenderStaticPages(state), - new RenderIndexPage(state), + new RenderStaticPages(state, memoryCache), + new RenderIndexPage(state, memoryCache), new RenderStabdaloneToc(state), new CreateEmptyIndexPagesForFolders(state), new GeneratePager(state) ); } - public static Pipeline CreateWordpressPipeLine() + public static Pipeline CreateWordpressPipeLine(IMemoryCache memoryCache) { var state = new WpState(); return new Pipeline( new CreateWpChannel(state), - new CreateWpPages(state), + new CreateWpPages(state, memoryCache), new WriteExportFile(state) ); } - public static Pipeline CreateEpubPileLine() + public static Pipeline CreateEpubPileLine(IMemoryCache memoryCache) { var state = new EpubState(); @@ -99,7 +100,7 @@ public static Pipeline CreateEpubPileLine() new Initialize(state), new CreateMimeType(state), new CreateContainer(state), - new CreateHtmlPages(state), + new CreateHtmlPages(state, memoryCache), new CreateImageFiles(state), new CreateFontFiles(state), new CreateEpubCoverAndStyle(state), @@ -110,12 +111,12 @@ public static Pipeline CreateEpubPileLine() } - public static Pipeline CreateFeedPipeline() + public static Pipeline CreateFeedPipeline(IMemoryCache memoryCache) { var state = new SyndicationFeedState(); return new Pipeline( new CreateFeed(state), - new CreateItems(state), + new CreateItems(state, memoryCache), new WriteFeeds(state) ); } diff --git a/Source/Bookgen.Lib/Pipeline/PostProcess/RenderPagesForPostProcess.cs b/Source/Bookgen.Lib/Pipeline/PostProcess/RenderPagesForPostProcess.cs index 78bea26a..c1324df3 100644 --- a/Source/Bookgen.Lib/Pipeline/PostProcess/RenderPagesForPostProcess.cs +++ b/Source/Bookgen.Lib/Pipeline/PostProcess/RenderPagesForPostProcess.cs @@ -1,9 +1,10 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- using Bookgen.Lib.Domain; +using Bookgen.Lib.Domain.IO; using Bookgen.Lib.Domain.IO.Configuration; using Bookgen.Lib.Domain.PostProcess; using Bookgen.Lib.ImageService; @@ -11,31 +12,41 @@ using Bookgen.Lib.JsInterop; using Bookgen.Lib.Markdown; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Bookgen.Lib.Pipeline.PostProcess; internal sealed class RenderPagesForPostProcess : PipeLineStep { - public RenderPagesForPostProcess(PostProcessState state) : base(state) { } + private readonly IMemoryCache _memoryCache; + + public RenderPagesForPostProcess(PostProcessState state, IMemoryCache memoryCache) : base(state) + { + _memoryCache = memoryCache; + } public override async Task ExecuteAsync(IBookEnvironment environment, ILogger logger) { - var imgService = new ImgService(environment.Source, logger, new ImageConfig() + var imgConfig = new ImageConfig() { ResizeAndRecodeImages = ImgRecodeOption.Passtrough, SvgRecode = SvgRecodeOption.Passtrough, - }); - var cached = new CachedImageService(imgService); + }; + + var imgService = new ImgService(environment.Source, logger, imgConfig); + + var cached = new CachedImageService(imgService, _memoryCache); - using var settings = new RenderSettings(cached) + using var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.PrintConfig.CssClasses, DeleteFirstH1 = false, HostUrl = string.Empty, - PrismJsInterop = new PrismJsInterop(environment), + PrismJsInterop = new SyntaxRenderJsInterop(environment), OffsetHeadingsBy = 1, - AutoEmbedSupportedLinks = false + AutoEmbedSupportedLinks = false, + ImageRenderJsInterop = new ImageRenderJsInterop(environment, imgConfig) }; using var markdown = new MarkdownConverter(settings); @@ -46,7 +57,7 @@ public override async Task ExecuteAsync(IBookEnvironment environment Chapters = new List() }; - foreach (var chapter in environment.TableOfContents.Chapters) + foreach (TocChapter chapter in environment.TableOfContents.Chapters) { ExportChapter exportChapter = new ExportChapter { diff --git a/Source/Bookgen.Lib/Pipeline/Print/RenderPages.cs b/Source/Bookgen.Lib/Pipeline/Print/RenderPages.cs index 91a6277f..8bf2d6b5 100644 --- a/Source/Bookgen.Lib/Pipeline/Print/RenderPages.cs +++ b/Source/Bookgen.Lib/Pipeline/Print/RenderPages.cs @@ -1,43 +1,49 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- using Bookgen.Lib.Domain; +using Bookgen.Lib.Domain.IO; using Bookgen.Lib.ImageService; using Bookgen.Lib.Internals; using Bookgen.Lib.JsInterop; using Bookgen.Lib.Markdown; using Bookgen.Lib.Templates; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Bookgen.Lib.Pipeline.Print; internal sealed class RenderPages : PipeLineStep { - public RenderPages(PrintState state) : base(state) + private readonly IMemoryCache _memoryCache; + + public RenderPages(PrintState state, IMemoryCache memoryCache) : base(state) { + _memoryCache = memoryCache; } public override async Task ExecuteAsync(IBookEnvironment environment, ILogger logger) { var imgService = new ImgService(environment.Source, logger, environment.Configuration.PrintConfig.Images); - var cached = new CachedImageService(imgService); + var cached = new CachedImageService(imgService, _memoryCache); - using var settings = new RenderSettings(cached) + using var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.PrintConfig.CssClasses, DeleteFirstH1 = false, HostUrl = string.Empty, - PrismJsInterop = new PrismJsInterop(environment), + PrismJsInterop = new SyntaxRenderJsInterop(environment), OffsetHeadingsBy = 1, - AutoEmbedSupportedLinks = false + AutoEmbedSupportedLinks = false, + ImageRenderJsInterop = new ImageRenderJsInterop(environment, environment.Configuration.PrintConfig.Images) }; using var markdown = new MarkdownConverter(settings); - foreach (var chapter in environment.TableOfContents.Chapters) + foreach (TocChapter chapter in environment.TableOfContents.Chapters) { logger.LogInformation("Rendering chapter {chapter}...", chapter.Title); State.Buffer.AppendH1(chapter.Title); diff --git a/Source/Bookgen.Lib/Pipeline/Print/WriteHtml.cs b/Source/Bookgen.Lib/Pipeline/Print/WriteHtml.cs index 8d22c624..f15c2747 100644 --- a/Source/Bookgen.Lib/Pipeline/Print/WriteHtml.cs +++ b/Source/Bookgen.Lib/Pipeline/Print/WriteHtml.cs @@ -34,7 +34,7 @@ public override async Task ExecuteAsync(IBookEnvironment environment AdditionalData = new(), }; - using var writer = environment.Output.CreateTextWriter("print.html"); + using TextWriter writer = environment.Output.CreateTextWriter("print.html"); renderer.Render(writer, tempate, viewData); return StepResult.Success; diff --git a/Source/Bookgen.Lib/Pipeline/Print/WriteXHtml.cs b/Source/Bookgen.Lib/Pipeline/Print/WriteXHtml.cs index 93b38f91..d3ba5c37 100644 --- a/Source/Bookgen.Lib/Pipeline/Print/WriteXHtml.cs +++ b/Source/Bookgen.Lib/Pipeline/Print/WriteXHtml.cs @@ -68,7 +68,7 @@ public override async Task ExecuteAsync(IBookEnvironment environment AdditionalData = new(), }; - using var writer = environment.Output.CreateTextWriter("print.xhtml.html"); + using TextWriter writer = environment.Output.CreateTextWriter("print.xhtml.html"); renderer.Render(writer, tempate, viewData); return StepResult.Success; diff --git a/Source/Bookgen.Lib/Pipeline/StaticWebsite/CreateEmptyIndexPagesForFolders.cs b/Source/Bookgen.Lib/Pipeline/StaticWebsite/CreateEmptyIndexPagesForFolders.cs index bbf8c329..6dd62480 100644 --- a/Source/Bookgen.Lib/Pipeline/StaticWebsite/CreateEmptyIndexPagesForFolders.cs +++ b/Source/Bookgen.Lib/Pipeline/StaticWebsite/CreateEmptyIndexPagesForFolders.cs @@ -15,7 +15,7 @@ public CreateEmptyIndexPagesForFolders(StaticWebState staticWebState) : base(sta public override async Task ExecuteAsync(IBookEnvironment environment, ILogger logger) { - var folders = environment.Output.GetDirectories(environment.Output.Scope, true); + IEnumerable folders = environment.Output.GetDirectories(environment.Output.Scope, true); var protect = environment.GetAsset(BundledAssets.ProtectHtml); diff --git a/Source/Bookgen.Lib/Pipeline/StaticWebsite/RenderIndexPage.cs b/Source/Bookgen.Lib/Pipeline/StaticWebsite/RenderIndexPage.cs index 9ec2f662..c95c12cd 100644 --- a/Source/Bookgen.Lib/Pipeline/StaticWebsite/RenderIndexPage.cs +++ b/Source/Bookgen.Lib/Pipeline/StaticWebsite/RenderIndexPage.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -7,34 +7,40 @@ using Bookgen.Lib.Domain; using Bookgen.Lib.ImageService; +using Bookgen.Lib.JsInterop; using Bookgen.Lib.Markdown; using Bookgen.Lib.Templates; using BookGen.Vfs; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Bookgen.Lib.Pipeline.StaticWebsite; internal sealed class RenderIndexPage : PipeLineStep { - public RenderIndexPage(StaticWebState state) : base(state) + private readonly IMemoryCache _memoryCache; + + public RenderIndexPage(StaticWebState state, IMemoryCache memoryCache) : base(state) { + _memoryCache = memoryCache; } public override async Task ExecuteAsync(IBookEnvironment environment, ILogger logger) { var imgService = new ImgService(environment.Source, logger, environment.Configuration.StaticWebsiteConfig.Images); - var cached = new CachedImageService(imgService); + var cached = new CachedImageService(imgService, _memoryCache); var renderer = new TemplateEngine(logger, environment); - using var settings = new RenderSettings(cached) + using var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.StaticWebsiteConfig.CssClasses, DeleteFirstH1 = false, HostUrl = environment.Configuration.StaticWebsiteConfig.DeployHost, PrismJsInterop = null, AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(environment, environment.Configuration.StaticWebsiteConfig.Images) }; using var markdown = new MarkdownConverter(settings); diff --git a/Source/Bookgen.Lib/Pipeline/StaticWebsite/RenderStaticPages.cs b/Source/Bookgen.Lib/Pipeline/StaticWebsite/RenderStaticPages.cs index 899de5df..04f34271 100644 --- a/Source/Bookgen.Lib/Pipeline/StaticWebsite/RenderStaticPages.cs +++ b/Source/Bookgen.Lib/Pipeline/StaticWebsite/RenderStaticPages.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -7,34 +7,40 @@ using Bookgen.Lib.Domain; using Bookgen.Lib.ImageService; +using Bookgen.Lib.JsInterop; using Bookgen.Lib.Markdown; using Bookgen.Lib.Templates; using BookGen.Vfs; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Bookgen.Lib.Pipeline.StaticWebsite; internal sealed class RenderStaticPages : PipeLineStep { - public RenderStaticPages(StaticWebState state) : base(state) + private readonly IMemoryCache _memoryCache; + + public RenderStaticPages(StaticWebState state, IMemoryCache memoryCache) : base(state) { + _memoryCache = memoryCache; } public override async Task ExecuteAsync(IBookEnvironment environment, ILogger logger) { var imgService = new ImgService(environment.Source, logger, environment.Configuration.StaticWebsiteConfig.Images); - var cached = new CachedImageService(imgService); + var cached = new CachedImageService(imgService, _memoryCache); var renderer = new TemplateEngine(logger, environment); - using var settings = new RenderSettings(cached) + using var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.StaticWebsiteConfig.CssClasses, DeleteFirstH1 = false, HostUrl = environment.Configuration.StaticWebsiteConfig.DeployHost, PrismJsInterop = null, AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(environment, environment.Configuration.StaticWebsiteConfig.Images) }; ParallelOptions options = new ParallelOptions diff --git a/Source/Bookgen.Lib/Pipeline/StaticWebsite/RenderTableOfContents.cs b/Source/Bookgen.Lib/Pipeline/StaticWebsite/RenderTableOfContents.cs index f220c99d..213de692 100644 --- a/Source/Bookgen.Lib/Pipeline/StaticWebsite/RenderTableOfContents.cs +++ b/Source/Bookgen.Lib/Pipeline/StaticWebsite/RenderTableOfContents.cs @@ -26,7 +26,7 @@ public override Task ExecuteAsync(IBookEnvironment environment, ILog { TocRenderer toc = new(environment.Configuration.StaticWebsiteConfig.TocConfiguration); toc.BeginContainer(); - foreach (var chapter in environment.TableOfContents.Chapters) + foreach (TocChapter chapter in environment.TableOfContents.Chapters) { toc.BeginChapter(chapter.Title); toc.BeginOuterItemContainer(); diff --git a/Source/Bookgen.Lib/Pipeline/Wordpress/CreateWpPages.cs b/Source/Bookgen.Lib/Pipeline/Wordpress/CreateWpPages.cs index cf922e99..743bd012 100644 --- a/Source/Bookgen.Lib/Pipeline/Wordpress/CreateWpPages.cs +++ b/Source/Bookgen.Lib/Pipeline/Wordpress/CreateWpPages.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- @@ -7,24 +7,31 @@ using System.Globalization; using System.Text; +using Bookgen.Lib.Domain; +using Bookgen.Lib.Domain.IO; using Bookgen.Lib.Domain.Wordpress; using Bookgen.Lib.ImageService; using Bookgen.Lib.Internals; +using Bookgen.Lib.JsInterop; using Bookgen.Lib.Markdown; using Bookgen.Lib.Templates; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Bookgen.Lib.Pipeline.Wordpress; internal sealed class CreateWpPages : PipeLineStep { + private readonly IMemoryCache _memoryCache; + #if DEBUG private readonly HashSet _usedids; #endif - public CreateWpPages(WpState state) : base(state) + public CreateWpPages(WpState state, IMemoryCache memoryCache) : base(state) { + _memoryCache = memoryCache; #if DEBUG _usedids = []; #endif @@ -114,17 +121,18 @@ public override async Task ExecuteAsync(IBookEnvironment environment { logger.LogInformation("Creating pages..."); - var imgService = new ImgService(environment.Source, logger, environment.Configuration.StaticWebsiteConfig.Images); - var cached = new CachedImageService(imgService); + var imgService = new ImgService(environment.Source, logger, environment.Configuration.WordpressConfig.Images); + var cached = new CachedImageService(imgService, _memoryCache); var renderer = new TemplateEngine(logger, environment); - using var settings = new RenderSettings(cached) + using var settings = new MarkdownRenderSettings(cached) { CssClasses = environment.Configuration.WordpressConfig.CssClasses, DeleteFirstH1 = false, HostUrl = environment.Configuration.WordpressConfig.DeployHost, PrismJsInterop = null, AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(environment, environment.Configuration.WordpressConfig.Images) }; using var markdown = new MarkdownConverter(settings); @@ -148,7 +156,7 @@ public override async Task ExecuteAsync(IBookEnvironment environment globalparent = uid; ++uid; - foreach (var chapter in environment.TableOfContents.Chapters) + foreach (TocChapter chapter in environment.TableOfContents.Chapters) { string chapterPath = $"{environment.Configuration.StaticWebsiteConfig.DeployHost}{EncodeTitle(chapter.Title)}"; int parent_uid = uid; @@ -169,7 +177,7 @@ public override async Task ExecuteAsync(IBookEnvironment environment { logger.LogDebug("Processing file {File}...", file); - var sourceData = await environment.Source.GetSourceFile(file, logger); + SourceFile sourceData = await environment.Source.GetSourceFile(file, logger); string subpath = $"{environment.Configuration.WordpressConfig.DeployHost}{EncodeTitle(chapter.Title)}/{EncodeTitle(sourceData.FrontMatter.Title)}"; string template = await environment.GetTemplate(frontMatterTemplate: sourceData.FrontMatter.Template, diff --git a/Source/Bookgen.Lib/Pipeline/Wordpress/WriteExportFile.cs b/Source/Bookgen.Lib/Pipeline/Wordpress/WriteExportFile.cs index aeabc1f0..6671be78 100644 --- a/Source/Bookgen.Lib/Pipeline/Wordpress/WriteExportFile.cs +++ b/Source/Bookgen.Lib/Pipeline/Wordpress/WriteExportFile.cs @@ -35,7 +35,7 @@ public override Task ExecuteAsync(IBookEnvironment environment, ILog xnames.Add("wp", "http://wordpress.org/export/1.2/"); var xs = new XmlSerializer(typeof(Rss)); - using var fileStream = environment.Output.CreateWriteStream("wodpress-export.xml"); + using Stream fileStream = environment.Output.CreateWriteStream("wodpress-export.xml"); xs.Serialize(fileStream, output, xnames); diff --git a/Source/Bookgen.Lib/Templates/Functions.cs b/Source/Bookgen.Lib/Templates/Functions.cs index 8f4e35e9..7634b7c0 100644 --- a/Source/Bookgen.Lib/Templates/Functions.cs +++ b/Source/Bookgen.Lib/Templates/Functions.cs @@ -21,7 +21,7 @@ public static TValue GetValueOrDefault(this IReadOnlyList argume return defaultValue; var value = arguments[index]; - if (TValue.TryParse(value, CultureInfo.InvariantCulture, out var parsedValue)) + if (TValue.TryParse(value, CultureInfo.InvariantCulture, out TValue? parsedValue)) return parsedValue; return defaultValue; diff --git a/Source/Bookgen.Lib/Templates/TemplateEngine.cs b/Source/Bookgen.Lib/Templates/TemplateEngine.cs index a69d1b32..261777ea 100644 --- a/Source/Bookgen.Lib/Templates/TemplateEngine.cs +++ b/Source/Bookgen.Lib/Templates/TemplateEngine.cs @@ -51,7 +51,7 @@ public string Render(string template, TData viewData) where TData : ViewD public void Render(TextWriter target, string template, TData viewData) where TData : ViewData { - var dataTable = viewData.GetDataTable(_comparer); + Dictionary dataTable = viewData.GetDataTable(_comparer); StringBuilder lineBuffer = new(120); @@ -60,7 +60,7 @@ public void Render(TextWriter target, string template, TData viewData) wh while ((line = reader.ReadLine()) != null) { - var templatePartsInLine = TemplatePartRegex().Matches(line); + MatchCollection templatePartsInLine = TemplatePartRegex().Matches(line); if (templatePartsInLine.Count < 1) { @@ -80,7 +80,7 @@ public void Render(TextWriter target, string template, TData viewData) wh string functionName = templateFunction.Skip(1).First(); string[] arguments = templateFunction.Skip(2).TakeWhile(f => f != "}}").ToArray(); - if (!_lambdaTable.TryGetValue(functionName, out var function)) + if (!_lambdaTable.TryGetValue(functionName, out Func? function)) { _logger.LogWarning("Function {FunctionName} is not registered.", functionName); lineBuffer.Append($"Function {functionName} is not registered."); diff --git a/Source/Bookgen.Lib/Templates/ViewData.cs b/Source/Bookgen.Lib/Templates/ViewData.cs index 597d2c7a..4556cc9e 100644 --- a/Source/Bookgen.Lib/Templates/ViewData.cs +++ b/Source/Bookgen.Lib/Templates/ViewData.cs @@ -34,16 +34,16 @@ public class ViewData public Dictionary GetDataTable(StringComparer comparer) { Dictionary result = new(comparer); - var properties = GetType() + IEnumerable properties = GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.Name != nameof(AdditionalData)); - foreach (var property in properties) + foreach (PropertyInfo? property in properties) { result.Add(property.Name, property.GetValue(this)?.ToString() ?? ""); } - foreach (var kvp in AdditionalData) + foreach (KeyValuePair kvp in AdditionalData) { result.Add(kvp.Key, kvp.Value); } diff --git a/Source/Bookgen.Lib/packages.lock.json b/Source/Bookgen.Lib/packages.lock.json index f155489c..ae9bb8bd 100644 --- a/Source/Bookgen.Lib/packages.lock.json +++ b/Source/Bookgen.Lib/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Markdig": { "type": "Direct", - "requested": "[0.44.0, )", - "resolved": "0.44.0", - "contentHash": "X+CYMjcUnh/yO24wOSQxVFLiGqWrrtXJ5M7toHiM1Zk4Fg9UMLN5fkaq6FSOWH+mIprsHHgDMlq3MJhmrXalhg==" + "requested": "[1.1.0, )", + "resolved": "1.1.0", + "contentHash": "ivaowI69dGxiyaKLy6+qo9Xm4DHwXDKB3orGIFOvA8k+eUBAV5UaW8GPIMc3h5jl7gZalyecR3x2t+nuLYdmFg==" }, "Microsoft.ClearScript": { "type": "Direct", @@ -35,42 +35,42 @@ }, "SkiaSharp": { "type": "Direct", - "requested": "[3.119.1, )", - "resolved": "3.119.1", - "contentHash": "+Ru1BTSZQne3Vp+vbSb50Ke3Nlc3ZnItxx4+751J9WZ8YzLKAV/n+9DAo4zFTyeCI//ueT63c+VybmTTpYBEiw==", + "requested": "[3.119.2, )", + "resolved": "3.119.2", + "contentHash": "nmy2dOFWPvQKMglfpjz8+/xQQcSrL9jzul3cUyzCJVSwrmSAw+6B1sEgU7jt6NZBptwGq2k/V0kjyu2GizMFtg==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "3.119.1", - "SkiaSharp.NativeAssets.macOS": "3.119.1" + "SkiaSharp.NativeAssets.Win32": "3.119.2", + "SkiaSharp.NativeAssets.macOS": "3.119.2" } }, "SkiaSharp.NativeAssets.Linux": { "type": "Direct", - "requested": "[3.119.1, )", - "resolved": "3.119.1", - "contentHash": "9YNoc4SeKvQhrwiqwT4ezkNfMywPdPSK+UFvo/CaoXqLixcnYOTsQKm5BF9mc4+q3vKgDtEgMt0d1ygZhJTEHg==" + "requested": "[3.119.2, )", + "resolved": "3.119.2", + "contentHash": "9WzxSyG/s9Id506j0Ht+Bi5ucOpWKPzd1XXr9TD4fuCafHHy2swRSlbZtC3IDQAsvCH63OerkDJajj43uSv5og==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Direct", - "requested": "[3.119.1, )", - "resolved": "3.119.1", - "contentHash": "8C4GSXVJqSr0y3Tyyv5jz6MJSTVUyYkMjeKrzK+VyZPGLo89MNoUEclVuYahzOCDdtbfXrd2HtxXfDuvoSXrUw==" + "requested": "[3.119.2, )", + "resolved": "3.119.2", + "contentHash": "uYe+da6+GXVgPKkCopzvIZ83DmC8SXXKeUAPrNcztJNsg0SjPQAxfKMOPZqmVjbzznrq/QUIjLUlJSZV/e0IPA==" }, "Svg.Skia": { "type": "Direct", - "requested": "[3.2.1, )", - "resolved": "3.2.1", - "contentHash": "pRJtMOc2hTcnxSu7cx6lf806S+2VrQ0eqUpGMBUQQ/pIIvzrtn0+lUjdABTvjr5rsdQF9OxE+r9npq2+3c6T4A==", + "requested": "[3.4.1, )", + "resolved": "3.4.1", + "contentHash": "fNGHIeIUtEDo41P4MYfVqjNL902t8EP5/tBY9vYDg8VbKSOD84ZlBfCL4KSxjf//HcJi1Uz4uZCUXuEAKJirCw==", "dependencies": { "SkiaSharp": "2.88.9", - "Svg.Custom": "3.2.1", - "Svg.Model": "3.2.1" + "Svg.Custom": "3.4.1", + "Svg.Model": "3.4.1" } }, "System.ServiceModel.Syndication": { "type": "Direct", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "Z3+s66hOp7JVDOhkVk0gH/sjsshn99NUMiILcXLaA+3qe2OkLr3nB+3bRzXdfF6arlJBxGfkkki9GP6R9kvdFg==" + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "mloJBxfbjYXgfcfMvH40UWwzekATlUzHLMKPQpg4qab+gpHRgQkhgo4DLz3v7Kacn6000sqoNkxiyk0pJ5x/ig==" }, "YamlDotNet": { "type": "Direct", @@ -131,40 +131,47 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "3.2.1", - "contentHash": "WyMf+0aj5IJcHkG/t89WApGFg756oXB30ehu9kYu9hjdfS0snkXHoAiTJZqVlqyLOvzC6eA+nOQ2hI84g//5Pg==" + "resolved": "3.4.1", + "contentHash": "ab3J5OGdwYLyXnom90OghS9NyRPH9dtsRtW5B3mL+F43YTlxido3nUtiR9NjB/sI3jvG/J744C29bsSBQ28AfQ==" }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "3.119.1", - "contentHash": "6hR3BdLhApjDxR1bFrJ7/lMydPfI01s3K+3WjIXFUlfC0MFCFCwRzv+JtzIkW9bDXs7XUVQS+6EVf0uzCasnGQ==" + "resolved": "3.119.2", + "contentHash": "I2jMGQ/26KOnc6iAoR+Mxh9vSJJ2vioJyj9aJ9OL5yEZyXothXJxf4vBMqnSaiXMqiiU1scG7KqtT0CLkmMmWA==" }, "Svg.Custom": { "type": "Transitive", - "resolved": "3.2.1", - "contentHash": "Wu0+Il+SZfY5+AT25b2PMlSbBrzGIo/6vMMMdOK8jd25I9tdJJbxOFJNG87gFgo9UY9OmpIFzVobGOHzFdJYeQ==", + "resolved": "3.4.1", + "contentHash": "ksHa7Zv8X1InxSWAeM0sSSvZM2W6FWgneXjU0bdYrQ20Q6q0nl3g0uaSWe53i/CC1OmR+/JAxsp8E3/wigtcng==", "dependencies": { "ExCSS": "4.3.1" } }, "Svg.Model": { "type": "Transitive", - "resolved": "3.2.1", - "contentHash": "N4yWlvqv1XWZ+VvI29lT4HkEgtiP3rS/poMfWiVzz6Ao/3IjQCfmo2qrKphq77IqTdMYyh7wGDknMId7YrQY5Q==", + "resolved": "3.4.1", + "contentHash": "Zn04CWWIwV+qlJ4x4vg8FdCYNew5dRcsvkcCpzZggFKHGExIn4WTBtJMuLH8htuiLnbbgF6woIYpUll+seHfjw==", "dependencies": { - "ShimSkiaSharp": "3.2.1", - "Svg.Custom": "3.2.1" + "ShimSkiaSharp": "3.4.1", + "Svg.Custom": "3.4.1" } }, "bookgen.shell.shared": { "type": "Project", "dependencies": { + "Spectre.Console": "[0.54.0, )", "Webmaster442.WindowsTerminal": "[4.1.1, )" } }, "bookgen.vfs": { "type": "Project" }, + "Spectre.Console": { + "type": "CentralTransitive", + "requested": "[0.54.0, )", + "resolved": "0.54.0", + "contentHash": "StDXCFayfy0yB1xzUHT2tgEpV1/HFTiS4JgsAQS49EYTfMixSwwucaQs/bIOCwXjWwIQTMuxjUIxcB5XsJkFJA==" + }, "Webmaster442.WindowsTerminal": { "type": "CentralTransitive", "requested": "[4.1.1, )", diff --git a/Test/Bookgen.Tests/Commands/CommandTestBase.cs b/Test/Bookgen.Tests/Commands/CommandTestBase.cs index 42ab4a12..6ea47afe 100644 --- a/Test/Bookgen.Tests/Commands/CommandTestBase.cs +++ b/Test/Bookgen.Tests/Commands/CommandTestBase.cs @@ -19,6 +19,7 @@ internal abstract class CommandTestBase where TCommand : ICommand protected readonly Mock LoggerMock = new Mock(MockBehavior.Strict); protected readonly Mock AssetSourceMock = new Mock(MockBehavior.Strict); protected readonly Mock CommandRunnerProxyMock = new Mock(MockBehavior.Strict); + protected readonly Mock FilesystemFactoryMock = new Mock(MockBehavior.Strict); protected ICommand Command { get; private set; } diff --git a/Test/Bookgen.Tests/Commands/UT_Md2HtmlCommand.cs b/Test/Bookgen.Tests/Commands/UT_Md2HtmlCommand.cs index 0601778d..af4a1704 100644 --- a/Test/Bookgen.Tests/Commands/UT_Md2HtmlCommand.cs +++ b/Test/Bookgen.Tests/Commands/UT_Md2HtmlCommand.cs @@ -15,19 +15,22 @@ namespace Bookgen.Tests.Commands; [TestFixture] internal class UT_Md2HtmlCommand : CommandTestBase { - protected override Md2HtmlCommand CreateSut() - => new Md2HtmlCommand(LoggerMock.Object, FileSystemMock.Object, AssetSourceMock.Object); + private readonly Mock MultiReadScopeMock = new Mock(MockBehavior.Strict); protected override void SetupMocks() { + MultiReadScopeMock.Setup(fs => fs.ReadAllText("test.md")).Returns("test"); + MultiReadScopeMock.Setup(fs => fs.GetLastModifiedUtc("test.md")).Returns(new DateTime(2024, 1, 1)); AssetSourceMock.Setup(a => a.GetAsset(BundledAssets.TemplateSinglePage)).Returns("

    {{Title}}

    {{Content}}"); AssetSourceMock.Setup(a => a.GetAsset(BundledAssets.PrismJs)).Returns(""); - FileSystemMock.As().Setup(fs => fs.ReadAllText("test.md")).Returns("test"); - FileSystemMock.As().Setup(fs => fs.GetLastModifiedUtc("test.md")).Returns(new DateTime(2024, 1, 1)); FileSystemMock.Setup(fs => fs.WriteAllText("out.html", It.IsAny())); - + FilesystemFactoryMock.Setup(f => f.CreateMultiReadScopeFileSystem(It.IsAny>())).Returns(MultiReadScopeMock.Object); + FilesystemFactoryMock.Setup(f => f.CreateWritableFileSystem(It.IsAny())).Returns(FileSystemMock.Object); } + protected override Md2HtmlCommand CreateSut() + => new Md2HtmlCommand(LoggerMock.Object, FilesystemFactoryMock.Object, AssetSourceMock.Object); + [Test] public async Task EnsureThat_GenerateRawWorks() { @@ -48,7 +51,7 @@ public async Task EnsureThat_GenerateRawWorks() using (Assert.EnterMultipleScope()) { Assert.That(exitCode, Is.EqualTo(0)); - FileSystemMock.Verify(fs => fs.ReadAllText("test.md"), Times.AtLeastOnce()); + MultiReadScopeMock.Verify(fs => fs.ReadAllText("test.md"), Times.AtLeastOnce()); FileSystemMock.Verify(fs => fs.WriteAllText("out.html", expectedContent), Times.Once); } } @@ -72,7 +75,7 @@ public async Task EnsureThat_GenerateHtml_Works() { Assert.That(exitCode, Is.EqualTo(0)); AssetSourceMock.Verify(a => a.GetAsset(BundledAssets.TemplateSinglePage), Times.Once); - FileSystemMock.Verify(fs => fs.ReadAllText("test.md"), Times.Once); + MultiReadScopeMock.Verify(fs => fs.ReadAllText("test.md"), Times.Once); FileSystemMock.Verify(fs => fs.WriteAllText("out.html", It.IsAny()), Times.Once); } } diff --git a/Test/Bookgen.Tests/EmbeddedTestFolder.cs b/Test/Bookgen.Tests/EmbeddedTestFolder.cs index d342d021..fd34bc20 100644 --- a/Test/Bookgen.Tests/EmbeddedTestFolder.cs +++ b/Test/Bookgen.Tests/EmbeddedTestFolder.cs @@ -52,20 +52,20 @@ public Stream OpenReadStream(string path) public TextReader OpenTextReader(string path) { - using var stream = OpenReadStream(path); + using Stream stream = OpenReadStream(path); return new StreamReader(stream); } public string ReadAllText(string path) { - using var stream = OpenReadStream(path); + using Stream stream = OpenReadStream(path); using var reader = new StreamReader(stream); return reader.ReadToEnd(); } public async Task ReadAllTextAsync(string path) { - using var stream = OpenReadStream(path); + using Stream stream = OpenReadStream(path); using var reader = new StreamReader(stream); return await reader.ReadToEndAsync(); } diff --git a/Test/Bookgen.Tests/Lib/UT_ImgService.cs b/Test/Bookgen.Tests/Lib/UT_ImgService.cs index ca5e4509..bd170f36 100644 --- a/Test/Bookgen.Tests/Lib/UT_ImgService.cs +++ b/Test/Bookgen.Tests/Lib/UT_ImgService.cs @@ -34,7 +34,7 @@ public void EnsureThat_SvgPassThroughReturnsExpected() SvgRecode = SvgRecodeOption.Passtrough, }); - var result = service.GetImageEmbedData("test.svg"); + ImageResult result = service.GetImageEmbedData("test.svg"); string expected = """ @@ -129,7 +129,7 @@ public void EnsureThat_Svg_Recode_Webp_ReturnsExpected() SvgRecode = SvgRecodeOption.AsWebp, }); - var result = service.GetImageEmbedData("test.svg"); + ImageResult result = service.GetImageEmbedData("test.svg"); Assert.Multiple(() => @@ -151,7 +151,7 @@ public void EnsureThat_Svg_Recode_Resize_Png_ReturnsExpected() ResizeHeight = 200, }); - var result = service.GetImageEmbedData("test.svg"); + ImageResult result = service.GetImageEmbedData("test.svg"); Assert.Multiple(() => @@ -167,7 +167,7 @@ public void EnsureThat_Png_Passtrough() { var service = new ImgService(_testFolder, _mockLogger.Object, new ImageConfig()); - var result = service.GetImageEmbedData("test.png"); + ImageResult result = service.GetImageEmbedData("test.png"); Assert.Multiple(() => { @@ -188,7 +188,7 @@ public void EnsureThat_Png_Recode_Webp_Works() ImageQualityOnResize = 80, }); - var result = service.GetImageEmbedData("test.png"); + ImageResult result = service.GetImageEmbedData("test.png"); Assert.Multiple(() => { diff --git a/Test/Bookgen.Tests/Lib/UT_JsonMerger.cs b/Test/Bookgen.Tests/Lib/UT_JsonMerger.cs new file mode 100644 index 00000000..a9c45f29 --- /dev/null +++ b/Test/Bookgen.Tests/Lib/UT_JsonMerger.cs @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------------- +// (c) 2019-2026 Ruzsinszki Gábor +// This code is licensed under MIT license (see LICENSE for details) +//----------------------------------------------------------------------------- + +using System.Text.Json; +using System.Text.Json.Nodes; + +using Bookgen.Lib.Confighandling; +using Bookgen.Lib.Domain.IO.Configuration; + +namespace Bookgen.Tests.Lib; + +[TestFixture] +public class UT_JsonMerger +{ + [Test] + public void EnsureThat_Merge_Works() + { + var config1 = new Config + { + Book2LetterISO639Language = "en", + BookAuthor = "Author One", + PrintConfig = new PrintConfig + { + Images = new ImageConfig + { + ImageQualityOnResize = 70, + ResizeAndRecodeImages = ImgRecodeOption.AsPng, + SvgRecode = SvgRecodeOption.AsPng, + } + }, + WordpressConfig = new WordpressConfig + { + CssClasses = new CssClasses + { + H1 = "custom-h1", + H2 = "custom-h2", + } + }, + }; + + var overlay = """ + { + "BookAuthor": "Author Two", + "PrintConfig": { + "Images": { + "ImageQualityOnResize": 85 + } + }, + "WordpressConfig": { + "CssClasses": { + "H2": "overridden-h2", + "H3": "custom-h3" + } + } + } + """; + + var node1 = JsonSerializer.SerializeToNode(config1) as JsonObject; + var nodeOverlay = JsonNode.Parse(overlay) as JsonObject; + + JsonMerger sut = new JsonMerger(node1!); + sut.Merge(nodeOverlay!); + + Config? result = sut.Deserialize(); + + Assert.That(result, Is.Not.Null); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result!.Book2LetterISO639Language, Is.EqualTo("en")); + Assert.That(result.BookAuthor, Is.EqualTo("Author Two")); + Assert.That(result.PrintConfig.Images.ImageQualityOnResize, Is.EqualTo(85)); + Assert.That(result.PrintConfig.Images.ResizeAndRecodeImages, Is.EqualTo(ImgRecodeOption.AsPng)); + Assert.That(result.PrintConfig.Images.SvgRecode, Is.EqualTo(SvgRecodeOption.AsPng)); + Assert.That(result.WordpressConfig.CssClasses.H1, Is.EqualTo("custom-h1")); + Assert.That(result.WordpressConfig.CssClasses.H2, Is.EqualTo("overridden-h2")); + Assert.That(result.WordpressConfig.CssClasses.H3, Is.EqualTo("custom-h3")); + } + } +} diff --git a/Test/Bookgen.Tests/Lib/UT_LevelList.cs b/Test/Bookgen.Tests/Lib/UT_LevelList.cs index a1aa8b22..80efd6c3 100644 --- a/Test/Bookgen.Tests/Lib/UT_LevelList.cs +++ b/Test/Bookgen.Tests/Lib/UT_LevelList.cs @@ -27,7 +27,7 @@ public string ToFormatedString(string prefix = "", StringBuilder? sb = null) sb.AppendLine(); } prefix += " "; - foreach (var item in Children) + foreach (TestLevelList item in Children) { item.ToFormatedString(prefix, sb); } diff --git a/Test/Bookgen.Tests/Lib/UT_MarkdownConverter.cs b/Test/Bookgen.Tests/Lib/UT_MarkdownConverter.cs index ec7f7a3e..5519f807 100644 --- a/Test/Bookgen.Tests/Lib/UT_MarkdownConverter.cs +++ b/Test/Bookgen.Tests/Lib/UT_MarkdownConverter.cs @@ -1,10 +1,11 @@ //----------------------------------------------------------------------------- -// (c) 2019-2025 Ruzsinszki Gábor +// (c) 2019-2026 Ruzsinszki Gábor // This code is licensed under MIT license (see LICENSE for details) //----------------------------------------------------------------------------- using Bookgen.Lib.Domain.IO.Configuration; using Bookgen.Lib.ImageService; +using Bookgen.Lib.JsInterop; using Bookgen.Lib.Markdown; using Moq; @@ -17,10 +18,12 @@ internal class UT_MarkdownConverter private string _markdown; private string _soruceCode; private readonly IEqualityComparer comparer = new LineEndingIgnoreComparer(); + private TestEnvironment _testEnvironment; [SetUp] public void Setup() { + _testEnvironment = new TestEnvironment(); _imgServiceMock = new Mock(MockBehavior.Strict); _imgServiceMock.Setup(x => x.GetImageEmbedData("img.svg")).Returns(new ImageResult { @@ -67,10 +70,16 @@ This is a [link](https://youtube.be) """; } + [TearDown] + public void TearDown() + { + _testEnvironment.Dispose(); + } + [Test] public void EnsureThat_Css_ClassesAreAplied() { - using var settings = new RenderSettings(_imgServiceMock.Object) + using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses { @@ -82,6 +91,7 @@ public void EnsureThat_Css_ClassesAreAplied() HostUrl = null, PrismJsInterop = null, AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(_testEnvironment, new ImageConfig()) }; using var sut = new MarkdownConverter(settings); @@ -111,13 +121,14 @@ public void EnsureThat_Css_ClassesAreAplied() [Test] public void EnsureThat_DeleteFirstH1_HostUrlTargeting_Works() { - using var settings = new RenderSettings(_imgServiceMock.Object) + using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = true, HostUrl = "https://my.domain", PrismJsInterop = null, AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(_testEnvironment, new ImageConfig()) }; using var sut = new MarkdownConverter(settings); @@ -146,13 +157,14 @@ public void EnsureThat_DeleteFirstH1_HostUrlTargeting_Works() [Test] public void EnsureThat_SourceCode_Works() { - using var settings = new RenderSettings(_imgServiceMock.Object) + using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = "https://my.domain", PrismJsInterop = null, AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(_testEnvironment, new ImageConfig()) }; using var sut = new MarkdownConverter(settings); @@ -173,14 +185,14 @@ public void EnsureThat_SourceCode_Works() [Test] public void EnsureThat_SourceCode_PreRender_Works() { - using var testEnvironment = new TestEnvironment(); - using var settings = new RenderSettings(_imgServiceMock.Object) + using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = "https://my.domain", - PrismJsInterop = new Bookgen.Lib.JsInterop.PrismJsInterop(testEnvironment), + PrismJsInterop = new Bookgen.Lib.JsInterop.SyntaxRenderJsInterop(_testEnvironment), AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(_testEnvironment, new ImageConfig()) }; using var sut = new MarkdownConverter(settings); @@ -200,13 +212,14 @@ public void EnsureThat_SourceCode_PreRender_Works() [Test] public void EnsureThat_Toc_NormalCase_RendersCorrectly() { - using var settings = new RenderSettings(_imgServiceMock.Object) + using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = null, PrismJsInterop = null, AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(_testEnvironment, new ImageConfig()) }; using var sut = new MarkdownConverter(settings); @@ -316,13 +329,14 @@ public void EnsureThat_Toc_NormalCase_RendersCorrectly() [Test] public void EnsureThat_Toc_WithTitle_RendersCorrectly() { - using var settings = new RenderSettings(_imgServiceMock.Object) + using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = null, PrismJsInterop = null, AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(_testEnvironment, new ImageConfig()) }; using var sut = new MarkdownConverter(settings); @@ -436,13 +450,14 @@ [toc] TOC*-*Title [Test] public void EnsureThat_Toc_Limited_RendersCorrectly() { - using var settings = new RenderSettings(_imgServiceMock.Object) + using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) { CssClasses = new CssClasses(), DeleteFirstH1 = false, HostUrl = null, PrismJsInterop = null, AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(_testEnvironment, new ImageConfig()) }; using var sut = new MarkdownConverter(settings); @@ -539,4 +554,128 @@ public void EnsureThat_Toc_Limited_RendersCorrectly() Assert.That(result, Is.EqualTo(expected).Using(comparer)); } + + [Test] + public void EnsureThat_TerminalRenderingWorks() + { + using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + { + CssClasses = new CssClasses(), + DeleteFirstH1 = false, + HostUrl = null, + PrismJsInterop = null, + AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(_testEnvironment, new ImageConfig()) + }; + + using var sut = new MarkdownConverter(settings); + + string input = """ + ```terminal + this is terminal output + ``` + """; + + string expected = """ +
    +
    $
    +
    this is terminal output
    +
    + """; + + string result = sut.RenderMarkdownToHtml(input); + + Assert.That(result, Is.EqualTo(expected).Using(comparer)); + } + + [Test] + public void EnsrureThat_Youtube_Autolink_Works() + { + string input = """ + https://www.youtube.com/watch?v=D5ivt3hNAW8 + """; + + string expected = """ +

    + + """; + + using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + { + CssClasses = new CssClasses(), + DeleteFirstH1 = false, + HostUrl = null, + PrismJsInterop = null, + AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(_testEnvironment, new ImageConfig()) + }; + + using var sut = new MarkdownConverter(settings); + + string result = sut.RenderMarkdownToHtml(input); + + Assert.That(result, Is.EqualTo(expected).Using(comparer)); + } + + [Test] + public void EnsureThat_NomnomlRender_PngWorks() + { + string input = """ + ```nomnoml + [Foo|a;b;c] + ``` + """; + + using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + { + CssClasses = new CssClasses(), + DeleteFirstH1 = false, + HostUrl = null, + PrismJsInterop = null, + AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(_testEnvironment, new ImageConfig + { + SvgRecode = SvgRecodeOption.AsPng + }) + }; + + using var sut = new MarkdownConverter(settings); + + string result = sut.RenderMarkdownToHtml(input); + + Assert.That(result, Does.StartWith("Foo|a;b;c] + ``` + """; + + using var settings = new MarkdownRenderSettings(_imgServiceMock.Object) + { + CssClasses = new CssClasses(), + DeleteFirstH1 = false, + HostUrl = null, + PrismJsInterop = null, + AutoEmbedSupportedLinks = true, + ImageRenderJsInterop = new ImageRenderJsInterop(_testEnvironment, new ImageConfig + { + SvgRecode = SvgRecodeOption.Passtrough + }) + }; + + using var sut = new MarkdownConverter(settings); + + string result = sut.RenderMarkdownToHtml(input); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Does.StartWith("")); + } + } } diff --git a/Test/Bookgen.Tests/Shell.Shared/UT_GitParser.cs b/Test/Bookgen.Tests/Shell.Shared/UT_GitParser.cs index cf994285..7497caf4 100644 --- a/Test/Bookgen.Tests/Shell.Shared/UT_GitParser.cs +++ b/Test/Bookgen.Tests/Shell.Shared/UT_GitParser.cs @@ -87,7 +87,7 @@ public void EnsureThat_ParseBranches_Works() remotes/origin/HEAD -> origin/master """; - var result = GitParser.ParseBranches(input); + HashSet result = GitParser.ParseBranches(input); Assert.Multiple(() => { Assert.That(result, Has.Count.EqualTo(2)); diff --git a/Test/Bookgen.Tests/Shell.Shared/UT_ShellAutoCompleteFilter.cs b/Test/Bookgen.Tests/Shell.Shared/UT_ShellAutoCompleteFilter.cs index edb72d11..99043447 100644 --- a/Test/Bookgen.Tests/Shell.Shared/UT_ShellAutoCompleteFilter.cs +++ b/Test/Bookgen.Tests/Shell.Shared/UT_ShellAutoCompleteFilter.cs @@ -13,7 +13,7 @@ public class UT_ShellAutoCompleteFilter [Test] public void EnsureThat_ShellAutoCompleteFilter_DoFilter_ReturnsGood() { - var results = ShellAutoCompleteFilter.DoFilter(["git add", "git add ."], "git a", 5); + IEnumerable results = ShellAutoCompleteFilter.DoFilter(["git add", "git add ."], "git a", 5); Assert.That(results, Is.EqualTo(new[] { "add", "add ." }).AsCollection); } @@ -25,14 +25,14 @@ public void EnsureThat_ShellAutoCompleteFilter_DoFilter_ReturnsGood_Mutiple_Posi [ "git merge master", ]; - var results = ShellAutoCompleteFilter.DoFilter(data, input, position); + IEnumerable results = ShellAutoCompleteFilter.DoFilter(data, input, position); Assert.That(results, Is.EqualTo(new[] { expected }).AsCollection); } [Test] public void EnsureThat_GetWordPositions_Works() { - var results = ShellAutoCompleteFilter.GetWordPositions("git merge master").ToArray(); + (int start, int end)[] results = ShellAutoCompleteFilter.GetWordPositions("git merge master").ToArray(); Assert.That(results, Is.EqualTo(new[] { (0, 3), (4, 9), (10, 16) }).AsCollection); } } diff --git a/Test/Bookgen.Tests/TestEnvironment.cs b/Test/Bookgen.Tests/TestEnvironment.cs index d5ab71f8..60efe72b 100644 --- a/Test/Bookgen.Tests/TestEnvironment.cs +++ b/Test/Bookgen.Tests/TestEnvironment.cs @@ -45,6 +45,6 @@ public bool TryGetAsset(string name, [NotNullWhen(true)] out string? content) public static bool IsBookGenFolder(string folder) => false; - public byte[] GetBinaryAsset(string name) - => _assetSoruce.GetBinaryAsset(name); + public Stream GetBinaryAssetStream(string name) + => _assetSoruce.GetBinaryAssetStream(name); } diff --git a/publish.ps1 b/publish.ps1 index cdfe4433..4751d593 100644 --- a/publish.ps1 +++ b/publish.ps1 @@ -1,31 +1,77 @@ Clear-Host -# publish windows & linux self-contained -dotnet publish -c release -o "bin\publish\windows\bin" --self-contained true -r win-x64 -p PublishReadyToRun BookGen.slnx -dotnet publish -c release -o "bin\publish\linux\bin" --self-contained true -r linux-x64 -p PublishReadyToRun BookGen.slnx - -# copy installer scripts -Copy-Item "Installers\install.cmd" "bin\publish\windows\install.cmd" -Copy-Item "Installers\install.sh" "bin\publish\linux\install.sh" - -# copy assets -Copy-Item "bin\Release\assets.zip" "bin\publish\windows\bin\assets.zip" -Copy-Item "bin\Release\assets.zip" "bin\publish\linux\bin\assets.zip" - -# write version.txt -.\bin\publish\windows\bin\BookGen.exe version > .\bin\publish\windows\version.txt -.\bin\publish\windows\bin\BookGen version > .\bin\publish\linux\version.txt - -# Generate docs -.\bin\publish\windows\bin\BookGen Schemas -.\bin\publish\windows\bin\BookGen md2html -i Schemas.md -o "bin\publish\windows\Schemas.html" -t "Configuration schemas" -.\bin\publish\windows\bin\BookGen md2html -i Schemas.md -o "bin\publish\linux\Schemas.html" -t "Configuration schemas" -.\bin\publish\windows\bin\BookGen md2html -i Changelog.md -o "bin\publish\windows\Changelog.html" -t "Change Log" -.\bin\publish\windows\bin\BookGen md2html -i Changelog.md -o "bin\publish\linux\Changelog.html" -t "Change Log" -.\bin\publish\windows\bin\BookGen md2html -i Commands.md -o "bin\publish\windows\Commands.html" -t "BookGen Commands" -.\bin\publish\windows\bin\BookGen md2html -i Commands.md -o "bin\publish\linux\Commands.html" -t "BookGen Commands" -Remove-Item Schemas.md - -# zip -Compress-Archive -Path "bin\publish\windows\*" -DestinationPath "bin\publish\BookGen-windows.zip" -Force -tar -czvf "bin\publish\BookGen-linux.tar.gz" -C "bin\publish\linux" . +function Invoke-Publish { + param( + [bool] $SelfContained, + [string] $WindowsArchiveName, + [string] $LinuxArchiveName + ) + + if (Test-Path "bin\publish\windows") { + Remove-Item "bin\publish\windows*" -Recurse -Force + } + + if (Test-Path "bin\publish\linux") { + Remove-Item "bin\publish\windows*" -Recurse -Force + } + + # publish windows & linux + if ($SelfContained) { + dotnet publish -c release -o "bin\publish\windows\bin" --self-contained true -r win-x64 -p PublishReadyToRun BookGen.slnx + dotnet publish -c release -o "bin\publish\linux\bin" --self-contained true -r linux-x64 -p PublishReadyToRun BookGen.slnx + } + else { + dotnet publish -c release -o "bin\publish\windows\bin" -r win-x64 -p PublishReadyToRun BookGen.slnx + dotnet publish -c release -o "bin\publish\linux\bin" -r linux-x64 -p PublishReadyToRun BookGen.slnx + } + + # copy installer scripts + Copy-Item "PublishFiles\install.cmd" "bin\publish\windows\install.cmd" + Copy-Item "PublishFiles\start_bookgen_shell.cmd" "bin\publish\windows\start_bookgen_shell.cmd" + + # copy assets + Copy-Item "bin\Release\assets.zip" "bin\publish\windows\bin\assets.zip" + Copy-Item "bin\Release\assets.zip" "bin\publish\linux\bin\assets.zip" + + # write version.txt + .\bin\publish\windows\bin\BookGen.exe version > .\bin\publish\windows\version.txt + .\bin\publish\windows\bin\BookGen version > .\bin\publish\linux\version.txt + + # make docs folder + New-Item -Path "bin\publish\windows\docs" -ItemType Directory -Force + New-Item -Path "bin\publish\linux\docs" -ItemType Directory -Force + + # copy license + Copy-Item ".\LICENCE" "bin\publish\windows\docs\LICENCE.txt" + Copy-Item ".\LICENCE" "bin\publish\linux\docs\LICENCE.txt" + + # Generate docs + .\bin\publish\windows\bin\BookGen Schemas + .\bin\publish\windows\bin\BookGen md2html -i Schemas.md -o "bin\publish\windows\docs\Schemas.html" -t "Configuration schemas" + .\bin\publish\windows\bin\BookGen md2html -i Schemas.md -o "bin\publish\linux\docs\Schemas.html" -t "Configuration schemas" + .\bin\publish\windows\bin\BookGen md2html -i Changelog.md -o "bin\publish\windows\docs\Changelog.html" -t "Change Log" + .\bin\publish\windows\bin\BookGen md2html -i Changelog.md -o "bin\publish\linux\docs\Changelog.html" -t "Change Log" + .\bin\publish\windows\bin\BookGen md2html -i Commands.md -o "bin\publish\windows\docs\Commands.html" -t "BookGen Commands" + .\bin\publish\windows\bin\BookGen md2html -i Commands.md -o "bin\publish\linux\docs\Commands.html" -t "BookGen Commands" + Remove-Item Schemas.md + + # zip + if ($SelfContained) { + Compress-Archive -Path "bin\publish\windows\*" -DestinationPath "bin\publish\$WindowsArchiveName" -Force + clear + tar -czvf "bin\publish\$LinuxArchiveName" -C "bin\publish\linux" . + } + else { + Compress-Archive -Path "bin\publish\windows\*" -DestinationPath "bin\publish\$WindowsArchiveName" -Force + clear + tar -czvf "bin\publish\$LinuxArchiveName" -C "bin\publish\linux" . + } +} + +# Framework-dependent build and archives +Invoke-Publish -SelfContained $false -WindowsArchiveName "BookGen-windows.zip" -LinuxArchiveName "BookGen-linux.tar.gz" + +# Self-contained build and archives +Invoke-Publish -SelfContained $true -WindowsArchiveName "BookGen-windows-selefcontained.zip" -LinuxArchiveName "BookGen-linux-selefcontained.tar.gz" + +.\PublishFiles\mkisofs.exe -V BookGen -o .\bin\publish\bookgen-windows.iso -udf .\bin\publish\windows \ No newline at end of file