using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace CadParamPluging.Common { public static class NoteTemplateEngine { public sealed class PlaceholderOccurrence { public int Index { get; set; } public int Start { get; set; } public int LineNumber { get; set; } public int ColumnInLine { get; set; } public string LineText { get; set; } } public static int CountPlaceholders(string text) { if (string.IsNullOrEmpty(text)) { return 0; } var count = 0; for (var i = 0; i < text.Length;) { if (text[i] != '*') { i++; continue; } var j = i; while (j < text.Length && text[j] == '*') { j++; } var len = j - i; if (len != 4) { count += len; } i = j; } return count; } public static List ParseOccurrences(string text) { var result = new List(); if (string.IsNullOrEmpty(text)) { return result; } var s = NormalizeNewLines(text); // Keep empty lines for correct line number mapping. var lines = s.Split(new[] { '\n' }, StringSplitOptions.None); // Build line start offsets. var lineStartOffsets = new int[lines.Length]; var offset = 0; for (var i = 0; i < lines.Length; i++) { lineStartOffsets[i] = offset; offset += (lines[i] ?? string.Empty).Length; if (i < lines.Length - 1) { offset += 1; // '\n' } } var placeholderIndex = 0; var lineIdx = 0; var lineStart = lines.Length > 0 ? lineStartOffsets[0] : 0; for (var i = 0; i < s.Length;) { // Move line index if needed. while (lineIdx + 1 < lineStartOffsets.Length && i >= lineStartOffsets[lineIdx] + (lines[lineIdx] ?? string.Empty).Length + 1) { lineIdx++; lineStart = lineStartOffsets[lineIdx]; } if (s[i] != '*') { i++; continue; } var j = i; while (j < s.Length && s[j] == '*') { j++; } var runLen = j - i; if (runLen == 4) { i = j; continue; } var lineText = (lineIdx >= 0 && lineIdx < lines.Length) ? (lines[lineIdx] ?? string.Empty) : string.Empty; for (var k = 0; k < runLen; k++) { placeholderIndex++; var pos = i + k; var col = pos - lineStart; if (col < 0) col = 0; result.Add(new PlaceholderOccurrence { Index = placeholderIndex, Start = pos, LineNumber = lineIdx + 1, ColumnInLine = col, LineText = lineText }); } i = j; } return result; } public static string BuildNumberedPreviewText(string templateText) { templateText = templateText ?? string.Empty; var s = NormalizeNewLines(templateText); var sb = new StringBuilder(s.Length + 64); var placeholderIndex = 0; for (var i = 0; i < s.Length;) { if (s[i] != '*') { sb.Append(s[i]); i++; continue; } var j = i; while (j < s.Length && s[j] == '*') { j++; } var runLen = j - i; if (runLen == 4) { sb.Append("****"); i = j; continue; } for (var k = 0; k < runLen; k++) { placeholderIndex++; sb.Append("【#"); sb.Append(placeholderIndex); sb.Append("】"); } i = j; } return sb.ToString(); } public static string Render(string templateText, IEnumerable bindings, Func getValue) { templateText = templateText ?? string.Empty; // Normalize newlines to \n var s = NormalizeNewLines(templateText); // Split into lines to process removal logic var lines = s.Split(new[] { '\n' }, StringSplitOptions.None); // Build map var map = new Dictionary(); foreach (var b in bindings ?? Enumerable.Empty()) { if (b != null && b.Index > 0 && !string.IsNullOrWhiteSpace(b.ParamKey)) { var key = b.ParamKey.Trim(); if (!map.ContainsKey(b.Index)) { map[b.Index] = key; } } } var resultLines = new List(); var placeholderIndex = 0; foreach (var line in lines) { // 1. Scan line to determine if it should be skipped (if any param value is "空") bool shouldSkip = false; int localPlaceholderCount = 0; // We must traverse the line to find placeholders correctly to sync index for (int i = 0; i < line.Length; ) { if (line[i] != '*') { i++; continue; } // Check for escape **** int j = i; while (j < line.Length && line[j] == '*') j++; if (j - i == 4) { // **** counts as text, not placeholder i = j; continue; } // Run of * (length != 4) int runLen = j - i; for (int k = 0; k < runLen; k++) { localPlaceholderCount++; int currentIdx = placeholderIndex + localPlaceholderCount; if (map.TryGetValue(currentIdx, out var key) && getValue != null) { var val = getValue(key); if (string.Equals(val, "__SKIP_NOTE__", StringComparison.OrdinalIgnoreCase)) { shouldSkip = true; } else if (string.Equals(val, "空", StringComparison.OrdinalIgnoreCase)) { // Exception: MarkingContent (标刻内容) and Hardness (硬度) should NOT cause line skip if (key == null || !(key.StartsWith("MarkingContent", StringComparison.OrdinalIgnoreCase) || key.StartsWith("Hardness", StringComparison.OrdinalIgnoreCase))) { shouldSkip = true; } } } } i = j; } // 2. If line is skipped, just update global index and continue if (shouldSkip) { placeholderIndex += localPlaceholderCount; continue; } // 3. If line matches, render it // We re-scan to apply values. // Using a temp index starting from where we were var sb = new StringBuilder(line.Length + 64); int tempIdx = placeholderIndex; for (int i = 0; i < line.Length; ) { if (line[i] != '*') { sb.Append(line[i]); i++; continue; } int j = i; while (j < line.Length && line[j] == '*') j++; if (j - i == 4) { sb.Append("****"); i = j; continue; } int runLen = j - i; for (int k = 0; k < runLen; k++) { tempIdx++; if (map.TryGetValue(tempIdx, out var key) && getValue != null) { var v = getValue(key); if (!string.IsNullOrWhiteSpace(v)) { // Handle "空" value if (string.Equals(v, "空", StringComparison.OrdinalIgnoreCase)) { if (key != null && (key.StartsWith("MarkingContent", StringComparison.OrdinalIgnoreCase) || key.StartsWith("Hardness", StringComparison.OrdinalIgnoreCase))) { // MarkingContent / Hardness -> render empty string if (key.StartsWith("Hardness", StringComparison.OrdinalIgnoreCase)) { int rIdx = sb.Length - 1; while (rIdx >= 0 && char.IsWhiteSpace(sb[rIdx])) { rIdx--; } if (rIdx >= 0) { char lc = sb[rIdx]; if (lc == ',' || lc == ',' || lc == '.' || lc == '。' || lc == ';' || lc == ';' || lc == '、') { sb.Length = rIdx; } } } // (Separators will be cleaned up later) } else { // Other keys -> render empty (should imply hidden line, // but if we are here, it means some OTHER key prevented hiding? // Or this case shouldn't happen if mixed? // Assuming if line is not skipped, we treat "空" as empty string.) } } else { sb.Append(v); } } else { // Keep * if value is missing/empty (but not "空" which was handled) sb.Append('*'); } } else { sb.Append('*'); } } i = j; } var renderedLine = sb.ToString(); // 3.1 Clean up redundant separators for MarkingContent removal // Replace "、、" with "、" renderedLine = Regex.Replace(renderedLine, @"、\s*、", "、"); // Replace ":、" with ":" (start of list) renderedLine = Regex.Replace(renderedLine, @"([::])\s*、", "$1"); // Replace "、。" with "。" (end of list) renderedLine = Regex.Replace(renderedLine, @"、\s*([。.;;])", "$1"); // Trim trailing "、" if it exists (no period) renderedLine = Regex.Replace(renderedLine, @"、\s*$", ""); resultLines.Add(renderedLine); placeholderIndex += localPlaceholderCount; } // 4. Renumbering Logic var renumberRegex = new Regex(@"^(\s*)(\d+)([、\.])"); int counter = 1; for (int i = 0; i < resultLines.Count; i++) { var match = renumberRegex.Match(resultLines[i]); if (match.Success) { var prefix = match.Groups[1].Value; var suffix = match.Groups[3].Value; var newLine = prefix + counter + suffix + resultLines[i].Substring(match.Length); resultLines[i] = newLine; counter++; } } return string.Join("\n", resultLines); } /// /// When the same ParamKey is bound by multiple placeholders, each placeholder should have its own value key, /// otherwise values would overwrite each other. /// This method returns an equivalent bindings list where ParamKey is replaced by an effective value key. /// public static List BuildEffectiveValueKeyBindings(IEnumerable bindings) { var list = (bindings ?? Enumerable.Empty()) .Where(b => b != null && b.Index > 0 && !string.IsNullOrWhiteSpace(b.ParamKey)) .Select(b => new NotePlaceholderBinding { Index = b.Index, ParamKey = b.ParamKey.Trim() }) .ToList(); var dup = list .GroupBy(b => b.ParamKey, StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase); foreach (var b in list) { if (dup.TryGetValue(b.ParamKey, out var cnt) && cnt > 1) { b.ParamKey = string.Format("{0}@{1}", b.ParamKey, b.Index); } } return list; } private static string NormalizeNewLines(string text) { return (text ?? string.Empty) .Replace("\r\n", "\n") .Replace("\r", "\n"); } } }