CadParamPluging/Common/NoteTemplateEngine.cs

410 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<PlaceholderOccurrence> ParseOccurrences(string text)
{
var result = new List<PlaceholderOccurrence>();
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<NotePlaceholderBinding> bindings, Func<string, string> 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<int, string>();
foreach (var b in bindings ?? Enumerable.Empty<NotePlaceholderBinding>())
{
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<string>();
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
// (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);
}
/// <summary>
/// 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.
/// </summary>
public static List<NotePlaceholderBinding> BuildEffectiveValueKeyBindings(IEnumerable<NotePlaceholderBinding> bindings)
{
var list = (bindings ?? Enumerable.Empty<NotePlaceholderBinding>())
.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");
}
}
}