webstatic status: Markdown-Rendering im Terminal + LICENSE mit Dep-Lizenzen

- MarkdownConsoleRenderer.cs: eigener Markdig-AST→ANSI-Renderer (keine
  neue NuGet-Dependency). Unterstützt Headings, Paragraphs, Listen,
  Code-Blöcke, Blockquotes, Tabellen, Thematic Breaks, Inline-Formatierung
  (bold/italic/code/links). ANSI-Styling mit Farben.
- Program.cs: 'webstatic status' / 'webstatic --status' Subcommand —
  rendert README.md der Site im Terminal (Default: cwd, --root überschreibt).
  Graceful error wenn README.md fehlt. Help-Eintrag ergänzt.
- LICENSE: Third-Party-Dependency-Lizenzen ergänzt (Markdig BSD-2-Clause,
  YamlDotNet MIT) mit vollständigen Lizenztexten, Links und Kompatibilitäts-Tabelle.
- README.md: aktualisiert als Status-Dokument (neuer status-Befehl,
  CLI-Befehle-Tabelle, bereinigte Environments, LICENSE-Verweis).
This commit is contained in:
2026-07-11 17:27:05 +02:00
parent 18c2108d81
commit 6ca81679d9
4 changed files with 385 additions and 2 deletions
+70
View File
@@ -19,3 +19,73 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
---
## Third-Party Dependencies
This software uses the following third-party NuGet packages. Their license
terms apply to the respective portions of the distributed binaries. The source
code of GCBoK.WebStaticBuilder itself remains under the MIT license above.
### Markdig
- **Package:** Markdig (https://www.nuget.org/packages/Markdig)
- **Purpose:** Markdown → HTML rendering (CommonMark compliant, extensible)
- **License:** BSD-2-Clause (https://github.com/xoofx/markdig/blob/master/license.txt)
- **Copyright:** © Alexandre Mutel (xoofx)
- **Repository:** https://github.com/xoofx/markdig
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
> 1. Redistributions of source code must retain the above copyright notice,
> this list of conditions and the following disclaimer.
> 2. Redistributions in binary form must reproduce the above copyright notice,
> this list of conditions and the following disclaimer in the documentation
> and/or other materials provided with the distribution.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
> ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
> LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
> CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
> WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
> OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
> ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
### YamlDotNet
- **Package:** YamlDotNet (https://www.nuget.org/packages/YamlDotNet)
- **Purpose:** YAML frontmatter parsing for Markdown content files
- **License:** MIT (https://github.com/aaubry/YamlDotNet/blob/master/LICENSE)
- **Copyright:** © Antoine Aubry and contributors
- **Repository:** https://github.com/aaubry/YamlDotNet
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 without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
### Summary
| Package | License | Compatible with MIT? |
|-------------|---------------|----------------------|
| Markdig | BSD-2-Clause | Yes (permissive) |
| YamlDotNet | MIT | Yes (same license) |
Both dependencies are permissive licenses compatible with MIT. No copyleft
(GPL/AGPL/LGPL) components are used.
+21 -2
View File
@@ -6,12 +6,13 @@ früheren C#-Builderskript der GCBoK-Website durch ein generisches, für KMU
einsetzbares Werkzeug. einsetzbares Werkzeug.
- **Sprache:** C# / .NET 10 (kompilierte Konsole) - **Sprache:** C# / .NET 10 (kompilierte Konsole)
- **Lizenz:** MIT - **Lizenz:** MIT (siehe `LICENSE` für Third-Party-Dependency-Lizenzen)
- **Kein Python:** einzige externe Abhängigkeit ist `npx pagefind` (Suchindex, optional) - **Kein Python:** einzige externe Abhängigkeit ist `npx pagefind` (Suchindex, optional)
- **Funktionen:** Markdown + YAML-Frontmatter → statisches HTML pro Sprache, - **Funktionen:** Markdown + YAML-Frontmatter → statisches HTML pro Sprache,
Navigation, SEO-Kopf (OpenGraph/JSON-LD), `sitemap.xml`, `robots.txt`, Navigation, SEO-Kopf (OpenGraph/JSON-LD), `sitemap.xml`, `robots.txt`,
Mermaid-Diagramme, client-seitige Volltextsuche (Pagefind), UI-Lokalisierung Mermaid-Diagramme, client-seitige Volltextsuche (Pagefind), UI-Lokalisierung
über `{{ i18n.* }}`, optionaler Machine-Translation-Helfer (Entwürfe). über `{{ i18n.* }}`, optionaler Machine-Translation-Helfer (Entwürfe),
Markdown-Rendering im Terminal via `webstatic status`.
## Schnellstart ## Schnellstart
@@ -26,8 +27,25 @@ dotnet ../../bin/Release/net10.0/webstatic.dll build --env development
# Lokal ansehen # Lokal ansehen
dotnet ../../bin/Release/net10.0/webstatic.dll serve --port 8000 dotnet ../../bin/Release/net10.0/webstatic.dll serve --port 8000
# Site-Status (README.md im Terminal rendern)
dotnet ../../bin/Release/net10.0/webstatic.dll status
``` ```
## CLI-Befehle
| Befehl | Bedeutung |
|--------|-----------|
| `webstatic build [Optionen]` | Website bauen |
| `webstatic serve [--port N]` | Lokalen Server starten (Vorschau) |
| `webstatic init <name>` | Beispiel-Site anlegen |
| `webstatic status [--root DIR]` | README.md der Site im Terminal anzeigen |
| `webstatic translate --check` | Fehlende Übersetzungen melden |
| `webstatic translate --draft` | Übersetzungs-Entwürfe erzeugen |
| `webstatic --version` | Version (Assembly + Git-Commit) anzeigen |
| `webstatic --help \| -h` | Hilfe anzeigen |
| `webstatic update` | Selbstaktualisierung (Clone-Installation) |
## Umgebungen ## Umgebungen
| `--env` | baseUrl (Beispiel) | Zweck | | `--env` | baseUrl (Beispiel) | Zweck |
@@ -81,4 +99,5 @@ src/ C#-Quellcode (GCBoK.WebStaticBuilder)
tests/ (künftig) Unit/Integrationstests tests/ (künftig) Unit/Integrationstests
webstatic.example/ Vorlage-Site für KMU webstatic.example/ Vorlage-Site für KMU
install.sh KMU-Installation install.sh KMU-Installation
LICENSE MIT + Third-Party-Dependency-Lizenzen
``` ```
+265
View File
@@ -0,0 +1,265 @@
using System.Text;
using System.Text.RegularExpressions;
using Markdig;
using Markdig.Extensions.Tables;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace GCBoK.WebStaticBuilder;
/// <summary>
/// Renders a Markdig AST to ANSI-styled console output. No external
/// dependency beyond Markdig (already referenced). Supports headings,
/// paragraphs, lists, code blocks, blockquotes, tables, thematic breaks,
/// and inline formatting (bold, italic, code, links).
/// </summary>
public static class MarkdownConsoleRenderer
{
// ANSI SGR codes
private const string Reset = "\x1b[0m";
private const string Bold = "\x1b[1m";
private const string Dim = "\x1b[2m";
private const string Italic = "\x1b[3m";
private const string Underline = "\x1b[4m";
private const string FgCyan = "\x1b[36m";
private const string FgBrightCyan = "\x1b[96m";
private const string FgBrightYellow = "\x1b[93m";
private const string FgBrightRed = "\x1b[91m";
private const string FgBrightGreen = "\x1b[92m";
private const string FgBrightBlack = "\x1b[90m";
/// <summary>Render a Markdown document to the console.</summary>
public static void Render(string markdown, TextWriter writer)
{
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
var doc = Markdig.Markdown.Parse(markdown, pipeline);
RenderBlocks(doc, writer, 0);
}
private static void RenderBlocks(ContainerBlock container, TextWriter w, int indent)
{
foreach (var block in container)
RenderBlock(block, w, indent);
}
private static void RenderBlock(Block block, TextWriter w, int indent)
{
var pad = new string(' ', indent * 2);
switch (block)
{
case HeadingBlock h:
RenderHeading(h, w);
break;
case ParagraphBlock p:
w.WriteLine(pad + RenderInlines(p.Inline!));
w.WriteLine();
break;
case ListBlock lb:
RenderList(lb, w, indent);
break;
case QuoteBlock qb:
RenderQuote(qb, w, indent);
break;
case FencedCodeBlock fc:
RenderCodeBlock(fc.Lines.ToString(), w, indent, fc.Info);
break;
case CodeBlock cb:
RenderCodeBlock(cb.Lines.ToString(), w, indent, null);
break;
case ThematicBreakBlock:
w.WriteLine(pad + $"{Dim}{'─'.ToString().PadLeft(Console.BufferWidth > 0 ? Math.Min(Console.BufferWidth, 60) : 60, '─')}{Reset}");
w.WriteLine();
break;
case Table table:
RenderTable(table, w, indent);
break;
default:
// Fallback: try to extract text content
if (block is ContainerBlock cb2)
RenderBlocks(cb2, w, indent);
break;
}
}
private static void RenderHeading(HeadingBlock h, TextWriter w)
{
var text = RenderInlines(h.Inline!);
var prefix = h.Level switch
{
1 => $"{Bold}{FgBrightYellow}",
2 => $"{Bold}{FgBrightCyan}",
3 => $"{Bold}{FgBrightGreen}",
_ => $"{Bold}",
};
w.WriteLine($"{prefix}{text}{Reset}");
w.WriteLine();
}
private static void RenderList(ListBlock lb, TextWriter w, int indent)
{
var pad = new string(' ', indent * 2);
int idx = 1;
foreach (ListItemBlock item in lb)
{
var marker = lb.IsOrdered ? $"{idx}{lb.OrderedDelimiter} " : "• ";
var firstBlock = item.FirstOrDefault();
if (firstBlock is ParagraphBlock p)
{
w.WriteLine($"{pad}{marker}{RenderInlines(p.Inline!)}");
}
else if (firstBlock != null)
{
w.WriteLine($"{pad}{marker}");
RenderBlock(firstBlock, w, indent + 1);
}
// Sub-blocks beyond the first paragraph (e.g., nested lists)
for (int i = 1; i < item.Count; i++)
{
if (item[i] is ListBlock subList)
RenderList(subList, w, indent + 1);
else
RenderBlock(item[i], w, indent + 1);
}
if (lb.IsOrdered) idx++;
}
w.WriteLine();
}
private static void RenderQuote(QuoteBlock qb, TextWriter w, int indent)
{
var pad = new string(' ', indent * 2);
var savedColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.DarkGray;
foreach (var sub in qb)
{
if (sub is ParagraphBlock p)
w.WriteLine($"{pad}{Dim}│ {RenderInlines(p.Inline!)}{Reset}");
else
RenderBlock(sub, w, indent + 1);
}
Console.ForegroundColor = savedColor;
w.WriteLine();
}
private static void RenderCodeBlock(string code, TextWriter w, int indent, string? lang)
{
var pad = new string(' ', indent * 2);
var lines = code.TrimEnd('\n', '\r').Split('\n');
w.WriteLine($"{pad}{Dim}```{lang ?? ""}{Reset}");
foreach (var line in lines)
w.WriteLine($"{pad}{FgBrightCyan}{line}{Reset}");
w.WriteLine($"{pad}{Dim}```{Reset}");
w.WriteLine();
}
private static void RenderTable(Table table, TextWriter w, int indent)
{
var pad = new string(' ', indent * 2);
// First pass: collect cell texts and compute column widths
var rows = new List<List<string>>();
foreach (TableRow row in table)
{
var cells = new List<string>();
foreach (TableCell cell in row)
{
var cellText = new StringBuilder();
foreach (var sub in cell)
{
if (sub is ParagraphBlock p)
cellText.Append(RenderInlines(p.Inline!));
}
cells.Add(cellText.ToString().Trim());
}
rows.Add(cells);
}
if (rows.Count == 0) return;
var colCount = rows.Max(r => r.Count);
var widths = new int[colCount];
foreach (var row in rows)
for (int c = 0; c < row.Count; c++)
widths[c] = Math.Max(widths[c], row[c].Length);
// Render rows
bool isFirstRow = true;
foreach (var row in rows)
{
var parts = new List<string>();
for (int c = 0; c < row.Count; c++)
{
var cellText = c < row.Count ? row[c] : "";
var padded = cellText.PadRight(widths[c]);
parts.Add(isFirstRow ? $"{Bold}{padded}{Reset}" : padded);
}
w.WriteLine($"{pad}│ {string.Join(" ", parts)} │");
if (isFirstRow)
{
// Separator
var sep = string.Join("─┬─", widths.Select(wd => new string('─', wd + 2)));
w.WriteLine($"{pad}├{sep}┤");
isFirstRow = false;
}
}
w.WriteLine();
}
/// <summary>Render inline elements to a single ANSI-styled string.</summary>
private static string RenderInlines(ContainerInline inlines)
{
var sb = new StringBuilder();
foreach (var il in inlines)
RenderInline(il, sb);
return sb.ToString();
}
private static void RenderInline(Inline inline, StringBuilder sb)
{
switch (inline)
{
case LiteralInline lit:
sb.Append(lit.Content.ToString());
break;
case EmphasisInline emp:
var code = emp.DelimiterCount == 2 ? Bold : Italic;
sb.Append(code);
foreach (var child in emp)
RenderInline(child, sb);
sb.Append(Reset);
if (emp.DelimiterCount == 2) sb.Append(""); // bold doesn't need reset of italic
break;
case CodeInline ci:
sb.Append($"{FgBrightRed}{ci.Content}{Reset}");
break;
case LinkInline link:
var text = new StringBuilder();
foreach (var child in link)
RenderInline(child, text);
if (link.IsImage)
sb.Append($"{Dim}[image: {text}{Reset}{Dim}]{Reset}");
else if (link.Url != null)
sb.Append($"{Underline}{FgCyan}{text}{Reset} {Dim}({link.Url}){Reset}");
else
sb.Append(text.ToString());
break;
case LineBreakInline:
sb.AppendLine();
break;
case HtmlInline html:
sb.Append($"{Dim}{html.Tag}{Reset}");
break;
case ContainerInline container:
foreach (var child in container)
RenderInline(child, sb);
break;
default:
// DelimiterInline, etc. — extract literal content
sb.Append(inline.ToString());
break;
}
}
}
+29
View File
@@ -96,6 +96,13 @@ if (argsList.Contains("init"))
return; return;
} }
// Subcommand: status — render the site's README.md to the console
if (argsList.Contains("status") || argsList.Contains("--status"))
{
ShowStatus(root);
return;
}
// Build // Build
// "development" folds work, preview and review into a single local/dev environment. // "development" folds work, preview and review into a single local/dev environment.
var envName = "development"; var envName = "development";
@@ -130,6 +137,7 @@ Aufruf:
webstatic build [Optionen] Website bauen webstatic build [Optionen] Website bauen
webstatic serve [--port N] Lokalen Server starten (Vorschau) webstatic serve [--port N] Lokalen Server starten (Vorschau)
webstatic init <name> Beispiel-Site anlegen webstatic init <name> Beispiel-Site anlegen
webstatic status README.md der Site im Terminal anzeigen
webstatic translate --check Fehlende Übersetzungen melden webstatic translate --check Fehlende Übersetzungen melden
webstatic translate --draft Übersetzungs-Entwürfe erzeugen webstatic translate --draft Übersetzungs-Entwürfe erzeugen
webstatic --version Version (Assembly + Git-Commit) anzeigen webstatic --version Version (Assembly + Git-Commit) anzeigen
@@ -382,3 +390,24 @@ static string MimeType(string file)
_ => "application/octet-stream", _ => "application/octet-stream",
}; };
} }
static void ShowStatus(string rootDir)
{
// Search for README.md in the site root (case-insensitive)
var readme = Directory.GetFiles(rootDir, "README.md", SearchOption.TopDirectoryOnly)
.FirstOrDefault()
?? Directory.GetFiles(rootDir, "readme.md", SearchOption.TopDirectoryOnly)
.FirstOrDefault();
if (readme == null)
{
Console.Error.WriteLine($"ERROR: no README.md found in {rootDir}");
Console.Error.WriteLine(" The status command renders the site's README.md to the console.");
Console.Error.WriteLine(" Specify --root <dir> if the site is not in the current directory.");
Environment.ExitCode = 1;
return;
}
var markdown = File.ReadAllText(readme);
MarkdownConsoleRenderer.Render(markdown, Console.Out);
}