426 lines
16 KiB
C#
426 lines
16 KiB
C#
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
|
||
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);
|
||
}
|
||
|
||
/// <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");
|
||
}
|
||
}
|
||
}
|