diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba3eab0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +bin +obj +v14 +packages +*.suo +*.user +*.userosscache +*.sln.docstates +*.userprefs +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +[Xx]64/ +[Xx]86/ +[Bb]uild/ +bld/ +#[Bb]in/ +[Oo]bj/ +.vs/ +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.VisualState.xml +TestResult.xml +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c +project.lock.json +artifacts/ +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc +_Chutzpah* +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.psess +*.vsp +*.vspx +*.sap + +node_modules/ \ No newline at end of file diff --git a/MSWordLite.Cmd/App.config b/MSWordLite.Cmd/App.config new file mode 100644 index 0000000..787dcbe --- /dev/null +++ b/MSWordLite.Cmd/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/MSWordLite.Cmd/MSWordLite.Cmd.csproj b/MSWordLite.Cmd/MSWordLite.Cmd.csproj new file mode 100644 index 0000000..8881faa --- /dev/null +++ b/MSWordLite.Cmd/MSWordLite.Cmd.csproj @@ -0,0 +1,62 @@ + + + + + Debug + AnyCPU + {8B51C380-7197-4191-BED0-9B7DDF9C7ACC} + Exe + MSWordLite.Cmd + MSWordLite.Cmd + v4.7.1 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + + + + + + + + + + + + + + + + + + + + + {2f59728e-ddab-426a-9c71-4aa1373b11c2} + MSWordLite + + + + \ No newline at end of file diff --git a/MSWordLite.Cmd/Program.cs b/MSWordLite.Cmd/Program.cs new file mode 100644 index 0000000..3b666d7 --- /dev/null +++ b/MSWordLite.Cmd/Program.cs @@ -0,0 +1,89 @@ +using MSWordLite.Orders; +using MSWordLite.Tasks; +using System; +using System.Collections.Generic; + +namespace MSWordLite.Cmd +{ + class ReplaceContent + { + public string Bookmark0 { get; set; } = "NewText0"; + public string Bookmark1 { get; set; } = "NewText1"; + public string Bookmark2 { get; set; } = "NewText2"; + } + + class Program + { + static void Main(string[] args) + { + var task = new GenerateTask() + { + //TemplatePath = @"C:\Developing\MSWordLite\template_replaceBookmark.docx", + TemplatePath = @"C:\Developing\MSWordLite\template_expandTable.docx", + //TemplatePath = @"C:\Developing\MSWordLite\template_duplicateTable.docx", + OutputPath = @"C:\Developing\MSWordLite\out.docx" + }; + + try + { + task.Orders.Add(ReplaceBookmarkOrder.CreateFrom(new ReplaceContent())); + + //task.Orders.Add(ReplaceBookmarkOrder.CreateFrom(new Dictionary() + //{ + // { "Bookmark0", "NewText0" }, + // { "Bookmark1", "NewText1" }, + // { "Bookmark2", "NewText2" } + //})); + + task.Orders.Add(ExpandTableOrder.CreateFrom(0, new List>() + { + new List() { "1", "Data1", "Description1" }, + new List() { "2", "Data2", "Description2" }, + new List() { "3", "Data3", "Description3" }, + })); + + //task.Orders.Add(DuplicateTableOrder.CreateFrom(0, new List>() + //{ + // new Dictionary() + // { + // { "Index", "1" }, + // { "All", "2" }, + // { "Insert0", "NewText0" }, + // { "Insert1", "NewText1" }, + // { "Insert2", "NewText2" }, + // { "Insert3", "NewText3" }, + // { "Insert4", "NewText4 NewText4 NewText4 NewText4" }, + // { "Insert5", "NewText5 NewText5 NewText5 NewText5 NewText5 NewText5 NewText5" }, + // }, + // new Dictionary() + // { + // { "Index", "2" }, + // { "All", "2" }, + // { "Insert0", "NewText0-1" }, + // { "Insert1", "NewText1-1" }, + // { "Insert2", "NewText2-1" }, + // { "Insert3", "NewText3-1" }, + // { "Insert4", "NewText4-1 NewText4 NewText4 NewText4-1" }, + // { "Insert5", "NewText5-1 NewText5 NewText5 NewText5 NewText5 NewText5 NewText5-1" }, + // } + //})); + + task.Orders.Add(new ClearBookmarkOrder()); + + WordProcess.Run(task).Wait(); + + if (task.State == ProcessState.Failure) + { + Console.WriteLine(task.Error); + } + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + } + + Console.ReadLine(); + task.Dispose(); + } + } +} diff --git a/MSWordLite.Cmd/Properties/AssemblyInfo.cs b/MSWordLite.Cmd/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2c52feb --- /dev/null +++ b/MSWordLite.Cmd/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// 組件的一般資訊是由下列的屬性集控制。 +// 變更這些屬性的值即可修改組件的相關 +// 資訊。 +[assembly: AssemblyTitle("MSWordLite.Cmd")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MSWordLite.Cmd")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 將 ComVisible 設為 false 可對 COM 元件隱藏 +// 組件中的類型。若必須從 COM 存取此組件中的類型, +// 的類型,請在該類型上將 ComVisible 屬性設定為 true。 +[assembly: ComVisible(false)] + +// 下列 GUID 為專案公開 (Expose) 至 COM 時所要使用的 typelib ID +[assembly: Guid("8b51c380-7197-4191-bed0-9b7ddf9c7acc")] + +// 組件的版本資訊由下列四個值所組成: +// +// 主要版本 +// 次要版本 +// 組建編號 +// 修訂編號 +// +// 您可以指定所有的值,或將組建編號或修訂編號設為預設值 +// 指定為預設值: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/MSWordLite.sln b/MSWordLite.sln new file mode 100644 index 0000000..ededf4b --- /dev/null +++ b/MSWordLite.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.168 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSWordLite", "MSWordLite\MSWordLite.csproj", "{2F59728E-DDAB-426A-9C71-4AA1373B11C2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSWordLite.Cmd", "MSWordLite.Cmd\MSWordLite.Cmd.csproj", "{8B51C380-7197-4191-BED0-9B7DDF9C7ACC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2F59728E-DDAB-426A-9C71-4AA1373B11C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F59728E-DDAB-426A-9C71-4AA1373B11C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F59728E-DDAB-426A-9C71-4AA1373B11C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F59728E-DDAB-426A-9C71-4AA1373B11C2}.Release|Any CPU.Build.0 = Release|Any CPU + {8B51C380-7197-4191-BED0-9B7DDF9C7ACC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B51C380-7197-4191-BED0-9B7DDF9C7ACC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B51C380-7197-4191-BED0-9B7DDF9C7ACC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B51C380-7197-4191-BED0-9B7DDF9C7ACC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AC3A6807-E0DE-4525-9CFA-D4B61AA0BAA8} + EndGlobalSection +EndGlobal diff --git a/MSWordLite/Contexts/ProcessState.cs b/MSWordLite/Contexts/ProcessState.cs new file mode 100644 index 0000000..70eb60c --- /dev/null +++ b/MSWordLite/Contexts/ProcessState.cs @@ -0,0 +1,33 @@ +namespace MSWordLite +{ + /// + /// 執行狀態 + /// + public enum ProcessState + { + /// + /// 等待中 + /// + Waiting, + + /// + /// 初始化中 + /// + Initilized, + + /// + /// 成功 + /// + Success, + + /// + /// 失敗 + /// + Failure, + + /// + /// 產出的檔案已刪除 + /// + Disposed + } +} diff --git a/MSWordLite/Elements/Bookmark.cs b/MSWordLite/Elements/Bookmark.cs new file mode 100644 index 0000000..1ca6c11 --- /dev/null +++ b/MSWordLite/Elements/Bookmark.cs @@ -0,0 +1,192 @@ +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Wordprocessing; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MSWordLite.Elements +{ + /// + /// Word 文件書籤 + /// + class Bookmark + { + public string Id { get; set; } + public string Name { get; set; } + public BookmarkStart Start { get; set; } + public BookmarkEnd End { get; set; } + public bool Valid => !string.IsNullOrEmpty(Name) && Start != null && End != null; + + public Bookmark(BookmarkStart start) + { + if (start != null) + { + Start = start; + Name = Start.Name; + Id = Start.Id; + } + } + + public Bookmark AppendEnd(BookmarkEnd end) + { + End = end; + return this; + } + + public bool Replace(string text) + { + if (!Valid) { return false; } + + var splitedText = text.Split(new string[] { "
" }, StringSplitOptions.None); + + _clearBetweenStartAndEnd(Start); + + var run = _createRunFromTexts(Start, splitedText); + Start.Parent.InsertAfter(run, Start); + + return true; + } + + private static void _clearBetweenStartAndEnd(BookmarkStart start) + { + var elem = start.NextSibling(); + var paragraph = start.Parent as Paragraph; + + while (elem != null && !(elem is BookmarkEnd)) + { + var nextElem = elem.NextSibling(); + elem.Remove(); + elem = nextElem; + } + + if (!(elem is BookmarkEnd)) + { + var pg = paragraph.NextSibling(); + while (pg != null && !(elem is BookmarkEnd)) + { + var bookmarkEnd = _findBookmarkEnd(pg, start.Id); + if (bookmarkEnd != null) + { + elem = bookmarkEnd; + + var nextElem = bookmarkEnd.NextSibling(); + paragraph.AppendChild(bookmarkEnd.CloneNode(true)); + while (nextElem != null) + { + paragraph.AppendChild(nextElem.CloneNode(true)); + nextElem = nextElem.NextSibling(); + } + + pg.Remove(); + } + else + { + var nextPg = pg.NextSibling(); + pg.Remove(); + pg = nextPg; + } + } + } + } + + private static BookmarkEnd _findBookmarkEnd(OpenXmlElement documentElement, string id) + { + foreach (var elem in documentElement.ChildElements) + { + if (!(elem is BookmarkEnd end) || end.Id != id) + { + var result = _findBookmarkEnd(elem, id); + if (result != null) + { + return result; + } + } + else + { + return end; + } + } + + return null; + } + + private static Run _createRunFromTexts(BookmarkStart start, IEnumerable text) + { + var run = new Run(); + var count = text.Count(); + for (var i = 0; i < count; i++) + { + run.Append(new Text(text.ElementAt(i))); + if (i < count - 1) + { + run.Append(new Break()); + } + } + + var paragraph = start.Parent as Paragraph; + var pgp = paragraph.ChildElements.Where(child => child is ParagraphProperties).FirstOrDefault(); + if (pgp != null) + { + var pgmrp = pgp.ChildElements.Where(child => child is ParagraphMarkRunProperties).FirstOrDefault(); + if (pgmrp != null) + { + run.RunProperties = new RunProperties(pgmrp.OuterXml); + } + } + + return run; + } + + public void Remove() + { + if (Start != null) + { + Start.Remove(); + } + + if (End != null) + { + End.Remove(); + } + } + + public static Dictionary SearchFrom( + OpenXmlElement documentElement, Dictionary existedMap = null) + { + if (existedMap == null) + { + existedMap = new Dictionary(); + } + + foreach (var element in documentElement.Elements()) + { + if (element is BookmarkStart) + { + var start = new Bookmark(element as BookmarkStart); + if (!existedMap.ContainsKey(start.Name)) + { + existedMap.Add(start.Name, start); + } + } + + if (element is BookmarkEnd) + { + var end = element as BookmarkEnd; + var name = existedMap + .Where(item => item.Value.Id == end.Id) + .Select(item => item.Value.Name) + .FirstOrDefault(); + + if (!string.IsNullOrEmpty(name)) + { + existedMap[name] = existedMap[name].AppendEnd(end); + } + } + + SearchFrom(element, existedMap); + } + + return existedMap; + } + } +} diff --git a/MSWordLite/Elements/Table.cs b/MSWordLite/Elements/Table.cs new file mode 100644 index 0000000..c3466a8 --- /dev/null +++ b/MSWordLite/Elements/Table.cs @@ -0,0 +1,40 @@ +using DocumentFormat.OpenXml; +using System.Collections.Generic; +using System.Linq; +using WordTable = DocumentFormat.OpenXml.Wordprocessing.Table; +using WordTableRow = DocumentFormat.OpenXml.Wordprocessing.TableRow; + +namespace MSWordLite.Elements +{ + /// + /// Word 文件表格 + /// + class Table + { + public WordTable WordTable { get; set; } + public IEnumerable Rows => WordTable.ChildElements + .Where(child => child is WordTableRow).Select(child => new TableRow(child as WordTableRow)); + public TableRow FirstRow => Rows.FirstOrDefault(); + public TableRow LastRow => Rows.LastOrDefault(); + public bool Valid => WordTable != default(WordTable); + + public Table(WordTable table) + { + WordTable = table; + } + + public Table Append(TableRow row) + { + WordTable.InsertAfter(row.WordRow, LastRow.WordRow); + return this; + } + + public static List SearchFrom(OpenXmlElement documentElement) + { + return documentElement.Elements() + .Where(element => element is WordTable) + .Select(element => new Table(element as WordTable)) + .ToList(); + } + } +} diff --git a/MSWordLite/Elements/TableCell.cs b/MSWordLite/Elements/TableCell.cs new file mode 100644 index 0000000..5dff2f9 --- /dev/null +++ b/MSWordLite/Elements/TableCell.cs @@ -0,0 +1,64 @@ +using DocumentFormat.OpenXml.Wordprocessing; +using System; +using System.Collections.Generic; +using System.Linq; +using WordTableCell = DocumentFormat.OpenXml.Wordprocessing.TableCell; + +namespace MSWordLite.Elements +{ + /// + /// Word 文件表格欄位 + /// + class TableCell + { + public WordTableCell WordCell { get; set; } + public IEnumerable Paragraphs => WordCell.ChildElements + .Where(child => child is Paragraph).Select(child => (Paragraph)child); + public IEnumerable Runs => Paragraphs + .SelectMany(pg => pg.ChildElements.Where(pgc => pgc is Run).Select(pgc => (Run)pgc)); + public bool Valid => WordCell != default(WordTableCell); + + public TableCell(WordTableCell cell) + { + WordCell = cell; + } + + public TableCell ReplaceText(string text) + { + var splitedText = text.Split(new string[] { "
" }, StringSplitOptions.None); + var pg = Paragraphs.FirstOrDefault(); + var run = Runs.FirstOrDefault(); + if (run != null) + { + var t = run.ChildElements[1] as Text; + t.Text = text; + } + else if (pg != null) + { + var newRun = new Run(); + var count = splitedText.Count(); + for (var i = 0; i < count; i++) + { + newRun.Append(new Text(splitedText.ElementAt(i))); + if (i < count - 1) + { + newRun.Append(new Break()); + } + } + + var pgp = pg.ChildElements.Where(child => child is ParagraphProperties).FirstOrDefault(); + if (pgp != null) + { + var pgmrp = pgp.ChildElements.Where(child => child is ParagraphMarkRunProperties).FirstOrDefault(); + if (pgmrp != null) + { + newRun.RunProperties = new RunProperties(pgmrp.OuterXml); + } + } + + pg.AppendChild(newRun); + } + return this; + } + } +} diff --git a/MSWordLite/Elements/TableRow.cs b/MSWordLite/Elements/TableRow.cs new file mode 100644 index 0000000..ebaf388 --- /dev/null +++ b/MSWordLite/Elements/TableRow.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using WordTableRow = DocumentFormat.OpenXml.Wordprocessing.TableRow; +using WordTableCell = DocumentFormat.OpenXml.Wordprocessing.TableCell; + +namespace MSWordLite.Elements +{ + /// + /// Word 文件表格行 + /// + class TableRow + { + public WordTableRow WordRow { get; set; } + public IEnumerable Cells => WordRow.ChildElements + .Where(child => child is WordTableCell).Select(child => new TableCell(child as WordTableCell)); + public TableCell FirstCell => Cells.FirstOrDefault(); + public TableCell LastCell => Cells.LastOrDefault(); + + public TableRow(WordTableRow row) + { + WordRow = row; + } + public bool Valid => WordRow != default(WordTableRow); + + public TableRow Clone() => + new TableRow((WordTableRow)WordRow.Clone()); + + public TableRow ReplaceText(IEnumerable texts) + { + for (var i = 0; i < texts.Count(); i++) + { + var cell = Cells.ElementAtOrDefault(i); + var text = texts.ElementAtOrDefault(i); + if (cell != null && text != "#COPY#") + { + cell.ReplaceText(text); + } + } + return this; + } + } +} diff --git a/MSWordLite/MSWordLite.csproj b/MSWordLite/MSWordLite.csproj new file mode 100644 index 0000000..dfd938f --- /dev/null +++ b/MSWordLite/MSWordLite.csproj @@ -0,0 +1,28 @@ + + + + netstandard1.3 + 7.3 + + + + true + true + Frank Tsai + https://creativecommons.org/licenses/by-sa/3.0/ + office word lite + This package provide several simple function: +- Replace bookmark in document, use dictionary or anonymous object as input. +- Expand specific table in document by input list of data. +- Duplicate table and replace bookmark in newly created table. + + + + C:\Users\frank\Source\Repos\MSWordLite\MSWordLite\MSWordLite.xml + + + + + + + diff --git a/MSWordLite/Orders/ClearBookmarkOrder.cs b/MSWordLite/Orders/ClearBookmarkOrder.cs new file mode 100644 index 0000000..0a1baba --- /dev/null +++ b/MSWordLite/Orders/ClearBookmarkOrder.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace MSWordLite.Orders +{ + /// + /// Remove bookmarks in document + /// + public class ClearBookmarkOrder : IOrder + { + /// + /// If this order is valid. + /// + public bool Valid => Names != null || Regex != null; + + public List Names { get; set; } = new List(); + + public Regex Regex { get; set; } + + /// + /// Create an clear order to remove all bookmarks in doucment. + /// + public ClearBookmarkOrder() { } + + /// + /// Create an clear order to remove specific name of bookmarks in document. + /// + /// + public ClearBookmarkOrder(List names) + { + Names = names; + } + + /// + /// Create an clear order and use regex pattern to test if bookmarks need to be remove in document. + /// + /// + public ClearBookmarkOrder(string regexPattern) + { + Regex = new Regex(regexPattern); + } + } +} diff --git a/MSWordLite/Orders/DuplicateTableOrder.cs b/MSWordLite/Orders/DuplicateTableOrder.cs new file mode 100644 index 0000000..487fc43 --- /dev/null +++ b/MSWordLite/Orders/DuplicateTableOrder.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace MSWordLite.Orders +{ + public class DuplicateTableOrder : IOrder + { + /// + /// Index of template table + /// + public int TableId { get; set; } + + /// + /// Contents to replace + /// + public List> ReplaceContents { get; set; } = new List>(); + + /// + /// Check if this order is valid. + /// + public bool Valid => ReplaceContents != null && TableId >= 0; + + /// + /// + /// + /// Index of template table. + /// Contents to replace. + public DuplicateTableOrder(int tableId, List> replaceContents) + { + TableId = tableId; + ReplaceContents = replaceContents; + } + + /// + /// + /// + /// Index of template table. + /// Contents to replace. + public DuplicateTableOrder(int tableId, List replaceContents) + { + TableId = tableId; + ReplaceContents = replaceContents.Select(o => _convertFromObject(o)).ToList(); + } + + /// + /// + /// + /// Index of template table. + /// Contents to replace. + /// + public static IOrder CreateFrom(int tableId, List> replaceContents) + { + return new DuplicateTableOrder(tableId, replaceContents); + } + + /// + /// + /// + /// Index of template table. + /// Contents to replace. + /// + public static IOrder CreateFrom(int tableId, List replaceContents) + { + return new DuplicateTableOrder(tableId, replaceContents); + } + + private static Dictionary _convertFromObject(object replaceContent) + { + return replaceContent.GetType().GetRuntimeProperties() + .Where(prop => prop.CanRead) + .ToDictionary(prop => prop.Name, prop => prop.GetValue(replaceContent, null)) + .Where(pair => pair.Value != null) + .ToDictionary(pair => pair.Key, pair => System.Convert.ToString(pair.Value)); + } + } +} diff --git a/MSWordLite/Orders/ExpandTableOrder.cs b/MSWordLite/Orders/ExpandTableOrder.cs new file mode 100644 index 0000000..f41a89a --- /dev/null +++ b/MSWordLite/Orders/ExpandTableOrder.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; + +namespace MSWordLite.Orders +{ + /// + /// 表格擴增作業,指定一個表格,並將資料附加在表格的最後一行以擴增表格 + /// + public class ExpandTableOrder : IOrder + { + /// + /// Index of table + /// + public int TableId { get; set; } + + /// + /// 要加入的內容 + /// + public List> Content { get; set; } + + /// + /// 是否不將表格的第一行視為標題 + /// + public bool WithoutHeader { get; set; } + + /// + /// 此作業是否有效 + /// + public bool Valid => TableId >= 0 && Content != null; + + /// + /// 初始化一個表格擴增作業。 + /// + /// Index of table. + /// Contents to append. + /// Determine if table contains head row. + public ExpandTableOrder(int tableId, List> content, bool withoutHeader = false) + { + TableId = tableId; + Content = content; + WithoutHeader = withoutHeader; + } + + /// + /// Create an order from table's id and, contents to append. + /// + /// Index of table. + /// Contents to append. + /// Determine if table contains head row. + /// + public static IOrder CreateFrom(int tableId, List> content, bool withoutHeader = false) + { + return new ExpandTableOrder(tableId, content, withoutHeader); + } + } +} diff --git a/MSWordLite/Orders/IOrder.cs b/MSWordLite/Orders/IOrder.cs new file mode 100644 index 0000000..9ac989a --- /dev/null +++ b/MSWordLite/Orders/IOrder.cs @@ -0,0 +1,7 @@ +namespace MSWordLite.Orders +{ + public interface IOrder + { + bool Valid { get; } + } +} diff --git a/MSWordLite/Orders/ReplaceBookmarkOrder.cs b/MSWordLite/Orders/ReplaceBookmarkOrder.cs new file mode 100644 index 0000000..b94424a --- /dev/null +++ b/MSWordLite/Orders/ReplaceBookmarkOrder.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace MSWordLite.Orders +{ + /// + /// Replace bookmark by text content in document. + /// + public class ReplaceBookmarkOrder : IOrder + { + /// + /// Content to replace + /// + public IDictionary ReplaceContent { get; set; } = new Dictionary(); + + /// + /// Check if this order is valid. + /// + public bool Valid => ReplaceContent != null; + + /// + /// Create an replace order. + /// + /// Content to replace + public ReplaceBookmarkOrder(Dictionary replaceContent) + { + ReplaceContent = replaceContent; + } + + /// + /// Create an replace order. + /// + /// Content to replace + public ReplaceBookmarkOrder(IDictionary replaceContent) + { + ReplaceContent = replaceContent; + } + + /// + /// Create an replace order. + /// + /// Content to replace + public ReplaceBookmarkOrder(object replaceContent) + { + ReplaceContent = _convertFromObject(replaceContent); + } + + /// + /// Create an order from content. + /// + /// Content to replace + public static IOrder CreateFrom(Dictionary replaceContent) + { + return new ReplaceBookmarkOrder(replaceContent); + } + + /// + /// Create an order from content. + /// + /// Content to replace + public static IOrder CreateFrom(IDictionary replaceContent) + { + return new ReplaceBookmarkOrder(replaceContent); + } + + /// + /// Create an order from content. + /// + /// Content to replace + public static IOrder CreateFrom(object replaceContent) + { + return new ReplaceBookmarkOrder(replaceContent); + } + + private static Dictionary _convertFromObject(object replaceContent) + { + return replaceContent.GetType().GetRuntimeProperties() + .Where(prop => prop.CanRead) + .ToDictionary(prop => prop.Name, prop => prop.GetValue(replaceContent, null)) + .Where(pair => pair.Value != null) + .ToDictionary(pair => pair.Key, pair => System.Convert.ToString(pair.Value)); + } + } +} diff --git a/MSWordLite/Processes/ClearBookmarkProcess.cs b/MSWordLite/Processes/ClearBookmarkProcess.cs new file mode 100644 index 0000000..6152b82 --- /dev/null +++ b/MSWordLite/Processes/ClearBookmarkProcess.cs @@ -0,0 +1,38 @@ +using MSWordLite.Orders; +using System.Linq; + +namespace MSWordLite.Processes +{ + class ClearBookmarkProcess : OrderProcess + { + public override OrderResult Initialize(Document document) + { + return new OrderResult(success: true); + } + + public override OrderResult Process(Document document) + { + if (document.HasBookmarks) + { + if (Order.Names.Count > 0) + { + foreach (var bookmark in document.WordBookmarks + .Where(p => Order.Names.Contains(p.Key))) + { + bookmark.Value.Remove(); + } + } + + if (Order.Regex != null) + { + foreach (var bookmark in document.WordBookmarks + .Where(p => p.Value != null && Order.Regex.IsMatch(p.Key))) + { + bookmark.Value.Remove(); + } + } + } + return new OrderResult(success: true); + } + } +} diff --git a/MSWordLite/Processes/DuplicateTableProcess.cs b/MSWordLite/Processes/DuplicateTableProcess.cs new file mode 100644 index 0000000..f1f3b5a --- /dev/null +++ b/MSWordLite/Processes/DuplicateTableProcess.cs @@ -0,0 +1,63 @@ +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Wordprocessing; +using MSWordLite.Elements; +using MSWordLite.Orders; +using System.Linq; +using Table = MSWordLite.Elements.Table; + +namespace MSWordLite.Processes +{ + class DuplicateTableProcess : OrderProcess + { + private Table _templateTable { get; set; } + + public override OrderResult Initialize(Document document) + { + Initializer.WordTables(document); + + if (document.WordTables.Count <= Order.TableId) + { + return new OrderResult(success: false, error: "invalid tableId"); + } + + _templateTable = document.WordTables.ElementAt(Order.TableId); + return new OrderResult(success: true); + } + + public override OrderResult Process(Document document) + { + var parent = _templateTable.WordTable.Parent; + OpenXmlElement currentTable = null; + + for (var index = 0; index < Order.ReplaceContents.Count; index++) + { + var replaceContent = Order.ReplaceContents[index]; + var newTable = _templateTable.WordTable.CloneNode(true); + + var bookmarkMapInNewTable = Bookmark.SearchFrom(newTable); + foreach (var bookmark in bookmarkMapInNewTable.Values) + { + bookmark.Replace(replaceContent[bookmark.Start.Name]); + bookmark.Start.Name = $"{bookmark.Start.Name}-dt-{Order.TableId}-{index}"; + document.WordBookmarks.Add(bookmark.Start.Name, bookmark); + } + + if (currentTable == null) + { + currentTable = newTable; + parent.InsertAfter(newTable, _templateTable.WordTable); + } + else + { + parent.InsertAfter(newTable, currentTable); + parent.InsertAfter(new Paragraph(), currentTable); + currentTable = newTable; + } + } + + _templateTable.WordTable.Remove(); + + return new OrderResult(success: true); + } + } +} diff --git a/MSWordLite/Processes/ExpandTableProcess.cs b/MSWordLite/Processes/ExpandTableProcess.cs new file mode 100644 index 0000000..3b1f066 --- /dev/null +++ b/MSWordLite/Processes/ExpandTableProcess.cs @@ -0,0 +1,35 @@ +using MSWordLite.Orders; +using System.Linq; +using Table = MSWordLite.Elements.Table; + +namespace MSWordLite.Processes +{ + class ExpandTableProcess : OrderProcess + { + private Table _targetTable { get; set; } + private int _rowCloneTargetNumber => Order.WithoutHeader ? 0 : 1; + + public override OrderResult Initialize(Document document) + { + Initializer.WordTables(document); + if (document.WordTables.Count <= Order.TableId) + { + return new OrderResult(success: false, error: "invalid tableId"); + } + + _targetTable = document.WordTables.ElementAt(Order.TableId); + return new OrderResult(success: true); + } + + public override OrderResult Process(Document document) + { + foreach (var rowContent in Order.Content) + { + _targetTable.Append(_targetTable.Rows.ElementAt(_rowCloneTargetNumber).Clone().ReplaceText(rowContent)); + } + _targetTable.Rows.ElementAt(_rowCloneTargetNumber).WordRow.Remove(); + + return new OrderResult(success: true); + } + } +} diff --git a/MSWordLite/Processes/IOrderProcess.cs b/MSWordLite/Processes/IOrderProcess.cs new file mode 100644 index 0000000..54a376d --- /dev/null +++ b/MSWordLite/Processes/IOrderProcess.cs @@ -0,0 +1,9 @@ +using MSWordLite.Orders; + +namespace MSWordLite.Processes +{ + interface IOrderProcess : IProcess where TOrder : IOrder + { + TOrder Order { get; set; } + } +} diff --git a/MSWordLite/Processes/IProcess.cs b/MSWordLite/Processes/IProcess.cs new file mode 100644 index 0000000..6baf64b --- /dev/null +++ b/MSWordLite/Processes/IProcess.cs @@ -0,0 +1,8 @@ +namespace MSWordLite.Processes +{ + interface IProcess + { + OrderResult Initialize(Document document); + OrderResult Process(Document document); + } +} diff --git a/MSWordLite/Processes/Initializer.cs b/MSWordLite/Processes/Initializer.cs new file mode 100644 index 0000000..4c8ad9f --- /dev/null +++ b/MSWordLite/Processes/Initializer.cs @@ -0,0 +1,36 @@ +using MSWordLite.Elements; +using System.Collections.Generic; +using System.Linq; +using Table = MSWordLite.Elements.Table; + +namespace MSWordLite.Processes +{ + class Initializer + { + public static void Bookmarks(Document document) + { + if (!document.HasBookmarks) + { + var bookmarkMap = new Dictionary(); + foreach (var element in document.RootElement.Elements()) + { + Bookmark.SearchFrom(element, existedMap: bookmarkMap); + } + + document.WordBookmarks = bookmarkMap + .Where(pair => pair.Value.Valid) + .ToDictionary(pair => pair.Key, pair => pair.Value); + } + } + + public static void WordTables(Document document) + { + if (!document.HasTables) + { + document.WordTables = document.RootElement.Elements() + .SelectMany(element => Table.SearchFrom(element)) + .ToList(); + } + } + } +} diff --git a/MSWordLite/Processes/OrderFactory.cs b/MSWordLite/Processes/OrderFactory.cs new file mode 100644 index 0000000..b69f045 --- /dev/null +++ b/MSWordLite/Processes/OrderFactory.cs @@ -0,0 +1,47 @@ +using MSWordLite.Orders; +using System.Collections.Generic; + +namespace MSWordLite.Processes +{ + class OrderFactory + { + private static Dictionary _processes { get; set; } = new Dictionary(); + + public static OrderResult Initialize(IOrder order, Document document) + { + var process = _retrieveProcess(order); + if (process != null) + { + _processes.Add(order, process); + return process.Initialize(document); + } + return new OrderResult(success: false, error: "invalid process"); + } + + public static OrderResult Process(IOrder order, Document document) + { + return _processes[order].Process(document); + } + + private static IProcess _retrieveProcess(IOrder order) + { + if (order is ClearBookmarkOrder clearBookmarkOrder) + { + return new ClearBookmarkProcess() { Order = clearBookmarkOrder }; + } + else if (order is ReplaceBookmarkOrder replaceBookmarkOrder) + { + return new ReplaceBookmarkProcess() { Order = replaceBookmarkOrder }; + } + else if (order is ExpandTableOrder expandTableOrder) + { + return new ExpandTableProcess() { Order = expandTableOrder }; + } + else if (order is DuplicateTableOrder duplicateTableOrder) + { + return new DuplicateTableProcess() { Order = duplicateTableOrder }; + } + return null; + } + } +} diff --git a/MSWordLite/Processes/OrderProcess.cs b/MSWordLite/Processes/OrderProcess.cs new file mode 100644 index 0000000..83143b6 --- /dev/null +++ b/MSWordLite/Processes/OrderProcess.cs @@ -0,0 +1,11 @@ +using MSWordLite.Orders; + +namespace MSWordLite.Processes +{ + abstract class OrderProcess : IOrderProcess where TOrder : IOrder + { + public TOrder Order { get; set; } + public abstract OrderResult Initialize(Document document); + public abstract OrderResult Process(Document document); + } +} diff --git a/MSWordLite/Processes/OrderResult.cs b/MSWordLite/Processes/OrderResult.cs new file mode 100644 index 0000000..490a042 --- /dev/null +++ b/MSWordLite/Processes/OrderResult.cs @@ -0,0 +1,19 @@ +namespace MSWordLite.Processes +{ + public class OrderResult + { + public bool Success { get; set; } + public string Error { get; set; } + + public OrderResult(bool success) + { + Success = success; + } + + public OrderResult(bool success, string error) + { + Success = success; + Error = error; + } + } +} diff --git a/MSWordLite/Processes/ReplaceBookmarkProcess.cs b/MSWordLite/Processes/ReplaceBookmarkProcess.cs new file mode 100644 index 0000000..fe0260f --- /dev/null +++ b/MSWordLite/Processes/ReplaceBookmarkProcess.cs @@ -0,0 +1,31 @@ +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Wordprocessing; +using MSWordLite.Elements; +using MSWordLite.Orders; +using System.Collections.Generic; +using System.Linq; + +namespace MSWordLite.Processes +{ + class ReplaceBookmarkProcess : OrderProcess + { + public override OrderResult Initialize(Document document) + { + Initializer.Bookmarks(document); + return new OrderResult(success: true); + } + + public override OrderResult Process(Document document) + { + foreach (var content in Order.ReplaceContent) + { + if (document.WordBookmarks.ContainsKey(content.Key)) + { + document.WordBookmarks[content.Key].Replace(content.Value); + } + } + + return new OrderResult(success: true); + } + } +} diff --git a/MSWordLite/Tasks/GenerateTask.cs b/MSWordLite/Tasks/GenerateTask.cs new file mode 100644 index 0000000..fada018 --- /dev/null +++ b/MSWordLite/Tasks/GenerateTask.cs @@ -0,0 +1,63 @@ +using MSWordLite.Orders; +using System; +using System.Collections.Generic; +using System.IO; + +namespace MSWordLite.Tasks +{ + /// + /// Word 文件產出作業 + /// + public class GenerateTask : IGenerateTask, IDisposable + { + /// + /// 文件範本路徑 + /// + public string TemplatePath { get; set; } + + /// + /// 範本檔案 + /// + public byte[] Template { get; set; } + + /// + /// 產出路徑 + /// + public string OutputPath { get; set; } + + /// + /// 產出檔案 + /// + public byte[] Output { get; set; } + + /// + /// 錯誤訊息 + /// + public string Error { get; set; } + + /// + /// 執行狀態 + /// + public ProcessState State { get; set; } + + /// + /// 產出時執行的命令 + /// + public List Orders { get; set; } = new List(); + + /// + /// 此作業是否符合執行的必要條件 + /// + public bool Valid => !string.IsNullOrEmpty(TemplatePath) || Template != null && Template.Length > 0; + + public void Dispose() + { + if (!string.IsNullOrEmpty(OutputPath) && File.Exists(OutputPath)) + { + File.Delete(OutputPath); + } + + State = ProcessState.Disposed; + } + } +} diff --git a/MSWordLite/Tasks/IGenerateTask.cs b/MSWordLite/Tasks/IGenerateTask.cs new file mode 100644 index 0000000..ab93ec7 --- /dev/null +++ b/MSWordLite/Tasks/IGenerateTask.cs @@ -0,0 +1,51 @@ +using MSWordLite.Orders; +using System.Collections.Generic; + +namespace MSWordLite.Tasks +{ + /// + /// Word generate task interface + /// + public interface IGenerateTask + { + /// + /// template path with file name and extension + /// + string TemplatePath { get; set; } + + /// + /// template file byte array + /// + byte[] Template { get; set; } + + /// + /// output path after generated, file name and extension is needed. + /// + string OutputPath { get; set; } + + /// + /// output file byte array + /// + byte[] Output { get; set; } + + /// + /// error message during process + /// + string Error { get; set; } + + /// + /// state of task + /// + ProcessState State { get; set; } + + /// + /// orders to process + /// + List Orders { get; set; } + + /// + /// if this task is valid + /// + bool Valid { get; } + } +} diff --git a/MSWordLite/WordDocument.cs b/MSWordLite/WordDocument.cs new file mode 100644 index 0000000..421e592 --- /dev/null +++ b/MSWordLite/WordDocument.cs @@ -0,0 +1,42 @@ +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Packaging; +using MSWordLite.Elements; +using System.Collections.Generic; + +namespace MSWordLite +{ + /// + /// Word 範本文件 + /// + class Document + { + /// + /// Word 文件 + /// + public WordprocessingDocument WordDocument { get; set; } + + /// + /// Word 文件根 + /// + public OpenXmlPartRootElement RootElement => WordDocument.MainDocumentPart.RootElement; + + /// + /// 範本中所有的表格 + /// + public List
WordTables { get; set; } = new List
(); + + public bool HasTables => WordTables != null && WordTables.Count > 0; + + /// + /// 範本中所有的書籤 + /// + public Dictionary WordBookmarks { get; set; } = new Dictionary(); + + public bool HasBookmarks => WordBookmarks != null && WordBookmarks.Count > 0; + + public Document(WordprocessingDocument wordDocument) + { + WordDocument = wordDocument; + } + } +} diff --git a/MSWordLite/WordProcess.cs b/MSWordLite/WordProcess.cs new file mode 100644 index 0000000..6329336 --- /dev/null +++ b/MSWordLite/WordProcess.cs @@ -0,0 +1,124 @@ +using DocumentFormat.OpenXml.Packaging; +using MSWordLite.Orders; +using MSWordLite.Processes; +using MSWordLite.Tasks; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace MSWordLite +{ + public class WordProcess + { + public static async Task Run(IGenerateTask task) + { + await _run(task); + return task; + } + + private static async Task _run(IGenerateTask task) + { + if (!task.Valid) + { + throw new Exception("both templatePath and template property are empty"); + } + + await _retrieveTemplate(task); + await _openAndProcess(task); + await _writeOutputFile(task); + } + + private static async Task _retrieveTemplate(IGenerateTask task) + { + if ((task.Template == null || task.Template.Length == 0) && !string.IsNullOrEmpty(task.TemplatePath)) + { + if (!File.Exists(task.TemplatePath)) + { + throw new FileNotFoundException("template file not found", task.TemplatePath); + } + + await Task.Run(() => task.Template = File.ReadAllBytes(task.TemplatePath)); + } + } + + private static async Task _openAndProcess(IGenerateTask task) + { + using (var newDocumentStream = new MemoryStream()) + { + await Task.Run(() => + { + newDocumentStream.Write(task.Template, 0, task.Template.Length); + using (var wordDocument = WordprocessingDocument.Open(newDocumentStream, isEditable: true)) + { + var document = new Document(wordDocument); + _initializeAndProcessOrder(task, document); + } + _generateOutput(task, newDocumentStream); + }); + } + } + + private static async Task _writeOutputFile(IGenerateTask task) + { + if (!string.IsNullOrEmpty(task.OutputPath) && task.Output != null && task.Output.Length > 0) + { + await Task.Run(() => File.WriteAllBytes(task.OutputPath, task.Output)); + } + } + + private static void _initializeAndProcessOrder(IGenerateTask task, Document document) + { + if (!_initializeOrders(task, document)) + { + task.State = ProcessState.Failure; + return; + } + + task.State = ProcessState.Initilized; + + if (!_processingOrders(task, document)) + { + task.State = ProcessState.Failure; + return; + } + + task.State = ProcessState.Success; + } + + private static bool _initializeOrders(IGenerateTask task, Document document) + { + foreach (IOrder order in task.Orders) + { + var result = OrderFactory.Initialize(order, document); + if (!result.Success) + { + task.Error = result.Error; + return false; + } + } + return true; + } + + private static bool _processingOrders(IGenerateTask task, Document document) + { + foreach (IOrder order in task.Orders) + { + var result = OrderFactory.Process(order, document); + if (!result.Success) + { + task.Error = result.Error; + return false; + } + } + return true; + } + + private static void _generateOutput(IGenerateTask task, MemoryStream newDocumentStream) + { + if (task.State == ProcessState.Success) + { + task.Output = newDocumentStream.ToArray(); + } + } + } +}