+
@((MarkupString)renderedHtml)
}
@@ -86,6 +87,7 @@ else
private ShareToken? shareToken = null;
private string markdownContent = "";
private string renderedHtml = "";
+ private string documentStyle = "modern";
protected override async Task OnInitializedAsync()
{
@@ -120,8 +122,10 @@ else
return;
}
- // Load the document content
+ // Load the document content & metadata
markdownContent = await SessionService.LoadSessionContentAsync(shareToken.SessionId);
+ var meta = await DocumentService.GetDocumentAsync(shareToken.SessionId);
+ documentStyle = meta?.DocStyle ?? "modern";
// Render to HTML
renderedHtml = MarkdownService.RenderToHtml(markdownContent);
diff --git a/Notemark/Components/Pages/SharedPreview.razor.css b/Notemark/Components/Pages/SharedPreview.razor.css
index b5af032..1d307ab 100644
--- a/Notemark/Components/Pages/SharedPreview.razor.css
+++ b/Notemark/Components/Pages/SharedPreview.razor.css
@@ -327,34 +327,26 @@
.preview-content table {
width: 100%;
- border-collapse: separate;
- border-spacing: 0;
+ border-collapse: collapse;
margin: 1.5em 0;
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
+ font-size: 0.9375rem;
}
.preview-content th,
.preview-content td {
padding: 0.75rem 1rem;
- border-bottom: 1px solid var(--border);
- border-right: 1px solid var(--border);
+ border: 1px solid var(--border);
text-align: left;
}
-.preview-content th:last-child,
-.preview-content td:last-child {
- border-right: none;
-}
-
-.preview-content tr:last-child td {
- border-bottom: none;
-}
-
.preview-content th {
background-color: var(--surface-hover);
font-weight: 600;
+ color: var(--primary-text);
+ border-bottom: 2px solid var(--border);
}
.preview-content tr:hover td {
@@ -385,6 +377,459 @@
margin: 1em 0;
}
+/* ========================================================= */
+/* DOCUMENT PRESET STYLES & GITHUB ALERTS */
+/* ========================================================= */
+
+/* Callouts / GitHub Alerts */
+.preview-content .markdown-alert {
+ padding: 0.75rem 1rem;
+ margin: 1.25rem 0;
+ border-left: 4px solid;
+ border-radius: 0 6px 6px 0;
+ background-color: var(--surface-hover);
+}
+
+.preview-content .markdown-alert-title {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 600;
+ margin-bottom: 0.35rem;
+}
+
+.preview-content .markdown-alert-note { border-left-color: #3b82f6; background-color: rgba(59, 130, 246, 0.08); }
+.preview-content .markdown-alert-tip { border-left-color: #10b981; background-color: rgba(16, 185, 129, 0.08); }
+.preview-content .markdown-alert-important { border-left-color: #8b5cf6; background-color: rgba(139, 92, 246, 0.08); }
+.preview-content .markdown-alert-warning { border-left-color: #f59e0b; background-color: rgba(245, 158, 11, 0.08); }
+.preview-content .markdown-alert-caution { border-left-color: #ef4444; background-color: rgba(239, 68, 68, 0.08); }
+
+/* ========================================================= */
+/* DISTINCT DOCUMENT PRESET STYLES */
+/* ========================================================= */
+
+/* === PRESET 1: MODERN (Clean SaaS / Notion Look) === */
+.preview-content[data-doc-style="modern"],
+.preview-content.doc-style-modern {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ line-height: 1.7;
+ color: var(--primary-text);
+}
+
+.preview-content[data-doc-style="modern"] h1,
+.preview-content.doc-style-modern h1 {
+ font-size: 2.3rem;
+ font-weight: 800;
+ letter-spacing: -0.03em;
+ background: linear-gradient(135deg, var(--primary-text) 60%, var(--accent));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ border-bottom: 2px solid var(--accent);
+ padding-bottom: 0.4em;
+ margin-top: 1.4em;
+}
+
+.preview-content[data-doc-style="modern"] h2,
+.preview-content.doc-style-modern h2 {
+ font-size: 1.6rem;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ border-bottom: 1px solid var(--border);
+ padding-bottom: 0.3em;
+ margin-top: 1.3em;
+}
+
+.preview-content[data-doc-style="modern"] table,
+.preview-content.doc-style-modern table {
+ width: 100%;
+ border-collapse: collapse;
+ border: 1.5px solid var(--border);
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
+ margin: 1.5em 0;
+}
+
+.preview-content[data-doc-style="modern"] th,
+.preview-content.doc-style-modern th {
+ background-color: var(--surface-hover);
+ color: var(--primary-text);
+ font-weight: 600;
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--border);
+ border-bottom: 2px solid var(--accent);
+}
+
+.preview-content[data-doc-style="modern"] td,
+.preview-content.doc-style-modern td {
+ padding: 0.7rem 1rem;
+ border: 1px solid var(--border);
+}
+
+.preview-content[data-doc-style="modern"] tr:hover td,
+.preview-content.doc-style-modern tr:hover td {
+ background-color: var(--surface-hover);
+}
+
+.preview-content[data-doc-style="modern"] blockquote,
+.preview-content.doc-style-modern blockquote {
+ border-left: 4px solid var(--accent);
+ background: linear-gradient(90deg, rgba(79, 70, 229, 0.08) 0%, transparent 100%);
+ border-radius: 0 10px 10px 0;
+ padding: 0.75rem 1.25rem;
+}
+
+
+/* === PRESET 2: ACADEMIC (LaTeX / Formal Research Paper) === */
+.preview-content[data-doc-style="academic"],
+.preview-content.doc-style-academic {
+ font-family: 'Merriweather', 'Georgia', 'Cambria', 'Times New Roman', serif;
+ line-height: 1.9;
+ font-size: 1.05rem;
+ background-color: #fdfdf9;
+ color: #1a1a1a;
+ border-radius: 8px;
+ box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.02);
+ padding: 2.5rem 3rem;
+ text-align: justify;
+}
+
+:root[data-theme="dark"] .preview-content[data-doc-style="academic"],
+:root[data-theme="dark"] .preview-content.doc-style-academic {
+ background-color: #121110;
+ color: #e5e0d8;
+ box-shadow: inset 0 0 40px rgba(255, 255, 255, 0.01);
+}
+
+.preview-content[data-doc-style="academic"] h1,
+.preview-content.doc-style-academic h1 {
+ font-family: 'Merriweather', 'Georgia', serif;
+ text-align: center;
+ font-size: 2.1rem;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
+ border-top: 3px double currentColor;
+ border-bottom: 1px solid currentColor;
+ padding: 0.8rem 0;
+ margin: 1.8em 0 1.2em 0;
+}
+
+.preview-content[data-doc-style="academic"] h2,
+.preview-content.doc-style-academic h2 {
+ font-family: 'Merriweather', 'Georgia', serif;
+ font-size: 1.45rem;
+ font-weight: 700;
+ font-variant: small-caps;
+ letter-spacing: 0.06em;
+ border-bottom: 1px solid currentColor;
+ padding-bottom: 0.25em;
+ margin-top: 1.6em;
+}
+
+.preview-content[data-doc-style="academic"] h3,
+.preview-content.doc-style-academic h3 {
+ font-family: 'Merriweather', 'Georgia', serif;
+ font-size: 1.2rem;
+ font-style: italic;
+ font-weight: 600;
+ margin-top: 1.3em;
+}
+
+/* Classical Academic Tables */
+.preview-content[data-doc-style="academic"] table,
+.preview-content.doc-style-academic table {
+ width: 100%;
+ border-collapse: collapse;
+ border: 1px solid rgba(128, 128, 128, 0.4);
+ border-top: 2.5px solid currentColor;
+ border-bottom: 2.5px solid currentColor;
+ margin: 2em auto;
+ font-size: 0.95rem;
+}
+
+.preview-content[data-doc-style="academic"] th,
+.preview-content.doc-style-academic th {
+ border: 1px solid rgba(128, 128, 128, 0.35);
+ border-bottom: 2px solid currentColor;
+ font-variant: small-caps;
+ font-size: 0.88rem;
+ letter-spacing: 0.08em;
+ background: rgba(0, 0, 0, 0.03);
+ padding: 0.65rem 1rem;
+ text-align: center;
+}
+
+.preview-content[data-doc-style="academic"] td,
+.preview-content.doc-style-academic td {
+ border: 1px solid rgba(128, 128, 128, 0.25);
+ padding: 0.6rem 1rem;
+}
+
+.preview-content[data-doc-style="academic"] blockquote,
+.preview-content.doc-style-academic blockquote {
+ font-style: italic;
+ border-left: none;
+ background: transparent;
+ padding: 0.8rem 2.5rem;
+ margin: 1.8em 0;
+ position: relative;
+ text-align: left;
+}
+
+.preview-content[data-doc-style="academic"] blockquote::before {
+ content: "“";
+ font-size: 3.5rem;
+ line-height: 0;
+ position: absolute;
+ left: 0.5rem;
+ top: 1.5rem;
+ opacity: 0.35;
+ font-family: Georgia, serif;
+}
+
+
+/* === PRESET 3: TECHNICAL (Cyber / Terminal / Dev Spec) === */
+.preview-content[data-doc-style="technical"],
+.preview-content.doc-style-technical {
+ font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', Consolas, monospace;
+ font-size: 0.92rem;
+ line-height: 1.65;
+ background-color: #0d1117;
+ color: #c9d1d9;
+ border: 1px solid #30363d;
+ border-radius: 8px;
+ padding: 2rem 2.5rem;
+}
+
+.preview-content[data-doc-style="technical"] h1,
+.preview-content.doc-style-technical h1 {
+ font-family: 'JetBrains Mono', monospace;
+ color: #58a6ff;
+ font-size: 1.8rem;
+ font-weight: 700;
+ border-bottom: 2px solid #238636;
+ padding-bottom: 0.4em;
+ letter-spacing: -0.02em;
+}
+
+.preview-content[data-doc-style="technical"] h1::before {
+ content: "#!/usr/bin/spec >> ";
+ color: #7ee787;
+ font-size: 0.85rem;
+ display: block;
+ font-weight: 400;
+ margin-bottom: 0.3rem;
+ opacity: 0.8;
+}
+
+.preview-content[data-doc-style="technical"] h2,
+.preview-content.doc-style-technical h2 {
+ font-family: 'JetBrains Mono', monospace;
+ color: #79c0ff;
+ font-size: 1.35rem;
+ border-bottom: 1px dashed #30363d;
+ padding-bottom: 0.3em;
+ margin-top: 1.5em;
+}
+
+.preview-content[data-doc-style="technical"] h2::before {
+ content: "## ";
+ color: #7ee787;
+ font-weight: bold;
+}
+
+.preview-content[data-doc-style="technical"] h3,
+.preview-content.doc-style-technical h3 {
+ font-family: 'JetBrains Mono', monospace;
+ color: #d2a8ff;
+ font-size: 1.15rem;
+}
+
+.preview-content[data-doc-style="technical"] h3::before {
+ content: "fn ";
+ color: #ff7b72;
+}
+
+/* Terminal Matrix Grid Table */
+.preview-content[data-doc-style="technical"] table,
+.preview-content.doc-style-technical table {
+ border: 1px solid #30363d;
+ background-color: #161b22;
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 0.85rem;
+}
+
+.preview-content[data-doc-style="technical"] th,
+.preview-content.doc-style-technical th {
+ background-color: #21262d;
+ color: #7ee787;
+ border: 1px solid #30363d;
+ padding: 0.5rem 0.85rem;
+ font-weight: 600;
+}
+
+.preview-content[data-doc-style="technical"] td,
+.preview-content.doc-style-technical td {
+ border: 1px solid #30363d;
+ padding: 0.5rem 0.85rem;
+}
+
+.preview-content[data-doc-style="technical"] pre,
+.preview-content.doc-style-technical pre {
+ background-color: #030712 !important;
+ border: 1px solid #06b6d4 !important;
+ box-shadow: 0 0 15px rgba(6, 182, 212, 0.12);
+ border-radius: 6px;
+}
+
+.preview-content[data-doc-style="technical"] blockquote,
+.preview-content.doc-style-technical blockquote {
+ border-left: 4px solid #238636;
+ background-color: rgba(35, 134, 54, 0.1);
+ color: #7ee787;
+ border-radius: 0 4px 4px 0;
+ padding: 0.6rem 1rem;
+}
+
+
+/* === PRESET 4: EXECUTIVE (Corporate Brief / Wall Street / McKinsey) === */
+.preview-content[data-doc-style="executive"],
+.preview-content.doc-style-executive {
+ font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ line-height: 1.75;
+ color: #0f172a;
+ background-color: #ffffff;
+ border-top: 5px solid #0f2744;
+ border-radius: 8px;
+ box-shadow: 0 10px 25px -5px rgba(15, 39, 68, 0.08);
+ padding: 2.5rem 3rem;
+}
+
+:root[data-theme="dark"] .preview-content[data-doc-style="executive"],
+:root[data-theme="dark"] .preview-content.doc-style-executive {
+ color: #f1f5f9;
+ background-color: #0f172a;
+ border-top: 5px solid #38bdf8;
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
+}
+
+.preview-content[data-doc-style="executive"] h1,
+.preview-content.doc-style-executive h1 {
+ font-size: 2.3rem;
+ font-weight: 800;
+ color: #0f2744;
+ letter-spacing: -0.02em;
+ border-bottom: 3px solid #d97706;
+ padding-bottom: 0.4em;
+ margin-top: 1.4em;
+}
+
+:root[data-theme="dark"] .preview-content[data-doc-style="executive"] h1,
+:root[data-theme="dark"] .preview-content.doc-style-executive h1 {
+ color: #38bdf8;
+ border-bottom-color: #fbbf24;
+}
+
+.preview-content[data-doc-style="executive"] h1::before {
+ content: "EXECUTIVE BRIEFING // CONFIDENTIAL";
+ display: block;
+ font-size: 0.72rem;
+ font-weight: 800;
+ letter-spacing: 0.15em;
+ color: #d97706;
+ margin-bottom: 0.4rem;
+}
+
+.preview-content[data-doc-style="executive"] h2,
+.preview-content.doc-style-executive h2 {
+ font-size: 1.35rem;
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #0f2744;
+ border-left: 4px solid #d97706;
+ padding-left: 0.75rem;
+ margin-top: 1.6em;
+}
+
+:root[data-theme="dark"] .preview-content[data-doc-style="executive"] h2,
+:root[data-theme="dark"] .preview-content.doc-style-executive h2 {
+ color: #38bdf8;
+ border-left-color: #fbbf24;
+}
+
+.preview-content[data-doc-style="executive"] table,
+.preview-content.doc-style-executive table {
+ width: 100%;
+ border-collapse: collapse;
+ border: 1.5px solid #cbd5e1;
+ box-shadow: 0 4px 6px -1px rgba(15, 39, 68, 0.06);
+ border-radius: 6px;
+ overflow: hidden;
+ margin: 1.5em 0;
+}
+
+:root[data-theme="dark"] .preview-content[data-doc-style="executive"] table,
+:root[data-theme="dark"] .preview-content.doc-style-executive table {
+ border-color: #334155;
+}
+
+.preview-content[data-doc-style="executive"] th,
+.preview-content.doc-style-executive th {
+ background-color: #0f2744;
+ color: #ffffff;
+ font-size: 0.8rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ padding: 0.75rem 1rem;
+ border: 1px solid #1e3a5f;
+}
+
+:root[data-theme="dark"] .preview-content[data-doc-style="executive"] th,
+:root[data-theme="dark"] .preview-content.doc-style-executive th {
+ background-color: #1e293b;
+ color: #38bdf8;
+ border: 1px solid #334155;
+}
+
+.preview-content[data-doc-style="executive"] td,
+.preview-content.doc-style-executive td {
+ padding: 0.65rem 1rem;
+ border: 1px solid #cbd5e1;
+}
+
+:root[data-theme="dark"] .preview-content[data-doc-style="executive"] td,
+:root[data-theme="dark"] .preview-content.doc-style-executive td {
+ border: 1px solid #334155;
+}
+
+.preview-content[data-doc-style="executive"] tr:nth-child(even) td {
+ background-color: rgba(15, 39, 68, 0.03);
+}
+
+:root[data-theme="dark"] .preview-content[data-doc-style="executive"] tr:nth-child(even) td {
+ background-color: rgba(255, 255, 255, 0.03);
+}
+
+.preview-content[data-doc-style="executive"] blockquote,
+.preview-content.doc-style-executive blockquote {
+ border-left: 4px solid #0f2744;
+ background-color: rgba(15, 39, 68, 0.05);
+ border-radius: 0 8px 8px 0;
+ font-weight: 500;
+ padding: 1rem 1.4rem;
+ border-right: 1px solid rgba(15, 39, 68, 0.1);
+}
+
+:root[data-theme="dark"] .preview-content[data-doc-style="executive"] blockquote,
+:root[data-theme="dark"] .preview-content.doc-style-executive blockquote {
+ border-left-color: #38bdf8;
+ background-color: rgba(56, 189, 248, 0.08);
+ border-right-color: rgba(56, 189, 248, 0.15);
+}
+
@media (max-width: 768px) {
.shared-header {
padding: 0.75rem 1rem;
diff --git a/Notemark/Models/DocumentMetadata.cs b/Notemark/Models/DocumentMetadata.cs
index b948e4f..debda25 100644
--- a/Notemark/Models/DocumentMetadata.cs
+++ b/Notemark/Models/DocumentMetadata.cs
@@ -9,4 +9,5 @@ public record DocumentMetadata
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
public DateTime LastModifiedAt { get; init; } = DateTime.UtcNow;
public string Snippet { get; init; } = "";
+ public string DocStyle { get; init; } = "modern";
}
diff --git a/Notemark/Program.cs b/Notemark/Program.cs
index 88b65bb..21ca0c3 100644
--- a/Notemark/Program.cs
+++ b/Notemark/Program.cs
@@ -23,6 +23,7 @@ builder.Services.AddHttpContextAccessor();
// Register Notemark services
builder.Services.AddSingleton
();
+builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddScoped();
builder.Services.AddScoped();
diff --git a/Notemark/Services/AutoStyleService.cs b/Notemark/Services/AutoStyleService.cs
new file mode 100644
index 0000000..ef5bed8
--- /dev/null
+++ b/Notemark/Services/AutoStyleService.cs
@@ -0,0 +1,473 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Notemark.Services;
+
+public class AutoStyleService : IAutoStyleService
+{
+ private static readonly Regex HeadingRegex = new(@"^(#{1,6})([^\s#].*)$", RegexOptions.Compiled);
+ private static readonly Regex HeadingTrailingHashRegex = new(@"^(#{1,6}\s+.*?)\s+#+\s*$", RegexOptions.Compiled);
+ private static readonly Regex BulletListRegex = new(@"^(\s*)[*+]( +.*)$", RegexOptions.Compiled);
+ private static readonly Regex TaskListEmptyRegex = new(@"^(\s*-\s*)\[\s*\]\s*(.*)$", RegexOptions.Compiled);
+ private static readonly Regex TaskListCheckedRegex = new(@"^(\s*-\s*)\[[xX]\]\s*(.*)$", RegexOptions.Compiled);
+
+ // Callout triggers for English and Slovak
+ private static readonly Regex WarningCalloutRegex = new(@"^(?:>\s*)?(?:Pozor|Varovanie|Warning|Caution):\s*(.*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ private static readonly Regex ImportantCalloutRegex = new(@"^(?:>\s*)?(?:Dôležité|Dolezite|Important):\s*(.*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ private static readonly Regex TipCalloutRegex = new(@"^(?:>\s*)?(?:Tip):\s*(.*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ private static readonly Regex NoteCalloutRegex = new(@"^(?:>\s*)?(?:Poznámka|Poznamka|Note|Info|Informácia|Informacia):\s*(.*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ private static readonly Regex CautionCalloutRegex = new(@"^(?:>\s*)?(?:Kritické|Kriticke|Chyba|Danger|Error):\s*(.*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ public string FormatMarkdown(string markdown)
+ {
+ if (string.IsNullOrWhiteSpace(markdown))
+ return string.Empty;
+
+ var rawLines = markdown.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n');
+ var processedLines = new List();
+
+ bool inCodeBlock = false;
+ string codeBlockFence = "";
+ bool inFrontmatter = false;
+
+ int lineIndex = 0;
+ while (lineIndex < rawLines.Length)
+ {
+ var line = rawLines[lineIndex];
+
+ // 1. Handle Frontmatter (YAML at document start)
+ if (lineIndex == 0 && line.Trim() == "---")
+ {
+ inFrontmatter = true;
+ processedLines.Add(line);
+ lineIndex++;
+ continue;
+ }
+ if (inFrontmatter)
+ {
+ processedLines.Add(line);
+ if (line.Trim() == "---")
+ {
+ inFrontmatter = false;
+ }
+ lineIndex++;
+ continue;
+ }
+
+ // 2. Handle Code Blocks
+ var trimmedLine = line.Trim();
+ if (trimmedLine.StartsWith("```") || trimmedLine.StartsWith("~~~"))
+ {
+ var fence = trimmedLine.Length >= 3 ? trimmedLine[..3] : "```";
+ if (!inCodeBlock)
+ {
+ inCodeBlock = true;
+ codeBlockFence = fence;
+ processedLines.Add(line.TrimEnd());
+ }
+ else if (trimmedLine.StartsWith(codeBlockFence))
+ {
+ inCodeBlock = false;
+ codeBlockFence = "";
+ processedLines.Add(line.TrimEnd());
+ }
+ else
+ {
+ processedLines.Add(line);
+ }
+ lineIndex++;
+ continue;
+ }
+
+ if (inCodeBlock)
+ {
+ // Preserve code block contents exactly as-is
+ processedLines.Add(line);
+ lineIndex++;
+ continue;
+ }
+
+ // 3. Detect and Format Markdown Tables
+ if (IsTableStart(rawLines, lineIndex))
+ {
+ if (processedLines.Count > 0 && !string.IsNullOrWhiteSpace(processedLines[^1]))
+ {
+ processedLines.Add(string.Empty);
+ }
+
+ var (formattedTableLines, nextIndex) = FormatTableBlock(rawLines, lineIndex);
+ processedLines.AddRange(formattedTableLines);
+
+ if (nextIndex < rawLines.Length && !string.IsNullOrWhiteSpace(rawLines[nextIndex]))
+ {
+ processedLines.Add(string.Empty);
+ }
+
+ lineIndex = nextIndex;
+ continue;
+ }
+
+ // 4. Format Regular Markdown Line
+ var formattedLine = FormatSingleLine(line);
+
+ // 5. Smart Callouts Conversion
+ var calloutLines = TryConvertSmartCallout(formattedLine);
+ if (calloutLines != null)
+ {
+ processedLines.AddRange(calloutLines);
+ }
+ else
+ {
+ processedLines.Add(formattedLine);
+ }
+
+ lineIndex++;
+ }
+
+ // 6. Clean up consecutive blank lines & ensure single final newline
+ return NormalizeBlankLines(processedLines);
+ }
+
+ private static string FormatSingleLine(string line)
+ {
+ var trimmedEnd = line.TrimEnd();
+
+ // Fix Heading missing space: e.g. #Heading -> # Heading
+ var headingMatch = HeadingRegex.Match(trimmedEnd);
+ if (headingMatch.Success)
+ {
+ trimmedEnd = $"{headingMatch.Groups[1].Value} {headingMatch.Groups[2].Value.Trim()}";
+ }
+
+ // Remove trailing hash decorations in headings: ## Heading ## -> ## Heading
+ var trailingHashMatch = HeadingTrailingHashRegex.Match(trimmedEnd);
+ if (trailingHashMatch.Success)
+ {
+ trimmedEnd = trailingHashMatch.Groups[1].Value.TrimEnd();
+ }
+
+ // Fix bullet list style: * item or + item -> - item
+ var bulletMatch = BulletListRegex.Match(trimmedEnd);
+ if (bulletMatch.Success)
+ {
+ trimmedEnd = $"{bulletMatch.Groups[1].Value}-{bulletMatch.Groups[2].Value}";
+ }
+
+ // Fix Task lists spacing
+ var taskEmptyMatch = TaskListEmptyRegex.Match(trimmedEnd);
+ if (taskEmptyMatch.Success)
+ {
+ trimmedEnd = $"{taskEmptyMatch.Groups[1].Value}[ ] {taskEmptyMatch.Groups[2].Value.TrimStart()}";
+ }
+ else
+ {
+ var taskCheckedMatch = TaskListCheckedRegex.Match(trimmedEnd);
+ if (taskCheckedMatch.Success)
+ {
+ trimmedEnd = $"{taskCheckedMatch.Groups[1].Value}[x] {taskCheckedMatch.Groups[2].Value.TrimStart()}";
+ }
+ }
+
+ return trimmedEnd;
+ }
+
+ private static List? TryConvertSmartCallout(string line)
+ {
+ var trimmed = line.Trim();
+ if (string.IsNullOrWhiteSpace(trimmed)) return null;
+
+ // Skip if already in GitHub alert syntax: > [!NOTE], > [!WARNING], etc.
+ if (trimmed.StartsWith("> [!") || trimmed.StartsWith("[!")) return null;
+
+ if (WarningCalloutRegex.IsMatch(trimmed))
+ {
+ var text = WarningCalloutRegex.Match(trimmed).Groups[1].Value.Trim();
+ return new List { "> [!WARNING]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" };
+ }
+ if (ImportantCalloutRegex.IsMatch(trimmed))
+ {
+ var text = ImportantCalloutRegex.Match(trimmed).Groups[1].Value.Trim();
+ return new List { "> [!IMPORTANT]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" };
+ }
+ if (TipCalloutRegex.IsMatch(trimmed))
+ {
+ var text = TipCalloutRegex.Match(trimmed).Groups[1].Value.Trim();
+ return new List { "> [!TIP]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" };
+ }
+ if (NoteCalloutRegex.IsMatch(trimmed))
+ {
+ var text = NoteCalloutRegex.Match(trimmed).Groups[1].Value.Trim();
+ return new List { "> [!NOTE]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" };
+ }
+ if (CautionCalloutRegex.IsMatch(trimmed))
+ {
+ var text = CautionCalloutRegex.Match(trimmed).Groups[1].Value.Trim();
+ return new List { "> [!CAUTION]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" };
+ }
+
+ return null;
+ }
+
+ private static bool IsTableStart(string[] lines, int currentIndex)
+ {
+ if (currentIndex + 1 >= lines.Length) return false;
+
+ var current = lines[currentIndex].Trim();
+ var next = lines[currentIndex + 1].Trim();
+
+ if (!current.Contains('|') || !next.Contains('|')) return false;
+
+ // Next line must be table separator: containing only |, -, :, and spaces
+ var separatorClean = next.Replace("|", "").Replace("-", "").Replace(":", "").Trim();
+ return separatorClean.Length == 0 && next.Contains('-');
+ }
+
+ private static (List FormattedLines, int NextIndex) FormatTableBlock(string[] lines, int startIndex)
+ {
+ var tableRawLines = new List();
+ int currentIndex = startIndex;
+
+ while (currentIndex < lines.Length)
+ {
+ var line = lines[currentIndex];
+ var trimmed = line.Trim();
+ if (string.IsNullOrWhiteSpace(trimmed) || !trimmed.Contains('|'))
+ {
+ break;
+ }
+ tableRawLines.Add(trimmed);
+ currentIndex++;
+ }
+
+ if (tableRawLines.Count < 2)
+ {
+ return (tableRawLines, currentIndex);
+ }
+
+ // Parse cells for all rows
+ var parsedRows = new List>();
+ foreach (var r in tableRawLines)
+ {
+ var cells = SplitTableCells(r);
+ parsedRows.Add(cells);
+ }
+
+ // Determine max column count
+ int colCount = parsedRows.Max(r => r.Count);
+ if (colCount == 0) return (tableRawLines, currentIndex);
+
+ // Normalize all rows to colCount
+ foreach (var row in parsedRows)
+ {
+ while (row.Count < colCount)
+ {
+ row.Add("");
+ }
+ }
+
+ // Trim trailing columns if they are empty in every non-separator row
+ while (colCount > 1)
+ {
+ bool allEmptyInLastCol = true;
+ for (int r = 0; r < parsedRows.Count; r++)
+ {
+ if (r == 1) continue; // skip separator
+ if (!string.IsNullOrWhiteSpace(parsedRows[r][colCount - 1]))
+ {
+ allEmptyInLastCol = false;
+ break;
+ }
+ }
+ if (allEmptyInLastCol)
+ {
+ colCount--;
+ foreach (var row in parsedRows)
+ {
+ if (row.Count > colCount) row.RemoveAt(colCount);
+ }
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ // Ensure header cells in row 0 are not blank
+ for (int c = 0; c < colCount; c++)
+ {
+ if (string.IsNullOrWhiteSpace(parsedRows[0][c]))
+ {
+ parsedRows[0][c] = $"Column {c + 1}";
+ }
+ }
+
+ // Parse alignments from separator (row 1)
+ var alignments = new List();
+ var separatorRow = parsedRows[1];
+ for (int c = 0; c < colCount; c++)
+ {
+ var cell = c < separatorRow.Count ? separatorRow[c].Trim() : "";
+ bool left = cell.StartsWith(':');
+ bool right = cell.EndsWith(':');
+
+ if (left && right)
+ alignments.Add(ColumnAlign.Center);
+ else if (right)
+ alignments.Add(ColumnAlign.Right);
+ else
+ alignments.Add(ColumnAlign.Left);
+ }
+
+ // Filter out any duplicate separator rows inside data rows (r >= 2)
+ var dataRows = new List>();
+ for (int r = 2; r < parsedRows.Count; r++)
+ {
+ var row = parsedRows[r];
+ bool isDashesOnly = row.All(c => {
+ var clean = c.Replace("-", "").Replace(":", "").Trim();
+ return clean.Length == 0 && c.Contains('-');
+ });
+ if (!isDashesOnly)
+ {
+ dataRows.Add(row);
+ }
+ }
+
+ // Determine column widths
+ var colWidths = new int[colCount];
+ for (int c = 0; c < colCount; c++)
+ {
+ int maxWidth = 3; // minimum 3 chars (e.g. ---)
+ maxWidth = Math.Max(maxWidth, parsedRows[0][c].Trim().Length);
+ for (int r = 0; r < dataRows.Count; r++)
+ {
+ maxWidth = Math.Max(maxWidth, dataRows[r][c].Trim().Length);
+ }
+ colWidths[c] = maxWidth;
+ }
+
+ // Reconstruct formatted table
+ var result = new List();
+
+ // 1. Header row
+ result.Add(FormatTableRow(parsedRows[0], colWidths, alignments));
+
+ // 2. Separator row
+ var sepSb = new StringBuilder();
+ sepSb.Append('|');
+ for (int c = 0; c < colCount; c++)
+ {
+ var width = colWidths[c];
+ var align = alignments[c];
+ sepSb.Append(' ');
+ if (align == ColumnAlign.Center)
+ {
+ sepSb.Append(':');
+ sepSb.Append(new string('-', Math.Max(1, width - 2)));
+ sepSb.Append(':');
+ }
+ else if (align == ColumnAlign.Right)
+ {
+ sepSb.Append(new string('-', Math.Max(2, width - 1)));
+ sepSb.Append(':');
+ }
+ else
+ {
+ sepSb.Append(':');
+ sepSb.Append(new string('-', Math.Max(2, width - 1)));
+ }
+ sepSb.Append(" |");
+ }
+ result.Add(sepSb.ToString());
+
+ // 3. Data rows
+ for (int r = 0; r < dataRows.Count; r++)
+ {
+ result.Add(FormatTableRow(dataRows[r], colWidths, alignments));
+ }
+
+ return (result, currentIndex);
+ }
+
+ private static List SplitTableCells(string row)
+ {
+ var trimmed = row.Trim();
+ if (trimmed.StartsWith('|')) trimmed = trimmed[1..];
+ if (trimmed.EndsWith('|')) trimmed = trimmed[..^1];
+
+ var raw = trimmed.Split('|');
+ return raw.Select(s => s.Trim()).ToList();
+ }
+
+ private static string FormatTableRow(List cells, int[] colWidths, List alignments)
+ {
+ var sb = new StringBuilder();
+ sb.Append('|');
+ for (int c = 0; c < colWidths.Length; c++)
+ {
+ var text = c < cells.Count ? cells[c].Trim() : "";
+ var width = colWidths[c];
+ var align = c < alignments.Count ? alignments[c] : ColumnAlign.Left;
+
+ sb.Append(' ');
+ if (align == ColumnAlign.Right)
+ {
+ sb.Append(text.PadLeft(width));
+ }
+ else if (align == ColumnAlign.Center)
+ {
+ int totalPad = width - text.Length;
+ int padLeft = totalPad / 2;
+ int padRight = totalPad - padLeft;
+ sb.Append(new string(' ', Math.Max(0, padLeft)));
+ sb.Append(text);
+ sb.Append(new string(' ', Math.Max(0, padRight)));
+ }
+ else
+ {
+ sb.Append(text.PadRight(width));
+ }
+ sb.Append(" |");
+ }
+ return sb.ToString();
+ }
+
+ private static string NormalizeBlankLines(List lines)
+ {
+ var output = new List();
+ bool prevWasBlank = false;
+
+ foreach (var line in lines)
+ {
+ var isBlank = string.IsNullOrWhiteSpace(line);
+ if (isBlank)
+ {
+ if (!prevWasBlank && output.Count > 0)
+ {
+ output.Add(string.Empty);
+ prevWasBlank = true;
+ }
+ }
+ else
+ {
+ output.Add(line);
+ prevWasBlank = false;
+ }
+ }
+
+ // Trim trailing blank lines
+ while (output.Count > 0 && string.IsNullOrWhiteSpace(output[^1]))
+ {
+ output.RemoveAt(output.Count - 1);
+ }
+
+ return string.Join("\n", output) + "\n";
+ }
+
+ private enum ColumnAlign
+ {
+ Left,
+ Center,
+ Right
+ }
+}
diff --git a/Notemark/Services/DocumentService.cs b/Notemark/Services/DocumentService.cs
index 8e4781f..b396539 100644
--- a/Notemark/Services/DocumentService.cs
+++ b/Notemark/Services/DocumentService.cs
@@ -107,7 +107,7 @@ public partial class DocumentService : IDocumentService
return docs.FirstOrDefault(d => d.Id.Equals(id, StringComparison.OrdinalIgnoreCase));
}
- public async Task SaveDocumentAsync(string id, string content, bool? isFlagged = null, string? customTitle = null, List? tags = null)
+ public async Task SaveDocumentAsync(string id, string content, bool? isFlagged = null, string? customTitle = null, List? tags = null, string? docStyle = null)
{
if (string.IsNullOrEmpty(id))
throw new ArgumentException("Document ID cannot be empty", nameof(id));
@@ -132,6 +132,7 @@ public partial class DocumentService : IDocumentService
Snippet = snippet,
IsFlagged = isFlagged ?? existing.IsFlagged,
Tags = extractedTags,
+ DocStyle = docStyle ?? existing.DocStyle ?? "modern",
LastModifiedAt = DateTime.UtcNow
};
docs[existingIndex] = doc;
@@ -145,6 +146,7 @@ public partial class DocumentService : IDocumentService
Snippet = snippet,
IsFlagged = isFlagged ?? false,
Tags = extractedTags,
+ DocStyle = docStyle ?? "modern",
CreatedAt = DateTime.UtcNow,
LastModifiedAt = DateTime.UtcNow
};
diff --git a/Notemark/Services/IAutoStyleService.cs b/Notemark/Services/IAutoStyleService.cs
new file mode 100644
index 0000000..01f99fc
--- /dev/null
+++ b/Notemark/Services/IAutoStyleService.cs
@@ -0,0 +1,9 @@
+namespace Notemark.Services;
+
+public interface IAutoStyleService
+{
+ ///
+ /// Formats and cleans markdown content (aligns tables, fixes headings, cleans whitespace, converts smart callouts).
+ ///
+ string FormatMarkdown(string markdown);
+}
diff --git a/Notemark/Services/IDocumentService.cs b/Notemark/Services/IDocumentService.cs
index c30d31d..1722310 100644
--- a/Notemark/Services/IDocumentService.cs
+++ b/Notemark/Services/IDocumentService.cs
@@ -11,7 +11,7 @@ public interface IDocumentService
Task> GetDocumentsByTagAsync(string tag);
Task GetMostRecentDocumentAsync();
Task GetDocumentAsync(string id);
- Task SaveDocumentAsync(string id, string content, bool? isFlagged = null, string? customTitle = null, List? tags = null);
+ Task SaveDocumentAsync(string id, string content, bool? isFlagged = null, string? customTitle = null, List? tags = null, string? docStyle = null);
Task ToggleFlagAsync(string id);
Task DeleteDocumentAsync(string id);
string ExtractTitle(string content);
diff --git a/Notemark/Services/MarkdownService.cs b/Notemark/Services/MarkdownService.cs
index dff370b..8d347c4 100644
--- a/Notemark/Services/MarkdownService.cs
+++ b/Notemark/Services/MarkdownService.cs
@@ -13,6 +13,7 @@ public class MarkdownService : IMarkdownService
// Configure Markdig pipeline with GitHub-style features
_pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions() // Tables, footnotes, task lists, etc.
+ .UseAlertBlocks() // GitHub-style alerts (> [!NOTE], > [!WARNING], etc.)
.UseSoftlineBreakAsHardlineBreak() // GitHub-style line breaks
.Build();
@@ -29,10 +30,127 @@ public class MarkdownService : IMarkdownService
if (string.IsNullOrEmpty(markdown))
return string.Empty;
+ // Preprocess markdown to ensure tables are correctly surrounded by blank lines for CommonMark / Markdig parser
+ var preprocessed = EnsureTableSpacing(markdown);
+
// Convert markdown to HTML
- var html = Markdown.ToHtml(markdown, _pipeline);
+ var html = Markdown.ToHtml(preprocessed, _pipeline);
// Sanitize HTML to prevent XSS attacks
return _sanitizer.Sanitize(html);
}
+
+ public static string EnsureTableSpacing(string markdown)
+ {
+ if (string.IsNullOrEmpty(markdown)) return markdown;
+
+ var lines = markdown.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n');
+ var result = new List(lines.Length + 8);
+ bool inCodeBlock = false;
+ string codeFence = "";
+ bool inFrontmatter = false;
+ bool inTable = false;
+
+ for (int i = 0; i < lines.Length; i++)
+ {
+ var line = lines[i];
+ var trimmed = line.Trim();
+
+ // 1. Frontmatter
+ if (i == 0 && trimmed == "---")
+ {
+ inFrontmatter = true;
+ result.Add(line);
+ continue;
+ }
+ if (inFrontmatter)
+ {
+ result.Add(line);
+ if (trimmed == "---") inFrontmatter = false;
+ continue;
+ }
+
+ // 2. Code blocks
+ if (trimmed.StartsWith("```") || trimmed.StartsWith("~~~"))
+ {
+ var fence = trimmed.Length >= 3 ? trimmed[..3] : "```";
+ if (!inCodeBlock)
+ {
+ inCodeBlock = true;
+ codeFence = fence;
+ }
+ else if (trimmed.StartsWith(codeFence))
+ {
+ inCodeBlock = false;
+ codeFence = "";
+ }
+ result.Add(line);
+ inTable = false;
+ continue;
+ }
+
+ if (inCodeBlock)
+ {
+ result.Add(line);
+ continue;
+ }
+
+ // 3. Table detection
+ if (!inTable && IsTableStartLine(lines, i))
+ {
+ // Ensure blank line before table if previous line is not blank
+ if (result.Count > 0 && !string.IsNullOrWhiteSpace(result[^1]))
+ {
+ result.Add(string.Empty);
+ }
+ inTable = true;
+ result.Add(line);
+ continue;
+ }
+
+ if (inTable)
+ {
+ if (IsTableRowLine(line))
+ {
+ result.Add(line);
+ continue;
+ }
+ else
+ {
+ // Table ended
+ inTable = false;
+ if (!string.IsNullOrWhiteSpace(line))
+ {
+ // Ensure blank line after table
+ result.Add(string.Empty);
+ }
+ result.Add(line);
+ continue;
+ }
+ }
+
+ result.Add(line);
+ }
+
+ return string.Join("\n", result);
+ }
+
+ private static bool IsTableStartLine(string[] lines, int index)
+ {
+ if (index + 1 >= lines.Length) return false;
+ var current = lines[index].Trim();
+ var next = lines[index + 1].Trim();
+
+ if (!current.Contains('|') || !next.Contains('|')) return false;
+
+ // Next line must be a table delimiter line containing only |, -, :, and whitespace
+ var cleanDelim = next.Replace("|", "").Replace("-", "").Replace(":", "").Trim();
+ return cleanDelim.Length == 0 && next.Contains('-');
+ }
+
+ private static bool IsTableRowLine(string line)
+ {
+ var trimmed = line.Trim();
+ return !string.IsNullOrWhiteSpace(trimmed) && trimmed.Contains('|');
+ }
}
\ No newline at end of file
diff --git a/Notemark/data/documents.json b/Notemark/data/documents.json
deleted file mode 100644
index dabf435..0000000
--- a/Notemark/data/documents.json
+++ /dev/null
@@ -1,11 +0,0 @@
-[
- {
- "id": "2ba5f046-c8d8-4e55-acf4-5e654dee5afc",
- "title": "Kopanie",
- "isFlagged": true,
- "tags": [],
- "createdAt": "2026-09-22T09:08:10.0528137Z",
- "lastModifiedAt": "2026-09-22T09:45:38.5280677Z",
- "snippet": "Kopanie Moj otec mi v\u017Edy hovoril, \u017Ee do pivnice sa nechod\u00ED. Nie preto, \u017Ee by tam bolo nebezpe\u010Dn\u00E9 \u2014 aspo\u0148 tak to tvrdil \u2014..."
- }
-]
\ No newline at end of file
diff --git a/Notemark/wwwroot/app.css b/Notemark/wwwroot/app.css
index 43772d0..f37a46a 100644
--- a/Notemark/wwwroot/app.css
+++ b/Notemark/wwwroot/app.css
@@ -123,4 +123,135 @@ a:hover {
.blazor-error-boundary::after {
content: "An error has occurred.";
+}
+
+/* ========================================================= */
+/* GLOBAL MARKDOWN PREVIEW STYLING */
+/* ========================================================= */
+
+/* Base Tables */
+.preview-content table {
+ width: 100% !important;
+ border-collapse: collapse !important;
+ margin: 1.5em 0 !important;
+ border: 1.5px solid var(--border) !important;
+ border-radius: 8px !important;
+ overflow: hidden !important;
+ font-size: 0.9375rem !important;
+ display: table !important;
+}
+
+.preview-content th,
+.preview-content td {
+ padding: 0.75rem 1rem !important;
+ border: 1px solid var(--border) !important;
+ text-align: left;
+}
+
+.preview-content th {
+ background-color: var(--surface-hover) !important;
+ font-weight: 600 !important;
+ color: var(--primary-text) !important;
+ border-bottom: 2px solid var(--border) !important;
+}
+
+.preview-content tr:hover td {
+ background-color: var(--surface-hover);
+}
+
+/* Modern Preset Tables */
+.preview-content[data-doc-style="modern"] table,
+.preview-content.doc-style-modern table {
+ border: 1.5px solid var(--border) !important;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
+}
+
+.preview-content[data-doc-style="modern"] th,
+.preview-content.doc-style-modern th {
+ background-color: var(--surface-hover) !important;
+ color: var(--primary-text) !important;
+ border: 1px solid var(--border) !important;
+ border-bottom: 2px solid var(--accent) !important;
+}
+
+.preview-content[data-doc-style="modern"] td,
+.preview-content.doc-style-modern td {
+ border: 1px solid var(--border) !important;
+}
+
+/* Academic Preset Tables */
+.preview-content[data-doc-style="academic"] table,
+.preview-content.doc-style-academic table {
+ border: 1px solid rgba(128, 128, 128, 0.4) !important;
+ border-top: 2.5px solid currentColor !important;
+ border-bottom: 2.5px solid currentColor !important;
+}
+
+.preview-content[data-doc-style="academic"] th,
+.preview-content.doc-style-academic th {
+ border: 1px solid rgba(128, 128, 128, 0.35) !important;
+ border-bottom: 2px solid currentColor !important;
+ background: rgba(0, 0, 0, 0.03) !important;
+}
+
+.preview-content[data-doc-style="academic"] td,
+.preview-content.doc-style-academic td {
+ border: 1px solid rgba(128, 128, 128, 0.25) !important;
+}
+
+/* Technical Preset Tables */
+.preview-content[data-doc-style="technical"] table,
+.preview-content.doc-style-technical table {
+ border: 1.5px solid #30363d !important;
+ background-color: #161b22 !important;
+ font-family: 'JetBrains Mono', monospace !important;
+}
+
+.preview-content[data-doc-style="technical"] th,
+.preview-content.doc-style-technical th {
+ background-color: #21262d !important;
+ color: #7ee787 !important;
+ border: 1px solid #30363d !important;
+}
+
+.preview-content[data-doc-style="technical"] td,
+.preview-content.doc-style-technical td {
+ border: 1px solid #30363d !important;
+ color: #c9d1d9 !important;
+}
+
+/* Executive Preset Tables */
+.preview-content[data-doc-style="executive"] table,
+.preview-content.doc-style-executive table {
+ border: 1.5px solid #cbd5e1 !important;
+ box-shadow: 0 4px 6px -1px rgba(15, 39, 68, 0.06) !important;
+}
+
+:root[data-theme="dark"] .preview-content[data-doc-style="executive"] table,
+:root[data-theme="dark"] .preview-content.doc-style-executive table {
+ border-color: #334155 !important;
+}
+
+.preview-content[data-doc-style="executive"] th,
+.preview-content.doc-style-executive th {
+ background-color: #0f2744 !important;
+ color: #ffffff !important;
+ border: 1px solid #1e3a5f !important;
+}
+
+:root[data-theme="dark"] .preview-content[data-doc-style="executive"] th,
+:root[data-theme="dark"] .preview-content.doc-style-executive th {
+ background-color: #1e293b !important;
+ color: #38bdf8 !important;
+ border: 1px solid #334155 !important;
+}
+
+.preview-content[data-doc-style="executive"] td,
+.preview-content.doc-style-executive td {
+ border: 1px solid #cbd5e1 !important;
+}
+
+:root[data-theme="dark"] .preview-content[data-doc-style="executive"] td,
+:root[data-theme="dark"] .preview-content.doc-style-executive td {
+ border: 1px solid #334155 !important;
}
\ No newline at end of file
diff --git a/Notemark/wwwroot/js/editor.js b/Notemark/wwwroot/js/editor.js
index b81c845..bf75fd2 100644
--- a/Notemark/wwwroot/js/editor.js
+++ b/Notemark/wwwroot/js/editor.js
@@ -256,29 +256,382 @@ window.notemarkEditor = {
}, { passive: false });
},
- // Download standalone styled HTML file
- downloadHtml: function(filename, title, htmlBody, theme) {
+ // Download standalone styled HTML file with chosen document style & theme
+ downloadHtml: function(filename, title, htmlBody, theme, docStyle) {
+ const style = docStyle || 'modern';
const fullHtml = `
-
+
${title || 'Notemark Document'}
+
+
+