Merge branch 'release-2.140.101'
[feisty_meow.git] / nucleus / applications / bookmark_tools / marks_maker.cpp
1 //////////////
2 // Name   : marks_maker
3 // Author : Chris Koeritz
4 //////////////
5 // Copyright (c) 2005-$now By Author.  This program is free software; you can
6 // redistribute it and/or modify it under the terms of the GNU General Public
7 // License as published by the Free Software Foundation:
8 //     http://www.gnu.org/licenses/gpl.html
9 // or under the terms of the GNU Library license:
10 //     http://www.gnu.org/licenses/lgpl.html
11 // at your preference.  Those licenses describe your legal rights to this
12 // software, and no other rights or warranties apply.
13 // Please send updates for this code to: fred@gruntose.com -- Thanks, fred.
14 //////////////
15
16 #include "bookmark_tree.h"
17
18 #include <application/hoople_main.h>
19 #include <application/command_line.h>
20 #include <basis/astring.h>
21 #include <basis/functions.h>
22 #include <basis/guards.h>
23 #include <filesystem/byte_filer.h>
24 #include <filesystem/filename.h>
25 #include <loggers/file_logger.h>
26 #include <timely/time_stamp.h>
27 #include <structures/static_memory_gremlin.h>
28 #include <textual/list_parsing.h>
29 #include <textual/string_manipulation.h>
30
31 using namespace application;
32 using namespace basis;
33 using namespace filesystem;
34 using namespace loggers;
35 using namespace nodes;
36 using namespace structures;
37 using namespace textual;
38 using namespace timely;
39
40 //#define DEBUG_MARKS
41   // uncomment to have more debugging noise.
42
43 #undef BASE_LOG
44 #define BASE_LOG(s) program_wide_logger::get().log(s, ALWAYS_PRINT)
45 #undef LOG
46 #define LOG(s) CLASS_EMERGENCY_LOG(program_wide_logger::get(), s)
47
48 const int MAX_FILE_SIZE = 4 * MEGABYTE;
49   // the largest file we'll read.
50
51 const int SPACING_CHUNK = 4;
52   // number of spaces per indentation level.
53
54 const int MAX_URL_DISPLAYED = 58;
55 const int MAX_DESCRIP_DISPLAYED = 72;
56
57 ////////////////////////////////////////////////////////////////////////////
58
59 class marks_maker : public application_shell
60 {
61 public:
62   marks_maker();
63
64   enum output_style {
65     ST_HUMAN_READABLE,
66     ST_MOZILLA_MARKS,
67 //    ST_JAVASCRIPT_BASED... separate implementation currently.
68   };
69
70   int write_marks_page(const astring &output_filename,
71           const astring &template_filename, output_style way);
72     // given a tree of links, this writes out a web page to "output_filename"
73     // using a template file "template_filename".
74
75   DEFINE_CLASS_NAME("marks_maker");
76   int print_instructions(const filename &program_name);
77   virtual int execute();
78
79 private:
80   bookmark_tree c_categories;  // our tree of categories.
81   int c_current_depth;  // current indentation depth in list.
82   output_style c_style;  // style of marks to write, set after construction.
83
84   void increase_nesting(astring &output);
85     // adds a new level of nesting to the text.
86
87   void decrease_nesting(astring &output);
88     // drops out of a level of nesting in the text.
89
90   astring recurse_on_node(inner_mark_tree *nod);
91     // the main recursive method that spiders down the tree.  it is important that it builds
92     // the string through composition rather than being given a string reference, since it
93     // expands all sub-trees as it goes.
94
95   void inject_javascript_function(astring &output);
96     // replaces a special phrase in the template file with our javascript-based link opener.
97
98   void write_category_start(const astring &name, int node_depth, astring &output);
99     // outputs the text for categories and adjusts the indentation level.
100
101   void write_category_end(int depth, astring &output);
102     // closes a category appropriately for the nesting depth.
103
104   void write_link(inner_mark_tree *node, const link_record &linko,
105          astring &output, int depth);
106     // outputs the text for web links.
107 };
108
109 ////////////////////////////////////////////////////////////////////////////
110
111 marks_maker::marks_maker()
112 : application_shell(),
113   c_current_depth(0),
114   c_style(ST_HUMAN_READABLE)
115 {}
116
117 int marks_maker::print_instructions(const filename &program_name)
118 {
119   a_sprintf to_show("%s:\n\
120 This program needs three filenames as command line parameters.  The -i flag\n\
121 is used to specify the input filename, the -t flag specifies a template web\n\
122 page which is used as the wrapper around the links, and the -o flag specifies\n\
123 the web page to be created.  The input file is expected to be in the HOOPLE\n\
124 link database format.  The output file will be created from the template file\n\
125 by finding the phrase $INSERT_LINKS_HERE in it and replacing that with html\n\
126 formatted link and categories from the input file.  Another tag of $TODAYS_DATE\n\
127 will be replaced with the date when the output file is regenerated.  A final\n\
128 tag of $INSERT_JAVASCRIPT_HERE is replaced with a link opening function.\n\
129 Note that an optional -s flag can specify a value of \"human\" readable\n\
130 or \"mozilla\" bookmarks style to specify the style of the output file\n\
131 generated.\n\
132 The HOOPLE link format is documented here:\n\
133     http://feistymeow.org/guides/link_database/format_manifesto.txt\n\
134 ", program_name.basename().raw().s(), program_name.basename().raw().s());
135   program_wide_logger::get().log(to_show, ALWAYS_PRINT);
136   return 12;
137 }
138
139 void marks_maker::increase_nesting(astring &output)
140 {
141   FUNCDEF("increase_nesting");
142   int spaces = SPACING_CHUNK * c_current_depth;
143   c_current_depth++;
144 #ifdef DEBUG_MARKS
145   LOG(a_sprintf("++increased depth to %d...", c_current_depth));
146 #endif
147   output += string_manipulation::indentation(spaces);
148   output += "<dl><p>";
149   output += parser_bits::platform_eol_to_chars();
150 }
151
152 void marks_maker::decrease_nesting(astring &output)
153 {
154   FUNCDEF("decrease_nesting");
155   c_current_depth--;
156 #ifdef DEBUG_MARKS
157   LOG(a_sprintf("--decreased depth to %d...", c_current_depth));
158 #endif
159   int spaces = SPACING_CHUNK * c_current_depth;
160   output += string_manipulation::indentation(spaces);
161   output += "</dl><p>";
162   output += parser_bits::platform_eol_to_chars();
163 }
164
165 void marks_maker::write_category_start(const astring &name, int node_depth, astring &output)
166 {
167   FUNCDEF("write_category_start");
168
169   // calculate proper heading number.
170   int heading_num = node_depth + 1;
171   astring heading = a_sprintf("%d", heading_num);
172   // force a weird requirement for mozilla bookmarks, all headings must be set at 3.
173   if (c_style == ST_MOZILLA_MARKS) heading = "3";
174
175 #ifdef DEBUG_MARKS
176   LOG(astring("header [") + name + "] level " + a_sprintf("%d", node_depth));
177 #endif
178
179   // output our heading.
180   output += string_manipulation::indentation(c_current_depth * SPACING_CHUNK);
181   output += "<dt><h";
182   output += heading;
183   output += ">";
184   output += name;
185   output += "</h";
186   output += heading;
187   output += ">";
188   output += "</dt>";
189   output += parser_bits::platform_eol_to_chars();
190
191   increase_nesting(output);
192 }
193
194 void marks_maker::write_category_end(int depth, astring &output)
195 {
196   FUNCDEF("write_category_end");
197   decrease_nesting(output);
198 }
199
200 void marks_maker::write_link(inner_mark_tree *formal(node),
201     const link_record &linko, astring &output, int depth)
202 {
203   FUNCDEF("write_link");
204   // write an html link definition.
205   if (!linko._url) {
206     // this just appears to be a comment line.
207
208     if (!linko._description) return;  // it's a nothing line.
209
210     output += linko._description;
211     output += "<br>";
212     output += parser_bits::platform_eol_to_chars();
213     return;
214   }
215
216   astring chomped_url = linko._url;
217   if (c_style != ST_MOZILLA_MARKS) {
218     if (chomped_url.length() > MAX_URL_DISPLAYED) {
219       chomped_url.zap(MAX_URL_DISPLAYED / 2,
220           chomped_url.length() - MAX_URL_DISPLAYED / 2 - 1);
221       chomped_url.insert(MAX_URL_DISPLAYED / 2, "...");
222     }
223   }
224
225   astring description = linko._description;
226   if (c_style != ST_MOZILLA_MARKS) {
227     // this is chopping the tail off, which seems reasonable for a very long description.
228     if (description.length() > MAX_DESCRIP_DISPLAYED) {
229       description.zap(MAX_DESCRIP_DISPLAYED - 1, description.end());
230       description += "...";
231     }
232   }
233
234   // new output format, totally clean and simple.  description is there
235   // in readable manner, and it's also a link.  plus, this takes up a fraction
236   // of the space the old way used.
237   astring indentulus = string_manipulation::indentation(c_current_depth * SPACING_CHUNK);
238   output += indentulus;
239   output += "<dt><li>";
240   output += "<a href=\"";
241   output += linko._url;
242   output += "\">";
243   output += description;
244   output += "</a>";
245
246   if (c_style != ST_MOZILLA_MARKS) {
247     output += "&nbsp;&nbsp;&nbsp;";
248     output += "<a href=\"javascript:open_mark('";
249     output += linko._url;
250     output += "')\">";
251     output += "[launch]";
252     output += "</a>";
253   }
254
255   output += "</li>";
256   output += "</dt>";
257   output += parser_bits::platform_eol_to_chars();
258 }
259
260 astring marks_maker::recurse_on_node(inner_mark_tree *nod)
261 {
262   FUNCDEF("recurse_on_node");
263   astring to_return;
264
265   // print out the category on this node.
266   write_category_start(nod->name(), nod->depth(), to_return);
267
268   // print the link for all of the ones stored at this node.
269   for (int i = 0; i < nod->_links.elements(); i++) {
270     link_record *lin = nod->_links.borrow(i);
271     write_link(nod, *lin, to_return, nod->depth());
272   }
273
274   // zoom down into sub-categories.
275   for (int i = 0; i < nod->branches(); i++) {
276     to_return += recurse_on_node((inner_mark_tree *)nod->branch(i));
277   }
278
279   // finish this category.
280   write_category_end(nod->depth(), to_return);
281
282   return to_return;
283 }
284
285 void marks_maker::inject_javascript_function(astring &output)
286 {
287   FUNCDEF("inject_javascript_function");
288   astring scrip = "\n\
289 <script language=\"javascript1.2\">\n\
290 <!--\n\
291 function open_mark(url) {\n\
292   if (typeof open_mark.next_num == 'undefined') {\n\
293     // must initialize this before use.\n\
294     open_mark.next_num = 0;\n\
295   }\n\
296   // pick the next number for our auto-generated name.\n\
297   open_mark.next_num++;\n\
298   winname = \"wingo\" + open_mark.next_num;\n\
299   // open URL they asked for and give its window all permissions.\n\
300   winner = window.open(url, winname);\n\
301   // bring that new window into focus so they can see it.\n\
302   winner.focus();\n\
303 }\n\
304 //-->\n\
305 </script>\n\
306 \n";
307
308   bool found_it = output.replace("$INSERT_JAVASCRIPT_HERE", scrip);
309   if (!found_it)
310     non_continuable_error(class_name(), func, "the template file is missing "
311         "the insertion point for '$INSERT_JAVASCRIPT_HERE'");
312 }
313
314 int marks_maker::write_marks_page(const astring &output_filename,
315     const astring &template_filename, output_style style)
316 {
317   FUNCDEF("write_marks_page");
318   c_style = style;  // set the overall output style here.
319   astring long_string;
320     // this is our accumulator of links.  it is the semi-final result that will
321     // be injected into the template file.
322
323   // generate the meaty portion of the bookmarks.
324   increase_nesting(long_string);
325   inner_mark_tree *top = (inner_mark_tree *)&c_categories.access_root();
326   long_string += recurse_on_node(top);
327   decrease_nesting(long_string);
328
329   byte_filer template_file(template_filename, "r");
330   astring full_template;
331   if (!template_file.good())
332     non_continuable_error(class_name(), func, "the template file could not be opened");
333   template_file.read(full_template, MAX_FILE_SIZE);
334   template_file.close();
335
336   // spice up the boring template with a nice link opening function.
337   inject_javascript_function(full_template);
338
339   // replace the tag with the long string we created.
340   bool found_it = full_template.replace("$INSERT_LINKS_HERE", long_string);
341   if (!found_it)
342     non_continuable_error(class_name(), func, "the template file is missing "
343         "the insertion point for '$INSERT_LINKS_HERE'");
344
345   full_template.replace("$TODAYS_DATE", time_stamp::notarize(true));
346
347   filename outname(output_filename);
348   byte_filer output_file(output_filename, "w");
349   if (!output_file.good())
350     non_continuable_error(class_name(), func, "the output file could not be opened");
351   // write the newly generated web page out now.
352   output_file.write(full_template);
353   output_file.close();
354
355 #ifdef DEBUG_MARKS
356   // show the tree.
357   BASE_LOG(astring());
358   BASE_LOG(astring("the tree, sir..."));
359   BASE_LOG(astring());
360   BASE_LOG(c_categories.access_root().text_form());
361 #endif
362
363   BASE_LOG(a_sprintf("wrote %d links in %d categories.",
364       c_categories.link_count(), c_categories.category_count()));
365   BASE_LOG(astring(""));
366
367   return 0;
368 }
369
370 int marks_maker::execute()
371 {
372   FUNCDEF("execute");
373   SETUP_COMBO_LOGGER;
374
375   command_line cmds(_global_argc, _global_argv);  // process the command line parameters.
376   astring input_filename;  // we'll store our link database name here.
377   astring output_filename;  // where the web page we're creating goes.
378   astring template_filename;  // the wrapper html code that we'll stuff.
379   astring style_used;  // type of output file style to create.
380   if (!cmds.get_value('i', input_filename, false))
381     return print_instructions(cmds.program_name());
382   if (!cmds.get_value('o', output_filename, false))
383     return print_instructions(cmds.program_name());
384   if (!cmds.get_value('t', template_filename, false))
385     return print_instructions(cmds.program_name());
386   cmds.get_value('s', style_used, false);
387   if (!style_used) style_used = "human";
388
389   BASE_LOG(astring("input file: ") + input_filename);
390   BASE_LOG(astring("output file: ") + output_filename);
391   BASE_LOG(astring("template file: ") + template_filename);
392   BASE_LOG(astring("style: ") + style_used);
393
394   filename outname(output_filename);
395   if (outname.exists()) {
396     non_continuable_error(class_name(), func, astring("the output file ")
397         + output_filename + " already exists.  It would be over-written if "
398         "we continued.");
399   }
400
401   output_style styley = ST_HUMAN_READABLE;
402   if (style_used == astring("mozilla")) styley = ST_MOZILLA_MARKS;
403
404   int ret = c_categories.read_csv_file(input_filename);
405   if (ret) return ret;
406
407   ret = write_marks_page(output_filename, template_filename, styley);
408   if (ret) return ret;
409   
410   return 0;
411 }
412
413 ////////////////////////////////////////////////////////////////////////////
414
415 HOOPLE_MAIN(marks_maker, )
416