множественные замены с параметром

This commit is contained in:
melekhin
2026-09-10 15:53:51 +07:00
parent eb3055a2e8
commit 70a19ad738
3 changed files with 621 additions and 25 deletions
@@ -0,0 +1,220 @@
namespace QWERTYkez.WordProcessor;
/// <summary>
/// Множественные динамические замены с извлечением параметра из "$key|param$".
/// Переиспользует движок <see cref="MultiReplaceExt"/> (форматирование Run-ов,
/// разрывы страниц, смена ориентации, разбиение на параграфы — всё как в оригинале).
/// </summary>
internal static class DynamicMultiReplaceExt
{
// ============================================================
// ReplaceString (Func<string, IEnumerable<string>>)
// ============================================================
internal static void ReplaceStringMultiInBody(
Body body,
string oldValue,
Func<string, IEnumerable<string>> factory,
StringComparison comparisonType)
{
if (body is null || factory is null) return;
DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
var paragraphs = body.Elements<Paragraph>().ToList();
for (int i = paragraphs.Count - 1; i >= 0; i--)
ProcessDynamicStringMulti(paragraphs[i], prefix, suffix, factory, comparisonType);
}
internal static void ReplaceStringMultiInTables(
OpenXmlElement element,
string oldValue,
Func<string, IEnumerable<string>> factory,
StringComparison comparisonType)
{
if (element is null || factory is null) return;
DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
foreach (var table in TableTextProcessor.GetAllTables(element))
{
var paragraphs = TableTextProcessor.GetTableParagraphs(table).ToList();
for (int i = paragraphs.Count - 1; i >= 0; i--)
ProcessDynamicStringMulti(paragraphs[i], prefix, suffix, factory, comparisonType);
}
}
internal static void ReplaceStringMultiInHeadersFooters(
WordprocessingDocument doc,
string oldValue,
Func<string, IEnumerable<string>> factory,
StringComparison comparisonType)
{
if (doc is null || factory is null) return;
DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
foreach (var header in HeaderFooterProcessor.GetAllHeaders(doc))
ProcessStringMultiInElement(header, prefix, suffix, factory, comparisonType);
foreach (var footer in HeaderFooterProcessor.GetAllFooters(doc))
ProcessStringMultiInElement(footer, prefix, suffix, factory, comparisonType);
}
private static void ProcessStringMultiInElement(
OpenXmlElement element,
string prefix,
string suffix,
Func<string, IEnumerable<string>> factory,
StringComparison comparisonType)
{
var paragraphs = element.Descendants<Paragraph>().ToList();
var tables = TableTextProcessor.GetAllTables(element).ToList();
var tableParagraphs = tables.SelectMany(t => TableTextProcessor.GetTableParagraphs(t)).ToList();
var allParagraphs = paragraphs.Concat(tableParagraphs).Distinct().ToList();
for (int i = allParagraphs.Count - 1; i >= 0; i--)
ProcessDynamicStringMulti(allParagraphs[i], prefix, suffix, factory, comparisonType);
}
private static void ProcessDynamicStringMulti(
Paragraph paragraph,
string prefix,
string suffix,
Func<string, IEnumerable<string>> factory,
StringComparison comparisonType)
{
if (paragraph is null || paragraph.Parent is null) return;
var text = paragraph.InnerText;
if (string.IsNullOrEmpty(text)) return;
var matches = DynamicReplaceExt.FindMatches(text, prefix, suffix, comparisonType);
if (matches.Count == 0) return;
// Строим словарь уникальных matchedText -> значения для этого параграфа
var dict = new Dictionary<string, IEnumerable<string>>();
for (int i = 0; i < matches.Count; i++)
{
var m = matches[i];
string matchedText = text.Substring(m.Start, m.End - m.Start);
if (dict.ContainsKey(matchedText)) continue;
IEnumerable<string> values;
try { values = factory(m.Param); }
catch { continue; }
if (values is null) continue;
var list = values as IList<string> ?? values.ToList();
if (list.Count == 0) continue;
dict[matchedText] = list;
}
if (dict.Count == 0) return;
var newParas = MultiReplaceExt.ProcessParagraphWithMergedBoundaries(paragraph, dict, comparisonType);
if (newParas is not null && newParas.Count > 0)
ParagraphReplacer.ReplaceParagraph(paragraph, newParas);
}
// ============================================================
// ReplaceItem (Func<string, IEnumerable<ReplaceItem>>)
// ============================================================
internal static void ReplaceItemMultiInBody(
Body body,
string oldValue,
Func<string, IEnumerable<ReplaceItem>> factory,
StringComparison comparisonType)
{
if (body is null || factory is null) return;
DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
var paragraphs = body.Elements<Paragraph>().ToList();
for (int i = paragraphs.Count - 1; i >= 0; i--)
ProcessDynamicItemMulti(paragraphs[i], prefix, suffix, factory, comparisonType);
}
internal static void ReplaceItemMultiInTables(
OpenXmlElement element,
string oldValue,
Func<string, IEnumerable<ReplaceItem>> factory,
StringComparison comparisonType)
{
if (element is null || factory is null) return;
DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
foreach (var table in TableTextProcessor.GetAllTables(element))
{
var paragraphs = TableTextProcessor.GetTableParagraphs(table).ToList();
for (int i = paragraphs.Count - 1; i >= 0; i--)
ProcessDynamicItemMulti(paragraphs[i], prefix, suffix, factory, comparisonType);
}
}
internal static void ReplaceItemMultiInHeadersFooters(
WordprocessingDocument doc,
string oldValue,
Func<string, IEnumerable<ReplaceItem>> factory,
StringComparison comparisonType)
{
if (doc is null || factory is null) return;
DynamicReplaceExt.ParsePattern(oldValue, out var prefix, out var suffix);
foreach (var header in HeaderFooterProcessor.GetAllHeaders(doc))
ProcessItemMultiInElement(header, prefix, suffix, factory, comparisonType);
foreach (var footer in HeaderFooterProcessor.GetAllFooters(doc))
ProcessItemMultiInElement(footer, prefix, suffix, factory, comparisonType);
}
private static void ProcessItemMultiInElement(
OpenXmlElement element,
string prefix,
string suffix,
Func<string, IEnumerable<ReplaceItem>> factory,
StringComparison comparisonType)
{
var paragraphs = element.Descendants<Paragraph>().ToList();
var tables = TableTextProcessor.GetAllTables(element).ToList();
var tableParagraphs = tables.SelectMany(t => TableTextProcessor.GetTableParagraphs(t)).ToList();
var allParagraphs = paragraphs.Concat(tableParagraphs).Distinct().ToList();
for (int i = allParagraphs.Count - 1; i >= 0; i--)
ProcessDynamicItemMulti(allParagraphs[i], prefix, suffix, factory, comparisonType);
}
private static void ProcessDynamicItemMulti(
Paragraph paragraph,
string prefix,
string suffix,
Func<string, IEnumerable<ReplaceItem>> factory,
StringComparison comparisonType)
{
if (paragraph is null || paragraph.Parent is null) return;
var text = paragraph.InnerText;
if (string.IsNullOrEmpty(text)) return;
var matches = DynamicReplaceExt.FindMatches(text, prefix, suffix, comparisonType);
if (matches.Count == 0) return;
var dict = new Dictionary<string, IEnumerable<ReplaceItem>>();
for (int i = 0; i < matches.Count; i++)
{
var m = matches[i];
string matchedText = text.Substring(m.Start, m.End - m.Start);
if (dict.ContainsKey(matchedText)) continue;
IEnumerable<ReplaceItem> values;
try { values = factory(m.Param); }
catch { continue; }
if (values is null) continue;
var list = values as IList<ReplaceItem> ?? values.ToList();
if (list.Count == 0) continue;
dict[matchedText] = list;
}
if (dict.Count == 0) return;
var newParas = MultiReplaceExt.ProcessParagraphWithMergedBoundaries(paragraph, dict, comparisonType);
if (newParas is not null && newParas.Count > 0)
ParagraphReplacer.ReplaceParagraph(paragraph, newParas);
}
}
+349 -1
View File
@@ -402,7 +402,7 @@ internal static class MultiReplaceExt
// ---------- ОСНОВНОЙ АЛГОРИТМ ---------- // ---------- ОСНОВНОЙ АЛГОРИТМ ----------
private static List<Paragraph>? ProcessMultiReplacements( internal static List<Paragraph>? ProcessMultiReplacements(
Paragraph original, Paragraph original,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? stringReplacements, IEnumerable<KeyValuePair<string, IEnumerable<string>>>? stringReplacements,
IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>>? itemReplacements, IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>>? itemReplacements,
@@ -848,4 +848,352 @@ internal static class MultiReplaceExt
} }
} }
} }
// ============================================================
// НОВЫЕ ПУБЛИЧНЫЕ ТОЧКИ ВХОДА (mergeBoundaryText: true)
// ============================================================
internal static List<Paragraph>? ProcessParagraphWithMergedBoundaries(
Paragraph paragraph,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> replacements,
StringComparison comparisonType)
{
return ProcessMultiReplacements(paragraph, replacements, null, comparisonType, mergeBoundaryText: true);
}
internal static List<Paragraph>? ProcessParagraphWithMergedBoundaries(
Paragraph paragraph,
IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>> replacements,
StringComparison comparisonType)
{
return ProcessMultiReplacements(paragraph, null, replacements, comparisonType, mergeBoundaryText: true);
}
// ============================================================
// ОСНОВНОЙ АЛГОРИТМ (изменена только сигнатура + 4 места)
// ============================================================
private static List<Paragraph>? ProcessMultiReplacements(
Paragraph original,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? stringReplacements,
IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>>? itemReplacements,
StringComparison comparisonType,
bool mergeBoundaryText = false) // <-- новый параметр
{
var body = original.Ancestors<Body>().FirstOrDefault();
bool hasEmptyParagraphsBefore = false;
var prevPara = original.PreviousSibling<Paragraph>();
while (prevPara is not null)
{
if (!string.IsNullOrEmpty(prevPara.InnerText)) break;
hasEmptyParagraphsBefore = true;
prevPara = prevPara.PreviousSibling<Paragraph>();
}
SectionProperties? followingSection = null;
if (original.ParagraphProperties?.GetFirstChild<SectionProperties>()?.CloneNode(true) is not SectionProperties sourceSection)
{
var nextPara = original.NextSibling<Paragraph>();
if (nextPara is not null)
{
followingSection = nextPara.ParagraphProperties?.GetFirstChild<SectionProperties>()?.CloneNode(true) as SectionProperties;
}
}
else
{
followingSection = sourceSection;
}
followingSection ??= new SectionProperties();
// 1. Сбор определений
var definitions = new List<MatchDefinition>();
if (stringReplacements is not null)
{
foreach (var kvp in stringReplacements)
{
if (string.IsNullOrEmpty(kvp.Key) || kvp.Value is null || kvp.Value.Count() == 0) continue;
var items = kvp.Value.Select(v => new ReplaceItem(v));
definitions.Add(new MatchDefinition(kvp.Key, items));
}
}
if (itemReplacements is not null)
{
foreach (var kvp in itemReplacements)
{
if (string.IsNullOrEmpty(kvp.Key) || kvp.Value is null || kvp.Value.Count() == 0) continue;
definitions.Add(new MatchDefinition(kvp.Key, kvp.Value));
}
}
if (definitions.Count == 0) return null;
// 2. Анализ структуры параграфа
var runs = original.Descendants<Run>().ToList();
if (runs.Count == 0) return null;
var structure = AnalyzeParagraphStructure(runs);
string fullText = structure.FullText;
if (fullText.Length == 0) return null;
// 3. Поиск всех вхождений
var matches = new List<Match>();
foreach (var def in definitions)
{
int pos = 0;
while ((pos = fullText.IndexOf(def.Key, pos, comparisonType)) != -1)
{
matches.Add(new Match { Definition = def, Start = pos, End = pos + def.Key.Length });
pos += def.Key.Length;
}
}
if (matches.Count == 0) return null;
matches.Sort((a, b) => a.Start.CompareTo(b.Start));
// 4. Книжная секция для копирования полей
SectionProperties? portraitSection = null;
if (body is not null)
portraitSection = body.Elements<SectionProperties>().FirstOrDefault()?.CloneNode(true) as SectionProperties;
portraitSection ??= new SectionProperties();
// 5. Построение результата
var resultParas = new List<Paragraph>();
Paragraph? currentPara = null;
int currentPos = 0;
bool sectionChangeInsideGroup = false;
BreakType? lastOrientation = null;
bool pageBreakBeforeNext = false;
BreakType? pendingOrientation = null;
bool firstSplitMarkerHandled = false;
int textCount = 0;
Paragraph? lastTextPara = null;
bool emptyParagraphCreated = false;
// НОВОЕ: буфер runs для склейки "before"-текста с первым value-параграфом
List<Run>? pendingBeforeRuns = null;
for (int i = 0; i < matches.Count; i++)
{
var match = matches[i];
// ---- Текст перед совпадением ----
if (currentPos < match.Start)
{
var beforePara = CloneParagraphWithoutSection(original);
foreach (var seg in structure.Segments)
{
if (seg.End <= currentPos) continue;
if (seg.Start >= match.Start) break;
if (seg.Start >= currentPos && seg.End <= match.Start)
{
beforePara.AppendChild(seg.Run.CloneNode(true));
}
else if (seg.Start < match.Start && seg.End > currentPos)
{
int startOffset = Math.Max(0, currentPos - seg.Start);
int endOffset = Math.Min(seg.Text.Length, match.Start - seg.Start);
var runClone = (Run)seg.Run.CloneNode(true);
foreach (var t in runClone.Elements<Text>().ToList()) t.Remove();
string textPart = seg.Text.Substring(startOffset, endOffset - startOffset);
runClone.AppendChild(new Text(textPart));
beforePara.AppendChild(runClone);
}
}
if (beforePara.ChildElements.OfType<Run>().Any())
{
if (!mergeBoundaryText)
{
resultParas.Add(beforePara);
currentPara = beforePara;
lastTextPara = beforePara;
}
else
{
// Копим runs, склеим их с первым value-параграфом
pendingBeforeRuns = new List<Run>();
foreach (var r in beforePara.Elements<Run>())
pendingBeforeRuns.Add((Run)r.CloneNode(true));
}
}
}
// ---- /Текст перед совпадением ----
var values = match.Definition.Values.ToList();
if (values.Count == 0) continue;
// Fast-path (одиночный простой текст) — оставлен как есть.
// В merge-режиме пропускаем, чтобы before/remainder могли склеиться.
if (!mergeBoundaryText
&& values.Count == 1
&& !string.IsNullOrEmpty(values[0].Text)
&& !values[0].BreakValue.HasValue)
{
var newText = values[0].Text;
var matchSeg = structure.Segments.FirstOrDefault(s => s.Start >= match.Start && s.End <= match.End);
if (matchSeg is not null)
{
var run = matchSeg.Run;
foreach (var textElem in run.Elements<Text>().ToList())
textElem.Remove();
run.AppendChild(new Text(newText));
return null;
}
}
// Сброс флагов для новой группы
pageBreakBeforeNext = false;
pendingOrientation = null;
firstSplitMarkerHandled = false;
textCount = 0;
lastTextPara = null;
emptyParagraphCreated = false;
foreach (var item in values)
{
if (!string.IsNullOrEmpty(item.Text))
{
ProcessTextItem(
item.Text,
ref pendingOrientation,
ref currentPara,
ref lastTextPara,
ref textCount,
ref pageBreakBeforeNext,
ref sectionChangeInsideGroup,
ref lastOrientation,
original,
structure,
match.Start,
followingSection,
portraitSection,
resultParas);
// НОВОЕ: вклеиваем before-runs в только что созданный параграф
if (mergeBoundaryText && pendingBeforeRuns is not null && currentPara is not null)
{
for (int j = pendingBeforeRuns.Count - 1; j >= 0; j--)
currentPara.InsertAt((Run)pendingBeforeRuns[j].CloneNode(true), 0);
pendingBeforeRuns = null;
}
}
else if (item.BreakValue.HasValue)
{
ProcessBreakItem(
item.BreakValue.Value,
ref pendingOrientation,
ref currentPara,
ref lastTextPara,
ref pageBreakBeforeNext,
ref firstSplitMarkerHandled,
ref sectionChangeInsideGroup,
ref lastOrientation,
ref textCount,
ref emptyParagraphCreated,
hasEmptyParagraphsBefore,
original,
followingSection,
portraitSection,
resultParas);
// НОВОЕ: если создался параграф под break (landscape/portrait), тоже вклеим
if (mergeBoundaryText && pendingBeforeRuns is not null && currentPara is not null)
{
// вставляем before-runs ПЕРЕД любыми break-runs
for (int j = pendingBeforeRuns.Count - 1; j >= 0; j--)
currentPara.InsertAt((Run)pendingBeforeRuns[j].CloneNode(true), 0);
pendingBeforeRuns = null;
}
}
}
// Закрытие секции (без изменений)
if (sectionChangeInsideGroup && lastOrientation.HasValue && currentPara is not null)
{
if (currentPara.ParagraphProperties?.GetFirstChild<SectionProperties>() is null)
{
bool addPageSize = (lastOrientation.Value == BreakType.NewLandscapeSection);
AddSectionProperties(currentPara, lastOrientation.Value, addPageSize, followingSection, portraitSection);
}
}
if (!sectionChangeInsideGroup && currentPara is not null)
{
var sourcePageSize = followingSection.GetFirstChild<PageSize>();
if (sourcePageSize is not null)
{
if (currentPara.ParagraphProperties?.GetFirstChild<SectionProperties>() is null)
{
currentPara.ParagraphProperties ??= new ParagraphProperties();
currentPara.ParagraphProperties.AppendChild(followingSection.CloneNode(true));
}
}
}
EnsureBodyLandscapeSection(body!, followingSection, sectionChangeInsideGroup, lastOrientation);
currentPos = match.End;
}
// ---- Обработка остатка текста ----
if (currentPos < fullText.Length)
{
var remainderPara = BuildRemainderParagraph(original, structure, currentPos);
if (remainderPara is not null)
{
bool canMerge = mergeBoundaryText
&& resultParas.Count > 0
&& !pendingOrientation.HasValue
&& !sectionChangeInsideGroup;
if (canMerge)
{
// Вклеиваем runs остатка в последний параграф
var lastPara = resultParas[resultParas.Count - 1];
foreach (var r in remainderPara.Elements<Run>().ToList())
lastPara.AppendChild(r.CloneNode(true));
}
else
{
// Оригинальное поведение
if (pendingOrientation.HasValue)
{
var breakRun = new Run(new Break { Type = BreakValues.Page });
remainderPara.InsertAt(breakRun, 0);
bool addPageSize = (pendingOrientation.Value == BreakType.NewLandscapeSection);
AddSectionProperties(remainderPara, pendingOrientation.Value, addPageSize, followingSection, portraitSection);
pendingOrientation = null;
}
else if (sectionChangeInsideGroup && lastOrientation.HasValue)
{
var breakRun = new Run(new Break { Type = BreakValues.Page });
remainderPara.InsertAt(breakRun, 0);
bool addPageSize = (lastOrientation.Value == BreakType.NewLandscapeSection);
AddSectionProperties(remainderPara, lastOrientation.Value, addPageSize, followingSection, portraitSection);
}
else
{
var sourcePageSize = followingSection.GetFirstChild<PageSize>();
if (sourcePageSize is not null)
{
remainderPara.ParagraphProperties ??= new ParagraphProperties();
remainderPara.ParagraphProperties.AppendChild(followingSection.CloneNode(true));
}
}
resultParas.Add(remainderPara);
}
}
}
// ---- /Обработка остатка текста ----
// 7. Очистка пустых параграфов
for (int i = resultParas.Count - 1; i >= 0; i--)
{
var p = resultParas[i];
if (!p.ChildElements.OfType<Run>().Any() && p.ParagraphProperties is null)
resultParas.RemoveAt(i);
}
return resultParas.Count > 0 ? resultParas : null;
}
} }
+50 -22
View File
@@ -344,7 +344,7 @@ internal sealed class WordWriter : WordReader, IWordWriter
#region динамические змены с параметром
public void ReplaceString(string oldValue, Func<string, string> newValues) public void ReplaceString(string oldValue, Func<string, string> newValues)
{ {
@@ -373,7 +373,6 @@ internal sealed class WordWriter : WordReader, IWordWriter
_isModified = true; _isModified = true;
} }
} }
public void ReplaceItem(string oldValue, Func<string, ReplaceItem> newValues) public void ReplaceItem(string oldValue, Func<string, ReplaceItem> newValues)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
@@ -401,7 +400,6 @@ internal sealed class WordWriter : WordReader, IWordWriter
_isModified = true; _isModified = true;
} }
} }
public void ReplaceToText(string oldValue, Action<string, IText> buildText) public void ReplaceToText(string oldValue, Action<string, IText> buildText)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
@@ -422,45 +420,75 @@ internal sealed class WordWriter : WordReader, IWordWriter
} }
} }
public void ReplaceString(string oldValue, Func<string, string[]> newValues)
=> ReplaceString(oldValue, (Func<string, IEnumerable<string>>)newValues);
public void ReplaceString(string oldValue, Func<string, IEnumerable<string>> newValues) public void ReplaceString(string oldValue, Func<string, IEnumerable<string>> newValues)
{ {
throw new NotImplementedException(); ThrowIfDisposed();
}
public void ReplaceString(string oldValue, Func<string, string[]> newValues) if (string.IsNullOrEmpty(oldValue))
throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
if (newValues is null) return;
#if DEBUG
Debug.WriteLine($"[DEBUG] [WordProcessor.ReplaceString(dynamic-multi)] START: '{oldValue}'");
#endif
lock (_syncLock)
{ {
throw new NotImplementedException(); // 1. Основной текст
} DynamicMultiReplaceExt.ReplaceStringMultiInBody(_body, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
// 2. Таблицы в основном тексте
DynamicMultiReplaceExt.ReplaceStringMultiInTables(_body, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
// 3. Колонтитулы
DynamicMultiReplaceExt.ReplaceStringMultiInHeadersFooters(_doc, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
_isModified = true;
}
}
public void ReplaceItem(string oldValue, Func<string, ReplaceItem[]> newValues)
=> ReplaceItem(oldValue, (Func<string, IEnumerable<ReplaceItem>>)newValues);
public void ReplaceItem(string oldValue, Func<string, IEnumerable<ReplaceItem>> newValues) public void ReplaceItem(string oldValue, Func<string, IEnumerable<ReplaceItem>> newValues)
{ {
throw new NotImplementedException(); ThrowIfDisposed();
}
public void ReplaceItem(string oldValue, Func<string, ReplaceItem[]> newValues) if (string.IsNullOrEmpty(oldValue))
throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
if (newValues is null) return;
#if DEBUG
Debug.WriteLine($"[DEBUG] [WordProcessor.ReplaceItem(dynamic-multi)] START: '{oldValue}'");
#endif
lock (_syncLock)
{ {
throw new NotImplementedException(); // 1. Основной текст
DynamicMultiReplaceExt.ReplaceItemMultiInBody(_body, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
// 2. Таблицы в основном тексте
DynamicMultiReplaceExt.ReplaceItemMultiInTables(_body, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
// 3. Колонтитулы
DynamicMultiReplaceExt.ReplaceItemMultiInHeadersFooters(_doc, oldValue, newValues, StringComparison.OrdinalIgnoreCase);
_isModified = true;
}
} }
public void ReplaceToTable(string oldValue, Action<string, ITable> buildTable) public void ReplaceToTable(string oldValue, Action<string, ITable> buildTable)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void ReplaceToTable(string oldValue, FontProps baseFont, Action<string, ITable> buildTable) public void ReplaceToTable(string oldValue, FontProps baseFont, Action<string, ITable> buildTable)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
#endregion