ssdiff: compare names too
[gnumeric.git] / src / ssdiff.c
blobdce7b151ad01822c4f288bad90cb000d7c016cf4
1 /*
2 * ssdiff.c: A diff program for spreadsheets.
4 * Author:
5 * Morten Welinder <terra@gnome.org>
7 * Copyright (C) 2012 Morten Welinder (terra@gnome.org)
8 */
10 #include <gnumeric-config.h>
11 #include <glib/gi18n.h>
12 #include "gnumeric.h"
13 #include <goffice/goffice.h>
14 #include "libgnumeric.h"
15 #include "gutils.h"
16 #include "command-context.h"
17 #include "command-context-stderr.h"
18 #include "gnm-plugin.h"
19 #include "workbook-view.h"
20 #include "workbook.h"
21 #include "sheet.h"
22 #include "sheet-style.h"
23 #include "style-border.h"
24 #include "style-color.h"
25 #include "cell.h"
26 #include "value.h"
27 #include "expr.h"
28 #include "ranges.h"
29 #include "mstyle.h"
30 #include "xml-sax.h"
31 #include "hlink.h"
32 #include "input-msg.h"
33 #include "expr-name.h"
34 #include "workbook-priv.h"
35 #include <gsf/gsf-libxml.h>
36 #include <gsf/gsf-output-stdio.h>
37 #include <gsf/gsf-input.h>
39 #define DIFF "s:"
40 #define SSDIFF_DTD "http://www.gnumeric.org/ssdiff.dtd" // No such file yet
42 static gboolean ssdiff_show_version = FALSE;
43 static gboolean ssdiff_highlight = FALSE;
44 static gboolean ssdiff_xml = FALSE;
45 static char *ssdiff_output = NULL;
47 static const GOptionEntry ssdiff_options [] = {
49 "version", 'v',
50 0, G_OPTION_ARG_NONE, &ssdiff_show_version,
51 N_("Display program version"),
52 NULL
56 "output", 'o',
57 0, G_OPTION_ARG_STRING, &ssdiff_output,
58 N_("Send output to file"),
59 N_("file")
63 "highlight", 'h',
64 0, G_OPTION_ARG_NONE, &ssdiff_highlight,
65 N_("Output copy highlighting differences"),
66 NULL
70 "xml", 'x',
71 0, G_OPTION_ARG_NONE, &ssdiff_xml,
72 N_("Output in xml format"),
73 NULL
76 /* ---------------------------------------- */
78 { NULL }
81 /* -------------------------------------------------------------------------- */
83 typedef struct GnmDiffState_ GnmDiffState;
85 typedef struct {
86 // Start comparison of two workbooks.
87 gboolean (*diff_start) (GnmDiffState *state);
89 // Finish comparison started with above.
90 void (*diff_end) (GnmDiffState *state);
92 // Clean up allocations
93 void (*dtor) (GnmDiffState *state);
95 /* ------------------------------ */
97 // Start looking at a sheet. Either sheet might be NULL.
98 void (*sheet_start) (GnmDiffState *state,
99 Sheet const *os, Sheet const *ns);
101 // Finish sheet started with above.
102 void (*sheet_end) (GnmDiffState *state);
104 // The order of sheets has changed.
105 void (*sheet_order_changed) (GnmDiffState *state);
107 // An integer attribute of the sheet has changed.
108 void (*sheet_attr_int_changed) (GnmDiffState *state, const char *name,
109 int o, int n);
111 /* ------------------------------ */
113 // Column or Row information changed
114 void (*colrow_changed) (GnmDiffState *state,
115 ColRowInfo const *oc, ColRowInfo const *nc,
116 gboolean is_cols, int i);
118 /* ------------------------------ */
120 // A cell was changed/added/removed.
121 void (*cell_changed) (GnmDiffState *state,
122 GnmCell const *oc, GnmCell const *nc);
124 /* ------------------------------ */
126 // The style of an area was changed.
127 void (*style_changed) (GnmDiffState *state, GnmRange const *r,
128 GnmStyle const *os, GnmStyle const *ns);
130 /* ------------------------------ */
132 // A defined name was changed
133 void (*name_changed) (GnmDiffState *state,
134 GnmNamedExpr const *on, GnmNamedExpr const *nn);
135 } GnmDiffActions;
137 typedef struct {
138 char *url;
139 GsfInput *input;
140 Workbook *wb;
141 WorkbookView *wbv;
142 } GnmDiffStateFile;
144 struct GnmDiffState_ {
145 GOIOContext *ioc;
146 GnmDiffStateFile old, new;
148 const GnmDiffActions *actions;
150 gboolean diff_found;
152 GsfOutput *output;
154 // Valid when comparing sheets
155 Sheet *old_sheet, *new_sheet;
156 GnmRange common_range;
158 // The following for xml mode.
159 GsfXMLOut *xml;
160 const char *xml_section;
161 GnmConventions *xml_convs;
163 // The following for highlight mode.
164 GnmDiffStateFile highlight;
165 GOFileSaver const *highlight_fs;
166 GnmStyle *highlight_style;
169 static gboolean
170 null_diff_start (G_GNUC_UNUSED GnmDiffState *state)
172 return FALSE;
175 static void
176 null_diff_end (G_GNUC_UNUSED GnmDiffState *state)
180 static void
181 null_dtor (G_GNUC_UNUSED GnmDiffState *state)
185 static void
186 null_sheet_start (G_GNUC_UNUSED GnmDiffState *state,
187 G_GNUC_UNUSED Sheet const *os,
188 G_GNUC_UNUSED Sheet const *ns)
192 static void
193 null_sheet_end (G_GNUC_UNUSED GnmDiffState *state)
197 static void
198 null_sheet_order_changed (G_GNUC_UNUSED GnmDiffState *state)
202 static void
203 null_sheet_attr_int_changed (G_GNUC_UNUSED GnmDiffState *state,
204 G_GNUC_UNUSED const char *name,
205 G_GNUC_UNUSED int o,
206 G_GNUC_UNUSED int n)
210 static void
211 null_colrow_changed (G_GNUC_UNUSED GnmDiffState *state,
212 G_GNUC_UNUSED ColRowInfo const *oc, G_GNUC_UNUSED ColRowInfo const *nc,
213 G_GNUC_UNUSED gboolean is_cols, G_GNUC_UNUSED int i)
217 static void
218 null_name_changed (G_GNUC_UNUSED GnmDiffState *state,
219 G_GNUC_UNUSED GnmNamedExpr const *on, G_GNUC_UNUSED GnmNamedExpr const *nn)
223 /* -------------------------------------------------------------------------- */
225 static gboolean
226 read_file (GnmDiffStateFile *dsf, const char *filename, GOIOContext *ioc)
228 GError *err = NULL;
230 dsf->url = go_shell_arg_to_uri (filename);
232 if (!dsf->input)
233 dsf->input = go_file_open (dsf->url, &err);
235 if (!dsf->input) {
236 g_printerr (_("%s: Failed to read %s: %s\n"),
237 g_get_prgname (),
238 filename,
239 err ? err->message : "?");
240 if (err)
241 g_error_free (err);
242 return TRUE;
245 dsf->wbv = workbook_view_new_from_input (dsf->input,
246 dsf->url, NULL,
247 ioc, NULL);
248 if (!dsf->wbv)
249 return TRUE;
250 dsf->wb = wb_view_get_workbook (dsf->wbv);
252 return FALSE;
255 static void
256 clear_file_state (GnmDiffStateFile *dsf)
258 g_free (dsf->url);
259 dsf->url = NULL;
260 g_clear_object (&dsf->wb);
261 g_clear_object (&dsf->input);
264 /* -------------------------------------------------------------------------- */
266 static const char *
267 def_cell_name (GnmCell const *oc)
269 static char *res;
270 g_free (res);
271 res = oc
272 ? g_strconcat (oc->base.sheet->name_quoted,
273 "!",
274 cell_name (oc),
275 NULL)
276 : NULL;
277 return res;
280 static void
281 def_sheet_start (GnmDiffState *state, Sheet const *os, Sheet const *ns)
283 if (os && ns)
284 gsf_output_printf (state->output, _("Differences for sheet %s:\n"), os->name_quoted);
285 else if (os)
286 gsf_output_printf (state->output, _("Sheet %s removed.\n"), os->name_quoted);
287 else if (ns)
288 gsf_output_printf (state->output, _("Sheet %s added.\n"), ns->name_quoted);
289 else
290 g_assert_not_reached ();
293 static void
294 def_sheet_order_changed (GnmDiffState *state)
296 gsf_output_printf (state->output, _("Sheet order changed.\n"));
299 static void
300 def_sheet_attr_int_changed (GnmDiffState *state, const char *name,
301 G_GNUC_UNUSED int o, G_GNUC_UNUSED int n)
303 gsf_output_printf (state->output, _("Sheet attribute %s changed.\n"),
304 name);
307 static void
308 def_colrow_changed (GnmDiffState *state, ColRowInfo const *oc, ColRowInfo const *nc,
309 gboolean is_cols, int i)
311 if (is_cols)
312 gsf_output_printf (state->output, _("Column %s changed.\n"),
313 col_name (i));
314 else
315 gsf_output_printf (state->output, _("Row %d changed.\n"),
316 i + 1);
319 static void
320 def_cell_changed (GnmDiffState *state, GnmCell const *oc, GnmCell const *nc)
322 if (oc && nc)
323 gsf_output_printf (state->output, _("Cell %s changed.\n"), def_cell_name (oc));
324 else if (oc)
325 gsf_output_printf (state->output, _("Cell %s removed.\n"), def_cell_name (oc));
326 else if (nc)
327 gsf_output_printf (state->output, _("Cell %s added.\n"), def_cell_name (nc));
328 else
329 g_assert_not_reached ();
332 static void
333 def_style_changed (GnmDiffState *state, GnmRange const *r,
334 G_GNUC_UNUSED GnmStyle const *os,
335 G_GNUC_UNUSED GnmStyle const *ns)
337 gsf_output_printf (state->output, _("Style of %s was changed.\n"),
338 range_as_string (r));
341 static void
342 def_name_changed (GnmDiffState *state,
343 GnmNamedExpr const *on, GnmNamedExpr const *nn)
345 if (on && nn)
346 gsf_output_printf (state->output, _("Name %s changed.\n"), expr_name_name (on));
347 else if (on)
348 gsf_output_printf (state->output, _("Name %s removed.\n"), expr_name_name (on));
349 else if (nn)
350 gsf_output_printf (state->output, _("Name %s added.\n"), expr_name_name (nn));
351 else
352 g_assert_not_reached ();
355 static const GnmDiffActions default_actions = {
356 null_diff_start,
357 null_diff_end,
358 null_dtor,
359 def_sheet_start,
360 null_sheet_end,
361 def_sheet_order_changed,
362 def_sheet_attr_int_changed,
363 def_colrow_changed,
364 def_cell_changed,
365 def_style_changed,
366 def_name_changed,
369 /* -------------------------------------------------------------------------- */
371 static gboolean
372 xml_diff_start (GnmDiffState *state)
374 char *attr;
376 state->xml = gsf_xml_out_new (state->output);
377 state->xml_convs = gnm_xml_io_conventions ();
379 gsf_xml_out_start_element (state->xml, DIFF "Diff");
380 attr = g_strdup ("xmlns:" DIFF);
381 attr[strlen (attr) - 1] = 0;
382 gsf_xml_out_add_cstr (state->xml, attr, SSDIFF_DTD);
383 g_free (attr);
385 return FALSE;
388 static void
389 xml_diff_end (GnmDiffState *state)
391 gsf_xml_out_end_element (state->xml); /* </Diff> */
394 static void
395 xml_dtor (GnmDiffState *state)
397 g_clear_object (&state->xml);
399 if (state->xml_convs) {
400 gnm_conventions_unref (state->xml_convs);
401 state->xml_convs = NULL;
405 static void
406 xml_close_section (GnmDiffState *state)
408 if (state->xml_section) {
409 gsf_xml_out_end_element (state->xml);
410 state->xml_section = NULL;
414 static void
415 xml_open_section (GnmDiffState *state, const char *section)
417 if (state->xml_section && g_str_equal (section, state->xml_section))
418 return;
420 xml_close_section (state);
421 gsf_xml_out_start_element (state->xml, section);
422 state->xml_section = section;
425 static void
426 xml_sheet_start (GnmDiffState *state, Sheet const *os, Sheet const *ns)
428 Sheet const *sheet = os ? os : ns;
430 // We might have an open section for global names
431 xml_close_section (state);
433 gsf_xml_out_start_element (state->xml, DIFF "Sheet");
434 gsf_xml_out_add_cstr (state->xml, "Name", sheet->name_unquoted);
435 if (os)
436 gsf_xml_out_add_int (state->xml, "Old", os->index_in_wb);
437 if (ns)
438 gsf_xml_out_add_int (state->xml, "New", ns->index_in_wb);
441 static void
442 xml_sheet_end (GnmDiffState *state)
444 xml_close_section (state);
445 gsf_xml_out_end_element (state->xml); /* </Sheet> */
448 static void
449 xml_sheet_attr_int_changed (GnmDiffState *state, const char *name,
450 int o, int n)
452 char *elem;
454 elem = g_strconcat (DIFF, name, NULL);
455 gsf_xml_out_start_element (state->xml, elem);
456 gsf_xml_out_add_int (state->xml, "Old", o);
457 gsf_xml_out_add_int (state->xml, "New", n);
458 gsf_xml_out_end_element (state->xml); /* elem */
459 g_free (elem);
462 static void
463 xml_output_texpr (GnmDiffState *state, GnmExprTop const *texpr, GnmParsePos const *pos,
464 const char *tag)
466 GnmConventionsOut out;
467 GString *str;
469 out.accum = str = g_string_sized_new (100);
470 out.pp = pos;
471 out.convs = state->xml_convs;
473 g_string_append_c (str, '=');
474 gnm_expr_top_as_gstring (texpr, &out);
476 gsf_xml_out_add_cstr (state->xml, tag, str->str);
477 g_string_free (str, TRUE);
480 static void
481 xml_output_cell (GnmDiffState *state, GnmCell const *cell,
482 const char *tag, const char *valtag, const char *fmttag)
484 if (!cell)
485 return;
487 if (gnm_cell_has_expr (cell)) {
488 GnmParsePos pp;
489 parse_pos_init_cell (&pp, cell);
490 xml_output_texpr (state, cell->base.texpr, &pp, tag);
491 } else {
492 GnmValue const *v = cell->value;
493 GString *str = g_string_sized_new (100);
494 value_get_as_gstring (v, str, state->xml_convs);
495 gsf_xml_out_add_cstr (state->xml, tag, str->str);
496 g_string_free (str, TRUE);
497 gsf_xml_out_add_int (state->xml, valtag, v->v_any.type);
498 if (VALUE_FMT (v))
499 gsf_xml_out_add_cstr (state->xml, fmttag, go_format_as_XL (VALUE_FMT (v)));
503 static void
504 xml_colrow_changed (GnmDiffState *state, ColRowInfo const *oc, ColRowInfo const *nc,
505 gboolean is_cols, int i)
507 xml_open_section (state, is_cols ? DIFF "Cols" : DIFF "Rows");
509 gsf_xml_out_start_element (state->xml, is_cols ? DIFF "ColInfo" : DIFF "RowInfo");
510 if (i >= 0) gsf_xml_out_add_int (state->xml, "No", i);
512 if (oc->size_pts != nc->size_pts) {
513 gsf_xml_out_add_float (state->xml, "OldUnit", oc->size_pts, 4);
514 gsf_xml_out_add_float (state->xml, "NewUnit", nc->size_pts, 4);
516 if (oc->hard_size != nc->hard_size) {
517 gsf_xml_out_add_bool (state->xml, "OldHardSize", oc->hard_size);
518 gsf_xml_out_add_bool (state->xml, "NewHardSize", nc->hard_size);
520 if (oc->visible != nc->visible) {
521 gsf_xml_out_add_bool (state->xml, "OldHidden", !oc->visible);
522 gsf_xml_out_add_bool (state->xml, "NewHidden", !nc->visible);
524 if (oc->is_collapsed != nc->is_collapsed) {
525 gsf_xml_out_add_bool (state->xml, "OldCollapsed", oc->is_collapsed);
526 gsf_xml_out_add_bool (state->xml, "NewCollapsed", nc->is_collapsed);
528 if (oc->outline_level != nc->outline_level) {
529 gsf_xml_out_add_int (state->xml, "OldOutlineLevel", oc->outline_level);
530 gsf_xml_out_add_int (state->xml, "NewOutlineLevel", nc->outline_level);
533 gsf_xml_out_end_element (state->xml); /* </ColInfo> or </RowInfo> */
536 static void
537 xml_cell_changed (GnmDiffState *state, GnmCell const *oc, GnmCell const *nc)
539 const GnmCellPos *pos;
541 xml_open_section (state, DIFF "Cells");
543 gsf_xml_out_start_element (state->xml, DIFF "Cell");
545 pos = oc ? &oc->pos : &nc->pos;
546 gsf_xml_out_add_int (state->xml, "Row", pos->row);
547 gsf_xml_out_add_int (state->xml, "Col", pos->col);
549 xml_output_cell (state, oc, "Old", "OldValueType", "OldValueFormat");
550 xml_output_cell (state, nc, "New", "NewValueType", "NewValueFormat");
552 gsf_xml_out_end_element (state->xml); /* </Cell> */
555 #define DO_INT(what,fun) \
556 do { \
557 gsf_xml_out_start_element (state->xml, DIFF what); \
558 gsf_xml_out_add_int (state->xml, "Old", (fun) (os)); \
559 gsf_xml_out_add_int (state->xml, "New", (fun) (ns)); \
560 gsf_xml_out_end_element (state->xml); \
561 } while (0)
563 #define DO_INTS(what,fun,oobj,nobj) \
564 do { \
565 int oi = (oobj) ? (fun) (oobj) : 0; \
566 int ni = (nobj) ? (fun) (nobj) : 0; \
567 if (oi != ni || !(oobj) != !(nobj)) { \
568 gsf_xml_out_start_element (state->xml, DIFF what); \
569 if (oobj) gsf_xml_out_add_int (state->xml, "Old", oi); \
570 if (nobj) gsf_xml_out_add_int (state->xml, "New", ni); \
571 gsf_xml_out_end_element (state->xml); \
573 } while (0)
575 #define DO_STRINGS(what,fun,oobj,nobj) \
576 do { \
577 const char *ostr = (oobj) ? (fun) (oobj) : NULL; \
578 const char *nstr = (nobj) ? (fun) (nobj) : NULL; \
579 if (g_strcmp0 (ostr, nstr)) { \
580 gsf_xml_out_start_element (state->xml, DIFF what); \
581 if (ostr) gsf_xml_out_add_cstr (state->xml, "Old", ostr); \
582 if (nstr) gsf_xml_out_add_cstr (state->xml, "New", nstr); \
583 gsf_xml_out_end_element (state->xml); \
585 } while (0)
587 static const char *
588 cb_validation_message (GnmValidation const *v)
590 return v->msg ? v->msg->str : NULL;
593 static const char *
594 cb_validation_title (GnmValidation const *v)
596 return v->title ? v->title->str : NULL;
599 static gboolean
600 cb_validation_allow_blank (GnmValidation const *v)
602 return v->allow_blank;
605 static gboolean
606 cb_validation_use_dropdown (GnmValidation const *v)
608 return v->use_dropdown;
611 static void
612 xml_style_changed (GnmDiffState *state, GnmRange const *r,
613 GnmStyle const *os, GnmStyle const *ns)
615 unsigned int conflicts;
616 GnmStyleElement e;
618 xml_open_section (state, DIFF "Styles");
620 gsf_xml_out_start_element (state->xml, DIFF "StyleRegion");
621 gsf_xml_out_add_uint (state->xml, "startCol", r->start.col);
622 gsf_xml_out_add_uint (state->xml, "startRow", r->start.row);
623 gsf_xml_out_add_uint (state->xml, "endCol", r->end.col);
624 gsf_xml_out_add_uint (state->xml, "endRow", r->end.row);
626 conflicts = gnm_style_find_differences (os, ns, TRUE);
627 for (e = 0; e < MSTYLE_ELEMENT_MAX; e++) {
628 if ((conflicts & (1u << e)) == 0)
629 continue;
630 switch (e) {
631 case MSTYLE_COLOR_BACK: {
632 GnmColor *oc = gnm_style_get_back_color (os);
633 GnmColor *nc = gnm_style_get_back_color (ns);
635 gsf_xml_out_start_element (state->xml, DIFF "BackColor");
636 gnm_xml_out_add_gocolor (state->xml, "Old", oc->go_color);
637 gnm_xml_out_add_gocolor (state->xml, "New", nc->go_color);
638 if (oc->is_auto != nc->is_auto) {
639 gsf_xml_out_add_int (state->xml, "OldAuto", oc->is_auto);
640 gsf_xml_out_add_int (state->xml, "NewAuto", nc->is_auto);
642 gsf_xml_out_end_element (state->xml);
643 break;
646 case MSTYLE_COLOR_PATTERN:
647 gsf_xml_out_start_element (state->xml, DIFF "PatternColor");
648 gnm_xml_out_add_gocolor (state->xml, "Old", gnm_style_get_pattern_color (os)->go_color);
649 gnm_xml_out_add_gocolor (state->xml, "New", gnm_style_get_pattern_color (ns)->go_color);
650 gsf_xml_out_end_element (state->xml);
651 break;
653 case MSTYLE_BORDER_TOP:
654 case MSTYLE_BORDER_BOTTOM:
655 case MSTYLE_BORDER_LEFT:
656 case MSTYLE_BORDER_RIGHT:
657 case MSTYLE_BORDER_REV_DIAGONAL:
658 case MSTYLE_BORDER_DIAGONAL: {
659 static char const *border_names[] = {
660 "Top",
661 "Bottom",
662 "Left",
663 "Right",
664 "Rev-Diagonal",
665 "Diagonal"
668 char *tag = g_strconcat (DIFF "Border",
669 border_names[e - MSTYLE_BORDER_TOP],
670 NULL);
671 GnmBorder const *ob = gnm_style_get_border (os, e);
672 GnmBorder const *nb = gnm_style_get_border (ns, e);
673 gsf_xml_out_start_element (state->xml, tag);
674 gsf_xml_out_add_int (state->xml, "OldType", ob->line_type);
675 gsf_xml_out_add_int (state->xml, "NewType", nb->line_type);
676 if (ob->line_type != GNM_STYLE_BORDER_NONE)
677 gnm_xml_out_add_gocolor (state->xml, "OldColor", ob->color->go_color);
678 if (nb->line_type != GNM_STYLE_BORDER_NONE)
679 gnm_xml_out_add_gocolor (state->xml, "NewColor", nb->color->go_color);
680 gsf_xml_out_end_element (state->xml);
681 g_free (tag);
682 break;
685 case MSTYLE_PATTERN:
686 DO_INT ("Pattern", gnm_style_get_pattern);
687 break;
689 case MSTYLE_FONT_COLOR:
690 gsf_xml_out_start_element (state->xml, DIFF "FontColor");
691 gnm_xml_out_add_gocolor (state->xml, "Old", gnm_style_get_font_color (os)->go_color);
692 gnm_xml_out_add_gocolor (state->xml, "New", gnm_style_get_font_color (ns)->go_color);
693 gsf_xml_out_end_element (state->xml);
694 break;
696 case MSTYLE_FONT_NAME:
697 gsf_xml_out_start_element (state->xml, DIFF "FontName");
698 gsf_xml_out_add_cstr (state->xml, "Old", gnm_style_get_font_name (os));
699 gsf_xml_out_add_cstr (state->xml, "New", gnm_style_get_font_name (ns));
700 gsf_xml_out_end_element (state->xml);
701 break;
703 case MSTYLE_FONT_BOLD:
704 DO_INT ("Bold", gnm_style_get_font_bold);
705 break;
707 case MSTYLE_FONT_ITALIC:
708 DO_INT ("Italic", gnm_style_get_font_italic);
709 break;
711 case MSTYLE_FONT_UNDERLINE:
712 DO_INT ("Underline", gnm_style_get_font_uline);
713 break;
715 case MSTYLE_FONT_STRIKETHROUGH:
716 DO_INT ("Strike", gnm_style_get_font_strike);
717 break;
719 case MSTYLE_FONT_SCRIPT:
720 DO_INT ("Script", gnm_style_get_font_script);
721 break;
723 case MSTYLE_FONT_SIZE:
724 gsf_xml_out_start_element (state->xml, DIFF "FontSize");
725 gsf_xml_out_add_float (state->xml, "Old", gnm_style_get_font_size (os), 4);
726 gsf_xml_out_add_float (state->xml, "New", gnm_style_get_font_size (ns), 4);
727 gsf_xml_out_end_element (state->xml);
728 break;
730 case MSTYLE_FORMAT:
731 gsf_xml_out_start_element (state->xml, DIFF "Format");
732 gsf_xml_out_add_cstr (state->xml, "Old", go_format_as_XL (gnm_style_get_format (os)));
733 gsf_xml_out_add_cstr (state->xml, "New", go_format_as_XL (gnm_style_get_format (ns)));
734 gsf_xml_out_end_element (state->xml);
735 break;
737 case MSTYLE_ALIGN_V:
738 DO_INT ("VALign", gnm_style_get_align_v);
739 break;
741 case MSTYLE_ALIGN_H:
742 DO_INT ("HALign", gnm_style_get_align_h);
743 break;
745 case MSTYLE_INDENT:
746 DO_INT ("Indent", gnm_style_get_indent);
747 break;
749 case MSTYLE_ROTATION:
750 DO_INT ("Rotation", gnm_style_get_rotation);
751 break;
753 case MSTYLE_TEXT_DIR:
754 DO_INT ("TextDirection", gnm_style_get_text_dir);
755 break;
757 case MSTYLE_WRAP_TEXT:
758 DO_INT ("WrapText", gnm_style_get_wrap_text);
759 break;
761 case MSTYLE_SHRINK_TO_FIT:
762 DO_INT ("ShrinkToFit", gnm_style_get_shrink_to_fit);
763 break;
765 case MSTYLE_CONTENTS_LOCKED:
766 DO_INT ("Locked", gnm_style_get_contents_locked);
767 break;
769 case MSTYLE_CONTENTS_HIDDEN:
770 DO_INT ("Hidden", gnm_style_get_contents_hidden);
771 break;
773 case MSTYLE_HLINK: {
774 GnmHLink const *ol = gnm_style_get_hlink (os);
775 GnmHLink const *nl = gnm_style_get_hlink (ns);
777 gsf_xml_out_start_element (state->xml, DIFF "HLink");
778 if (ol) {
779 gsf_xml_out_add_cstr (state->xml, "OldTarget", gnm_hlink_get_target (ol));
780 gsf_xml_out_add_cstr (state->xml, "OldTip", gnm_hlink_get_tip (ol));
782 if (nl) {
783 gsf_xml_out_add_cstr (state->xml, "NewTarget", gnm_hlink_get_target (nl));
784 gsf_xml_out_add_cstr (state->xml, "NewTip", gnm_hlink_get_tip (nl));
786 gsf_xml_out_end_element (state->xml); /* </HLink> */
788 break;
791 case MSTYLE_VALIDATION: {
792 GnmValidation const *ov = gnm_style_get_validation (os);
793 GnmValidation const *nv = gnm_style_get_validation (ns);
794 gsf_xml_out_start_element (state->xml, DIFF "Validation");
795 DO_STRINGS ("Message", cb_validation_message, ov, nv);
796 DO_STRINGS ("Title", cb_validation_title, ov, nv);
797 DO_INTS ("AllowBlank", cb_validation_allow_blank, ov, nv);
798 DO_INTS ("UseDropdown", cb_validation_use_dropdown, ov, nv);
799 gsf_xml_out_end_element (state->xml); /* </Validation> */
800 break;
803 case MSTYLE_INPUT_MSG: {
804 GnmInputMsg const *om = gnm_style_get_input_msg (os);
805 GnmInputMsg const *nm = gnm_style_get_input_msg (ns);
807 gsf_xml_out_start_element (state->xml, DIFF "InputMessage");
808 DO_STRINGS ("Message", gnm_input_msg_get_msg, om, nm);
809 DO_STRINGS ("Title", gnm_input_msg_get_title, om, nm);
810 gsf_xml_out_end_element (state->xml); /* </InputMessage> */
811 break;
814 case MSTYLE_CONDITIONS:
815 gsf_xml_out_start_element (state->xml, DIFF "Conditions");
816 gsf_xml_out_end_element (state->xml); /* </Conditions> */
817 break;
819 default:
820 gsf_xml_out_start_element (state->xml, DIFF "Other");
821 gsf_xml_out_end_element (state->xml); /* </Other> */
822 break;
826 gsf_xml_out_end_element (state->xml); /* </StyleRegion> */
829 #undef DO_INT
830 #undef DO_INTS
831 #undef DO_STRINGS
833 static void
834 xml_name_changed (GnmDiffState *state,
835 GnmNamedExpr const *on, GnmNamedExpr const *nn)
837 xml_open_section (state, DIFF "Names");
839 gsf_xml_out_start_element (state->xml, DIFF "Name");
840 gsf_xml_out_add_cstr (state->xml, "Name", expr_name_name (on ? on : nn));
841 if (on)
842 xml_output_texpr (state, on->texpr, &on->pos, "Old");
843 if (nn)
844 xml_output_texpr (state, nn->texpr, &nn->pos, "New");
845 gsf_xml_out_end_element (state->xml); /* </Name> */
848 static const GnmDiffActions xml_actions = {
849 xml_diff_start,
850 xml_diff_end,
851 xml_dtor,
852 xml_sheet_start,
853 xml_sheet_end,
854 null_sheet_order_changed,
855 xml_sheet_attr_int_changed,
856 xml_colrow_changed,
857 xml_cell_changed,
858 xml_style_changed,
859 xml_name_changed,
862 /* -------------------------------------------------------------------------- */
864 static gboolean
865 highlight_diff_start (GnmDiffState *state)
867 const char *dst = state->new.url;
869 state->highlight_fs = go_file_saver_for_file_name (ssdiff_output);
870 if (!state->highlight_fs) {
871 g_printerr (_("%s: Unable to guess exporter to use for %s.\n"),
872 g_get_prgname (),
873 ssdiff_output);
875 return TRUE;
878 // We need a copy of one of the files. Rereading is easy.
879 g_object_ref ((state->highlight.input = state->new.input));
880 gsf_input_seek (state->highlight.input, 0, G_SEEK_SET);
881 if (read_file (&state->highlight, dst, state->ioc))
882 return TRUE;
884 // We apply a solid #F3F315 to changed cells.
885 state->highlight_style = gnm_style_new ();
886 gnm_style_set_back_color (state->highlight_style,
887 gnm_color_new_rgb8 (0xf3, 0xf3, 0x15));
888 gnm_style_set_pattern (state->highlight_style, 1);
890 return FALSE;
893 static void
894 highlight_diff_end (GnmDiffState *state)
896 wbv_save_to_output (state->highlight.wbv, state->highlight_fs,
897 state->output, state->ioc);
900 static void
901 highlight_dtor (GnmDiffState *state)
903 clear_file_state (&state->highlight);
904 if (state->highlight_style) {
905 gnm_style_unref (state->highlight_style);
906 state->highlight_style = NULL;
910 static void
911 highlight_apply (GnmDiffState *state, const GnmRange *r)
913 // We want the highlight sheet corresponding to new_sheet.
914 Sheet *sheet = workbook_sheet_by_index (state->highlight.wb,
915 state->new_sheet->index_in_wb);
916 g_return_if_fail (IS_SHEET (sheet));
918 gnm_style_ref (state->highlight_style);
919 sheet_style_apply_range (sheet, r, state->highlight_style);
922 static void
923 highlight_cell_changed (GnmDiffState *state,
924 GnmCell const *oc, GnmCell const *nc)
926 GnmRange r;
927 highlight_apply (state, range_init_cellpos (&r, &(nc ? nc : oc)->pos));
930 static void
931 highlight_style_changed (GnmDiffState *state, GnmRange const *r,
932 G_GNUC_UNUSED GnmStyle const *os,
933 G_GNUC_UNUSED GnmStyle const *ns)
935 highlight_apply (state, r);
939 static const GnmDiffActions highlight_actions = {
940 highlight_diff_start,
941 highlight_diff_end,
942 highlight_dtor,
943 null_sheet_start,
944 null_sheet_end,
945 null_sheet_order_changed,
946 null_sheet_attr_int_changed,
947 null_colrow_changed,
948 highlight_cell_changed,
949 highlight_style_changed,
950 null_name_changed,
953 /* -------------------------------------------------------------------------- */
955 static gboolean
956 compare_texpr_equal (GnmExprTop const *oe, GnmParsePos const *opp,
957 GnmExprTop const *ne, GnmParsePos const *npp,
958 GnmConventions const *convs)
960 char *so, *sn;
961 gboolean eq;
963 if (gnm_expr_top_equal (oe, ne))
964 return TRUE;
966 // Not equal, but with references to sheets, that is not
967 // necessary. Compare as strings.
969 so = gnm_expr_top_as_string (oe, opp, convs);
970 sn = gnm_expr_top_as_string (ne, npp, convs);
972 eq = g_strcmp0 (so, sn) == 0;
974 g_free (so);
975 g_free (sn);
977 return eq;
980 static gboolean
981 compare_corresponding_cells (GnmCell const *co, GnmCell const *cn)
983 gboolean has_expr = gnm_cell_has_expr (co);
984 gboolean has_value = co->value != NULL;
986 if (has_expr != gnm_cell_has_expr (cn))
987 return TRUE;
988 if (has_expr) {
989 GnmParsePos opp, npp;
990 parse_pos_init_cell (&opp, co);
991 parse_pos_init_cell (&npp, cn);
992 return !compare_texpr_equal (co->base.texpr, &opp,
993 cn->base.texpr, &npp,
994 sheet_get_conventions (cn->base.sheet));
997 if (has_value != (cn->value != NULL))
998 return TRUE;
999 if (has_value)
1000 return !(value_equal (co->value, cn->value) &&
1001 go_format_eq (VALUE_FMT (co->value),
1002 VALUE_FMT (cn->value)));
1005 return FALSE;
1008 static gboolean
1009 ignore_cell (GnmCell const *cell)
1011 if (cell) {
1012 if (gnm_cell_has_expr (cell)) {
1013 return gnm_expr_top_is_array_elem (cell->base.texpr,
1014 NULL, NULL);
1015 } else {
1016 return VALUE_IS_EMPTY (cell->value);
1019 return FALSE;
1022 static void
1023 diff_sheets_cells (GnmDiffState *state)
1025 GPtrArray *old_cells = sheet_cells (state->old_sheet, NULL);
1026 GPtrArray *new_cells = sheet_cells (state->new_sheet, NULL);
1027 size_t io = 0, in = 0;
1029 // Make code below simpler.
1030 g_ptr_array_add (old_cells, NULL);
1031 g_ptr_array_add (new_cells, NULL);
1033 while (TRUE) {
1034 GnmCell const *co, *cn;
1036 while (ignore_cell ((co = g_ptr_array_index (old_cells, io))))
1037 io++;
1039 while (ignore_cell ((cn = g_ptr_array_index (new_cells, in))))
1040 in++;
1042 if (co && cn) {
1043 int order = co->pos.row == cn->pos.row
1044 ? co->pos.col - cn->pos.col
1045 : co->pos.row - cn->pos.row;
1046 if (order < 0)
1047 cn = NULL;
1048 else if (order > 0)
1049 co = NULL;
1050 else {
1051 if (compare_corresponding_cells (co, cn)) {
1052 state->diff_found = TRUE;
1053 state->actions->cell_changed (state, co, cn);
1055 io++, in++;
1056 continue;
1060 if (co) {
1061 state->diff_found = TRUE;
1062 state->actions->cell_changed (state, co, NULL);
1063 io++;
1064 } else if (cn) {
1065 state->diff_found = TRUE;
1066 state->actions->cell_changed (state, NULL, cn);
1067 in++;
1068 } else
1069 break;
1072 g_ptr_array_free (old_cells, TRUE);
1073 g_ptr_array_free (new_cells, TRUE);
1076 static void
1077 diff_sheets_colrow (GnmDiffState *state, gboolean is_cols)
1079 ColRowInfo const *old_def =
1080 sheet_colrow_get_default (state->old_sheet, is_cols);
1081 ColRowInfo const *new_def =
1082 sheet_colrow_get_default (state->new_sheet, is_cols);
1083 int i, U;
1085 if (!colrow_equal (old_def, new_def)) {
1086 state->diff_found = TRUE;
1087 state->actions->colrow_changed (state, old_def, new_def, is_cols, -1);
1090 U = is_cols
1091 ? state->common_range.end.col
1092 : state->common_range.end.row;
1093 for (i = 0; i <= U; i++) {
1094 ColRowInfo const *ocr =
1095 sheet_colrow_get (state->old_sheet, i, is_cols);
1096 ColRowInfo const *ncr =
1097 sheet_colrow_get (state->new_sheet, i, is_cols);
1099 if (ocr == ncr)
1100 continue; // Considered equal, even if defaults are different
1101 if (!ocr) ocr = old_def;
1102 if (!ncr) ncr = new_def;
1103 if (!colrow_equal (ocr, ncr)) {
1104 state->diff_found = TRUE;
1105 state->actions->colrow_changed (state, ocr, ncr, is_cols, i);
1110 #define DO_INT(field,attr) \
1111 do { \
1112 if (state->old_sheet->field != state->new_sheet->field) { \
1113 state->diff_found = TRUE; \
1114 state->actions->sheet_attr_int_changed \
1115 (state, attr, state->old_sheet->field, state->new_sheet->field); \
1117 } while (0)
1119 static void
1120 diff_sheets_attrs (GnmDiffState *state)
1122 GnmSheetSize const *os = gnm_sheet_get_size (state->old_sheet);
1123 GnmSheetSize const *ns = gnm_sheet_get_size (state->new_sheet);
1125 if (os->max_cols != ns->max_cols) {
1126 state->diff_found = TRUE;
1127 state->actions->sheet_attr_int_changed
1128 (state, "Cols", os->max_cols, ns->max_cols);
1130 if (os->max_rows != ns->max_rows) {
1131 state->diff_found = TRUE;
1132 state->actions->sheet_attr_int_changed
1133 (state, "Rows", os->max_rows, ns->max_rows);
1136 DO_INT (display_formulas, "DisplayFormulas");
1137 DO_INT (hide_zero, "HideZero");
1138 DO_INT (hide_grid, "HideGrid");
1139 DO_INT (hide_col_header, "HideColHeader");
1140 DO_INT (hide_row_header, "HideRowHeader");
1141 DO_INT (display_outlines, "DisplayOutlines");
1142 DO_INT (outline_symbols_below, "OutlineSymbolsBelow");
1143 DO_INT (outline_symbols_right, "OutlineSymbolsRight");
1144 DO_INT (text_is_rtl, "RTL_Layout");
1145 DO_INT (is_protected, "Protected");
1146 DO_INT (visibility, "Visibility");
1148 #undef DO_INT
1150 struct cb_diff_sheets_styles {
1151 GnmDiffState *state;
1152 GnmStyle *old_style;
1155 static void
1156 cb_diff_sheets_styles_2 (G_GNUC_UNUSED gpointer key,
1157 gpointer sr_, gpointer user_data)
1159 GnmStyleRegion *sr = sr_;
1160 struct cb_diff_sheets_styles *data = user_data;
1161 GnmDiffState *state = data->state;
1162 GnmRange r = sr->range;
1164 if (gnm_style_find_differences (data->old_style, sr->style, TRUE) == 0)
1165 return;
1167 state->diff_found = TRUE;
1169 state->actions->style_changed (state, &r, data->old_style, sr->style);
1172 static void
1173 cb_diff_sheets_styles_1 (G_GNUC_UNUSED gpointer key,
1174 gpointer sr_, gpointer user_data)
1176 GnmStyleRegion *sr = sr_;
1177 struct cb_diff_sheets_styles *data = user_data;
1178 GnmDiffState *state = data->state;
1180 data->old_style = sr->style;
1181 sheet_style_range_foreach (state->new_sheet, &sr->range,
1182 cb_diff_sheets_styles_2,
1183 data);
1186 static void
1187 diff_sheets_styles (GnmDiffState *state)
1189 struct cb_diff_sheets_styles data;
1191 data.state = state;
1192 sheet_style_range_foreach (state->old_sheet, &state->common_range,
1193 cb_diff_sheets_styles_1,
1194 &data);
1197 static int
1198 cb_expr_name_by_name (GnmNamedExpr const *a, GnmNamedExpr const *b)
1200 return g_strcmp0 (expr_name_name (a), expr_name_name (b));
1203 static void
1204 diff_names (GnmDiffState *state,
1205 GnmNamedExprCollection const *onames, GnmNamedExprCollection const *nnames)
1207 GSList *old_names = gnm_named_expr_collection_list (onames);
1208 GSList *new_names = gnm_named_expr_collection_list (nnames);
1209 GSList *lo, *ln;
1210 GnmConventions const *convs;
1212 if (state->new_sheet)
1213 convs = sheet_get_conventions (state->new_sheet);
1214 else
1215 // Hmm... It's not terribly important where we get them
1216 convs = sheet_get_conventions (workbook_sheet_by_index (state->new.wb, 0));
1218 old_names = g_slist_sort (old_names, (GCompareFunc)cb_expr_name_by_name);
1219 new_names = g_slist_sort (new_names, (GCompareFunc)cb_expr_name_by_name);
1221 lo = old_names;
1222 ln = new_names;
1223 while (lo || ln) {
1224 GnmNamedExpr const *on = lo ? lo->data : NULL;
1225 GnmNamedExpr const *nn = ln ? ln->data : NULL;
1227 if (!nn || (on && cb_expr_name_by_name (on, nn)) < 0) {
1228 // Old name got removed
1229 state->diff_found = TRUE;
1230 state->actions->name_changed (state, on, nn);
1231 lo = lo->next;
1232 continue;
1235 if (!on || (nn && cb_expr_name_by_name (on, nn)) > 0) {
1236 // New name got added
1237 state->diff_found = TRUE;
1238 state->actions->name_changed (state, on, nn);
1239 ln = ln->next;
1240 continue;
1243 if (!compare_texpr_equal (on->texpr, &on->pos,
1244 nn->texpr, &nn->pos,
1245 convs)) {
1246 state->diff_found = TRUE;
1247 state->actions->name_changed (state, on, nn);
1250 lo = lo->next;
1251 ln = ln->next;
1254 g_slist_free (old_names);
1255 g_slist_free (new_names);
1259 static void
1260 diff_sheets (GnmDiffState *state, Sheet *old_sheet, Sheet *new_sheet)
1262 GnmRange or, nr;
1264 state->old_sheet = old_sheet;
1265 state->new_sheet = new_sheet;
1267 range_init_full_sheet (&or, old_sheet);
1268 range_init_full_sheet (&nr, new_sheet);
1269 range_intersection (&state->common_range, &or, &nr);
1271 diff_sheets_attrs (state);
1272 diff_names (state, state->old_sheet->names, state->new_sheet->names);
1273 diff_sheets_colrow (state, TRUE);
1274 diff_sheets_colrow (state, FALSE);
1275 diff_sheets_cells (state);
1276 diff_sheets_styles (state);
1278 state->old_sheet = state->new_sheet = NULL;
1281 static int
1282 diff (char const *oldfilename, char const *newfilename,
1283 GOIOContext *ioc,
1284 GnmDiffActions const *actions, GsfOutput *output)
1286 GnmDiffState state;
1287 int res = 0;
1288 int i, count;
1289 gboolean sheet_order_changed = FALSE;
1290 int last_index = -1;
1291 GnmLocale *locale;
1293 locale = gnm_push_C_locale ();
1295 memset (&state, 0, sizeof (state));
1296 state.actions = actions;
1297 state.ioc = ioc;
1298 state.output = output;
1300 if (read_file (&state.old, oldfilename, ioc))
1301 goto error;
1302 if (read_file (&state.new, newfilename, ioc))
1303 goto error;
1305 /* ---------------------------------------- */
1307 if (state.actions->diff_start (&state))
1308 goto error;
1310 diff_names (&state, state.old.wb->names, state.new.wb->names);
1312 // This doesn't handle sheet renames very well, but simply considers
1313 // that a sheet deletion and a sheet insert.
1314 count = workbook_sheet_count (state.old.wb);
1315 for (i = 0; i < count; i++) {
1316 Sheet *old_sheet = workbook_sheet_by_index (state.old.wb, i);
1317 Sheet *new_sheet = workbook_sheet_by_name (state.new.wb,
1318 old_sheet->name_unquoted);
1319 state.actions->sheet_start (&state, old_sheet, new_sheet);
1321 if (new_sheet) {
1322 if (new_sheet->index_in_wb < last_index)
1323 sheet_order_changed = TRUE;
1324 last_index = new_sheet->index_in_wb;
1326 diff_sheets (&state, old_sheet, new_sheet);
1327 } else
1328 state.diff_found = TRUE;
1330 state.actions->sheet_end (&state);
1333 count = workbook_sheet_count (state.new.wb);
1334 for (i = 0; i < count; i++) {
1335 Sheet *new_sheet = workbook_sheet_by_index (state.new.wb, i);
1336 Sheet *old_sheet = workbook_sheet_by_name (state.old.wb,
1337 new_sheet->name_unquoted);
1338 if (old_sheet)
1339 ; // Nothing -- already done above.
1340 else {
1341 state.diff_found = TRUE;
1342 state.actions->sheet_start (&state, old_sheet, new_sheet);
1343 state.actions->sheet_end (&state);
1347 if (sheet_order_changed) {
1348 state.diff_found = TRUE;
1349 state.actions->sheet_order_changed (&state);
1352 state.actions->diff_end (&state);
1354 out:
1355 clear_file_state (&state.old);
1356 clear_file_state (&state.new);
1357 state.actions->dtor (&state);
1359 gnm_pop_C_locale (locale);
1361 if (res == 0)
1362 res = state.diff_found ? 1 : 0;
1364 return res;
1366 error:
1367 res = 2;
1368 goto out;
1372 main (int argc, char const **argv)
1374 GOErrorInfo *plugin_errs;
1375 int res = 0;
1376 GOCmdContext *cc;
1377 GOptionContext *ocontext;
1378 GError *error = NULL;
1379 const GnmDiffActions *actions;
1380 char *output_uri;
1381 GsfOutput *output;
1383 // No code before here, we need to init threads
1384 argv = gnm_pre_parse_init (argc, argv);
1386 ocontext = g_option_context_new (_("OLDFILE NEWFILE"));
1387 g_option_context_add_main_entries (ocontext, ssdiff_options, GETTEXT_PACKAGE);
1388 g_option_context_add_group (ocontext, gnm_get_option_group ());
1389 g_option_context_parse (ocontext, &argc, (char ***)&argv, &error);
1390 g_option_context_free (ocontext);
1392 if (error) {
1393 g_printerr (_("%s\nRun '%s --help' to see a full list of available command line options.\n"),
1394 error->message, argv[0]);
1395 g_error_free (error);
1396 return 1;
1399 if (ssdiff_show_version) {
1400 g_print (_("ssdiff version '%s'\ndatadir := '%s'\nlibdir := '%s'\n"),
1401 GNM_VERSION_FULL, gnm_sys_data_dir (), gnm_sys_lib_dir ());
1402 return 0;
1405 if (ssdiff_xml + ssdiff_highlight > 1) {
1406 g_printerr (_("%s: Only one output format may be specified.\n"),
1407 g_get_prgname ());
1408 return 1;
1411 if (ssdiff_highlight) {
1412 actions = &highlight_actions;
1413 } else if (ssdiff_xml) {
1414 actions = &xml_actions;
1415 } else {
1416 actions = &default_actions;
1419 if (!ssdiff_output)
1420 ssdiff_output = g_strdup ("fd://1");
1421 output_uri = go_shell_arg_to_uri (ssdiff_output);
1422 output = go_file_create (output_uri, &error);
1423 g_free (output_uri);
1424 if (!output) {
1425 g_printerr (_("%s: Failed to create output file: %s\n"),
1426 g_get_prgname (),
1427 error ? error->message : "?");
1428 if (error)
1429 g_error_free (error);
1430 return 1;
1433 gnm_init ();
1435 cc = gnm_cmd_context_stderr_new ();
1436 gnm_plugins_init (GO_CMD_CONTEXT (cc));
1437 go_plugin_db_activate_plugin_list (
1438 go_plugins_get_available_plugins (), &plugin_errs);
1439 if (plugin_errs) {
1440 // FIXME: What do we want to do here?
1441 go_error_info_free (plugin_errs);
1443 go_component_set_default_command_context (cc);
1445 if (argc == 3) {
1446 GOIOContext *ioc = go_io_context_new (cc);
1447 res = diff (argv[1], argv[2], ioc, actions, output);
1448 g_object_unref (ioc);
1449 } else {
1450 g_printerr (_("Usage: %s [OPTION...] %s\n"),
1451 g_get_prgname (),
1452 _("OLDFILE NEWFILE"));
1453 res = 2;
1456 // Release cached string.
1457 def_cell_name (NULL);
1458 g_object_unref (output);
1460 go_component_set_default_command_context (NULL);
1461 g_object_unref (cc);
1462 gnm_shutdown ();
1463 gnm_pre_parse_shutdown ();
1465 return res;