@copylee/dsh-office 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +15 -2
- package/helper/OfficeHelper.cs +637 -32
- package/lib/client.js +49 -1
- package/lib/index.js +197 -91
- package/lib/types/format.d.ts +2 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/settings.d.ts +8 -0
- package/package.json +1 -1
package/helper/OfficeHelper.cs
CHANGED
|
@@ -100,8 +100,158 @@ class MessageFilter : IOleMessageFilter
|
|
|
100
100
|
public int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType) { return 2; }
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
|
|
104
|
+
/// A small always-on-top card shown while the agent edits: what it is doing, and two switches the user
|
|
105
|
+
/// can flip at any moment (follow the edits / write like typing). It never takes the focus.
|
|
106
|
+
class Card : Form
|
|
107
|
+
{
|
|
108
|
+
static Card instance;
|
|
109
|
+
static readonly ManualResetEvent Ready = new ManualResetEvent(false);
|
|
110
|
+
/// Set once the user has flipped a switch here: from then on the card, not the settings page, decides.
|
|
111
|
+
public static volatile bool FollowChosen, TypingChosen;
|
|
112
|
+
|
|
113
|
+
string line = "";
|
|
114
|
+
DateTime shown = DateTime.MinValue;
|
|
115
|
+
readonly float scale;
|
|
116
|
+
Rectangle followBox, typingBox;
|
|
117
|
+
Point grip;
|
|
118
|
+
bool pressed, dragged;
|
|
119
|
+
readonly System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
|
|
120
|
+
|
|
121
|
+
[DllImport("user32.dll")] static extern bool ShowWindow(IntPtr handle, int command);
|
|
122
|
+
|
|
123
|
+
protected override bool ShowWithoutActivation { get { return true; } }
|
|
124
|
+
|
|
125
|
+
protected override CreateParams CreateParams
|
|
126
|
+
{
|
|
127
|
+
get
|
|
128
|
+
{
|
|
129
|
+
CreateParams p = base.CreateParams;
|
|
130
|
+
p.ExStyle |= 0x08000000 | 0x00000080 | 0x00000008; // no-activate, tool window, topmost
|
|
131
|
+
return p;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Card()
|
|
136
|
+
{
|
|
137
|
+
FormBorderStyle = FormBorderStyle.None;
|
|
138
|
+
ShowInTaskbar = false;
|
|
139
|
+
StartPosition = FormStartPosition.Manual;
|
|
140
|
+
TopMost = true;
|
|
141
|
+
DoubleBuffered = true;
|
|
142
|
+
BackColor = Color.FromArgb(34, 34, 38);
|
|
143
|
+
Opacity = 0.94;
|
|
144
|
+
using (Graphics g = CreateGraphics()) scale = g.DpiX / 96f;
|
|
145
|
+
Size = new Size(S(330), S(38));
|
|
146
|
+
Rectangle area = Screen.PrimaryScreen.WorkingArea;
|
|
147
|
+
Location = new Point(area.Right - Width - S(18), area.Bottom - Height - S(18));
|
|
148
|
+
using (System.Drawing.Drawing2D.GraphicsPath path = Rounded(new Rectangle(0, 0, Width, Height), S(10))) Region = new Region(path);
|
|
149
|
+
timer.Interval = 500;
|
|
150
|
+
timer.Tick += delegate { if (Visible && (DateTime.UtcNow - shown).TotalSeconds > 6) Hide(); };
|
|
151
|
+
timer.Start();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
int S(int value) { return (int)Math.Round(value * scale); }
|
|
155
|
+
|
|
156
|
+
static System.Drawing.Drawing2D.GraphicsPath Rounded(Rectangle r, int radius)
|
|
157
|
+
{
|
|
158
|
+
System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath();
|
|
159
|
+
int d = radius * 2;
|
|
160
|
+
path.AddArc(r.X, r.Y, d, d, 180, 90);
|
|
161
|
+
path.AddArc(r.Right - d, r.Y, d, d, 270, 90);
|
|
162
|
+
path.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90);
|
|
163
|
+
path.AddArc(r.X, r.Bottom - d, d, d, 90, 90);
|
|
164
|
+
path.CloseFigure();
|
|
165
|
+
return path;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
void Chip(Graphics g, Font font, string label, bool on, ref int right, out Rectangle box)
|
|
169
|
+
{
|
|
170
|
+
Size size = TextRenderer.MeasureText(label, font);
|
|
171
|
+
box = new Rectangle(right - size.Width - S(14), (Height - S(24)) / 2, size.Width + S(14), S(24));
|
|
172
|
+
right = box.Left - S(6);
|
|
173
|
+
Color accent = Color.FromArgb(217, 119, 87);
|
|
174
|
+
using (System.Drawing.Drawing2D.GraphicsPath path = Rounded(box, S(12)))
|
|
175
|
+
{
|
|
176
|
+
if (on) using (SolidBrush fill = new SolidBrush(accent)) g.FillPath(fill, path);
|
|
177
|
+
else using (Pen pen = new Pen(Color.FromArgb(110, 110, 118))) g.DrawPath(pen, path);
|
|
178
|
+
}
|
|
179
|
+
TextRenderer.DrawText(g, label, font, box, on ? Color.White : Color.FromArgb(170, 170, 178), TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
protected override void OnPaint(PaintEventArgs e)
|
|
183
|
+
{
|
|
184
|
+
Graphics g = e.Graphics;
|
|
185
|
+
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
|
186
|
+
using (Font font = new Font("Microsoft YaHei UI", 9f))
|
|
187
|
+
{
|
|
188
|
+
int right = Width - S(10);
|
|
189
|
+
Chip(g, font, "逐字", Program.Typing, ref right, out typingBox);
|
|
190
|
+
Chip(g, font, "跟随", Program.Following, ref right, out followBox);
|
|
191
|
+
using (SolidBrush dot = new SolidBrush(Color.FromArgb(217, 119, 87))) g.FillEllipse(dot, S(12), (Height - S(8)) / 2, S(8), S(8));
|
|
192
|
+
Rectangle text = new Rectangle(S(26), 0, right - S(30), Height);
|
|
193
|
+
TextRenderer.DrawText(g, line, font, text, Color.FromArgb(235, 235, 240), TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
protected override void OnMouseDown(MouseEventArgs e) { pressed = true; dragged = false; grip = e.Location; }
|
|
198
|
+
|
|
199
|
+
protected override void OnMouseMove(MouseEventArgs e)
|
|
200
|
+
{
|
|
201
|
+
if (!pressed) return;
|
|
202
|
+
if (!dragged && Math.Abs(e.X - grip.X) + Math.Abs(e.Y - grip.Y) < S(4)) return;
|
|
203
|
+
dragged = true;
|
|
204
|
+
Location = new Point(Location.X + e.X - grip.X, Location.Y + e.Y - grip.Y);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
protected override void OnMouseUp(MouseEventArgs e)
|
|
208
|
+
{
|
|
209
|
+
pressed = false;
|
|
210
|
+
if (dragged) return;
|
|
211
|
+
if (followBox.Contains(e.Location)) { Program.Following = !Program.Following; FollowChosen = true; }
|
|
212
|
+
else if (typingBox.Contains(e.Location)) { Program.Typing = !Program.Typing; TypingChosen = true; }
|
|
213
|
+
shown = DateTime.UtcNow;
|
|
214
|
+
Invalidate();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public static void Start()
|
|
218
|
+
{
|
|
219
|
+
Thread thread = new Thread(delegate()
|
|
220
|
+
{
|
|
221
|
+
try { instance = new Card(); IntPtr handle = instance.Handle; }
|
|
222
|
+
catch (Exception) { instance = null; }
|
|
223
|
+
Ready.Set();
|
|
224
|
+
if (instance != null) Application.Run();
|
|
225
|
+
});
|
|
226
|
+
thread.SetApartmentState(ApartmentState.STA);
|
|
227
|
+
thread.IsBackground = true;
|
|
228
|
+
thread.Start();
|
|
229
|
+
Ready.WaitOne(5000);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// Show the card (if it is not up yet) with this line; it goes away by itself a few seconds after the last report.
|
|
233
|
+
public static void Report(string text)
|
|
234
|
+
{
|
|
235
|
+
Card card = instance;
|
|
236
|
+
if (card == null) return;
|
|
237
|
+
try
|
|
238
|
+
{
|
|
239
|
+
card.BeginInvoke(new MethodInvoker(delegate
|
|
240
|
+
{
|
|
241
|
+
card.line = text;
|
|
242
|
+
card.shown = DateTime.UtcNow;
|
|
243
|
+
if (!card.Visible) ShowWindow(card.Handle, 4);
|
|
244
|
+
card.Invalidate();
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
catch (Exception) { }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
103
251
|
static class Program
|
|
104
252
|
{
|
|
253
|
+
[DllImport("user32.dll")] static extern bool SetProcessDPIAware();
|
|
254
|
+
|
|
105
255
|
static readonly JavaScriptSerializer Json = new JavaScriptSerializer();
|
|
106
256
|
static TextWriter Out;
|
|
107
257
|
|
|
@@ -113,6 +263,7 @@ static class Program
|
|
|
113
263
|
Out = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false));
|
|
114
264
|
TextReader input = new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false));
|
|
115
265
|
MessageFilter.Register();
|
|
266
|
+
try { SetProcessDPIAware(); } catch (Exception) { }
|
|
116
267
|
Send(new Dictionary<string, object> { { "event", "ready" } });
|
|
117
268
|
string line;
|
|
118
269
|
while ((line = input.ReadLine()) != null)
|
|
@@ -320,6 +471,20 @@ static class Program
|
|
|
320
471
|
if (cmd == "render") return Render(kind, app, doc, a);
|
|
321
472
|
if (cmd == "save") return Save(kind, app, doc, a);
|
|
322
473
|
if (cmd == "close") return Close(kind, doc, a);
|
|
474
|
+
if (cmd == "view")
|
|
475
|
+
{
|
|
476
|
+
// Where the window is looking (for checking the follow mode).
|
|
477
|
+
Dictionary<string, object> view = new Dictionary<string, object>();
|
|
478
|
+
if (kind == "word")
|
|
479
|
+
{
|
|
480
|
+
dynamic window = doc.Windows[1];
|
|
481
|
+
view["scrolled"] = (int)window.VerticalPercentScrolled;
|
|
482
|
+
view["windows"] = (int)doc.Windows.Count;
|
|
483
|
+
view["viewType"] = (int)window.View.Type;
|
|
484
|
+
view["selection"] = (int)window.Selection.Start;
|
|
485
|
+
}
|
|
486
|
+
return view;
|
|
487
|
+
}
|
|
323
488
|
if (cmd == "undo")
|
|
324
489
|
{
|
|
325
490
|
if (kind != "word") throw new Fail("BAD_ARGS", "Only Word keeps the agent's edits on its undo stack.");
|
|
@@ -503,6 +668,226 @@ static class Program
|
|
|
503
668
|
return text.Replace("\r\n", "\r").Replace("\n", "\r");
|
|
504
669
|
}
|
|
505
670
|
|
|
671
|
+
|
|
672
|
+
// ───────────────────────── watching the work ─────────────────────────
|
|
673
|
+
|
|
674
|
+
/// Scroll the window to where the agent is working (off = silent mode: the user's view is left alone).
|
|
675
|
+
public static volatile bool Following;
|
|
676
|
+
/// Write text a few characters at a time, the way a person types, instead of all at once.
|
|
677
|
+
public static volatile bool Typing;
|
|
678
|
+
|
|
679
|
+
static bool cardStarted;
|
|
680
|
+
|
|
681
|
+
static void Follow(dynamic doc, dynamic range)
|
|
682
|
+
{
|
|
683
|
+
if (!Following) return;
|
|
684
|
+
try { doc.Windows[1].ScrollIntoView(range, true); } catch (Exception) { }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/// Pieces to write a text in: up to 40 of them, so that a long paragraph still takes about a second.
|
|
688
|
+
static List<string> Pieces(string text)
|
|
689
|
+
{
|
|
690
|
+
List<string> pieces = new List<string>();
|
|
691
|
+
int size = Math.Max(2, (int)Math.Ceiling(text.Length / 40.0));
|
|
692
|
+
for (int i = 0; i < text.Length; i += size) pieces.Add(text.Substring(i, Math.Min(size, text.Length - i)));
|
|
693
|
+
return pieces;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/// Put text at a position of a Word document; returns the position after it.
|
|
697
|
+
static int Write(dynamic doc, int at, string text)
|
|
698
|
+
{
|
|
699
|
+
if (text.Length == 0) return at;
|
|
700
|
+
if (!Typing || text.Length < 4)
|
|
701
|
+
{
|
|
702
|
+
doc.Range(at, at).InsertAfter(text);
|
|
703
|
+
return at + text.Length;
|
|
704
|
+
}
|
|
705
|
+
foreach (string piece in Pieces(text))
|
|
706
|
+
{
|
|
707
|
+
doc.Range(at, at).InsertAfter(piece);
|
|
708
|
+
at += piece.Length;
|
|
709
|
+
Thread.Sleep(22);
|
|
710
|
+
}
|
|
711
|
+
return at;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ───────────────────────── equations ─────────────────────────
|
|
715
|
+
|
|
716
|
+
static readonly Dictionary<string, string> TexSymbols = new Dictionary<string, string>
|
|
717
|
+
{
|
|
718
|
+
{ "alpha", "α" }, { "beta", "β" }, { "gamma", "γ" }, { "delta", "δ" }, { "epsilon", "ε" }, { "varepsilon", "ε" }, { "zeta", "ζ" }, { "eta", "η" },
|
|
719
|
+
{ "theta", "θ" }, { "vartheta", "ϑ" }, { "iota", "ι" }, { "kappa", "κ" }, { "lambda", "λ" }, { "mu", "μ" }, { "nu", "ν" }, { "xi", "ξ" },
|
|
720
|
+
{ "pi", "π" }, { "rho", "ρ" }, { "sigma", "σ" }, { "tau", "τ" }, { "upsilon", "υ" }, { "phi", "φ" }, { "varphi", "φ" }, { "chi", "χ" },
|
|
721
|
+
{ "psi", "ψ" }, { "omega", "ω" }, { "Gamma", "Γ" }, { "Delta", "Δ" }, { "Theta", "Θ" }, { "Lambda", "Λ" }, { "Xi", "Ξ" }, { "Pi", "Π" },
|
|
722
|
+
{ "Sigma", "Σ" }, { "Phi", "Φ" }, { "Psi", "Ψ" }, { "Omega", "Ω" },
|
|
723
|
+
{ "cdot", "⋅" }, { "times", "×" }, { "div", "÷" }, { "pm", "±" }, { "mp", "∓" }, { "approx", "≈" }, { "neq", "≠" }, { "ne", "≠" },
|
|
724
|
+
{ "leq", "≤" }, { "le", "≤" }, { "geq", "≥" }, { "ge", "≥" }, { "ll", "≪" }, { "gg", "≫" }, { "equiv", "≡" }, { "propto", "∝" }, { "sim", "∼" },
|
|
725
|
+
{ "infty", "∞" }, { "partial", "∂" }, { "nabla", "∇" }, { "sum", "∑" }, { "prod", "∏" }, { "int", "∫" }, { "oint", "∮" },
|
|
726
|
+
{ "to", "→" }, { "rightarrow", "→" }, { "leftarrow", "←" }, { "Rightarrow", "⇒" }, { "Leftrightarrow", "⇔" }, { "in", "∈" }, { "notin", "∉" },
|
|
727
|
+
{ "subset", "⊂" }, { "cup", "∪" }, { "cap", "∩" }, { "forall", "∀" }, { "exists", "∃" }, { "angle", "∠" }, { "perp", "⊥" }, { "parallel", "∥" },
|
|
728
|
+
{ "circ", "∘" }, { "degree", "°" }, { "ldots", "…" }, { "cdots", "⋯" }, { "dots", "…" }, { "prime", "′" }, { "hbar", "ℏ" }, { "ell", "ℓ" },
|
|
729
|
+
{ "quad", " " }, { "qquad", " " }, { "left", "" }, { "right", "" }, { "displaystyle", "" }, { "limits", "" }, { "lim", "lim" },
|
|
730
|
+
{ "sin", "sin" }, { "cos", "cos" }, { "tan", "tan" }, { "cot", "cot" }, { "ln", "ln" }, { "log", "log" }, { "exp", "exp" }, { "max", "max" }, { "min", "min" },
|
|
731
|
+
{ "arcsin", "arcsin" }, { "arccos", "arccos" }, { "arctan", "arctan" }, { "sinh", "sinh" }, { "cosh", "cosh" }, { "tanh", "tanh" },
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
/// One TeX argument starting at i: a {group}, a \command, or a single character. Returns it converted.
|
|
735
|
+
static string TexArg(string s, ref int i)
|
|
736
|
+
{
|
|
737
|
+
while (i < s.Length && s[i] == ' ') i++;
|
|
738
|
+
if (i >= s.Length) return "";
|
|
739
|
+
if (s[i] == '{')
|
|
740
|
+
{
|
|
741
|
+
int depth = 0, start = i + 1;
|
|
742
|
+
for (; i < s.Length; i++)
|
|
743
|
+
{
|
|
744
|
+
if (s[i] == '{') depth++;
|
|
745
|
+
else if (s[i] == '}' && --depth == 0) break;
|
|
746
|
+
}
|
|
747
|
+
string inner = s.Substring(start, Math.Max(0, Math.Min(i, s.Length) - start));
|
|
748
|
+
i++;
|
|
749
|
+
return Tex(inner);
|
|
750
|
+
}
|
|
751
|
+
if (s[i] == '\\')
|
|
752
|
+
{
|
|
753
|
+
int start = i;
|
|
754
|
+
i++;
|
|
755
|
+
while (i < s.Length && char.IsLetter(s[i])) i++;
|
|
756
|
+
if (i == start + 1 && i < s.Length) i++;
|
|
757
|
+
return Tex(s.Substring(start, i - start));
|
|
758
|
+
}
|
|
759
|
+
return s[i++].ToString();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/// A space ends the fraction or root just written; without it Word pulls what follows into the denominator.
|
|
763
|
+
static void Gap(StringBuilder o, string s, int i)
|
|
764
|
+
{
|
|
765
|
+
while (i < s.Length && s[i] == ' ') i++;
|
|
766
|
+
if (i < s.Length && (char.IsLetterOrDigit(s[i]) || s[i] == '\\' || s[i] == '(' || s[i] == '{')) o.Append(' ');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/// The common part of LaTeX math to Word's linear format (UnicodeMath), which Word then builds into a native equation.
|
|
770
|
+
static string Tex(string s)
|
|
771
|
+
{
|
|
772
|
+
StringBuilder o = new StringBuilder();
|
|
773
|
+
int i = 0;
|
|
774
|
+
while (i < s.Length)
|
|
775
|
+
{
|
|
776
|
+
char c = s[i];
|
|
777
|
+
if (c == '\\')
|
|
778
|
+
{
|
|
779
|
+
int start = ++i;
|
|
780
|
+
while (i < s.Length && char.IsLetter(s[i])) i++;
|
|
781
|
+
if (i == start)
|
|
782
|
+
{
|
|
783
|
+
// \, \; \! \ are spacing; \{ \} \% and the like are the character itself.
|
|
784
|
+
char next = i < s.Length ? s[i++] : ' ';
|
|
785
|
+
if (next == ',' || next == ';' || next == ':' || next == ' ') o.Append(' ');
|
|
786
|
+
else if (next == '\\') o.Append(' ');
|
|
787
|
+
else if (next != '!') o.Append(next);
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
string name = s.Substring(start, i - start);
|
|
791
|
+
if (name == "frac" || name == "dfrac" || name == "tfrac") {
|
|
792
|
+
string top = TexArg(s, ref i), bottom = TexArg(s, ref i);
|
|
793
|
+
// A factor written right before the fraction (2rac{a}{b}) must not be read into its numerator.
|
|
794
|
+
if (o.Length > 0 && char.IsLetterOrDigit(o[o.Length - 1])) o.Append(' ');
|
|
795
|
+
o.Append("(" + top + ")/(" + bottom + ")");
|
|
796
|
+
Gap(o, s, i);
|
|
797
|
+
}
|
|
798
|
+
else if (name == "sqrt")
|
|
799
|
+
{
|
|
800
|
+
string degree = null;
|
|
801
|
+
if (i < s.Length && s[i] == '[') { int end = s.IndexOf(']', i); if (end > i) { degree = Tex(s.Substring(i + 1, end - i - 1)); i = end + 1; } }
|
|
802
|
+
string body = TexArg(s, ref i);
|
|
803
|
+
o.Append(degree == null ? "√(" + body + ")" : "√(" + degree + "&" + body + ")");
|
|
804
|
+
Gap(o, s, i);
|
|
805
|
+
}
|
|
806
|
+
else if (name == "bar" || name == "overline") o.Append("¯(" + TexArg(s, ref i) + ")");
|
|
807
|
+
else if (name == "hat") o.Append(TexArg(s, ref i) + "\u0302");
|
|
808
|
+
else if (name == "vec") o.Append(TexArg(s, ref i) + "\u20D7");
|
|
809
|
+
else if (name == "dot") o.Append(TexArg(s, ref i) + "\u0307");
|
|
810
|
+
else if (name == "ddot") o.Append(TexArg(s, ref i) + "\u0308");
|
|
811
|
+
else if (name == "tilde") o.Append(TexArg(s, ref i) + "\u0303");
|
|
812
|
+
else if (name == "text" || name == "mathrm" || name == "textrm" || name == "operatorname")
|
|
813
|
+
{
|
|
814
|
+
while (i < s.Length && s[i] == ' ') i++;
|
|
815
|
+
if (i < s.Length && s[i] == '{') { int end = s.IndexOf('}', i); if (end < 0) end = s.Length; o.Append("\"" + s.Substring(i + 1, end - i - 1) + "\""); i = Math.Min(s.Length, end + 1); }
|
|
816
|
+
}
|
|
817
|
+
else if (name == "mathbf" || name == "boldsymbol" || name == "mathit" || name == "mathbb" || name == "mathcal") o.Append(TexArg(s, ref i));
|
|
818
|
+
else
|
|
819
|
+
{
|
|
820
|
+
string symbol;
|
|
821
|
+
if (TexSymbols.TryGetValue(name, out symbol))
|
|
822
|
+
{
|
|
823
|
+
o.Append(symbol);
|
|
824
|
+
// A function name is applied to what follows: Word needs a space after it.
|
|
825
|
+
if (symbol.Length > 1 && symbol == name && i < s.Length && s[i] != ' ' && s[i] != '^' && s[i] != '_') o.Append(' ');
|
|
826
|
+
}
|
|
827
|
+
else o.Append('\\').Append(name);
|
|
828
|
+
}
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (c == '^' || c == '_')
|
|
832
|
+
{
|
|
833
|
+
i++;
|
|
834
|
+
bool braced = i < s.Length && s[i] == '{';
|
|
835
|
+
string arg = TexArg(s, ref i);
|
|
836
|
+
o.Append(c).Append(braced && arg.Length > 1 ? "(" + arg + ")" : arg);
|
|
837
|
+
// Otherwise Word reads the next letter into the script: x^2y.
|
|
838
|
+
if (i < s.Length && (char.IsLetterOrDigit(s[i]) || s[i] == '\\')) o.Append(' ');
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (c == '{') { string group = TexArg(s, ref i); o.Append("〖" + group + "〗"); continue; }
|
|
842
|
+
if (c == '}') { i++; continue; }
|
|
843
|
+
if (c == '~') { o.Append(' '); i++; continue; }
|
|
844
|
+
o.Append(c);
|
|
845
|
+
i++;
|
|
846
|
+
}
|
|
847
|
+
return o.ToString();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
static readonly System.Text.RegularExpressions.Regex Dollars = new System.Text.RegularExpressions.Regex(@"\$\$([^$\r]+?)\$\$|\$([^$\r]+?)\$(?!\$)");
|
|
851
|
+
|
|
852
|
+
static readonly System.Text.RegularExpressions.Regex Tag = new System.Text.RegularExpressions.Regex(@"\\tag\s*\{([^}]*)\}");
|
|
853
|
+
|
|
854
|
+
/// Turn every $...$ (inline) and $$...$$ (on a line of its own) inside a range into a native Word equation.
|
|
855
|
+
static int Mathify(dynamic doc, dynamic range)
|
|
856
|
+
{
|
|
857
|
+
string text = (string)range.Text;
|
|
858
|
+
if (text == null || text.IndexOf('$') < 0) return 0;
|
|
859
|
+
int start = (int)range.Start, made = 0;
|
|
860
|
+
System.Text.RegularExpressions.MatchCollection found = Dollars.Matches(text);
|
|
861
|
+
for (int k = found.Count - 1; k >= 0; k--)
|
|
862
|
+
{
|
|
863
|
+
System.Text.RegularExpressions.Match m = found[k];
|
|
864
|
+
bool display = m.Groups[1].Success;
|
|
865
|
+
string source = (display ? m.Groups[1].Value : m.Groups[2].Value).Trim();
|
|
866
|
+
// ag{1} numbers a display equation: Word sets "#(1)" flush right on the equation's line.
|
|
867
|
+
string tag = null;
|
|
868
|
+
System.Text.RegularExpressions.Match tagged = Tag.Match(source);
|
|
869
|
+
if (tagged.Success) { tag = tagged.Groups[1].Value.Trim(); source = source.Remove(tagged.Index, tagged.Length).Trim(); }
|
|
870
|
+
dynamic spot = null;
|
|
871
|
+
if (m.Length <= 250)
|
|
872
|
+
{
|
|
873
|
+
// Found by its text: positions counted in the text are off wherever the range holds a field (a caption number).
|
|
874
|
+
dynamic search = range.Duplicate;
|
|
875
|
+
if ((bool)search.Find.Execute(FindText: m.Value.Replace("^", "^^"), MatchCase: true, MatchWildcards: false, Forward: true, Wrap: 0)) spot = search;
|
|
876
|
+
}
|
|
877
|
+
if (spot == null) spot = doc.Range(start + m.Index, start + m.Index + m.Length);
|
|
878
|
+
spot.Text = Tex(source) + (display && !string.IsNullOrEmpty(tag) ? "#(" + tag + ")" : "");
|
|
879
|
+
try
|
|
880
|
+
{
|
|
881
|
+
dynamic math = doc.OMaths.Add(spot);
|
|
882
|
+
math.OMaths[1].BuildUp();
|
|
883
|
+
if (display) { try { math.OMaths[1].Type = 0; math.OMaths[1].Justification = 1; } catch (COMException) { } }
|
|
884
|
+
made++;
|
|
885
|
+
}
|
|
886
|
+
catch (COMException) { }
|
|
887
|
+
}
|
|
888
|
+
return made;
|
|
889
|
+
}
|
|
890
|
+
|
|
506
891
|
// ───────────────────────── Word ─────────────────────────
|
|
507
892
|
|
|
508
893
|
static readonly Dictionary<string, int> WordStyles = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
|
|
@@ -530,6 +915,7 @@ static class Program
|
|
|
530
915
|
int total = (int)doc.Paragraphs.Count;
|
|
531
916
|
if (index < 1 || index > total) throw new Fail("ANCHOR_MISSING", "There is no paragraph " + index + " (the document has " + total + "). Call office_read again.");
|
|
532
917
|
dynamic p = doc.Paragraphs[index];
|
|
918
|
+
Follow(doc, p.Range);
|
|
533
919
|
if (expect != null)
|
|
534
920
|
{
|
|
535
921
|
string text = Squash(Clean((string)p.Range.Text)), want = Squash(expect);
|
|
@@ -572,7 +958,7 @@ static class Program
|
|
|
572
958
|
int from = Math.Max(1, a.Int("from", 1)), count = Math.Max(1, Math.Min(400, a.Int("count", 120)));
|
|
573
959
|
bool full = a.Flag("full", false);
|
|
574
960
|
List<object> items = new List<object>();
|
|
575
|
-
int shown = from - 1;
|
|
961
|
+
int shown = from - 1, openTable = -1;
|
|
576
962
|
string titleStyle = null;
|
|
577
963
|
try { titleStyle = (string)doc.Styles[-63].NameLocal; } catch (Exception) { }
|
|
578
964
|
if (from <= total)
|
|
@@ -596,19 +982,38 @@ static class Program
|
|
|
596
982
|
try { inTable = Truthy(p.Range.Information[12]); } catch (Exception) { }
|
|
597
983
|
if (inTable)
|
|
598
984
|
{
|
|
599
|
-
// A table is one entry, not one per cell: its cells are read with "table".
|
|
600
985
|
dynamic table = p.Range.Tables[1];
|
|
601
|
-
int
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
986
|
+
int tableStart = (int)table.Range.Start, end = (int)table.Range.End;
|
|
987
|
+
if (tableStart != openTable)
|
|
988
|
+
{
|
|
989
|
+
int inside = (int)table.Range.Paragraphs.Count, cells = (int)table.Range.Cells.Count, rows = (int)table.Rows.Count;
|
|
990
|
+
bool nested = false;
|
|
991
|
+
try { nested = (int)table.NestingLevel > 1; } catch (Exception) { }
|
|
992
|
+
Dictionary<string, object> head = new Dictionary<string, object>();
|
|
993
|
+
head["i"] = i;
|
|
994
|
+
head["to"] = i + inside - 1;
|
|
995
|
+
if (!nested) head["table"] = (int)doc.Range(0, end).Tables.Count;
|
|
996
|
+
string size = "?";
|
|
997
|
+
try { size = rows + "x" + (int)table.Columns.Count; } catch (COMException) { size = cells + " cells"; }
|
|
998
|
+
head["size"] = size;
|
|
999
|
+
// A grid of short cells is one entry (read its cells with "table"). A table whose cells hold the
|
|
1000
|
+
// document's text, as in a form or a report template, is listed paragraph by paragraph.
|
|
1001
|
+
bool open = inside > cells + rows + 2;
|
|
1002
|
+
if (open) head["open"] = true;
|
|
1003
|
+
items.Add(head);
|
|
1004
|
+
if (!open)
|
|
1005
|
+
{
|
|
1006
|
+
i += inside - 1;
|
|
1007
|
+
p = i < total ? doc.Range(end, end).Paragraphs[1] : null;
|
|
1008
|
+
shown = i;
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
openTable = tableStart;
|
|
1012
|
+
}
|
|
1013
|
+
bool rowEnd = false;
|
|
1014
|
+
try { rowEnd = Truthy(p.Range.Information[31]); } catch (Exception) { }
|
|
1015
|
+
if (rowEnd) { shown = i; p = p.Next(); continue; }
|
|
1016
|
+
item["inTable"] = true;
|
|
612
1017
|
}
|
|
613
1018
|
items.Add(item);
|
|
614
1019
|
shown = i;
|
|
@@ -683,7 +1088,62 @@ static class Program
|
|
|
683
1088
|
{
|
|
684
1089
|
int end = (int)paragraph.Range.End;
|
|
685
1090
|
paragraph.Range.InsertParagraphAfter();
|
|
686
|
-
|
|
1091
|
+
dynamic made = doc.Range(end, end).Paragraphs[1];
|
|
1092
|
+
// Word gives the new paragraph the look of whatever FOLLOWS (a caption, a heading); it should
|
|
1093
|
+
// continue the paragraph it was added after.
|
|
1094
|
+
try
|
|
1095
|
+
{
|
|
1096
|
+
made.Style = paragraph.Style;
|
|
1097
|
+
made.Range.ParagraphFormat = paragraph.Range.ParagraphFormat.Duplicate;
|
|
1098
|
+
made.Range.Font = paragraph.Range.Font.Duplicate;
|
|
1099
|
+
}
|
|
1100
|
+
catch (COMException) { }
|
|
1101
|
+
return made;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/// Breathing room around a table or figure: at least this much space before / after a paragraph.
|
|
1105
|
+
/// Paragraph spacing, not blank lines, so nothing has to be cleaned up later.
|
|
1106
|
+
const float Air = 8f;
|
|
1107
|
+
|
|
1108
|
+
static void Space(dynamic paragraph, bool before)
|
|
1109
|
+
{
|
|
1110
|
+
if (paragraph == null) return;
|
|
1111
|
+
try
|
|
1112
|
+
{
|
|
1113
|
+
dynamic format = paragraph.Range.ParagraphFormat;
|
|
1114
|
+
if (before) { if ((float)format.SpaceBefore < Air) format.SpaceBefore = Air; }
|
|
1115
|
+
else if ((float)format.SpaceAfter < Air) format.SpaceAfter = Air;
|
|
1116
|
+
}
|
|
1117
|
+
catch (Exception) { }
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/// A numbered caption, the way reports set them: "表 1 ..." centred above a table, "图 1 ..." centred below
|
|
1121
|
+
/// a figure. The number is a Word caption field, so it renumbers itself and can be cross-referenced.
|
|
1122
|
+
static void Caption(dynamic doc, dynamic target, bool table, string title, string fontName, string fontFarEast)
|
|
1123
|
+
{
|
|
1124
|
+
string label = table ? "表" : "图";
|
|
1125
|
+
dynamic labels = doc.Application.CaptionLabels;
|
|
1126
|
+
bool known = false;
|
|
1127
|
+
foreach (dynamic item in labels) if ((string)item.Name == label) { known = true; break; }
|
|
1128
|
+
if (!known) labels.Add(label);
|
|
1129
|
+
int start = (int)target.Start, end = (int)target.End;
|
|
1130
|
+
target.InsertCaption(Label: label, Title: " " + title.Trim(), Position: table ? 0 : 1);
|
|
1131
|
+
// Above a table the caption is the paragraph that now sits where the table began; below a figure it is the next one.
|
|
1132
|
+
dynamic caption = table ? doc.Range(start, start).Paragraphs[1] : doc.Range(end, end).Paragraphs[1].Next();
|
|
1133
|
+
if (caption == null) return;
|
|
1134
|
+
try
|
|
1135
|
+
{
|
|
1136
|
+
dynamic range = caption.Range;
|
|
1137
|
+
range.ParagraphFormat.Alignment = 1;
|
|
1138
|
+
range.ParagraphFormat.CharacterUnitFirstLineIndent = 0; range.ParagraphFormat.FirstLineIndent = 0;
|
|
1139
|
+
range.Font.Bold = 0; range.Font.Italic = 0; range.Font.Color = -16777216; range.Font.Size = 10.5f;
|
|
1140
|
+
if (!string.IsNullOrEmpty(fontName)) range.Font.Name = fontName;
|
|
1141
|
+
if (!string.IsNullOrEmpty(fontFarEast)) range.Font.NameFarEast = fontFarEast;
|
|
1142
|
+
range.ParagraphFormat.SpaceBefore = table ? Air : 3f;
|
|
1143
|
+
range.ParagraphFormat.SpaceAfter = table ? 3f : Air;
|
|
1144
|
+
Mathify(doc, caption.Range);
|
|
1145
|
+
}
|
|
1146
|
+
catch (COMException) { }
|
|
687
1147
|
}
|
|
688
1148
|
|
|
689
1149
|
/// The number of a paragraph, counted from the start of the document.
|
|
@@ -692,6 +1152,23 @@ static class Program
|
|
|
692
1152
|
return (int)doc.Range(0, paragraph.Range.End).Paragraphs.Count;
|
|
693
1153
|
}
|
|
694
1154
|
|
|
1155
|
+
/// A new paragraph with no style named: body text that matches its surroundings. Written next to body
|
|
1156
|
+
/// text it keeps that text's font, size and spacing (a template's own look) but not the emphasis of the
|
|
1157
|
+
/// line it was split from (a bold label, a red hint); written after a heading it becomes Normal.
|
|
1158
|
+
static void Body(dynamic paragraph)
|
|
1159
|
+
{
|
|
1160
|
+
int level = 10;
|
|
1161
|
+
try { level = (int)paragraph.OutlineLevel; } catch (Exception) { }
|
|
1162
|
+
if (level != 10) { Plain(paragraph, "Normal"); return; }
|
|
1163
|
+
try
|
|
1164
|
+
{
|
|
1165
|
+
dynamic font = paragraph.Range.Font;
|
|
1166
|
+
font.Bold = 0; font.Italic = 0; font.Underline = 0; font.Color = -16777216;
|
|
1167
|
+
paragraph.Range.ParagraphFormat.Alignment = 3;
|
|
1168
|
+
}
|
|
1169
|
+
catch (COMException) { }
|
|
1170
|
+
}
|
|
1171
|
+
|
|
695
1172
|
static void Plain(dynamic paragraph, string style)
|
|
696
1173
|
{
|
|
697
1174
|
SetStyle(paragraph.Range, style);
|
|
@@ -721,7 +1198,7 @@ static class Program
|
|
|
721
1198
|
IList items = op.List("items");
|
|
722
1199
|
if (items == null) { items = new ArrayList(); items.Add(new Dictionary<string, object> { { "text", op.Need("text") } }); }
|
|
723
1200
|
dynamic current = null;
|
|
724
|
-
int made = 0;
|
|
1201
|
+
int made = 0, equations = 0;
|
|
725
1202
|
foreach (object raw in items)
|
|
726
1203
|
{
|
|
727
1204
|
Bag item = raw is string ? new Bag(new Dictionary<string, object> { { "text", raw } }) : new Bag(raw);
|
|
@@ -730,16 +1207,20 @@ static class Program
|
|
|
730
1207
|
dynamic p;
|
|
731
1208
|
if (current == null) p = NewParagraph(doc, op);
|
|
732
1209
|
else p = After(doc, current);
|
|
733
|
-
|
|
734
|
-
if (
|
|
1210
|
+
string style = item.Str("style", op.Str("style", null));
|
|
1211
|
+
if (style != null) Plain(p, style);
|
|
1212
|
+
else Body(p);
|
|
735
1213
|
WordFormat(p.Range, StyleLess(op));
|
|
736
1214
|
WordFormat(p.Range, StyleLess(item));
|
|
1215
|
+
Follow(doc, p.Range);
|
|
1216
|
+
Write(doc, (int)p.Range.Start, text);
|
|
1217
|
+
equations += Mathify(doc, p.Range);
|
|
737
1218
|
current = p;
|
|
738
1219
|
made++;
|
|
739
1220
|
}
|
|
740
1221
|
}
|
|
741
1222
|
int lastIndex = Index(doc, current);
|
|
742
|
-
return "inserted " + made + " paragraph(s), now paragraphs " + (lastIndex - made + 1) + "–" + lastIndex + " of " + (int)doc.Paragraphs.Count;
|
|
1223
|
+
return "inserted " + made + " paragraph(s), now paragraphs " + (lastIndex - made + 1) + "–" + lastIndex + " of " + (int)doc.Paragraphs.Count + (equations > 0 ? ", " + equations + " equation(s)" : "");
|
|
743
1224
|
}
|
|
744
1225
|
if ((type == "set_text" || (type == "delete_range" && op.Has("para"))) && op.Raw("expect") == null)
|
|
745
1226
|
{
|
|
@@ -750,8 +1231,10 @@ static class Program
|
|
|
750
1231
|
dynamic p = Para(doc, op.Int("para", 0), op.Str("expect", null));
|
|
751
1232
|
dynamic range = p.Range;
|
|
752
1233
|
range.MoveEnd(1, -1);
|
|
753
|
-
range.Text =
|
|
754
|
-
|
|
1234
|
+
range.Text = "";
|
|
1235
|
+
Write(doc, (int)p.Range.Start, Lines(op.Raw("text") ?? ""));
|
|
1236
|
+
int built = Mathify(doc, p.Range);
|
|
1237
|
+
return "paragraph " + op.Int("para", 0) + " rewritten" + (built > 0 ? ", " + built + " equation(s)" : "");
|
|
755
1238
|
}
|
|
756
1239
|
if (type == "format_text" || type == "set_style")
|
|
757
1240
|
{
|
|
@@ -762,8 +1245,20 @@ static class Program
|
|
|
762
1245
|
if (type == "delete_range")
|
|
763
1246
|
{
|
|
764
1247
|
if (!op.Has("para") && !op.Has("find")) throw new Fail("BAD_ARGS", "Say what to delete with \"para\" (and \"to\") or \"find\".");
|
|
765
|
-
WordRange(doc, op)
|
|
766
|
-
|
|
1248
|
+
dynamic gone = WordRange(doc, op);
|
|
1249
|
+
string note = "";
|
|
1250
|
+
try { gone.Delete(); }
|
|
1251
|
+
catch (COMException)
|
|
1252
|
+
{
|
|
1253
|
+
// The mark that ends a table cell cannot be deleted: take everything before it.
|
|
1254
|
+
int limit = (int)gone.End - 1;
|
|
1255
|
+
try { int from = (int)gone.Start; limit = Math.Min(limit, (int)doc.Range(from, from).Cells[1].Range.End - 1); } catch (Exception) { }
|
|
1256
|
+
gone.End = limit;
|
|
1257
|
+
if ((int)gone.End <= (int)gone.Start) throw new Fail("BAD_ARGS", "This is the last paragraph of a table cell; a cell always keeps one. Leave it empty.");
|
|
1258
|
+
gone.Delete();
|
|
1259
|
+
note = " (the last paragraph of the cell stays, a cell always keeps one)";
|
|
1260
|
+
}
|
|
1261
|
+
return "deleted, " + (int)doc.Paragraphs.Count + " paragraphs now" + note;
|
|
767
1262
|
}
|
|
768
1263
|
if (type == "insert_table")
|
|
769
1264
|
{
|
|
@@ -772,7 +1267,18 @@ static class Program
|
|
|
772
1267
|
if (data != null) foreach (object row in data) { IList cells = row as IList; if (cells != null) cols = Math.Max(cols, cells.Count); }
|
|
773
1268
|
if (rows < 1 || cols < 1) throw new Fail("BAD_ARGS", "Give \"data\" (rows of cells) or \"rows\" and \"cols\".");
|
|
774
1269
|
dynamic p = NewParagraph(doc, op);
|
|
1270
|
+
string fontName = null, fontFarEast = null;
|
|
1271
|
+
float fontSize = 0;
|
|
1272
|
+
try
|
|
1273
|
+
{
|
|
1274
|
+
if ((int)p.OutlineLevel == 10)
|
|
1275
|
+
{
|
|
1276
|
+
fontName = (string)p.Range.Font.Name; fontFarEast = (string)p.Range.Font.NameFarEast; fontSize = (float)p.Range.Font.Size;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
catch (Exception) { }
|
|
775
1280
|
Plain(p, "Normal");
|
|
1281
|
+
Follow(doc, p.Range);
|
|
776
1282
|
dynamic table = doc.Tables.Add(p.Range, rows, cols);
|
|
777
1283
|
// The cells take the formatting of the paragraph that follows (a heading, say): make them plain.
|
|
778
1284
|
SetStyle(table.Range, "Normal");
|
|
@@ -782,36 +1288,76 @@ static class Program
|
|
|
782
1288
|
table.Range.ParagraphFormat.CharacterUnitFirstLineIndent = 0; table.Range.ParagraphFormat.FirstLineIndent = 0;
|
|
783
1289
|
}
|
|
784
1290
|
catch (COMException) { }
|
|
1291
|
+
try
|
|
1292
|
+
{
|
|
1293
|
+
// Same typeface and size as the body text the table sits in; cells centred like a data table.
|
|
1294
|
+
if (!string.IsNullOrEmpty(fontName)) table.Range.Font.Name = fontName;
|
|
1295
|
+
if (!string.IsNullOrEmpty(fontFarEast)) table.Range.Font.NameFarEast = fontFarEast;
|
|
1296
|
+
if (fontSize > 4 && fontSize < 100) table.Range.Font.Size = fontSize;
|
|
1297
|
+
table.Range.ParagraphFormat.Alignment = 1;
|
|
1298
|
+
table.Range.Cells.VerticalAlignment = 1;
|
|
1299
|
+
}
|
|
1300
|
+
catch (COMException) { }
|
|
785
1301
|
table.Borders.Enable = 1;
|
|
1302
|
+
try { table.AutoFitBehavior(2); } catch (COMException) { }
|
|
786
1303
|
if (data != null)
|
|
787
1304
|
{
|
|
788
1305
|
for (int r = 0; r < data.Count && r < rows; r++)
|
|
789
1306
|
{
|
|
790
1307
|
IList cells = data[r] as IList;
|
|
791
1308
|
if (cells == null) continue;
|
|
792
|
-
for (int c = 0; c < cells.Count; c++)
|
|
1309
|
+
for (int c = 0; c < cells.Count; c++)
|
|
1310
|
+
{
|
|
1311
|
+
if (cells[c] == null) continue;
|
|
1312
|
+
dynamic cell = table.Cell(r + 1, c + 1).Range;
|
|
1313
|
+
string value = Convert.ToString(cells[c], System.Globalization.CultureInfo.InvariantCulture);
|
|
1314
|
+
cell.Text = value;
|
|
1315
|
+
if (value.IndexOf('$') >= 0) Mathify(doc, table.Cell(r + 1, c + 1).Range);
|
|
1316
|
+
}
|
|
1317
|
+
if (Typing) Thread.Sleep(25);
|
|
793
1318
|
}
|
|
794
1319
|
}
|
|
795
1320
|
if (op.Flag("header", true)) table.Rows[1].Range.Font.Bold = 1;
|
|
1321
|
+
if (op.Has("caption")) Caption(doc, table.Range, true, op.Need("caption"), fontName, fontFarEast);
|
|
1322
|
+
else
|
|
1323
|
+
{
|
|
1324
|
+
int tableStart = (int)table.Range.Start;
|
|
1325
|
+
if (tableStart > 0) Space(doc.Range(tableStart - 1, tableStart - 1).Paragraphs[1], false);
|
|
1326
|
+
}
|
|
1327
|
+
int afterTable = (int)table.Range.End;
|
|
1328
|
+
if (afterTable < (int)doc.Content.End) Space(doc.Range(afterTable, afterTable).Paragraphs[1], true);
|
|
796
1329
|
int tableEnd = (int)doc.Range(0, table.Range.End).Paragraphs.Count;
|
|
797
|
-
|
|
1330
|
+
bool nested = false;
|
|
1331
|
+
try { nested = (int)table.NestingLevel > 1; } catch (Exception) { }
|
|
1332
|
+
return (nested ? "table inserted inside a cell of table " + (int)doc.Range(0, table.Range.End).Tables.Count : "table " + (int)doc.Range(0, table.Range.End).Tables.Count + " inserted") + " (" + rows + "×" + cols + "), now paragraphs " + (tableEnd - (int)table.Range.Paragraphs.Count + 1) + "–" + tableEnd + " of " + (int)doc.Paragraphs.Count;
|
|
798
1333
|
}
|
|
799
1334
|
if (type == "set_cell")
|
|
800
1335
|
{
|
|
801
1336
|
int n = op.Int("table", 1);
|
|
802
1337
|
if (n < 1 || n > (int)doc.Tables.Count) throw new Fail("ANCHOR_MISSING", "There is no table " + n + ".");
|
|
803
|
-
doc.Tables[n].Cell(op.Int("row", 1), op.Int("col", 1))
|
|
804
|
-
|
|
1338
|
+
dynamic cell = doc.Tables[n].Cell(op.Int("row", 1), op.Int("col", 1));
|
|
1339
|
+
Follow(doc, cell.Range);
|
|
1340
|
+
cell.Range.Text = "";
|
|
1341
|
+
Write(doc, (int)cell.Range.Start, Lines(op.Raw("text") ?? ""));
|
|
1342
|
+
int built = Mathify(doc, doc.Tables[n].Cell(op.Int("row", 1), op.Int("col", 1)).Range);
|
|
1343
|
+
return "cell set" + (built > 0 ? ", " + built + " equation(s)" : "");
|
|
805
1344
|
}
|
|
806
1345
|
if (type == "insert_image")
|
|
807
1346
|
{
|
|
808
1347
|
string path = op.Need("path");
|
|
809
1348
|
if (!File.Exists(path)) throw new Fail("BAD_ARGS", "Image \"" + path + "\" does not exist.");
|
|
810
1349
|
dynamic p = NewParagraph(doc, op);
|
|
1350
|
+
string bodyFont = null, bodyFarEast = null;
|
|
1351
|
+
try { if ((int)p.OutlineLevel == 10) { bodyFont = (string)p.Range.Font.Name; bodyFarEast = (string)p.Range.Font.NameFarEast; } } catch (Exception) { }
|
|
811
1352
|
Plain(p, "Normal");
|
|
1353
|
+
Follow(doc, p.Range);
|
|
812
1354
|
dynamic picture = p.Range.InlineShapes.AddPicture(FileName: path, LinkToFile: false, SaveWithDocument: true);
|
|
813
1355
|
if (op.Has("width")) { picture.LockAspectRatio = -1; picture.Width = (float)op.Num("width", 300); }
|
|
1356
|
+
try { p.Range.ParagraphFormat.CharacterUnitFirstLineIndent = 0; p.Range.ParagraphFormat.FirstLineIndent = 0; p.Range.ParagraphFormat.Alignment = 1; } catch (COMException) { }
|
|
814
1357
|
WordFormat(p.Range, StyleLess(op));
|
|
1358
|
+
p.Range.ParagraphFormat.SpaceBefore = Air;
|
|
1359
|
+
if (op.Has("caption")) Caption(doc, picture.Range, false, op.Need("caption"), bodyFont, bodyFarEast);
|
|
1360
|
+
else p.Range.ParagraphFormat.SpaceAfter = Air;
|
|
815
1361
|
return "image inserted";
|
|
816
1362
|
}
|
|
817
1363
|
throw new Fail("BAD_ARGS", "Unknown Word operation \"" + type + "\".");
|
|
@@ -953,6 +1499,23 @@ static class Program
|
|
|
953
1499
|
if (op.Has("rowHeight")) range.RowHeight = op.Num("rowHeight", 15);
|
|
954
1500
|
}
|
|
955
1501
|
|
|
1502
|
+
/// Bring the cells being worked on into view, without touching the selection.
|
|
1503
|
+
static void Show(dynamic book, dynamic sheet, dynamic range)
|
|
1504
|
+
{
|
|
1505
|
+
if (!Following) return;
|
|
1506
|
+
try
|
|
1507
|
+
{
|
|
1508
|
+
sheet.Activate();
|
|
1509
|
+
dynamic window = book.Windows[1];
|
|
1510
|
+
dynamic visible = window.VisibleRange;
|
|
1511
|
+
int row = (int)range.Row, column = (int)range.Column;
|
|
1512
|
+
int top = (int)visible.Row, left = (int)visible.Column;
|
|
1513
|
+
if (row < top || row >= top + (int)visible.Rows.Count - 1) window.ScrollRow = Math.Max(1, row - 3);
|
|
1514
|
+
if (column < left || column >= left + (int)visible.Columns.Count - 1) window.ScrollColumn = Math.Max(1, column - 1);
|
|
1515
|
+
}
|
|
1516
|
+
catch (Exception) { }
|
|
1517
|
+
}
|
|
1518
|
+
|
|
956
1519
|
static string ExcelOp(dynamic app, dynamic book, string type, Bag op)
|
|
957
1520
|
{
|
|
958
1521
|
if (type == "add_sheet")
|
|
@@ -977,7 +1540,18 @@ static class Program
|
|
|
977
1540
|
for (int c = 0; c < cells.Count; c++) grid[r, c] = Scalar(cells[c]);
|
|
978
1541
|
}
|
|
979
1542
|
dynamic target = Cells(ws, op.Str("range", "A1")).Cells[1, 1].Resize[rows.Count, width];
|
|
980
|
-
|
|
1543
|
+
Show(book, ws, target);
|
|
1544
|
+
if (Typing && rows.Count > 1 && rows.Count <= 60)
|
|
1545
|
+
{
|
|
1546
|
+
for (int r = 0; r < rows.Count; r++)
|
|
1547
|
+
{
|
|
1548
|
+
object[,] line = new object[1, width];
|
|
1549
|
+
for (int c = 0; c < width; c++) line[0, c] = grid[r, c];
|
|
1550
|
+
target.Rows[r + 1].Formula = line;
|
|
1551
|
+
Thread.Sleep(35);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
else target.Formula = grid;
|
|
981
1555
|
ExcelFormat(target, op);
|
|
982
1556
|
return "wrote " + (string)target.Address[false, false];
|
|
983
1557
|
}
|
|
@@ -1103,9 +1677,22 @@ static class Program
|
|
|
1103
1677
|
return result;
|
|
1104
1678
|
}
|
|
1105
1679
|
|
|
1680
|
+
static void PptWrite(dynamic textRange, string text)
|
|
1681
|
+
{
|
|
1682
|
+
if (!Typing || text.Length < 4) { textRange.Text = text; return; }
|
|
1683
|
+
textRange.Text = "";
|
|
1684
|
+
foreach (string piece in Pieces(text)) { textRange.InsertAfter(piece); Thread.Sleep(22); }
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
static void GoTo(dynamic deck, int slide)
|
|
1688
|
+
{
|
|
1689
|
+
if (!Following) return;
|
|
1690
|
+
try { deck.Windows[1].View.GotoSlide(slide); } catch (Exception) { }
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1106
1693
|
static void PptText(dynamic shape, Bag op)
|
|
1107
1694
|
{
|
|
1108
|
-
if (op.Has("text")) shape.TextFrame.TextRange
|
|
1695
|
+
if (op.Has("text")) PptWrite(shape.TextFrame.TextRange, Lines(op.Raw("text")));
|
|
1109
1696
|
if (!(op.Has("font") || op.Has("size") || op.Has("bold") || op.Has("italic") || op.Has("color") || op.Has("align"))) return;
|
|
1110
1697
|
dynamic range = shape.TextFrame.TextRange;
|
|
1111
1698
|
if (op.Has("font")) { string font = op.Need("font"); range.Font.Name = font; try { range.Font.NameFarEast = font; } catch (COMException) { } }
|
|
@@ -1144,12 +1731,14 @@ static class Program
|
|
|
1144
1731
|
string name = op.Str("layout", "title_content");
|
|
1145
1732
|
if (!Layouts.TryGetValue(name, out layout)) throw new Fail("BAD_ARGS", "Unknown layout \"" + name + "\": use " + string.Join(", ", new List<string>(Layouts.Keys).ToArray()) + ".");
|
|
1146
1733
|
dynamic slide = deck.Slides.Add(index, layout);
|
|
1147
|
-
|
|
1148
|
-
if (op.Has("
|
|
1734
|
+
GoTo(deck, index);
|
|
1735
|
+
if (op.Has("title")) PptWrite(slide.Shapes.Title.TextFrame.TextRange, Lines(op.Raw("title")));
|
|
1736
|
+
if (op.Has("body")) PptWrite(slide.Shapes.Placeholders[2].TextFrame.TextRange, Lines(op.Raw("body")));
|
|
1149
1737
|
if (op.Has("notes")) slide.NotesPage.Shapes.Placeholders[2].TextFrame.TextRange.Text = Lines(op.Raw("notes"));
|
|
1150
1738
|
return "slide " + index + " added";
|
|
1151
1739
|
}
|
|
1152
1740
|
dynamic target = Slide(deck, op);
|
|
1741
|
+
GoTo(deck, op.Int("slide", 0));
|
|
1153
1742
|
if (type == "delete_slide") { target.Delete(); return "slide deleted"; }
|
|
1154
1743
|
if (type == "move_slide") { target.MoveTo(op.Int("to", 1)); return "slide moved to " + op.Int("to", 1); }
|
|
1155
1744
|
if (type == "set_notes") { target.NotesPage.Shapes.Placeholders[2].TextFrame.TextRange.Text = Lines(op.Raw("text") ?? ""); return "notes set"; }
|
|
@@ -1189,6 +1778,12 @@ static class Program
|
|
|
1189
1778
|
if (ops == null || ops.Count == 0) throw new Fail("BAD_ARGS", "\"ops\" must list at least one operation.");
|
|
1190
1779
|
List<object> done = new List<object>();
|
|
1191
1780
|
Dictionary<string, object> result = new Dictionary<string, object>();
|
|
1781
|
+
// The settings give the two modes, until the user flips one on the card.
|
|
1782
|
+
if (!Card.FollowChosen) Following = a.Flag("follow", false);
|
|
1783
|
+
if (!Card.TypingChosen) Typing = a.Flag("typing", false);
|
|
1784
|
+
bool card = a.Flag("card", false);
|
|
1785
|
+
if (card && !cardStarted) { cardStarted = true; Card.Start(); }
|
|
1786
|
+
string docName = (string)doc.Name;
|
|
1192
1787
|
dynamic undo = null;
|
|
1193
1788
|
// One undo step for the whole batch: the user takes it back with a single Ctrl+Z.
|
|
1194
1789
|
if (kind == "word") { try { undo = app.UndoRecord; undo.StartCustomRecord("AI edit"); } catch (COMException) { undo = null; } }
|
|
@@ -1201,6 +1796,7 @@ static class Program
|
|
|
1201
1796
|
{
|
|
1202
1797
|
Bag op = new Bag(ops[i]);
|
|
1203
1798
|
type = op.Need("op");
|
|
1799
|
+
if (card) Card.Report("AI 正在编辑 " + docName + (ops.Count > 1 ? " · " + (i + 1) + "/" + ops.Count : ""));
|
|
1204
1800
|
string line = kind == "word" ? WordOp(doc, type, op) : kind == "excel" ? ExcelOp(app, doc, type, op) : PptOp(doc, type, op);
|
|
1205
1801
|
string ignored = op.Unread();
|
|
1206
1802
|
done.Add(ignored.Length == 0 ? line : line + " — IGNORED field(s) " + ignored + ": " + type + " does not take them, check the operation list");
|
|
@@ -1222,6 +1818,7 @@ static class Program
|
|
|
1222
1818
|
{
|
|
1223
1819
|
if (undo != null) { try { undo.EndCustomRecord(); } catch (COMException) { } }
|
|
1224
1820
|
}
|
|
1821
|
+
if (card) Card.Report(result.ContainsKey("failed") ? "AI 的修改停在第 " + (done.Count + 1) + " 项" : "AI 已改好 " + docName);
|
|
1225
1822
|
result["done"] = done;
|
|
1226
1823
|
result["total"] = ops.Count;
|
|
1227
1824
|
return result;
|
|
@@ -1252,13 +1849,21 @@ static class Program
|
|
|
1252
1849
|
using (Metafile meta = new Metafile(stream))
|
|
1253
1850
|
{
|
|
1254
1851
|
int height = (int)Math.Round(width * (double)meta.Height / meta.Width);
|
|
1852
|
+
// Drawn straight at the final size, hairlines (table borders) fall between pixels and vanish:
|
|
1853
|
+
// draw the page large first, then scale the picture down.
|
|
1854
|
+
int factor = Math.Max(2, (int)Math.Ceiling(3000.0 / width));
|
|
1855
|
+
using (Bitmap large = new Bitmap(width * factor, height * factor))
|
|
1255
1856
|
using (Bitmap bitmap = new Bitmap(width, height))
|
|
1256
1857
|
{
|
|
1257
|
-
using (Graphics g = Graphics.FromImage(
|
|
1858
|
+
using (Graphics g = Graphics.FromImage(large))
|
|
1258
1859
|
{
|
|
1259
1860
|
g.Clear(Color.White);
|
|
1861
|
+
g.DrawImage(meta, 0, 0, large.Width, large.Height);
|
|
1862
|
+
}
|
|
1863
|
+
using (Graphics g = Graphics.FromImage(bitmap))
|
|
1864
|
+
{
|
|
1260
1865
|
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
1261
|
-
g.DrawImage(
|
|
1866
|
+
g.DrawImage(large, 0, 0, width, height);
|
|
1262
1867
|
}
|
|
1263
1868
|
bitmap.Save(path, ImageFormat.Png);
|
|
1264
1869
|
}
|