@copylee/dsh-office 0.1.0 → 0.2.1
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 +17 -2
- package/helper/OfficeHelper.cs +649 -33
- package/lib/client.js +61 -1
- package/lib/index.js +251 -94
- package/lib/types/format.d.ts +2 -0
- package/lib/types/index.d.ts +5 -0
- package/lib/types/prompt.d.ts +4 -0
- package/lib/types/settings.d.ts +10 -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.");
|
|
@@ -395,6 +560,13 @@ static class Program
|
|
|
395
560
|
}
|
|
396
561
|
Dictionary<string, object> info = Info(kind, doc, Active(kind, app));
|
|
397
562
|
info["how"] = how;
|
|
563
|
+
if (a.Flag("card", false))
|
|
564
|
+
{
|
|
565
|
+
if (!cardStarted) { cardStarted = true; Card.Start(); }
|
|
566
|
+
Following = Card.FollowChosen ? Following : a.Flag("follow", false);
|
|
567
|
+
Typing = Card.TypingChosen ? Typing : a.Flag("typing", false);
|
|
568
|
+
Card.Report("AI 打开了 " + (string)doc.Name);
|
|
569
|
+
}
|
|
398
570
|
return info;
|
|
399
571
|
}
|
|
400
572
|
|
|
@@ -503,6 +675,226 @@ static class Program
|
|
|
503
675
|
return text.Replace("\r\n", "\r").Replace("\n", "\r");
|
|
504
676
|
}
|
|
505
677
|
|
|
678
|
+
|
|
679
|
+
// ───────────────────────── watching the work ─────────────────────────
|
|
680
|
+
|
|
681
|
+
/// Scroll the window to where the agent is working (off = silent mode: the user's view is left alone).
|
|
682
|
+
public static volatile bool Following;
|
|
683
|
+
/// Write text a few characters at a time, the way a person types, instead of all at once.
|
|
684
|
+
public static volatile bool Typing;
|
|
685
|
+
|
|
686
|
+
static bool cardStarted;
|
|
687
|
+
|
|
688
|
+
static void Follow(dynamic doc, dynamic range)
|
|
689
|
+
{
|
|
690
|
+
if (!Following) return;
|
|
691
|
+
try { doc.Windows[1].ScrollIntoView(range, true); } catch (Exception) { }
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/// Pieces to write a text in: up to 40 of them, so that a long paragraph still takes about a second.
|
|
695
|
+
static List<string> Pieces(string text)
|
|
696
|
+
{
|
|
697
|
+
List<string> pieces = new List<string>();
|
|
698
|
+
int size = Math.Max(2, (int)Math.Ceiling(text.Length / 40.0));
|
|
699
|
+
for (int i = 0; i < text.Length; i += size) pieces.Add(text.Substring(i, Math.Min(size, text.Length - i)));
|
|
700
|
+
return pieces;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/// Put text at a position of a Word document; returns the position after it.
|
|
704
|
+
static int Write(dynamic doc, int at, string text)
|
|
705
|
+
{
|
|
706
|
+
if (text.Length == 0) return at;
|
|
707
|
+
if (!Typing || text.Length < 4)
|
|
708
|
+
{
|
|
709
|
+
doc.Range(at, at).InsertAfter(text);
|
|
710
|
+
return at + text.Length;
|
|
711
|
+
}
|
|
712
|
+
foreach (string piece in Pieces(text))
|
|
713
|
+
{
|
|
714
|
+
doc.Range(at, at).InsertAfter(piece);
|
|
715
|
+
at += piece.Length;
|
|
716
|
+
Thread.Sleep(22);
|
|
717
|
+
}
|
|
718
|
+
return at;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ───────────────────────── equations ─────────────────────────
|
|
722
|
+
|
|
723
|
+
static readonly Dictionary<string, string> TexSymbols = new Dictionary<string, string>
|
|
724
|
+
{
|
|
725
|
+
{ "alpha", "α" }, { "beta", "β" }, { "gamma", "γ" }, { "delta", "δ" }, { "epsilon", "ε" }, { "varepsilon", "ε" }, { "zeta", "ζ" }, { "eta", "η" },
|
|
726
|
+
{ "theta", "θ" }, { "vartheta", "ϑ" }, { "iota", "ι" }, { "kappa", "κ" }, { "lambda", "λ" }, { "mu", "μ" }, { "nu", "ν" }, { "xi", "ξ" },
|
|
727
|
+
{ "pi", "π" }, { "rho", "ρ" }, { "sigma", "σ" }, { "tau", "τ" }, { "upsilon", "υ" }, { "phi", "φ" }, { "varphi", "φ" }, { "chi", "χ" },
|
|
728
|
+
{ "psi", "ψ" }, { "omega", "ω" }, { "Gamma", "Γ" }, { "Delta", "Δ" }, { "Theta", "Θ" }, { "Lambda", "Λ" }, { "Xi", "Ξ" }, { "Pi", "Π" },
|
|
729
|
+
{ "Sigma", "Σ" }, { "Phi", "Φ" }, { "Psi", "Ψ" }, { "Omega", "Ω" },
|
|
730
|
+
{ "cdot", "⋅" }, { "times", "×" }, { "div", "÷" }, { "pm", "±" }, { "mp", "∓" }, { "approx", "≈" }, { "neq", "≠" }, { "ne", "≠" },
|
|
731
|
+
{ "leq", "≤" }, { "le", "≤" }, { "geq", "≥" }, { "ge", "≥" }, { "ll", "≪" }, { "gg", "≫" }, { "equiv", "≡" }, { "propto", "∝" }, { "sim", "∼" },
|
|
732
|
+
{ "infty", "∞" }, { "partial", "∂" }, { "nabla", "∇" }, { "sum", "∑" }, { "prod", "∏" }, { "int", "∫" }, { "oint", "∮" },
|
|
733
|
+
{ "to", "→" }, { "rightarrow", "→" }, { "leftarrow", "←" }, { "Rightarrow", "⇒" }, { "Leftrightarrow", "⇔" }, { "in", "∈" }, { "notin", "∉" },
|
|
734
|
+
{ "subset", "⊂" }, { "cup", "∪" }, { "cap", "∩" }, { "forall", "∀" }, { "exists", "∃" }, { "angle", "∠" }, { "perp", "⊥" }, { "parallel", "∥" },
|
|
735
|
+
{ "circ", "∘" }, { "degree", "°" }, { "ldots", "…" }, { "cdots", "⋯" }, { "dots", "…" }, { "prime", "′" }, { "hbar", "ℏ" }, { "ell", "ℓ" },
|
|
736
|
+
{ "quad", " " }, { "qquad", " " }, { "left", "" }, { "right", "" }, { "displaystyle", "" }, { "limits", "" }, { "lim", "lim" },
|
|
737
|
+
{ "sin", "sin" }, { "cos", "cos" }, { "tan", "tan" }, { "cot", "cot" }, { "ln", "ln" }, { "log", "log" }, { "exp", "exp" }, { "max", "max" }, { "min", "min" },
|
|
738
|
+
{ "arcsin", "arcsin" }, { "arccos", "arccos" }, { "arctan", "arctan" }, { "sinh", "sinh" }, { "cosh", "cosh" }, { "tanh", "tanh" },
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
/// One TeX argument starting at i: a {group}, a \command, or a single character. Returns it converted.
|
|
742
|
+
static string TexArg(string s, ref int i)
|
|
743
|
+
{
|
|
744
|
+
while (i < s.Length && s[i] == ' ') i++;
|
|
745
|
+
if (i >= s.Length) return "";
|
|
746
|
+
if (s[i] == '{')
|
|
747
|
+
{
|
|
748
|
+
int depth = 0, start = i + 1;
|
|
749
|
+
for (; i < s.Length; i++)
|
|
750
|
+
{
|
|
751
|
+
if (s[i] == '{') depth++;
|
|
752
|
+
else if (s[i] == '}' && --depth == 0) break;
|
|
753
|
+
}
|
|
754
|
+
string inner = s.Substring(start, Math.Max(0, Math.Min(i, s.Length) - start));
|
|
755
|
+
i++;
|
|
756
|
+
return Tex(inner);
|
|
757
|
+
}
|
|
758
|
+
if (s[i] == '\\')
|
|
759
|
+
{
|
|
760
|
+
int start = i;
|
|
761
|
+
i++;
|
|
762
|
+
while (i < s.Length && char.IsLetter(s[i])) i++;
|
|
763
|
+
if (i == start + 1 && i < s.Length) i++;
|
|
764
|
+
return Tex(s.Substring(start, i - start));
|
|
765
|
+
}
|
|
766
|
+
return s[i++].ToString();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/// A space ends the fraction or root just written; without it Word pulls what follows into the denominator.
|
|
770
|
+
static void Gap(StringBuilder o, string s, int i)
|
|
771
|
+
{
|
|
772
|
+
while (i < s.Length && s[i] == ' ') i++;
|
|
773
|
+
if (i < s.Length && (char.IsLetterOrDigit(s[i]) || s[i] == '\\' || s[i] == '(' || s[i] == '{')) o.Append(' ');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/// The common part of LaTeX math to Word's linear format (UnicodeMath), which Word then builds into a native equation.
|
|
777
|
+
static string Tex(string s)
|
|
778
|
+
{
|
|
779
|
+
StringBuilder o = new StringBuilder();
|
|
780
|
+
int i = 0;
|
|
781
|
+
while (i < s.Length)
|
|
782
|
+
{
|
|
783
|
+
char c = s[i];
|
|
784
|
+
if (c == '\\')
|
|
785
|
+
{
|
|
786
|
+
int start = ++i;
|
|
787
|
+
while (i < s.Length && char.IsLetter(s[i])) i++;
|
|
788
|
+
if (i == start)
|
|
789
|
+
{
|
|
790
|
+
// \, \; \! \ are spacing; \{ \} \% and the like are the character itself.
|
|
791
|
+
char next = i < s.Length ? s[i++] : ' ';
|
|
792
|
+
if (next == ',' || next == ';' || next == ':' || next == ' ') o.Append(' ');
|
|
793
|
+
else if (next == '\\') o.Append(' ');
|
|
794
|
+
else if (next != '!') o.Append(next);
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
string name = s.Substring(start, i - start);
|
|
798
|
+
if (name == "frac" || name == "dfrac" || name == "tfrac") {
|
|
799
|
+
string top = TexArg(s, ref i), bottom = TexArg(s, ref i);
|
|
800
|
+
// A factor written right before the fraction (2rac{a}{b}) must not be read into its numerator.
|
|
801
|
+
if (o.Length > 0 && char.IsLetterOrDigit(o[o.Length - 1])) o.Append(' ');
|
|
802
|
+
o.Append("(" + top + ")/(" + bottom + ")");
|
|
803
|
+
Gap(o, s, i);
|
|
804
|
+
}
|
|
805
|
+
else if (name == "sqrt")
|
|
806
|
+
{
|
|
807
|
+
string degree = null;
|
|
808
|
+
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; } }
|
|
809
|
+
string body = TexArg(s, ref i);
|
|
810
|
+
o.Append(degree == null ? "√(" + body + ")" : "√(" + degree + "&" + body + ")");
|
|
811
|
+
Gap(o, s, i);
|
|
812
|
+
}
|
|
813
|
+
else if (name == "bar" || name == "overline") o.Append("¯(" + TexArg(s, ref i) + ")");
|
|
814
|
+
else if (name == "hat") o.Append(TexArg(s, ref i) + "\u0302");
|
|
815
|
+
else if (name == "vec") o.Append(TexArg(s, ref i) + "\u20D7");
|
|
816
|
+
else if (name == "dot") o.Append(TexArg(s, ref i) + "\u0307");
|
|
817
|
+
else if (name == "ddot") o.Append(TexArg(s, ref i) + "\u0308");
|
|
818
|
+
else if (name == "tilde") o.Append(TexArg(s, ref i) + "\u0303");
|
|
819
|
+
else if (name == "text" || name == "mathrm" || name == "textrm" || name == "operatorname")
|
|
820
|
+
{
|
|
821
|
+
while (i < s.Length && s[i] == ' ') i++;
|
|
822
|
+
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); }
|
|
823
|
+
}
|
|
824
|
+
else if (name == "mathbf" || name == "boldsymbol" || name == "mathit" || name == "mathbb" || name == "mathcal") o.Append(TexArg(s, ref i));
|
|
825
|
+
else
|
|
826
|
+
{
|
|
827
|
+
string symbol;
|
|
828
|
+
if (TexSymbols.TryGetValue(name, out symbol))
|
|
829
|
+
{
|
|
830
|
+
o.Append(symbol);
|
|
831
|
+
// A function name is applied to what follows: Word needs a space after it.
|
|
832
|
+
if (symbol.Length > 1 && symbol == name && i < s.Length && s[i] != ' ' && s[i] != '^' && s[i] != '_') o.Append(' ');
|
|
833
|
+
}
|
|
834
|
+
else o.Append('\\').Append(name);
|
|
835
|
+
}
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (c == '^' || c == '_')
|
|
839
|
+
{
|
|
840
|
+
i++;
|
|
841
|
+
bool braced = i < s.Length && s[i] == '{';
|
|
842
|
+
string arg = TexArg(s, ref i);
|
|
843
|
+
o.Append(c).Append(braced && arg.Length > 1 ? "(" + arg + ")" : arg);
|
|
844
|
+
// Otherwise Word reads the next letter into the script: x^2y.
|
|
845
|
+
if (i < s.Length && (char.IsLetterOrDigit(s[i]) || s[i] == '\\')) o.Append(' ');
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if (c == '{') { string group = TexArg(s, ref i); o.Append("〖" + group + "〗"); continue; }
|
|
849
|
+
if (c == '}') { i++; continue; }
|
|
850
|
+
if (c == '~') { o.Append(' '); i++; continue; }
|
|
851
|
+
o.Append(c);
|
|
852
|
+
i++;
|
|
853
|
+
}
|
|
854
|
+
return o.ToString();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
static readonly System.Text.RegularExpressions.Regex Dollars = new System.Text.RegularExpressions.Regex(@"\$\$([^$\r]+?)\$\$|\$([^$\r]+?)\$(?!\$)");
|
|
858
|
+
|
|
859
|
+
static readonly System.Text.RegularExpressions.Regex Tag = new System.Text.RegularExpressions.Regex(@"\\tag\s*\{([^}]*)\}");
|
|
860
|
+
|
|
861
|
+
/// Turn every $...$ (inline) and $$...$$ (on a line of its own) inside a range into a native Word equation.
|
|
862
|
+
static int Mathify(dynamic doc, dynamic range)
|
|
863
|
+
{
|
|
864
|
+
string text = (string)range.Text;
|
|
865
|
+
if (text == null || text.IndexOf('$') < 0) return 0;
|
|
866
|
+
int start = (int)range.Start, made = 0;
|
|
867
|
+
System.Text.RegularExpressions.MatchCollection found = Dollars.Matches(text);
|
|
868
|
+
for (int k = found.Count - 1; k >= 0; k--)
|
|
869
|
+
{
|
|
870
|
+
System.Text.RegularExpressions.Match m = found[k];
|
|
871
|
+
bool display = m.Groups[1].Success;
|
|
872
|
+
string source = (display ? m.Groups[1].Value : m.Groups[2].Value).Trim();
|
|
873
|
+
// ag{1} numbers a display equation: Word sets "#(1)" flush right on the equation's line.
|
|
874
|
+
string tag = null;
|
|
875
|
+
System.Text.RegularExpressions.Match tagged = Tag.Match(source);
|
|
876
|
+
if (tagged.Success) { tag = tagged.Groups[1].Value.Trim(); source = source.Remove(tagged.Index, tagged.Length).Trim(); }
|
|
877
|
+
dynamic spot = null;
|
|
878
|
+
if (m.Length <= 250)
|
|
879
|
+
{
|
|
880
|
+
// Found by its text: positions counted in the text are off wherever the range holds a field (a caption number).
|
|
881
|
+
dynamic search = range.Duplicate;
|
|
882
|
+
if ((bool)search.Find.Execute(FindText: m.Value.Replace("^", "^^"), MatchCase: true, MatchWildcards: false, Forward: true, Wrap: 0)) spot = search;
|
|
883
|
+
}
|
|
884
|
+
if (spot == null) spot = doc.Range(start + m.Index, start + m.Index + m.Length);
|
|
885
|
+
spot.Text = Tex(source) + (display && !string.IsNullOrEmpty(tag) ? "#(" + tag + ")" : "");
|
|
886
|
+
try
|
|
887
|
+
{
|
|
888
|
+
dynamic math = doc.OMaths.Add(spot);
|
|
889
|
+
math.OMaths[1].BuildUp();
|
|
890
|
+
if (display) { try { math.OMaths[1].Type = 0; math.OMaths[1].Justification = 1; } catch (COMException) { } }
|
|
891
|
+
made++;
|
|
892
|
+
}
|
|
893
|
+
catch (COMException) { }
|
|
894
|
+
}
|
|
895
|
+
return made;
|
|
896
|
+
}
|
|
897
|
+
|
|
506
898
|
// ───────────────────────── Word ─────────────────────────
|
|
507
899
|
|
|
508
900
|
static readonly Dictionary<string, int> WordStyles = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
|
|
@@ -530,6 +922,7 @@ static class Program
|
|
|
530
922
|
int total = (int)doc.Paragraphs.Count;
|
|
531
923
|
if (index < 1 || index > total) throw new Fail("ANCHOR_MISSING", "There is no paragraph " + index + " (the document has " + total + "). Call office_read again.");
|
|
532
924
|
dynamic p = doc.Paragraphs[index];
|
|
925
|
+
Follow(doc, p.Range);
|
|
533
926
|
if (expect != null)
|
|
534
927
|
{
|
|
535
928
|
string text = Squash(Clean((string)p.Range.Text)), want = Squash(expect);
|
|
@@ -572,7 +965,7 @@ static class Program
|
|
|
572
965
|
int from = Math.Max(1, a.Int("from", 1)), count = Math.Max(1, Math.Min(400, a.Int("count", 120)));
|
|
573
966
|
bool full = a.Flag("full", false);
|
|
574
967
|
List<object> items = new List<object>();
|
|
575
|
-
int shown = from - 1;
|
|
968
|
+
int shown = from - 1, openTable = -1;
|
|
576
969
|
string titleStyle = null;
|
|
577
970
|
try { titleStyle = (string)doc.Styles[-63].NameLocal; } catch (Exception) { }
|
|
578
971
|
if (from <= total)
|
|
@@ -596,19 +989,38 @@ static class Program
|
|
|
596
989
|
try { inTable = Truthy(p.Range.Information[12]); } catch (Exception) { }
|
|
597
990
|
if (inTable)
|
|
598
991
|
{
|
|
599
|
-
// A table is one entry, not one per cell: its cells are read with "table".
|
|
600
992
|
dynamic table = p.Range.Tables[1];
|
|
601
|
-
int
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
993
|
+
int tableStart = (int)table.Range.Start, end = (int)table.Range.End;
|
|
994
|
+
if (tableStart != openTable)
|
|
995
|
+
{
|
|
996
|
+
int inside = (int)table.Range.Paragraphs.Count, cells = (int)table.Range.Cells.Count, rows = (int)table.Rows.Count;
|
|
997
|
+
bool nested = false;
|
|
998
|
+
try { nested = (int)table.NestingLevel > 1; } catch (Exception) { }
|
|
999
|
+
Dictionary<string, object> head = new Dictionary<string, object>();
|
|
1000
|
+
head["i"] = i;
|
|
1001
|
+
head["to"] = i + inside - 1;
|
|
1002
|
+
if (!nested) head["table"] = (int)doc.Range(0, end).Tables.Count;
|
|
1003
|
+
string size = "?";
|
|
1004
|
+
try { size = rows + "x" + (int)table.Columns.Count; } catch (COMException) { size = cells + " cells"; }
|
|
1005
|
+
head["size"] = size;
|
|
1006
|
+
// A grid of short cells is one entry (read its cells with "table"). A table whose cells hold the
|
|
1007
|
+
// document's text, as in a form or a report template, is listed paragraph by paragraph.
|
|
1008
|
+
bool open = inside > cells + rows + 2;
|
|
1009
|
+
if (open) head["open"] = true;
|
|
1010
|
+
items.Add(head);
|
|
1011
|
+
if (!open)
|
|
1012
|
+
{
|
|
1013
|
+
i += inside - 1;
|
|
1014
|
+
p = i < total ? doc.Range(end, end).Paragraphs[1] : null;
|
|
1015
|
+
shown = i;
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
openTable = tableStart;
|
|
1019
|
+
}
|
|
1020
|
+
bool rowEnd = false;
|
|
1021
|
+
try { rowEnd = Truthy(p.Range.Information[31]); } catch (Exception) { }
|
|
1022
|
+
if (rowEnd) { shown = i; p = p.Next(); continue; }
|
|
1023
|
+
item["inTable"] = true;
|
|
612
1024
|
}
|
|
613
1025
|
items.Add(item);
|
|
614
1026
|
shown = i;
|
|
@@ -663,7 +1075,11 @@ static class Program
|
|
|
663
1075
|
{
|
|
664
1076
|
string where = op.Str("where", op.Has("para") ? "after" : "end");
|
|
665
1077
|
// A new, empty document: its one empty paragraph is where the text goes, wherever the operation points.
|
|
666
|
-
if ((int)doc.Paragraphs.Count == 1 && (string)doc.Paragraphs[1].Range.Text == "\r")
|
|
1078
|
+
if ((int)doc.Paragraphs.Count == 1 && (string)doc.Paragraphs[1].Range.Text == "\r")
|
|
1079
|
+
{
|
|
1080
|
+
op.Raw("para"); op.Raw("expect");
|
|
1081
|
+
return doc.Paragraphs[1];
|
|
1082
|
+
}
|
|
667
1083
|
if (where == "end")
|
|
668
1084
|
{
|
|
669
1085
|
dynamic last = doc.Paragraphs.Last;
|
|
@@ -683,7 +1099,62 @@ static class Program
|
|
|
683
1099
|
{
|
|
684
1100
|
int end = (int)paragraph.Range.End;
|
|
685
1101
|
paragraph.Range.InsertParagraphAfter();
|
|
686
|
-
|
|
1102
|
+
dynamic made = doc.Range(end, end).Paragraphs[1];
|
|
1103
|
+
// Word gives the new paragraph the look of whatever FOLLOWS (a caption, a heading); it should
|
|
1104
|
+
// continue the paragraph it was added after.
|
|
1105
|
+
try
|
|
1106
|
+
{
|
|
1107
|
+
made.Style = paragraph.Style;
|
|
1108
|
+
made.Range.ParagraphFormat = paragraph.Range.ParagraphFormat.Duplicate;
|
|
1109
|
+
made.Range.Font = paragraph.Range.Font.Duplicate;
|
|
1110
|
+
}
|
|
1111
|
+
catch (COMException) { }
|
|
1112
|
+
return made;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/// Breathing room around a table or figure: at least this much space before / after a paragraph.
|
|
1116
|
+
/// Paragraph spacing, not blank lines, so nothing has to be cleaned up later.
|
|
1117
|
+
const float Air = 8f;
|
|
1118
|
+
|
|
1119
|
+
static void Space(dynamic paragraph, bool before)
|
|
1120
|
+
{
|
|
1121
|
+
if (paragraph == null) return;
|
|
1122
|
+
try
|
|
1123
|
+
{
|
|
1124
|
+
dynamic format = paragraph.Range.ParagraphFormat;
|
|
1125
|
+
if (before) { if ((float)format.SpaceBefore < Air) format.SpaceBefore = Air; }
|
|
1126
|
+
else if ((float)format.SpaceAfter < Air) format.SpaceAfter = Air;
|
|
1127
|
+
}
|
|
1128
|
+
catch (Exception) { }
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/// A numbered caption, the way reports set them: "表 1 ..." centred above a table, "图 1 ..." centred below
|
|
1132
|
+
/// a figure. The number is a Word caption field, so it renumbers itself and can be cross-referenced.
|
|
1133
|
+
static void Caption(dynamic doc, dynamic target, bool table, string title, string fontName, string fontFarEast)
|
|
1134
|
+
{
|
|
1135
|
+
string label = table ? "表" : "图";
|
|
1136
|
+
dynamic labels = doc.Application.CaptionLabels;
|
|
1137
|
+
bool known = false;
|
|
1138
|
+
foreach (dynamic item in labels) if ((string)item.Name == label) { known = true; break; }
|
|
1139
|
+
if (!known) labels.Add(label);
|
|
1140
|
+
int start = (int)target.Start, end = (int)target.End;
|
|
1141
|
+
target.InsertCaption(Label: label, Title: " " + title.Trim(), Position: table ? 0 : 1);
|
|
1142
|
+
// Above a table the caption is the paragraph that now sits where the table began; below a figure it is the next one.
|
|
1143
|
+
dynamic caption = table ? doc.Range(start, start).Paragraphs[1] : doc.Range(end, end).Paragraphs[1].Next();
|
|
1144
|
+
if (caption == null) return;
|
|
1145
|
+
try
|
|
1146
|
+
{
|
|
1147
|
+
dynamic range = caption.Range;
|
|
1148
|
+
range.ParagraphFormat.Alignment = 1;
|
|
1149
|
+
range.ParagraphFormat.CharacterUnitFirstLineIndent = 0; range.ParagraphFormat.FirstLineIndent = 0;
|
|
1150
|
+
range.Font.Bold = 0; range.Font.Italic = 0; range.Font.Color = -16777216; range.Font.Size = 10.5f;
|
|
1151
|
+
if (!string.IsNullOrEmpty(fontName)) range.Font.Name = fontName;
|
|
1152
|
+
if (!string.IsNullOrEmpty(fontFarEast)) range.Font.NameFarEast = fontFarEast;
|
|
1153
|
+
range.ParagraphFormat.SpaceBefore = table ? Air : 3f;
|
|
1154
|
+
range.ParagraphFormat.SpaceAfter = table ? 3f : Air;
|
|
1155
|
+
Mathify(doc, caption.Range);
|
|
1156
|
+
}
|
|
1157
|
+
catch (COMException) { }
|
|
687
1158
|
}
|
|
688
1159
|
|
|
689
1160
|
/// The number of a paragraph, counted from the start of the document.
|
|
@@ -692,6 +1163,23 @@ static class Program
|
|
|
692
1163
|
return (int)doc.Range(0, paragraph.Range.End).Paragraphs.Count;
|
|
693
1164
|
}
|
|
694
1165
|
|
|
1166
|
+
/// A new paragraph with no style named: body text that matches its surroundings. Written next to body
|
|
1167
|
+
/// text it keeps that text's font, size and spacing (a template's own look) but not the emphasis of the
|
|
1168
|
+
/// line it was split from (a bold label, a red hint); written after a heading it becomes Normal.
|
|
1169
|
+
static void Body(dynamic paragraph)
|
|
1170
|
+
{
|
|
1171
|
+
int level = 10;
|
|
1172
|
+
try { level = (int)paragraph.OutlineLevel; } catch (Exception) { }
|
|
1173
|
+
if (level != 10) { Plain(paragraph, "Normal"); return; }
|
|
1174
|
+
try
|
|
1175
|
+
{
|
|
1176
|
+
dynamic font = paragraph.Range.Font;
|
|
1177
|
+
font.Bold = 0; font.Italic = 0; font.Underline = 0; font.Color = -16777216;
|
|
1178
|
+
paragraph.Range.ParagraphFormat.Alignment = 3;
|
|
1179
|
+
}
|
|
1180
|
+
catch (COMException) { }
|
|
1181
|
+
}
|
|
1182
|
+
|
|
695
1183
|
static void Plain(dynamic paragraph, string style)
|
|
696
1184
|
{
|
|
697
1185
|
SetStyle(paragraph.Range, style);
|
|
@@ -721,7 +1209,7 @@ static class Program
|
|
|
721
1209
|
IList items = op.List("items");
|
|
722
1210
|
if (items == null) { items = new ArrayList(); items.Add(new Dictionary<string, object> { { "text", op.Need("text") } }); }
|
|
723
1211
|
dynamic current = null;
|
|
724
|
-
int made = 0;
|
|
1212
|
+
int made = 0, equations = 0;
|
|
725
1213
|
foreach (object raw in items)
|
|
726
1214
|
{
|
|
727
1215
|
Bag item = raw is string ? new Bag(new Dictionary<string, object> { { "text", raw } }) : new Bag(raw);
|
|
@@ -730,16 +1218,20 @@ static class Program
|
|
|
730
1218
|
dynamic p;
|
|
731
1219
|
if (current == null) p = NewParagraph(doc, op);
|
|
732
1220
|
else p = After(doc, current);
|
|
733
|
-
|
|
734
|
-
if (
|
|
1221
|
+
string style = item.Str("style", op.Str("style", null));
|
|
1222
|
+
if (style != null) Plain(p, style);
|
|
1223
|
+
else Body(p);
|
|
735
1224
|
WordFormat(p.Range, StyleLess(op));
|
|
736
1225
|
WordFormat(p.Range, StyleLess(item));
|
|
1226
|
+
Follow(doc, p.Range);
|
|
1227
|
+
Write(doc, (int)p.Range.Start, text);
|
|
1228
|
+
equations += Mathify(doc, p.Range);
|
|
737
1229
|
current = p;
|
|
738
1230
|
made++;
|
|
739
1231
|
}
|
|
740
1232
|
}
|
|
741
1233
|
int lastIndex = Index(doc, current);
|
|
742
|
-
return "inserted " + made + " paragraph(s), now paragraphs " + (lastIndex - made + 1) + "–" + lastIndex + " of " + (int)doc.Paragraphs.Count;
|
|
1234
|
+
return "inserted " + made + " paragraph(s), now paragraphs " + (lastIndex - made + 1) + "–" + lastIndex + " of " + (int)doc.Paragraphs.Count + (equations > 0 ? ", " + equations + " equation(s)" : "");
|
|
743
1235
|
}
|
|
744
1236
|
if ((type == "set_text" || (type == "delete_range" && op.Has("para"))) && op.Raw("expect") == null)
|
|
745
1237
|
{
|
|
@@ -750,8 +1242,10 @@ static class Program
|
|
|
750
1242
|
dynamic p = Para(doc, op.Int("para", 0), op.Str("expect", null));
|
|
751
1243
|
dynamic range = p.Range;
|
|
752
1244
|
range.MoveEnd(1, -1);
|
|
753
|
-
range.Text =
|
|
754
|
-
|
|
1245
|
+
range.Text = "";
|
|
1246
|
+
Write(doc, (int)p.Range.Start, Lines(op.Raw("text") ?? ""));
|
|
1247
|
+
int built = Mathify(doc, p.Range);
|
|
1248
|
+
return "paragraph " + op.Int("para", 0) + " rewritten" + (built > 0 ? ", " + built + " equation(s)" : "");
|
|
755
1249
|
}
|
|
756
1250
|
if (type == "format_text" || type == "set_style")
|
|
757
1251
|
{
|
|
@@ -762,8 +1256,20 @@ static class Program
|
|
|
762
1256
|
if (type == "delete_range")
|
|
763
1257
|
{
|
|
764
1258
|
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
|
-
|
|
1259
|
+
dynamic gone = WordRange(doc, op);
|
|
1260
|
+
string note = "";
|
|
1261
|
+
try { gone.Delete(); }
|
|
1262
|
+
catch (COMException)
|
|
1263
|
+
{
|
|
1264
|
+
// The mark that ends a table cell cannot be deleted: take everything before it.
|
|
1265
|
+
int limit = (int)gone.End - 1;
|
|
1266
|
+
try { int from = (int)gone.Start; limit = Math.Min(limit, (int)doc.Range(from, from).Cells[1].Range.End - 1); } catch (Exception) { }
|
|
1267
|
+
gone.End = limit;
|
|
1268
|
+
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.");
|
|
1269
|
+
gone.Delete();
|
|
1270
|
+
note = " (the last paragraph of the cell stays, a cell always keeps one)";
|
|
1271
|
+
}
|
|
1272
|
+
return "deleted, " + (int)doc.Paragraphs.Count + " paragraphs now" + note;
|
|
767
1273
|
}
|
|
768
1274
|
if (type == "insert_table")
|
|
769
1275
|
{
|
|
@@ -772,7 +1278,18 @@ static class Program
|
|
|
772
1278
|
if (data != null) foreach (object row in data) { IList cells = row as IList; if (cells != null) cols = Math.Max(cols, cells.Count); }
|
|
773
1279
|
if (rows < 1 || cols < 1) throw new Fail("BAD_ARGS", "Give \"data\" (rows of cells) or \"rows\" and \"cols\".");
|
|
774
1280
|
dynamic p = NewParagraph(doc, op);
|
|
1281
|
+
string fontName = null, fontFarEast = null;
|
|
1282
|
+
float fontSize = 0;
|
|
1283
|
+
try
|
|
1284
|
+
{
|
|
1285
|
+
if ((int)p.OutlineLevel == 10)
|
|
1286
|
+
{
|
|
1287
|
+
fontName = (string)p.Range.Font.Name; fontFarEast = (string)p.Range.Font.NameFarEast; fontSize = (float)p.Range.Font.Size;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
catch (Exception) { }
|
|
775
1291
|
Plain(p, "Normal");
|
|
1292
|
+
Follow(doc, p.Range);
|
|
776
1293
|
dynamic table = doc.Tables.Add(p.Range, rows, cols);
|
|
777
1294
|
// The cells take the formatting of the paragraph that follows (a heading, say): make them plain.
|
|
778
1295
|
SetStyle(table.Range, "Normal");
|
|
@@ -782,36 +1299,76 @@ static class Program
|
|
|
782
1299
|
table.Range.ParagraphFormat.CharacterUnitFirstLineIndent = 0; table.Range.ParagraphFormat.FirstLineIndent = 0;
|
|
783
1300
|
}
|
|
784
1301
|
catch (COMException) { }
|
|
1302
|
+
try
|
|
1303
|
+
{
|
|
1304
|
+
// Same typeface and size as the body text the table sits in; cells centred like a data table.
|
|
1305
|
+
if (!string.IsNullOrEmpty(fontName)) table.Range.Font.Name = fontName;
|
|
1306
|
+
if (!string.IsNullOrEmpty(fontFarEast)) table.Range.Font.NameFarEast = fontFarEast;
|
|
1307
|
+
if (fontSize > 4 && fontSize < 100) table.Range.Font.Size = fontSize;
|
|
1308
|
+
table.Range.ParagraphFormat.Alignment = 1;
|
|
1309
|
+
table.Range.Cells.VerticalAlignment = 1;
|
|
1310
|
+
}
|
|
1311
|
+
catch (COMException) { }
|
|
785
1312
|
table.Borders.Enable = 1;
|
|
1313
|
+
try { table.AutoFitBehavior(2); } catch (COMException) { }
|
|
786
1314
|
if (data != null)
|
|
787
1315
|
{
|
|
788
1316
|
for (int r = 0; r < data.Count && r < rows; r++)
|
|
789
1317
|
{
|
|
790
1318
|
IList cells = data[r] as IList;
|
|
791
1319
|
if (cells == null) continue;
|
|
792
|
-
for (int c = 0; c < cells.Count; c++)
|
|
1320
|
+
for (int c = 0; c < cells.Count; c++)
|
|
1321
|
+
{
|
|
1322
|
+
if (cells[c] == null) continue;
|
|
1323
|
+
dynamic cell = table.Cell(r + 1, c + 1).Range;
|
|
1324
|
+
string value = Convert.ToString(cells[c], System.Globalization.CultureInfo.InvariantCulture);
|
|
1325
|
+
cell.Text = value;
|
|
1326
|
+
if (value.IndexOf('$') >= 0) Mathify(doc, table.Cell(r + 1, c + 1).Range);
|
|
1327
|
+
}
|
|
1328
|
+
if (Typing) Thread.Sleep(25);
|
|
793
1329
|
}
|
|
794
1330
|
}
|
|
795
1331
|
if (op.Flag("header", true)) table.Rows[1].Range.Font.Bold = 1;
|
|
1332
|
+
if (op.Has("caption")) Caption(doc, table.Range, true, op.Need("caption"), fontName, fontFarEast);
|
|
1333
|
+
else
|
|
1334
|
+
{
|
|
1335
|
+
int tableStart = (int)table.Range.Start;
|
|
1336
|
+
if (tableStart > 0) Space(doc.Range(tableStart - 1, tableStart - 1).Paragraphs[1], false);
|
|
1337
|
+
}
|
|
1338
|
+
int afterTable = (int)table.Range.End;
|
|
1339
|
+
if (afterTable < (int)doc.Content.End) Space(doc.Range(afterTable, afterTable).Paragraphs[1], true);
|
|
796
1340
|
int tableEnd = (int)doc.Range(0, table.Range.End).Paragraphs.Count;
|
|
797
|
-
|
|
1341
|
+
bool nested = false;
|
|
1342
|
+
try { nested = (int)table.NestingLevel > 1; } catch (Exception) { }
|
|
1343
|
+
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
1344
|
}
|
|
799
1345
|
if (type == "set_cell")
|
|
800
1346
|
{
|
|
801
1347
|
int n = op.Int("table", 1);
|
|
802
1348
|
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
|
-
|
|
1349
|
+
dynamic cell = doc.Tables[n].Cell(op.Int("row", 1), op.Int("col", 1));
|
|
1350
|
+
Follow(doc, cell.Range);
|
|
1351
|
+
cell.Range.Text = "";
|
|
1352
|
+
Write(doc, (int)cell.Range.Start, Lines(op.Raw("text") ?? ""));
|
|
1353
|
+
int built = Mathify(doc, doc.Tables[n].Cell(op.Int("row", 1), op.Int("col", 1)).Range);
|
|
1354
|
+
return "cell set" + (built > 0 ? ", " + built + " equation(s)" : "");
|
|
805
1355
|
}
|
|
806
1356
|
if (type == "insert_image")
|
|
807
1357
|
{
|
|
808
1358
|
string path = op.Need("path");
|
|
809
1359
|
if (!File.Exists(path)) throw new Fail("BAD_ARGS", "Image \"" + path + "\" does not exist.");
|
|
810
1360
|
dynamic p = NewParagraph(doc, op);
|
|
1361
|
+
string bodyFont = null, bodyFarEast = null;
|
|
1362
|
+
try { if ((int)p.OutlineLevel == 10) { bodyFont = (string)p.Range.Font.Name; bodyFarEast = (string)p.Range.Font.NameFarEast; } } catch (Exception) { }
|
|
811
1363
|
Plain(p, "Normal");
|
|
1364
|
+
Follow(doc, p.Range);
|
|
812
1365
|
dynamic picture = p.Range.InlineShapes.AddPicture(FileName: path, LinkToFile: false, SaveWithDocument: true);
|
|
813
1366
|
if (op.Has("width")) { picture.LockAspectRatio = -1; picture.Width = (float)op.Num("width", 300); }
|
|
1367
|
+
try { p.Range.ParagraphFormat.CharacterUnitFirstLineIndent = 0; p.Range.ParagraphFormat.FirstLineIndent = 0; p.Range.ParagraphFormat.Alignment = 1; } catch (COMException) { }
|
|
814
1368
|
WordFormat(p.Range, StyleLess(op));
|
|
1369
|
+
p.Range.ParagraphFormat.SpaceBefore = Air;
|
|
1370
|
+
if (op.Has("caption")) Caption(doc, picture.Range, false, op.Need("caption"), bodyFont, bodyFarEast);
|
|
1371
|
+
else p.Range.ParagraphFormat.SpaceAfter = Air;
|
|
815
1372
|
return "image inserted";
|
|
816
1373
|
}
|
|
817
1374
|
throw new Fail("BAD_ARGS", "Unknown Word operation \"" + type + "\".");
|
|
@@ -953,6 +1510,23 @@ static class Program
|
|
|
953
1510
|
if (op.Has("rowHeight")) range.RowHeight = op.Num("rowHeight", 15);
|
|
954
1511
|
}
|
|
955
1512
|
|
|
1513
|
+
/// Bring the cells being worked on into view, without touching the selection.
|
|
1514
|
+
static void Show(dynamic book, dynamic sheet, dynamic range)
|
|
1515
|
+
{
|
|
1516
|
+
if (!Following) return;
|
|
1517
|
+
try
|
|
1518
|
+
{
|
|
1519
|
+
sheet.Activate();
|
|
1520
|
+
dynamic window = book.Windows[1];
|
|
1521
|
+
dynamic visible = window.VisibleRange;
|
|
1522
|
+
int row = (int)range.Row, column = (int)range.Column;
|
|
1523
|
+
int top = (int)visible.Row, left = (int)visible.Column;
|
|
1524
|
+
if (row < top || row >= top + (int)visible.Rows.Count - 1) window.ScrollRow = Math.Max(1, row - 3);
|
|
1525
|
+
if (column < left || column >= left + (int)visible.Columns.Count - 1) window.ScrollColumn = Math.Max(1, column - 1);
|
|
1526
|
+
}
|
|
1527
|
+
catch (Exception) { }
|
|
1528
|
+
}
|
|
1529
|
+
|
|
956
1530
|
static string ExcelOp(dynamic app, dynamic book, string type, Bag op)
|
|
957
1531
|
{
|
|
958
1532
|
if (type == "add_sheet")
|
|
@@ -977,7 +1551,18 @@ static class Program
|
|
|
977
1551
|
for (int c = 0; c < cells.Count; c++) grid[r, c] = Scalar(cells[c]);
|
|
978
1552
|
}
|
|
979
1553
|
dynamic target = Cells(ws, op.Str("range", "A1")).Cells[1, 1].Resize[rows.Count, width];
|
|
980
|
-
|
|
1554
|
+
Show(book, ws, target);
|
|
1555
|
+
if (Typing && rows.Count > 1 && rows.Count <= 60)
|
|
1556
|
+
{
|
|
1557
|
+
for (int r = 0; r < rows.Count; r++)
|
|
1558
|
+
{
|
|
1559
|
+
object[,] line = new object[1, width];
|
|
1560
|
+
for (int c = 0; c < width; c++) line[0, c] = grid[r, c];
|
|
1561
|
+
target.Rows[r + 1].Formula = line;
|
|
1562
|
+
Thread.Sleep(35);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
else target.Formula = grid;
|
|
981
1566
|
ExcelFormat(target, op);
|
|
982
1567
|
return "wrote " + (string)target.Address[false, false];
|
|
983
1568
|
}
|
|
@@ -1103,9 +1688,22 @@ static class Program
|
|
|
1103
1688
|
return result;
|
|
1104
1689
|
}
|
|
1105
1690
|
|
|
1691
|
+
static void PptWrite(dynamic textRange, string text)
|
|
1692
|
+
{
|
|
1693
|
+
if (!Typing || text.Length < 4) { textRange.Text = text; return; }
|
|
1694
|
+
textRange.Text = "";
|
|
1695
|
+
foreach (string piece in Pieces(text)) { textRange.InsertAfter(piece); Thread.Sleep(22); }
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
static void GoTo(dynamic deck, int slide)
|
|
1699
|
+
{
|
|
1700
|
+
if (!Following) return;
|
|
1701
|
+
try { deck.Windows[1].View.GotoSlide(slide); } catch (Exception) { }
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1106
1704
|
static void PptText(dynamic shape, Bag op)
|
|
1107
1705
|
{
|
|
1108
|
-
if (op.Has("text")) shape.TextFrame.TextRange
|
|
1706
|
+
if (op.Has("text")) PptWrite(shape.TextFrame.TextRange, Lines(op.Raw("text")));
|
|
1109
1707
|
if (!(op.Has("font") || op.Has("size") || op.Has("bold") || op.Has("italic") || op.Has("color") || op.Has("align"))) return;
|
|
1110
1708
|
dynamic range = shape.TextFrame.TextRange;
|
|
1111
1709
|
if (op.Has("font")) { string font = op.Need("font"); range.Font.Name = font; try { range.Font.NameFarEast = font; } catch (COMException) { } }
|
|
@@ -1144,12 +1742,14 @@ static class Program
|
|
|
1144
1742
|
string name = op.Str("layout", "title_content");
|
|
1145
1743
|
if (!Layouts.TryGetValue(name, out layout)) throw new Fail("BAD_ARGS", "Unknown layout \"" + name + "\": use " + string.Join(", ", new List<string>(Layouts.Keys).ToArray()) + ".");
|
|
1146
1744
|
dynamic slide = deck.Slides.Add(index, layout);
|
|
1147
|
-
|
|
1148
|
-
if (op.Has("
|
|
1745
|
+
GoTo(deck, index);
|
|
1746
|
+
if (op.Has("title")) PptWrite(slide.Shapes.Title.TextFrame.TextRange, Lines(op.Raw("title")));
|
|
1747
|
+
if (op.Has("body")) PptWrite(slide.Shapes.Placeholders[2].TextFrame.TextRange, Lines(op.Raw("body")));
|
|
1149
1748
|
if (op.Has("notes")) slide.NotesPage.Shapes.Placeholders[2].TextFrame.TextRange.Text = Lines(op.Raw("notes"));
|
|
1150
1749
|
return "slide " + index + " added";
|
|
1151
1750
|
}
|
|
1152
1751
|
dynamic target = Slide(deck, op);
|
|
1752
|
+
GoTo(deck, op.Int("slide", 0));
|
|
1153
1753
|
if (type == "delete_slide") { target.Delete(); return "slide deleted"; }
|
|
1154
1754
|
if (type == "move_slide") { target.MoveTo(op.Int("to", 1)); return "slide moved to " + op.Int("to", 1); }
|
|
1155
1755
|
if (type == "set_notes") { target.NotesPage.Shapes.Placeholders[2].TextFrame.TextRange.Text = Lines(op.Raw("text") ?? ""); return "notes set"; }
|
|
@@ -1189,6 +1789,12 @@ static class Program
|
|
|
1189
1789
|
if (ops == null || ops.Count == 0) throw new Fail("BAD_ARGS", "\"ops\" must list at least one operation.");
|
|
1190
1790
|
List<object> done = new List<object>();
|
|
1191
1791
|
Dictionary<string, object> result = new Dictionary<string, object>();
|
|
1792
|
+
// The settings give the two modes, until the user flips one on the card.
|
|
1793
|
+
if (!Card.FollowChosen) Following = a.Flag("follow", false);
|
|
1794
|
+
if (!Card.TypingChosen) Typing = a.Flag("typing", false);
|
|
1795
|
+
bool card = a.Flag("card", false);
|
|
1796
|
+
if (card && !cardStarted) { cardStarted = true; Card.Start(); }
|
|
1797
|
+
string docName = (string)doc.Name;
|
|
1192
1798
|
dynamic undo = null;
|
|
1193
1799
|
// One undo step for the whole batch: the user takes it back with a single Ctrl+Z.
|
|
1194
1800
|
if (kind == "word") { try { undo = app.UndoRecord; undo.StartCustomRecord("AI edit"); } catch (COMException) { undo = null; } }
|
|
@@ -1201,6 +1807,7 @@ static class Program
|
|
|
1201
1807
|
{
|
|
1202
1808
|
Bag op = new Bag(ops[i]);
|
|
1203
1809
|
type = op.Need("op");
|
|
1810
|
+
if (card) Card.Report("AI 正在编辑 " + docName + (ops.Count > 1 ? " · " + (i + 1) + "/" + ops.Count : ""));
|
|
1204
1811
|
string line = kind == "word" ? WordOp(doc, type, op) : kind == "excel" ? ExcelOp(app, doc, type, op) : PptOp(doc, type, op);
|
|
1205
1812
|
string ignored = op.Unread();
|
|
1206
1813
|
done.Add(ignored.Length == 0 ? line : line + " — IGNORED field(s) " + ignored + ": " + type + " does not take them, check the operation list");
|
|
@@ -1222,6 +1829,7 @@ static class Program
|
|
|
1222
1829
|
{
|
|
1223
1830
|
if (undo != null) { try { undo.EndCustomRecord(); } catch (COMException) { } }
|
|
1224
1831
|
}
|
|
1832
|
+
if (card) Card.Report(result.ContainsKey("failed") ? "AI 的修改停在第 " + (done.Count + 1) + " 项" : "AI 已改好 " + docName);
|
|
1225
1833
|
result["done"] = done;
|
|
1226
1834
|
result["total"] = ops.Count;
|
|
1227
1835
|
return result;
|
|
@@ -1252,13 +1860,21 @@ static class Program
|
|
|
1252
1860
|
using (Metafile meta = new Metafile(stream))
|
|
1253
1861
|
{
|
|
1254
1862
|
int height = (int)Math.Round(width * (double)meta.Height / meta.Width);
|
|
1863
|
+
// Drawn straight at the final size, hairlines (table borders) fall between pixels and vanish:
|
|
1864
|
+
// draw the page large first, then scale the picture down.
|
|
1865
|
+
int factor = Math.Max(2, (int)Math.Ceiling(3000.0 / width));
|
|
1866
|
+
using (Bitmap large = new Bitmap(width * factor, height * factor))
|
|
1255
1867
|
using (Bitmap bitmap = new Bitmap(width, height))
|
|
1256
1868
|
{
|
|
1257
|
-
using (Graphics g = Graphics.FromImage(
|
|
1869
|
+
using (Graphics g = Graphics.FromImage(large))
|
|
1258
1870
|
{
|
|
1259
1871
|
g.Clear(Color.White);
|
|
1872
|
+
g.DrawImage(meta, 0, 0, large.Width, large.Height);
|
|
1873
|
+
}
|
|
1874
|
+
using (Graphics g = Graphics.FromImage(bitmap))
|
|
1875
|
+
{
|
|
1260
1876
|
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
1261
|
-
g.DrawImage(
|
|
1877
|
+
g.DrawImage(large, 0, 0, width, height);
|
|
1262
1878
|
}
|
|
1263
1879
|
bitmap.Save(path, ImageFormat.Png);
|
|
1264
1880
|
}
|