]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
mdhelp: many changes, it's starting to be useful
authorJaroslav Kysela <perex@perex.cz>
Mon, 4 Apr 2016 16:18:56 +0000 (18:18 +0200)
committerJaroslav Kysela <perex@perex.cz>
Mon, 4 Apr 2016 16:18:58 +0000 (18:18 +0200)
- moved from micromarkdown.js to marked.js
- Help window is auto-resized and positioned
- Help window is exlusive (one)
- some CSS styling for TOC
- added last pages (history) to the TOC float area
- EPG help is fully moved to new mdhelp

Makefile.webui
docs/markdown/epg.md [new file with mode: 0644]
src/webui/doc_md.c
src/webui/static/app/acleditor.js
src/webui/static/app/epg.js
src/webui/static/app/ext.css
src/webui/static/app/marked.js [new file with mode: 0644]
src/webui/static/app/micromarkdown.js [deleted file]
src/webui/static/app/tvheadend.js
src/webui/static/app/wizard.js
support/doc/md_to_c.py

index c83cab3e4a7e623078becf423d0608e96a14c2af..83102509ec1a08681029bde8900fd4f9290821d8 100644 (file)
@@ -82,7 +82,7 @@ CSS_ACCESS += $(EXTJSPATH)/resources/css/xtheme-access.css
 
 JAVASCRIPT += $(ROOTPATH)/app/i18n.js
 JAVASCRIPT += $(ROOTPATH)/app/extensions.js
-JAVASCRIPT += $(ROOTPATH)/app/micromarkdown.js
+JAVASCRIPT += $(ROOTPATH)/app/marked.js
 JAVASCRIPT += $(ROOTPATH)/livegrid/livegrid-all.js
 JAVASCRIPT += $(ROOTPATH)/lovcombo/lovcombo-all.js
 JAVASCRIPT += $(ROOTPATH)/multiselect/multiselect.js
diff --git a/docs/markdown/epg.md b/docs/markdown/epg.md
new file mode 100644 (file)
index 0000000..2b73960
--- /dev/null
@@ -0,0 +1,224 @@
+##Electronic Program Guide
+
+Tvheadend has a built-in Electronic Program Guide. The EPG is an
+in-memory database populated with all the information about events
+received from the DVB networks over-the-air or from external grabbers
+such as XMLTV.
+
+The EPG tab displays a filterable grid containing all events, sorted
+based on start time.
+
+!['Electronic Program Guide' Tab](docresources/epg.png)
+
+---
+
+###Menu Bar/Buttons
+
+The following functions are available:
+
+####Filtering (or searching)
+
+In the EPG top tool bar you can access five input fields. These are used
+to filter/search for events. The form uses implicit AND between the
+input fields. This means that all filters must match for an event to be
+displayed.
+
+Filter                     | Function
+---------------------------| --------
+**Search title...**        | Only display events that match the given title.
+                           | The filter uses case-insensitive regular expressions. If you don’t know what a regular expression is, this simply means that you can type just parts of the title and filter on that - there’s no need for full, exact matching. If the fulltext checkbox is checked, the title text is matched against title, subtitle, summary and description.
+**Filter channel...**      | Only display events from the selected channel.
+                           | Channels in the drop down are ordered by name and can be filtered (by name) by typing in the box.
+**Filter tag...**          | Only display events from channels which are included in the selected tag.
+                           | Tags are used for grouping channels together - such as ‘Radio’ or ‘HDTV’ - and are configured by the administrator. You can start typing a tag name to filter the list.
+**Filter content type...** | Only display events that match the given content type tag.
+                           | Most DVB networks classify their events into content groups. This field allows you to filter based on content type (e.g. “Sports” or “Game Show”). Supported tags are determined by your broadcaster. Again, simply start typing to filter the entries if you have a long list to choose from.
+**Filter duration...**     | Only display events that fall between the given minimum and maximum durations.
+                           | This allows you to filter for or against, say, a daily broadcast and a weekly omnibus edition of a programme, or only look for short news bulletins and not the 24-hour rolling broadcasts.
+
+*Title*, *Channel*, *Tag* and *Content Type* are dependent on your configuration and on what your 
+broadcaster sends. Options for the *Duration* are as follows:
+
+Filter Range           | Example Purpose
+-----------------------|----------------
+00:00:01 to 00:15:00   | Very short news bulletins, children's programmes, etc.
+00:15:01 to 00:30:00   | Short programmes, e.g. daily soap operas
+00:30:01 to 01:30:00   | Medium-length programmes, e.g. documentaries
+01:30:01 to 03:00:00   | Longer programmes, e.g. films
+03:00:00 to no maximum | Very long programmes, e.g. major sporting events
+
+So, if you only want to see Movies from your available HD channels, you
+would select ‘HDTV’ in the *[Filter tag…]* field, and select ‘Movie /
+Drama’ in the *[Filter content type…]* field. If you wish, you could
+then further limit the search to programmes of between 90 minutes and 3
+hours by selecting ‘01:30:01 to 03:00:00’ in the *[Filter duration…]*
+field.
+
+Note that you don’t have to press a ‘Search’ button: the grid
+immediately updates itself as you change the filters.
+
+You can clear an individual filter by simply deleting its contents, or
+by selecting *‘(Clear filter)’* as appropriate on all except the title
+filter. If you want to clear all filters, just press the *[Reset All]*
+button.
+
+####Buttons
+
+The following buttons are also available:
+
+Button              | Function
+--------------------|----------
+**Reset All**       | Clears all search filters.
+**Watch TV**        | Launches Live TV via HTML5 video (see below).
+**Create Autorec**  | Creates an auto-recording rule based on the current filter criteria (see below).
+**Help**            | Display this help page.
+
+---
+
+###Grid Items
+
+The main grid items have the following functions:
+
+**Details**
+: Displays the current status of a recording event for this programme if 
+  one applies: 
+
+Icon                                                   | Description
+-------------------------------------------------------|-------------
+![Clock icon](icons/clock.png)                         | the programme is scheduled for recording
+![Recording icon](icons/rec.png)                       | the programme is currently recording
+![Broadcast details icon](icons/broadcast_details.png) | click to call up more detailed information about an event
+
+**Progress**
+: A bar graph display of how far through a programme we currently are.
+
+**Title**
+: The title of the programme. *You can automatically set a filter to the
+  value of this field by clicking on it (e.g. click on 'Daily News' will
+  automatically filter the whole grid to only show programmes with the same
+  name).*
+
+**SubTitle**
+: The subtitle of the programme, if gien by your EPG provider. Note that some
+  (notably, UK) providers use this for a programme synopsis instead of a true
+  subtitle.
+
+**Episode**
+: Episode number, if given by your EPG provider.
+
+**Start Time**
+: The scheduled start time of the programme.
+
+**End Time**
+: The scheduled end time of the programme.
+
+**Duration**
+: The scheduled duration (i.e. start time to end time) of the programme. 
+
+**Number**
+: The channel number of the broadcasting channel, if defined.
+
+**Channel**
+: The name of the broadcasting channel. *You can automatically set a filter to the
+  value of this field by clicking on it (e.g. click on 'Channel 4 HD' will
+  automatically filter the whole grid to only show programmes from that channel).*
+
+**Stars**
+: Rating (in stars) of the programme.
+
+**Age**
+: Age rating of the programme.
+
+**Content Type**
+: Any content/genre information as provided by the EPG provider. *You can
+  automatically set a filter to the value of this field by clicking on it
+  (e.g. click on 'Movie/Drama' will automatically filter the whole grid
+  to only show programmes of the same type).*
+
+---
+
+###Event details and recording
+
+If you click on a single event, a popup will display detailed
+information about the event. It also allows you to schedule the event
+for recording by clicking on the *[Record program]* button.
+
+For EPG providers that supply series link information there will also be
+a *[Record series]* button that will record all entries in the series.
+
+![EPG Detail 1](docresources/epg2.png)
+
+For events without any series link information, an *[Autorec]* button
+will be provided to create a pseudo-series link using the autorec
+feature.
+
+![EPG Detail 2](docresources/epg3.png)
+
+If you schedule any kind of recording from this point, you can choose a
+specific DVR profile that will apply to the recording or autorec rule.
+This will normally show as *(default)*, but you can define different
+profiles in the **Configuration -\> Recording -\> Digital Video
+Recorder** tab. This allows you to set, for example, more post-
+broadcast padding for a channel that always runs late, or perhaps define
+a different post-processing command to strip adverts out on a commercial
+channel.
+
+You will also see a *[Search IMDB]* button to look for the programme by name
+on imdb.com, and a *[Play program]* button to watch a programme that’s already
+in progress. This second button downloads a playlist file (XSPF or M3U
+depending on your startup options); if your system is configured for it,
+this will automatically launch an appropriate player, otherwise you will
+need to manually open the playlist to start watching (normally a
+double-click on the downloaded file).
+
+To close the popup, just click on the [X] window button. The popup isn’t
+modal, so you don’t have to close it before doing something else, and
+you can open as many detailed information popups as you want.
+
+---
+
+###Autorecordings
+
+Should you wish to record all events matching a specific query (to
+record your favourite show every week, for example) you can press the
+*[Create AutoRec]* button in the top toolbar.
+
+A popup with details about the to-be-created autorecording rule needs to
+be confirmed before the rule takes effect.
+
+![Autorec Dialogue Box](docresources/autorecpopup.png)
+
+You can change or delete the autorec rules in the **Digital Video
+Recorder** tab. Use that editor if you temporarily want to disable an
+autorecording or make adjustments to the channel, tag, or similar.
+
+---
+
+###Watch TV
+
+If you want to watch live TV in the web UI, the *[Watch TV]* button will
+pop up a HTML5 video player, where you can select the channel to watch and a
+stream profile to use. A transcoding stream profile is required to transcode 
+the stream to a format that is supported by your browser, as browsers only
+support certain formats and codecs.
+
+####Supported formats (containers)
+
+Browser | MPEG-TS | MPEG-PS | Matroska | WebM
+------- | :-----: | :-----: | :------: | :--:
+Google Chrome | ![no](icons/exclamation.png) | ![no](icons/exclamation.png) | ![yes](icons/accept.png) | ![yes](icons/accept.png)
+Mozilla Firefox | ![no](icons/exclamation.png) | ![no](icons/exclamation.png) |  | ![yes](icons/accept.png)
+
+####Supported video codecs
+
+Browser | MPEG2 Video | H.264 | VP8
+------- | :---------: | :---: | :-:
+Google Chrome | ![no](icons/exclamation.png) | ![yes](icons/accept.png) | ![yes](icons/accept.png)
+Mozilla Firefox | ![no](icons/exclamation.png) |  | ![yes](icons/accept.png)
+
+####Supported audio codecs
+
+Browser | MPEG2 Audio | Dolby Digital (AC3) | AAC | Vorbis
+------- | :---------: | :-----------------: | :-: | :----:
+Google Chrome | ![no](icons/exclamation.png) | ![no](icons/exclamation.png) | ![yes](icons/accept.png) | ![yes](icons/accept.png)
+Mozilla Firefox | ![no](icons/exclamation.png) | ![no](icons/exclamation.png) |  | ![yes](icons/accept.png)
index d2a58829956b5664f1000eb8cdb549423d8070c5..4c4b14dfc1362832bbf870f5cbc73ebd64fecdea 100644 (file)
@@ -144,7 +144,7 @@ http_markdown_class(http_connection_t *hc, const char *clazz)
   pthread_mutex_unlock(&global_lock);
   s = htsmsg_get_str(m, "caption");
   if (s) {
-    md_header(hq, "##", s);
+    md_header(hq, "## ", s);
     nl = md_nl(hq, 1);
   }
   if (doc) {
@@ -165,7 +165,7 @@ http_markdown_class(http_connection_t *hc, const char *clazz)
     if (!s) continue;
     if (first) {
       nl = md_nl(hq, nl);
-      htsbuf_append_str(hq, "####");
+      htsbuf_append_str(hq, "#### ");
       htsbuf_append_str(hq, tvh_gettext_lang(lang, N_("Items")));
       md_nl(hq, 1);
       md_nl(hq, 1);
index d2da5c559d19df974daa51c3bece55c75f9a3af8..60757dccc594ed987f73d62f911e6b31c54b1bf2 100644 (file)
@@ -102,7 +102,7 @@ tvheadend.passwdeditor = function(panel, index)
         del: true,
         list: list,
         help: function() {
-            new tvheadend.help(_('Password Control Entries'), 'config_passwords.html');
+            new tvheadend.mdhelp('class/passwd');
         }
     });
 };
@@ -141,7 +141,7 @@ tvheadend.ipblockeditor = function(panel, index)
         del: true,
         list: list,
         help: function() {
-            new tvheadend.help(_('IP Blocking Entries'), 'config_ipblock.html');
+            new tvheadend.mdhelp('class/ipblocking');
         }
     });
 };
index 9b3aa702cc5bd87027df288d03d451da229561a1..b66a6d94e64cc0ab294c9ef8c1d7d745157f91ef 100644 (file)
@@ -910,7 +910,7 @@ tvheadend.epg = function() {
             text: _('Help'),
             iconCls: 'help',
             handler: function() {
-                new tvheadend.help(_('Electronic Program Guide'), 'epg.html');
+                new tvheadend.mdhelp('epg');
             }
         }
     ];
index f119dd8cbb99d64f299baf7a0c6084fc9a3f8825..f9462b1edd9cd6afe4e070e0211f7631dc95cfb2 100644 (file)
     list-style: disc outside;
 }
 
+.x-wizard-description strong {
+    font-weight: bold;
+}
+
 /* Table styles for webUI help */
 
 .hts-doc-text table, .hts-doc-text th, .hts-doc-text td {
 
 .hts-doc-toc {
     width: 200px;
-    padding: 5px 5px 5px 5px;
+    padding: 0;
+    margin: 0 5px 0 5px;
     float: right;
+    border-left: dotted 1px;
+    border-bottom: dotted 1px;
 }
 
 .hts-doc-toc a {
     cursor: pointer;
 }
 
+.hts-doc-toc a:hover {
+    cursor: pointer;
+    background-color: black;
+    color: white;
+}
+
 .hts-doc-toc p {
     margin: 3px 0 1px 5px;
 }
     padding-bottom: 5px;
 }
 
+.hts-doc-toc ol {
+    list-style-type: arabic;
+    list-style-position: outside;
+    padding-left: 2em;
+}
+
+.hts-doc-toc ol li {
+    padding-top: 0px;
+    padding-bottom: 0px
+}
+
 .hts-doc-toc ul {
     list-style-type: disc;
+    list-style-position: outside;
     padding-left: 2em;
 }
 
 .hts-doc-toc ul ul {
     list-style-type: circle;
+    list-style-position: outside;
     padding-left: 1em;
 }
 
 .hts-doc-toc ul ul ul {
     list-style-type: square;
+    list-style-position: outside;
     padding-left: 1em;
 }
 
 
 .hts-doc-text img {
   margin: 0px auto;
-  display: block;
+  /* display: block; */
   border: none;
   padding: 10px;
   max-width: 97%
diff --git a/src/webui/static/app/marked.js b/src/webui/static/app/marked.js
new file mode 100644 (file)
index 0000000..03251f3
--- /dev/null
@@ -0,0 +1,1285 @@
+/**
+ * marked - a markdown parser
+ * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed)
+ * https://github.com/chjj/marked
+ */
+
+;(function() {
+
+/**
+ * Block-Level Grammar
+ */
+
+var block = {
+  newline: /^\n+/,
+  code: /^( {4}[^\n]+\n*)+/,
+  fences: noop,
+  hr: /^( *[-*_]){3,} *(?:\n+|$)/,
+  heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
+  nptable: noop,
+  lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,
+  blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,
+  list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
+  html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,
+  def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
+  table: noop,
+  paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,
+  text: /^[^\n]+/
+};
+
+block.bullet = /(?:[*+-]|\d+\.)/;
+block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;
+block.item = replace(block.item, 'gm')
+  (/bull/g, block.bullet)
+  ();
+
+block.list = replace(block.list)
+  (/bull/g, block.bullet)
+  ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))')
+  ('def', '\\n+(?=' + block.def.source + ')')
+  ();
+
+block.blockquote = replace(block.blockquote)
+  ('def', block.def)
+  ();
+
+block._tag = '(?!(?:'
+  + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code'
+  + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo'
+  + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b';
+
+block.html = replace(block.html)
+  ('comment', /<!--[\s\S]*?-->/)
+  ('closed', /<(tag)[\s\S]+?<\/\1>/)
+  ('closing', /<tag(?:"[^"]*"|'[^']*'|[^'">])*?>/)
+  (/tag/g, block._tag)
+  ();
+
+block.paragraph = replace(block.paragraph)
+  ('hr', block.hr)
+  ('heading', block.heading)
+  ('lheading', block.lheading)
+  ('blockquote', block.blockquote)
+  ('tag', '<' + block._tag)
+  ('def', block.def)
+  ();
+
+/**
+ * Normal Block Grammar
+ */
+
+block.normal = merge({}, block);
+
+/**
+ * GFM Block Grammar
+ */
+
+block.gfm = merge({}, block.normal, {
+  fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,
+  paragraph: /^/,
+  heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/
+});
+
+block.gfm.paragraph = replace(block.paragraph)
+  ('(?!', '(?!'
+    + block.gfm.fences.source.replace('\\1', '\\2') + '|'
+    + block.list.source.replace('\\1', '\\3') + '|')
+  ();
+
+/**
+ * GFM + Tables Block Grammar
+ */
+
+block.tables = merge({}, block.gfm, {
+  nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,
+  table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/
+});
+
+/**
+ * Block Lexer
+ */
+
+function Lexer(options) {
+  this.tokens = [];
+  this.tokens.links = {};
+  this.options = options || marked.defaults;
+  this.rules = block.normal;
+
+  if (this.options.gfm) {
+    if (this.options.tables) {
+      this.rules = block.tables;
+    } else {
+      this.rules = block.gfm;
+    }
+  }
+}
+
+/**
+ * Expose Block Rules
+ */
+
+Lexer.rules = block;
+
+/**
+ * Static Lex Method
+ */
+
+Lexer.lex = function(src, options) {
+  var lexer = new Lexer(options);
+  return lexer.lex(src);
+};
+
+/**
+ * Preprocessing
+ */
+
+Lexer.prototype.lex = function(src) {
+  src = src
+    .replace(/\r\n|\r/g, '\n')
+    .replace(/\t/g, '    ')
+    .replace(/\u00a0/g, ' ')
+    .replace(/\u2424/g, '\n');
+
+  return this.token(src, true);
+};
+
+/**
+ * Lexing
+ */
+
+Lexer.prototype.token = function(src, top, bq) {
+  var src = src.replace(/^ +$/gm, '')
+    , next
+    , loose
+    , cap
+    , bull
+    , b
+    , item
+    , space
+    , i
+    , l;
+
+  while (src) {
+    // newline
+    if (cap = this.rules.newline.exec(src)) {
+      src = src.substring(cap[0].length);
+      if (cap[0].length > 1) {
+        this.tokens.push({
+          type: 'space'
+        });
+      }
+    }
+
+    // code
+    if (cap = this.rules.code.exec(src)) {
+      src = src.substring(cap[0].length);
+      cap = cap[0].replace(/^ {4}/gm, '');
+      this.tokens.push({
+        type: 'code',
+        text: !this.options.pedantic
+          ? cap.replace(/\n+$/, '')
+          : cap
+      });
+      continue;
+    }
+
+    // fences (gfm)
+    if (cap = this.rules.fences.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'code',
+        lang: cap[2],
+        text: cap[3] || ''
+      });
+      continue;
+    }
+
+    // heading
+    if (cap = this.rules.heading.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'heading',
+        depth: cap[1].length,
+        text: cap[2]
+      });
+      continue;
+    }
+
+    // table no leading pipe (gfm)
+    if (top && (cap = this.rules.nptable.exec(src))) {
+      src = src.substring(cap[0].length);
+
+      item = {
+        type: 'table',
+        header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+        align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+        cells: cap[3].replace(/\n$/, '').split('\n')
+      };
+
+      for (i = 0; i < item.align.length; i++) {
+        if (/^ *-+: *$/.test(item.align[i])) {
+          item.align[i] = 'right';
+        } else if (/^ *:-+: *$/.test(item.align[i])) {
+          item.align[i] = 'center';
+        } else if (/^ *:-+ *$/.test(item.align[i])) {
+          item.align[i] = 'left';
+        } else {
+          item.align[i] = null;
+        }
+      }
+
+      for (i = 0; i < item.cells.length; i++) {
+        item.cells[i] = item.cells[i].split(/ *\| */);
+      }
+
+      this.tokens.push(item);
+
+      continue;
+    }
+
+    // lheading
+    if (cap = this.rules.lheading.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'heading',
+        depth: cap[2] === '=' ? 1 : 2,
+        text: cap[1]
+      });
+      continue;
+    }
+
+    // hr
+    if (cap = this.rules.hr.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'hr'
+      });
+      continue;
+    }
+
+    // blockquote
+    if (cap = this.rules.blockquote.exec(src)) {
+      src = src.substring(cap[0].length);
+
+      this.tokens.push({
+        type: 'blockquote_start'
+      });
+
+      cap = cap[0].replace(/^ *> ?/gm, '');
+
+      // Pass `top` to keep the current
+      // "toplevel" state. This is exactly
+      // how markdown.pl works.
+      this.token(cap, top, true);
+
+      this.tokens.push({
+        type: 'blockquote_end'
+      });
+
+      continue;
+    }
+
+    // list
+    if (cap = this.rules.list.exec(src)) {
+      src = src.substring(cap[0].length);
+      bull = cap[2];
+
+      this.tokens.push({
+        type: 'list_start',
+        ordered: bull.length > 1
+      });
+
+      // Get each top-level item.
+      cap = cap[0].match(this.rules.item);
+
+      next = false;
+      l = cap.length;
+      i = 0;
+
+      for (; i < l; i++) {
+        item = cap[i];
+
+        // Remove the list item's bullet
+        // so it is seen as the next token.
+        space = item.length;
+        item = item.replace(/^ *([*+-]|\d+\.) +/, '');
+
+        // Outdent whatever the
+        // list item contains. Hacky.
+        if (~item.indexOf('\n ')) {
+          space -= item.length;
+          item = !this.options.pedantic
+            ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
+            : item.replace(/^ {1,4}/gm, '');
+        }
+
+        // Determine whether the next list item belongs here.
+        // Backpedal if it does not belong in this list.
+        if (this.options.smartLists && i !== l - 1) {
+          b = block.bullet.exec(cap[i + 1])[0];
+          if (bull !== b && !(bull.length > 1 && b.length > 1)) {
+            src = cap.slice(i + 1).join('\n') + src;
+            i = l - 1;
+          }
+        }
+
+        // Determine whether item is loose or not.
+        // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
+        // for discount behavior.
+        loose = next || /\n\n(?!\s*$)/.test(item);
+        if (i !== l - 1) {
+          next = item.charAt(item.length - 1) === '\n';
+          if (!loose) loose = next;
+        }
+
+        this.tokens.push({
+          type: loose
+            ? 'loose_item_start'
+            : 'list_item_start'
+        });
+
+        // Recurse.
+        this.token(item, false, bq);
+
+        this.tokens.push({
+          type: 'list_item_end'
+        });
+      }
+
+      this.tokens.push({
+        type: 'list_end'
+      });
+
+      continue;
+    }
+
+    // html
+    if (cap = this.rules.html.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: this.options.sanitize
+          ? 'paragraph'
+          : 'html',
+        pre: !this.options.sanitizer
+          && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
+        text: cap[0]
+      });
+      continue;
+    }
+
+    // def
+    if ((!bq && top) && (cap = this.rules.def.exec(src))) {
+      src = src.substring(cap[0].length);
+      this.tokens.links[cap[1].toLowerCase()] = {
+        href: cap[2],
+        title: cap[3]
+      };
+      continue;
+    }
+
+    // table (gfm)
+    if (top && (cap = this.rules.table.exec(src))) {
+      src = src.substring(cap[0].length);
+
+      item = {
+        type: 'table',
+        header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+        align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+        cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n')
+      };
+
+      for (i = 0; i < item.align.length; i++) {
+        if (/^ *-+: *$/.test(item.align[i])) {
+          item.align[i] = 'right';
+        } else if (/^ *:-+: *$/.test(item.align[i])) {
+          item.align[i] = 'center';
+        } else if (/^ *:-+ *$/.test(item.align[i])) {
+          item.align[i] = 'left';
+        } else {
+          item.align[i] = null;
+        }
+      }
+
+      for (i = 0; i < item.cells.length; i++) {
+        item.cells[i] = item.cells[i]
+          .replace(/^ *\| *| *\| *$/g, '')
+          .split(/ *\| */);
+      }
+
+      this.tokens.push(item);
+
+      continue;
+    }
+
+    // top-level paragraph
+    if (top && (cap = this.rules.paragraph.exec(src))) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'paragraph',
+        text: cap[1].charAt(cap[1].length - 1) === '\n'
+          ? cap[1].slice(0, -1)
+          : cap[1]
+      });
+      continue;
+    }
+
+    // text
+    if (cap = this.rules.text.exec(src)) {
+      // Top-level should never reach here.
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'text',
+        text: cap[0]
+      });
+      continue;
+    }
+
+    if (src) {
+      throw new
+        Error('Infinite loop on byte: ' + src.charCodeAt(0));
+    }
+  }
+
+  return this.tokens;
+};
+
+/**
+ * Inline-Level Grammar
+ */
+
+var inline = {
+  escape: /^\\([\\`*{}\[\]()#+\-.!_>])/,
+  autolink: /^<([^ >]+(@|:\/)[^ >]+)>/,
+  url: noop,
+  tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,
+  link: /^!?\[(inside)\]\(href\)/,
+  reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/,
+  nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,
+  strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,
+  em: /^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
+  code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,
+  br: /^ {2,}\n(?!\s*$)/,
+  del: noop,
+  text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/
+};
+
+inline._inside = /(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/;
+inline._href = /\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/;
+
+inline.link = replace(inline.link)
+  ('inside', inline._inside)
+  ('href', inline._href)
+  ();
+
+inline.reflink = replace(inline.reflink)
+  ('inside', inline._inside)
+  ();
+
+/**
+ * Normal Inline Grammar
+ */
+
+inline.normal = merge({}, inline);
+
+/**
+ * Pedantic Inline Grammar
+ */
+
+inline.pedantic = merge({}, inline.normal, {
+  strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,
+  em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/
+});
+
+/**
+ * GFM Inline Grammar
+ */
+
+inline.gfm = merge({}, inline.normal, {
+  escape: replace(inline.escape)('])', '~|])')(),
+  url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,
+  del: /^~~(?=\S)([\s\S]*?\S)~~/,
+  text: replace(inline.text)
+    (']|', '~]|')
+    ('|', '|https?://|')
+    ()
+});
+
+/**
+ * GFM + Line Breaks Inline Grammar
+ */
+
+inline.breaks = merge({}, inline.gfm, {
+  br: replace(inline.br)('{2,}', '*')(),
+  text: replace(inline.gfm.text)('{2,}', '*')()
+});
+
+/**
+ * Inline Lexer & Compiler
+ */
+
+function InlineLexer(links, options) {
+  this.options = options || marked.defaults;
+  this.links = links;
+  this.rules = inline.normal;
+  this.renderer = this.options.renderer || new Renderer;
+  this.renderer.options = this.options;
+
+  if (!this.links) {
+    throw new
+      Error('Tokens array requires a `links` property.');
+  }
+
+  if (this.options.gfm) {
+    if (this.options.breaks) {
+      this.rules = inline.breaks;
+    } else {
+      this.rules = inline.gfm;
+    }
+  } else if (this.options.pedantic) {
+    this.rules = inline.pedantic;
+  }
+}
+
+/**
+ * Expose Inline Rules
+ */
+
+InlineLexer.rules = inline;
+
+/**
+ * Static Lexing/Compiling Method
+ */
+
+InlineLexer.output = function(src, links, options) {
+  var inline = new InlineLexer(links, options);
+  return inline.output(src);
+};
+
+/**
+ * Lexing/Compiling
+ */
+
+InlineLexer.prototype.output = function(src) {
+  var out = ''
+    , link
+    , text
+    , href
+    , cap;
+
+  while (src) {
+    // escape
+    if (cap = this.rules.escape.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += cap[1];
+      continue;
+    }
+
+    // autolink
+    if (cap = this.rules.autolink.exec(src)) {
+      src = src.substring(cap[0].length);
+      if (cap[2] === '@') {
+        text = cap[1].charAt(6) === ':'
+          ? this.mangle(cap[1].substring(7))
+          : this.mangle(cap[1]);
+        href = this.mangle('mailto:') + text;
+      } else {
+        text = escape(cap[1]);
+        href = text;
+      }
+      out += this.renderer.link(href, null, text);
+      continue;
+    }
+
+    // url (gfm)
+    if (!this.inLink && (cap = this.rules.url.exec(src))) {
+      src = src.substring(cap[0].length);
+      text = escape(cap[1]);
+      href = text;
+      out += this.renderer.link(href, null, text);
+      continue;
+    }
+
+    // tag
+    if (cap = this.rules.tag.exec(src)) {
+      if (!this.inLink && /^<a /i.test(cap[0])) {
+        this.inLink = true;
+      } else if (this.inLink && /^<\/a>/i.test(cap[0])) {
+        this.inLink = false;
+      }
+      src = src.substring(cap[0].length);
+      out += this.options.sanitize
+        ? this.options.sanitizer
+          ? this.options.sanitizer(cap[0])
+          : escape(cap[0])
+        : cap[0]
+      continue;
+    }
+
+    // link
+    if (cap = this.rules.link.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.inLink = true;
+      out += this.outputLink(cap, {
+        href: cap[2],
+        title: cap[3]
+      });
+      this.inLink = false;
+      continue;
+    }
+
+    // reflink, nolink
+    if ((cap = this.rules.reflink.exec(src))
+        || (cap = this.rules.nolink.exec(src))) {
+      src = src.substring(cap[0].length);
+      link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
+      link = this.links[link.toLowerCase()];
+      if (!link || !link.href) {
+        out += cap[0].charAt(0);
+        src = cap[0].substring(1) + src;
+        continue;
+      }
+      this.inLink = true;
+      out += this.outputLink(cap, link);
+      this.inLink = false;
+      continue;
+    }
+
+    // strong
+    if (cap = this.rules.strong.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.renderer.strong(this.output(cap[2] || cap[1]));
+      continue;
+    }
+
+    // em
+    if (cap = this.rules.em.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.renderer.em(this.output(cap[2] || cap[1]));
+      continue;
+    }
+
+    // code
+    if (cap = this.rules.code.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.renderer.codespan(escape(cap[2], true));
+      continue;
+    }
+
+    // br
+    if (cap = this.rules.br.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.renderer.br();
+      continue;
+    }
+
+    // del (gfm)
+    if (cap = this.rules.del.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.renderer.del(this.output(cap[1]));
+      continue;
+    }
+
+    // text
+    if (cap = this.rules.text.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.renderer.text(escape(this.smartypants(cap[0])));
+      continue;
+    }
+
+    if (src) {
+      throw new
+        Error('Infinite loop on byte: ' + src.charCodeAt(0));
+    }
+  }
+
+  return out;
+};
+
+/**
+ * Compile Link
+ */
+
+InlineLexer.prototype.outputLink = function(cap, link) {
+  var href = escape(link.href)
+    , title = link.title ? escape(link.title) : null;
+
+  return cap[0].charAt(0) !== '!'
+    ? this.renderer.link(href, title, this.output(cap[1]))
+    : this.renderer.image(href, title, escape(cap[1]));
+};
+
+/**
+ * Smartypants Transformations
+ */
+
+InlineLexer.prototype.smartypants = function(text) {
+  if (!this.options.smartypants) return text;
+  return text
+    // em-dashes
+    .replace(/---/g, '\u2014')
+    // en-dashes
+    .replace(/--/g, '\u2013')
+    // opening singles
+    .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018')
+    // closing singles & apostrophes
+    .replace(/'/g, '\u2019')
+    // opening doubles
+    .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c')
+    // closing doubles
+    .replace(/"/g, '\u201d')
+    // ellipses
+    .replace(/\.{3}/g, '\u2026');
+};
+
+/**
+ * Mangle Links
+ */
+
+InlineLexer.prototype.mangle = function(text) {
+  if (!this.options.mangle) return text;
+  var out = ''
+    , l = text.length
+    , i = 0
+    , ch;
+
+  for (; i < l; i++) {
+    ch = text.charCodeAt(i);
+    if (Math.random() > 0.5) {
+      ch = 'x' + ch.toString(16);
+    }
+    out += '&#' + ch + ';';
+  }
+
+  return out;
+};
+
+/**
+ * Renderer
+ */
+
+function Renderer(options) {
+  this.options = options || {};
+}
+
+Renderer.prototype.code = function(code, lang, escaped) {
+  if (this.options.highlight) {
+    var out = this.options.highlight(code, lang);
+    if (out != null && out !== code) {
+      escaped = true;
+      code = out;
+    }
+  }
+
+  if (!lang) {
+    return '<pre><code>'
+      + (escaped ? code : escape(code, true))
+      + '\n</code></pre>';
+  }
+
+  return '<pre><code class="'
+    + this.options.langPrefix
+    + escape(lang, true)
+    + '">'
+    + (escaped ? code : escape(code, true))
+    + '\n</code></pre>\n';
+};
+
+Renderer.prototype.blockquote = function(quote) {
+  return '<blockquote>\n' + quote + '</blockquote>\n';
+};
+
+Renderer.prototype.html = function(html) {
+  return html;
+};
+
+Renderer.prototype.heading = function(text, level, raw) {
+  return '<h'
+    + level
+    + ' id="'
+    + this.options.headerPrefix
+    + raw.toLowerCase().replace(/[^\w]+/g, '-')
+    + '">'
+    + text
+    + '</h'
+    + level
+    + '>\n';
+};
+
+Renderer.prototype.hr = function() {
+  return this.options.xhtml ? '<hr/>\n' : '<hr>\n';
+};
+
+Renderer.prototype.list = function(body, ordered) {
+  var type = ordered ? 'ol' : 'ul';
+  return '<' + type + '>\n' + body + '</' + type + '>\n';
+};
+
+Renderer.prototype.listitem = function(text) {
+  return '<li>' + text + '</li>\n';
+};
+
+Renderer.prototype.paragraph = function(text) {
+  return '<p>' + text + '</p>\n';
+};
+
+Renderer.prototype.table = function(header, body) {
+  return '<table>\n'
+    + '<thead>\n'
+    + header
+    + '</thead>\n'
+    + '<tbody>\n'
+    + body
+    + '</tbody>\n'
+    + '</table>\n';
+};
+
+Renderer.prototype.tablerow = function(content) {
+  return '<tr>\n' + content + '</tr>\n';
+};
+
+Renderer.prototype.tablecell = function(content, flags) {
+  var type = flags.header ? 'th' : 'td';
+  var tag = flags.align
+    ? '<' + type + ' style="text-align:' + flags.align + '">'
+    : '<' + type + '>';
+  return tag + content + '</' + type + '>\n';
+};
+
+// span level renderer
+Renderer.prototype.strong = function(text) {
+  return '<strong>' + text + '</strong>';
+};
+
+Renderer.prototype.em = function(text) {
+  return '<em>' + text + '</em>';
+};
+
+Renderer.prototype.codespan = function(text) {
+  return '<code>' + text + '</code>';
+};
+
+Renderer.prototype.br = function() {
+  return this.options.xhtml ? '<br/>' : '<br>';
+};
+
+Renderer.prototype.del = function(text) {
+  return '<del>' + text + '</del>';
+};
+
+Renderer.prototype.link = function(href, title, text) {
+  if (this.options.sanitize) {
+    try {
+      var prot = decodeURIComponent(unescape(href))
+        .replace(/[^\w:]/g, '')
+        .toLowerCase();
+    } catch (e) {
+      return '';
+    }
+    if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0) {
+      return '';
+    }
+  }
+  var out = '<a href="' + href + '"';
+  if (title) {
+    out += ' title="' + title + '"';
+  }
+  out += '>' + text + '</a>';
+  return out;
+};
+
+Renderer.prototype.image = function(href, title, text) {
+  var out = '<img src="' + href + '" alt="' + text + '"';
+  if (title) {
+    out += ' title="' + title + '"';
+  }
+  out += this.options.xhtml ? '/>' : '>';
+  return out;
+};
+
+Renderer.prototype.text = function(text) {
+  return text;
+};
+
+/**
+ * Parsing & Compiling
+ */
+
+function Parser(options) {
+  this.tokens = [];
+  this.token = null;
+  this.options = options || marked.defaults;
+  this.options.renderer = this.options.renderer || new Renderer;
+  this.renderer = this.options.renderer;
+  this.renderer.options = this.options;
+}
+
+/**
+ * Static Parse Method
+ */
+
+Parser.parse = function(src, options, renderer) {
+  var parser = new Parser(options, renderer);
+  return parser.parse(src);
+};
+
+/**
+ * Parse Loop
+ */
+
+Parser.prototype.parse = function(src) {
+  this.inline = new InlineLexer(src.links, this.options, this.renderer);
+  this.tokens = src.reverse();
+
+  var out = '';
+  while (this.next()) {
+    out += this.tok();
+  }
+
+  return out;
+};
+
+/**
+ * Next Token
+ */
+
+Parser.prototype.next = function() {
+  return this.token = this.tokens.pop();
+};
+
+/**
+ * Preview Next Token
+ */
+
+Parser.prototype.peek = function() {
+  return this.tokens[this.tokens.length - 1] || 0;
+};
+
+/**
+ * Parse Text Tokens
+ */
+
+Parser.prototype.parseText = function() {
+  var body = this.token.text;
+
+  while (this.peek().type === 'text') {
+    body += '\n' + this.next().text;
+  }
+
+  return this.inline.output(body);
+};
+
+/**
+ * Parse Current Token
+ */
+
+Parser.prototype.tok = function() {
+  switch (this.token.type) {
+    case 'space': {
+      return '';
+    }
+    case 'hr': {
+      return this.renderer.hr();
+    }
+    case 'heading': {
+      return this.renderer.heading(
+        this.inline.output(this.token.text),
+        this.token.depth,
+        this.token.text);
+    }
+    case 'code': {
+      return this.renderer.code(this.token.text,
+        this.token.lang,
+        this.token.escaped);
+    }
+    case 'table': {
+      var header = ''
+        , body = ''
+        , i
+        , row
+        , cell
+        , flags
+        , j;
+
+      // header
+      cell = '';
+      for (i = 0; i < this.token.header.length; i++) {
+        flags = { header: true, align: this.token.align[i] };
+        cell += this.renderer.tablecell(
+          this.inline.output(this.token.header[i]),
+          { header: true, align: this.token.align[i] }
+        );
+      }
+      header += this.renderer.tablerow(cell);
+
+      for (i = 0; i < this.token.cells.length; i++) {
+        row = this.token.cells[i];
+
+        cell = '';
+        for (j = 0; j < row.length; j++) {
+          cell += this.renderer.tablecell(
+            this.inline.output(row[j]),
+            { header: false, align: this.token.align[j] }
+          );
+        }
+
+        body += this.renderer.tablerow(cell);
+      }
+      return this.renderer.table(header, body);
+    }
+    case 'blockquote_start': {
+      var body = '';
+
+      while (this.next().type !== 'blockquote_end') {
+        body += this.tok();
+      }
+
+      return this.renderer.blockquote(body);
+    }
+    case 'list_start': {
+      var body = ''
+        , ordered = this.token.ordered;
+
+      while (this.next().type !== 'list_end') {
+        body += this.tok();
+      }
+
+      return this.renderer.list(body, ordered);
+    }
+    case 'list_item_start': {
+      var body = '';
+
+      while (this.next().type !== 'list_item_end') {
+        body += this.token.type === 'text'
+          ? this.parseText()
+          : this.tok();
+      }
+
+      return this.renderer.listitem(body);
+    }
+    case 'loose_item_start': {
+      var body = '';
+
+      while (this.next().type !== 'list_item_end') {
+        body += this.tok();
+      }
+
+      return this.renderer.listitem(body);
+    }
+    case 'html': {
+      var html = !this.token.pre && !this.options.pedantic
+        ? this.inline.output(this.token.text)
+        : this.token.text;
+      return this.renderer.html(html);
+    }
+    case 'paragraph': {
+      return this.renderer.paragraph(this.inline.output(this.token.text));
+    }
+    case 'text': {
+      return this.renderer.paragraph(this.parseText());
+    }
+  }
+};
+
+/**
+ * Helpers
+ */
+
+function escape(html, encode) {
+  return html
+    .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;');
+}
+
+function unescape(html) {
+  return html.replace(/&([#\w]+);/g, function(_, n) {
+    n = n.toLowerCase();
+    if (n === 'colon') return ':';
+    if (n.charAt(0) === '#') {
+      return n.charAt(1) === 'x'
+        ? String.fromCharCode(parseInt(n.substring(2), 16))
+        : String.fromCharCode(+n.substring(1));
+    }
+    return '';
+  });
+}
+
+function replace(regex, opt) {
+  regex = regex.source;
+  opt = opt || '';
+  return function self(name, val) {
+    if (!name) return new RegExp(regex, opt);
+    val = val.source || val;
+    val = val.replace(/(^|[^\[])\^/g, '$1');
+    regex = regex.replace(name, val);
+    return self;
+  };
+}
+
+function noop() {}
+noop.exec = noop;
+
+function merge(obj) {
+  var i = 1
+    , target
+    , key;
+
+  for (; i < arguments.length; i++) {
+    target = arguments[i];
+    for (key in target) {
+      if (Object.prototype.hasOwnProperty.call(target, key)) {
+        obj[key] = target[key];
+      }
+    }
+  }
+
+  return obj;
+}
+
+
+/**
+ * Marked
+ */
+
+function marked(src, opt, callback) {
+  if (callback || typeof opt === 'function') {
+    if (!callback) {
+      callback = opt;
+      opt = null;
+    }
+
+    opt = merge({}, marked.defaults, opt || {});
+
+    var highlight = opt.highlight
+      , tokens
+      , pending
+      , i = 0;
+
+    try {
+      tokens = Lexer.lex(src, opt)
+    } catch (e) {
+      return callback(e);
+    }
+
+    pending = tokens.length;
+
+    var done = function(err) {
+      if (err) {
+        opt.highlight = highlight;
+        return callback(err);
+      }
+
+      var out;
+
+      try {
+        out = Parser.parse(tokens, opt);
+      } catch (e) {
+        err = e;
+      }
+
+      opt.highlight = highlight;
+
+      return err
+        ? callback(err)
+        : callback(null, out);
+    };
+
+    if (!highlight || highlight.length < 3) {
+      return done();
+    }
+
+    delete opt.highlight;
+
+    if (!pending) return done();
+
+    for (; i < tokens.length; i++) {
+      (function(token) {
+        if (token.type !== 'code') {
+          return --pending || done();
+        }
+        return highlight(token.text, token.lang, function(err, code) {
+          if (err) return done(err);
+          if (code == null || code === token.text) {
+            return --pending || done();
+          }
+          token.text = code;
+          token.escaped = true;
+          --pending || done();
+        });
+      })(tokens[i]);
+    }
+
+    return;
+  }
+  try {
+    if (opt) opt = merge({}, marked.defaults, opt);
+    return Parser.parse(Lexer.lex(src, opt), opt);
+  } catch (e) {
+    e.message += '\nPlease report this to https://github.com/chjj/marked.';
+    if ((opt || marked.defaults).silent) {
+      return '<p>An error occured:</p><pre>'
+        + escape(e.message + '', true)
+        + '</pre>';
+    }
+    throw e;
+  }
+}
+
+/**
+ * Options
+ */
+
+marked.options =
+marked.setOptions = function(opt) {
+  merge(marked.defaults, opt);
+  return marked;
+};
+
+marked.defaults = {
+  gfm: true,
+  tables: true,
+  breaks: false,
+  pedantic: false,
+  sanitize: false,
+  sanitizer: null,
+  mangle: true,
+  smartLists: false,
+  silent: false,
+  highlight: null,
+  langPrefix: 'lang-',
+  smartypants: false,
+  headerPrefix: '',
+  renderer: new Renderer,
+  xhtml: false
+};
+
+/**
+ * Expose
+ */
+
+marked.Parser = Parser;
+marked.parser = Parser.parse;
+
+marked.Renderer = Renderer;
+
+marked.Lexer = Lexer;
+marked.lexer = Lexer.lex;
+
+marked.InlineLexer = InlineLexer;
+marked.inlineLexer = InlineLexer.output;
+
+marked.parse = marked;
+
+if (typeof module !== 'undefined' && typeof exports === 'object') {
+  module.exports = marked;
+} else if (typeof define === 'function' && define.amd) {
+  define(function() { return marked; });
+} else {
+  this.marked = marked;
+}
+
+}).call(function() {
+  return this || (typeof window !== 'undefined' ? window : global);
+}());
diff --git a/src/webui/static/app/micromarkdown.js b/src/webui/static/app/micromarkdown.js
deleted file mode 100644 (file)
index 92ef85f..0000000
+++ /dev/null
@@ -1,437 +0,0 @@
-/*
- * µmarkdown.js
- * markdown in under 5kb
- *
- * Copyright 2015, Simon Waldherr - http://simon.waldherr.eu/
- * Released under the MIT Licence
- * http://simon.waldherr.eu/license/mit/
- *
- * The MIT License (MIT)
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the »Software«), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included
- * in all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED »AS IS«, WITHOUT WARRANTY OF ANY KIND, EXPRESS
- * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- * IN THE SOFTWARE.
- *
- * Github:  https://github.com/simonwaldherr/micromarkdown.js/
- * Version: 0.3.4
- */
-
-/*jslint browser: true, node: true, plusplus: true, indent: 2, regexp: true, ass: true */
-/*global ActiveXObject, define */
-
-var micromarkdown = {
-  useajax: false,
-  regexobject: {
-    headline: /^(\#{1,6})([^\#\n]+)$/m,
-    code: /\s\`\`\`\n?([^`]+)\`\`\`/g,
-    hr: /^(?:([\*\-_] ?)+)\1\1$/gm,
-    lists: /^((\s*((\*|\-)|\d(\.|\))) [^\n]+)\n)+/gm,
-    bolditalic: /(?:([\*_~]{1,3}))([^\*_~\n]+[^\*_~\s])\1/g,
-    links: /!?\[([^\]<>]+)\]\(([^ \)<>]+)( "[^\(\)\"]+")?\)/g,
-    reflinks: /\[([^\]]+)\]\[([^\]]+)\]/g,
-    smlinks: /\@([a-z0-9]{3,})\@(t|gh|fb|gp|adn)/gi,
-    mail: /<(([a-z0-9_\-\.])+\@([a-z0-9_\-\.])+\.([a-z]{2,7}))>/gmi,
-    tables: /\n(([^|\n]+ *\| *)+([^|\n]+\n))((:?\-+:?\|)+(:?\-+:?)*\n)((([^|\n]+ *\| *)+([^|\n]+)\n)+)/g,
-    include: /[\[<]include (\S+) from (https?:\/\/[a-z0-9\.\-]+\.[a-z]{2,9}[a-z0-9\.\-\?\&\/]+)[\]>]/gi,
-    url: /<([a-zA-Z0-9@:%_\+.~#?&\/=]{2,256}\.[a-z]{2,4}\b(\/[\-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)?)>/g,
-    url2: /[ \t\n]([a-zA-Z]{2,16}:\/\/[a-zA-Z0-9@:%_\+.~#?&=]{2,256}.[a-z]{2,4}\b(\/[\-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)?)[ \t\n]/g
-  },
-  codeblocks: {},
-  parse: function (str, strict, linkfcn) {
-    'use strict';
-    var line, nstatus = 0,
-      status, cel, calign, indent, helper, helper1, helper2, count, repstr, stra, trashgc = [],
-      casca = 0,
-      i = 0,
-      j = 0,
-      crc32str = '';
-    str = '\n' + str + '\n';
-
-    if (strict !== true) {
-      micromarkdown.regexobject.lists = /^((\s*(\*|\d\.) [^\n]+)\n)+/gm;
-    }
-
-    str = str.replace('$&', '&#x0024&amp;');
-
-    /* code */
-    while ((stra = micromarkdown.regexobject.code.exec(str)) !== null) {
-      crc32str = micromarkdown.crc32(stra[0]);
-      micromarkdown.codeblocks[crc32str] = '<code>\n' + micromarkdown.htmlEncode(stra[1]).replace(/\n/gm, '<br/>').replace(/\ /gm, '&nbsp;') + '</code>\n';
-      str = str.replace(stra[0], ' §§§' + crc32str + '§§§ '); 
-    }
-
-    /* headlines */
-    while ((stra = micromarkdown.regexobject.headline.exec(str)) !== null) {
-      count = stra[1].length;
-      str = str.replace(stra[0], '<h' + count + '>' + stra[2] + '</h' + count + '>' + '\n');
-    }
-
-    /* lists */
-    while ((stra = micromarkdown.regexobject.lists.exec(str)) !== null) {
-      casca = 0;
-      if ((stra[0].trim().substr(0, 1) === '*') || (stra[0].trim().substr(0, 1) === '-')) {
-        repstr = '<ul>';
-      } else {
-        repstr = '<ol>';
-      }
-      helper = stra[0].split('\n');
-      helper1 = [];
-      status = 0;
-      indent = false;
-      for (i = 0; i < helper.length; i++) {
-        if ((line = /^((\s*)((\*|\-)|\d(\.|\))) ([^\n]+))/.exec(helper[i])) !== null) {
-          if ((line[2] === undefined) || (line[2].length === 0)) {
-            nstatus = 0;
-          } else {
-            if (indent === false) {
-              indent = line[2].replace(/\t/, '    ').length;
-            }
-            nstatus = Math.round(line[2].replace(/\t/, '    ').length / indent);
-          }
-          while (status > nstatus) {
-            repstr += helper1.pop();
-            status--;
-            casca--;
-          }
-          while (status < nstatus) {
-            if ((line[0].trim().substr(0, 1) === '*') || (line[0].trim().substr(0, 1) === '-')) {
-              repstr += '<ul>';
-              helper1.push('</ul>');
-            } else {
-              repstr += '<ol>';
-              helper1.push('</ol>');
-            }
-            status++;
-            casca++;
-          }
-          repstr += '<li>' + line[6] + '</li>' + '\n';
-        }
-      }
-      while (casca > 0) {
-        repstr += '</ul>';
-        casca--;
-      }
-      if ((stra[0].trim().substr(0, 1) === '*') || (stra[0].trim().substr(0, 1) === '-')) {
-        repstr += '</ul>';
-      } else {
-        repstr += '</ol>';
-      }
-      str = str.replace(stra[0], repstr + '\n');
-    }
-
-    /* tables */
-    while ((stra = micromarkdown.regexobject.tables.exec(str)) !== null) {
-      repstr = '<table><tr>';
-      helper = stra[1].split('|');
-      calign = stra[4].split('|');
-      for (i = 0; i < helper.length; i++) {
-        if (calign.length <= i) {
-          calign.push(0);
-        } else if ((calign[i].trimRight().slice(-1) === ':') && (strict !== true)) {
-          if (calign[i][0] === ':') {
-            calign[i] = 3;
-          } else {
-            calign[i] = 2;
-          }
-        } else if (strict !== true) {
-          if (calign[i][0] === ':') {
-            calign[i] = 1;
-          } else {
-            calign[i] = 0;
-          }
-        } else {
-          calign[i] = 0;
-        }
-      }
-      cel = ['<th>', '<th align="left">', '<th align="right">', '<th align="center">'];
-      for (i = 0; i < helper.length; i++) {
-        repstr += cel[calign[i]] + helper[i].trim() + '</th>';
-      }
-      repstr += '</tr>';
-      cel = ['<td>', '<td align="left">', '<td align="right">', '<td align="center">'];
-      helper1 = stra[7].split('\n');
-      for (i = 0; i < helper1.length; i++) {
-        helper2 = helper1[i].split('|');
-        if (helper2[0].length !== 0) {
-          while (calign.length < helper2.length) {
-            calign.push(0);
-          }
-          repstr += '<tr>';
-          for (j = 0; j < helper2.length; j++) {
-            repstr += cel[calign[j]] + helper2[j].trim() + '</td>';
-          }
-          repstr += '</tr>' + '\n';
-        }
-      }
-      repstr += '</table>';
-      str = str.replace(stra[0], repstr);
-    }
-
-    /* bold and italic */
-    for (i = 0; i < 3; i++) {
-      while ((stra = micromarkdown.regexobject.bolditalic.exec(str)) !== null) {
-        repstr = [];
-        if (stra[1] === '~~') {
-          str = str.replace(stra[0], '<del>' + stra[2] + '</del>');
-        } else {
-          switch (stra[1].length) {
-          case 1:
-            repstr = ['<i>', '</i>'];
-            break;
-          case 2:
-            repstr = ['<b>', '</b>'];
-            break;
-          case 3:
-            repstr = ['<i><b>', '</b></i>'];
-            break;
-          }
-          str = str.replace(stra[0], repstr[0] + stra[2] + repstr[1]);
-        }
-      }
-    }
-
-    /* links */
-    while ((stra = micromarkdown.regexobject.links.exec(str)) !== null) {
-      if (stra[0].substr(0, 1) === '!') {
-        var src = stra[2];
-        if (linkfcn && src.indexOf('://') === -1)
-          src = linkfcn('src', src);
-        str = str.replace(stra[0], '<img src="' + src + '" alt="' + stra[1] + '" title="' + stra[1] + '" />\n');
-      } else {
-        var href = stra[2];
-        if (linkfcn && href.indexOf('://') === -1)
-          href = linkfcn('href', href);
-        else if (href)
-          href = 'href="' + href + '"'
-        str = str.replace(stra[0], '<a ' + micromarkdown.mmdCSSclass(stra[2], strict) + href + '>' + stra[1] + '</a>\n');
-      }
-    }
-    while ((stra = micromarkdown.regexobject.mail.exec(str)) !== null) {
-      str = str.replace(stra[0], '<a href="mailto:' + stra[1] + '">' + stra[1] + '</a>');
-    }
-    while ((stra = micromarkdown.regexobject.url.exec(str)) !== null) {
-      repstr = stra[1];
-      if (repstr.indexOf('://') === -1) {
-        repstr = 'http://' + repstr;
-      }
-      str = str.replace(stra[0], '<a ' + micromarkdown.mmdCSSclass(repstr, strict) + 'href="' + repstr + '">' + repstr.replace(/(https:\/\/|http:\/\/|mailto:|ftp:\/\/)/gmi, '') + '</a>');
-    }
-    while ((stra = micromarkdown.regexobject.reflinks.exec(str)) !== null) {
-      helper1 = new RegExp('\\[' + stra[2] + '\\]: ?([^ \n]+)', "gi");
-      if ((helper = helper1.exec(str)) !== null) {
-        str = str.replace(stra[0], '<a ' + micromarkdown.mmdCSSclass(helper[1], strict) + 'href="' + helper[1] + '">' + stra[1] + '</a>');
-        trashgc.push(helper[0]);
-      }
-    }
-    for (i = 0; i < trashgc.length; i++) {
-      str = str.replace(trashgc[i], '');
-    }
-    while ((stra = micromarkdown.regexobject.smlinks.exec(str)) !== null) {
-      switch (stra[2]) {
-      case 't':
-        repstr = 'https://twitter.com/' + stra[1];
-        break;
-      case 'gh':
-        repstr = 'https://github.com/' + stra[1];
-        break;
-      case 'fb':
-        repstr = 'https://www.facebook.com/' + stra[1];
-        break;
-      case 'gp':
-        repstr = 'https://plus.google.com/+' + stra[1];
-        break;
-      case 'adn':
-        repstr = 'https://alpha.app.net/' + stra[1];
-        break;
-      }
-      str = str.replace(stra[0], '<a ' + micromarkdown.mmdCSSclass(repstr, strict) + 'href="' + repstr + '">' + stra[1] + '</a>');
-    }
-    while ((stra = micromarkdown.regexobject.url2.exec(str)) !== null) {
-      repstr = stra[1];
-      str = str.replace(stra[0], '<a ' + micromarkdown.mmdCSSclass(repstr, strict) + 'href="' + repstr + '">' + repstr + '</a>');
-    }
-
-    /* horizontal line */
-    while ((stra = micromarkdown.regexobject.hr.exec(str)) !== null) {
-      str = str.replace(stra[0], '\n<hr/>\n');
-    }
-
-    /* include */
-    if ((micromarkdown.useajax !== false) && (strict !== true)) {
-      while ((stra = micromarkdown.regexobject.include.exec(str)) !== null) {
-        helper = stra[2].replace(/[\.\:\/]+/gm, '');
-        helper1 = '';
-        if (document.getElementById(helper)) {
-          helper1 = document.getElementById(helper).innerHTML.trim();
-        } else {
-          micromarkdown.ajax(stra[2]);
-        }
-        if ((stra[1] === 'csv') && (helper1 !== '')) {
-          helper2 = {
-            ';': [],
-            '\t': [],
-            ',': [],
-            '|': []
-          };
-          helper2[0] = [';', '\t', ',', '|'];
-          helper1 = helper1.split('\n');
-          for (j = 0; j < helper2[0].length; j++) {
-            for (i = 0; i < helper1.length; i++) {
-              if (i > 0) {
-                if (helper2[helper2[0][j]] !== false) {
-                  if ((helper2[helper2[0][j]][i] !== helper2[helper2[0][j]][i - 1]) || (helper2[helper2[0][j]][i] === 1)) {
-                    helper2[helper2[0][j]] = false;
-                  }
-                }
-              }
-            }
-          }
-          if ((helper2[';'] !== false) || (helper2['\t'] !== false) || (helper2[','] !== false) || (helper2['|'] !== false)) {
-            if (helper2[';'] !== false) {
-              helper2 = ';';
-            } else if (helper2['\t']) {
-              helper2 = '\t';
-            } else if (helper2[',']) {
-              helper2 = ',';
-            } else if (helper2['|']) {
-              helper2 = '|';
-            }
-            repstr = '<table>';
-            for (i = 0; i < helper1.length; i++) {
-              helper = helper1[i].split(helper2);
-              repstr += '<tr>';
-              for (j = 0; j < helper.length; j++) {
-                repstr += '<td>' + micromarkdown.htmlEncode(helper[j]) + '</td>';
-              }
-              repstr += '</tr>';
-            }
-            repstr += '</table>';
-            str = str.replace(stra[0], repstr);
-          } else {
-            str = str.replace(stra[0], '<code>' + helper1.join('\n') + '</code>');
-          }
-        } else {
-          str = str.replace(stra[0], '');
-        }
-      }
-    }
-
-    str = str.replace(/ {2,}[\n]{1,}/gmi, '<p>');
-
-    str = str.replace(/[\n]{2,}/gmi, '<p>');
-
-    for(var index in micromarkdown.codeblocks) { 
-      if(micromarkdown.codeblocks.hasOwnProperty(index)) {
-        str = str.replace('§§§' + index + '§§§', micromarkdown.codeblocks[index]); 
-      }
-    }
-    str = str.replace('&#x0024&amp;', '$&');
-
-    return str;
-  },
-  ajax: function (str) {
-    'use strict';
-    var xhr;
-    if (document.getElementById(str.replace(/[\.\:\/]+/gm, ''))) {
-      return false;
-    }
-    if (window.ActiveXObject) {
-      try {
-        xhr = new ActiveXObject("Microsoft.XMLHTTP");
-      } catch (e) {
-        xhr = null;
-        return e;
-      }
-    } else {
-      xhr = new XMLHttpRequest();
-    }
-    xhr.onreadystatechange = function () {
-      if (xhr.readyState === 4) {
-        var ele = document.createElement('code');
-        ele.innerHTML = xhr.responseText;
-        ele.id = str.replace(/[\.\:\/]+/gm, '');
-        ele.style.display = 'none';
-        document.getElementsByTagName('body')[0].appendChild(ele);
-        micromarkdown.useajax();
-      }
-    };
-    xhr.open('GET', str, true);
-    xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
-    xhr.send();
-  },
-  crc32: function (string) {
-    "use strict";
-    var crc = 0,
-      n, x, i, len, table = ["00000000", "77073096", "EE0E612C", "990951BA", "076DC419", "706AF48F", "E963A535", "9E6495A3", "0EDB8832", "79DCB8A4", "E0D5E91E", "97D2D988", "09B64C2B", "7EB17CBD", "E7B82D07", "90BF1D91", "1DB71064", "6AB020F2", "F3B97148", "84BE41DE", "1ADAD47D", "6DDDE4EB", "F4D4B551", "83D385C7", "136C9856", "646BA8C0", "FD62F97A", "8A65C9EC", "14015C4F", "63066CD9", "FA0F3D63", "8D080DF5", "3B6E20C8", "4C69105E", "D56041E4", "A2677172", "3C03E4D1", "4B04D447", "D20D85FD", "A50AB56B", "35B5A8FA", "42B2986C", "DBBBC9D6", "ACBCF940", "32D86CE3", "45DF5C75", "DCD60DCF", "ABD13D59", "26D930AC", "51DE003A", "C8D75180", "BFD06116", "21B4F4B5", "56B3C423", "CFBA9599", "B8BDA50F", "2802B89E", "5F058808", "C60CD9B2", "B10BE924", "2F6F7C87", "58684C11", "C1611DAB", "B6662D3D", "76DC4190", "01DB7106", "98D220BC", "EFD5102A", "71B18589", "06B6B51F", "9FBFE4A5", "E8B8D433", "7807C9A2", "0F00F934", "9609A88E", "E10E9818", "7F6A0DBB", "086D3D2D", "91646C97", "E6635C01", "6B6B51F4", "1C6C6162", "856530D8", "F262004E", "6C0695ED", "1B01A57B", "8208F4C1", "F50FC457", "65B0D9C6", "12B7E950", "8BBEB8EA", "FCB9887C", "62DD1DDF", "15DA2D49", "8CD37CF3", "FBD44C65", "4DB26158", "3AB551CE", "A3BC0074", "D4BB30E2", "4ADFA541", "3DD895D7", "A4D1C46D", "D3D6F4FB", "4369E96A", "346ED9FC", "AD678846", "DA60B8D0", "44042D73", "33031DE5", "AA0A4C5F", "DD0D7CC9", "5005713C", "270241AA", "BE0B1010", "C90C2086", "5768B525", "206F85B3", "B966D409", "CE61E49F", "5EDEF90E", "29D9C998", "B0D09822", "C7D7A8B4", "59B33D17", "2EB40D81", "B7BD5C3B", "C0BA6CAD", "EDB88320", "9ABFB3B6", "03B6E20C", "74B1D29A", "EAD54739", "9DD277AF", "04DB2615", "73DC1683", "E3630B12", "94643B84", "0D6D6A3E", "7A6A5AA8", "E40ECF0B", "9309FF9D", "0A00AE27", "7D079EB1", "F00F9344", "8708A3D2", "1E01F268", "6906C2FE", "F762575D", "806567CB", "196C3671", "6E6B06E7", "FED41B76", "89D32BE0", "10DA7A5A", "67DD4ACC", "F9B9DF6F", "8EBEEFF9", "17B7BE43", "60B08ED5", "D6D6A3E8", "A1D1937E", "38D8C2C4", "4FDFF252", "D1BB67F1", "A6BC5767", "3FB506DD", "48B2364B", "D80D2BDA", "AF0A1B4C", "36034AF6", "41047A60", "DF60EFC3", "A867DF55", "316E8EEF", "4669BE79", "CB61B38C", "BC66831A", "256FD2A0", "5268E236", "CC0C7795", "BB0B4703", "220216B9", "5505262F", "C5BA3BBE", "B2BD0B28", "2BB45A92", "5CB36A04", "C2D7FFA7", "B5D0CF31", "2CD99E8B", "5BDEAE1D", "9B64C2B0", "EC63F226", "756AA39C", "026D930A", "9C0906A9", "EB0E363F", "72076785", "05005713", "95BF4A82", "E2B87A14", "7BB12BAE", "0CB61B38", "92D28E9B", "E5D5BE0D", "7CDCEFB7", "0BDBDF21", "86D3D2D4", "F1D4E242", "68DDB3F8", "1FDA836E", "81BE16CD", "F6B9265B", "6FB077E1", "18B74777", "88085AE6", "FF0F6A70", "66063BCA", "11010B5C", "8F659EFF", "F862AE69", "616BFFD3", "166CCF45", "A00AE278", "D70DD2EE", "4E048354", "3903B3C2", "A7672661", "D06016F7", "4969474D", "3E6E77DB", "AED16A4A", "D9D65ADC", "40DF0B66", "37D83BF0", "A9BCAE53", "DEBB9EC5", "47B2CF7F", "30B5FFE9", "BDBDF21C", "CABAC28A", "53B39330", "24B4A3A6", "BAD03605", "CDD70693", "54DE5729", "23D967BF", "B3667A2E", "C4614AB8", "5D681B02", "2A6F2B94", "B40BBE37", "C30C8EA1", "5A05DF1B", "2D02EF8D"];
-    n = 0;
-    x = 0;
-    len = string.length;
-    crc = crc ^ (-1);
-    for (i = 0; i < len; i++) {
-      n = (crc ^ string.charCodeAt(i)) & 0xFF;
-      x = "0x" + table[n];
-      crc = (crc >>> 8) ^ x;
-    }
-    return crc ^ (-1);
-  },
-  countingChars: function (str, split) {
-    'use strict';
-    str = str.split(split);
-    if (typeof str === 'object') {
-      return str.length - 1;
-    }
-    return 0;
-  },
-  htmlEncode: function (str) {
-    'use strict';
-    var div = document.createElement('div');
-    div.appendChild(document.createTextNode(str));
-    str = div.innerHTML;
-    div = undefined;
-    return str;
-  },
-  mmdCSSclass: function (str, strict) {
-    'use strict';
-    var urlTemp;
-    if ((str.indexOf('/') !== -1) && (strict !== true)) {
-      urlTemp = str.split('/');
-      if (urlTemp[1].length === 0) {
-        if (urlTemp[2].indexOf('.') !== -1)
-          urlTemp = urlTemp[2].split('.');
-      } else {
-        if (urlTemp[0].indexOf('.') !== -1)
-          urlTemp = urlTemp[0].split('.');
-      }
-      return 'class="tvh_mmd_' + urlTemp[urlTemp.length - 2].replace(/[^\w\d]/g, '') + urlTemp[urlTemp.length - 1] + '" ';
-    }
-    return '';
-  }
-};
-
-(function (root, factory) {
-  "use strict";
-  if (typeof define === 'function' && define.amd) {
-    define([], factory);
-  } else if (typeof exports === 'object') {
-    module.exports = factory();
-  } else {
-    root.returnExports = factory();
-  }
-}(this, function () {
-  'use strict';
-  return micromarkdown;
-}));
index 88036615af49c60841bc47528be68ecb0302f5ec..8d29cf5276cb18e13376e614e9c9d459a565acac 100644 (file)
@@ -8,6 +8,8 @@ tvheadend.uilevel_nochange = false;
 tvheadend.quicktips = true;
 tvheadend.wizard = null;
 tvheadend.docs_toc = null;
+tvheadend.doc_history = [];
+tvheadend.doc_win = null;
 
 tvheadend.cookieProvider = new Ext.state.CookieProvider({
   // 7 days from now
@@ -94,27 +96,59 @@ tvheadend.help = function(title, pagename) {
 
 tvheadend.mdhelp = function(pagename) {
 
-    var linkfcn = function(type, href) {
-        if (type == 'src') {
-            if (href.substring(0, 7) == 'images/')
-               return 'static/img' + href.substring(6);
-            return href;
-        } else {
-            return 'page="' + href + '"';
-        }
+    var parse = function(text) {
+        var renderer = new marked.Renderer;
+        renderer.link = function(href, title, text) {
+            if (href.indexOf(':/') === -1) {
+                var r = '<a page="' + href + '"';
+                if (title) r += ' title="' + title + '"';
+                return r + '>' + text + '</a>';
+            }
+            return marked.Renderer.prototype.link.call(this, href, title, text);
+        };
+        renderer.image = function(href, title, text) {
+            if (href) {
+                if (href.substring(0, 7) == 'images/')
+                    href = 'static/img' + href.substring(6);
+                else if (href.substring(0, 6) == 'icons/')
+                    href = 'static/' + href;
+            }
+            return marked.Renderer.prototype.image.call(this, href, title, text);
+        };
+        opts = { renderer: renderer };
+        return marked(text, opts);
     }
 
     var fcn = function(result) {
         var mdtext = result.responseText;
         var title = mdtext.split('\n')[0].split('#');
+        var history = '';
+
+        if (tvheadend.doc_win) {
+            tvheadend.doc_win.close();
+            tvheadend.doc_win = null;
+        }
         
         if (title)
             title = title[title.length-1];
+
+        if (tvheadend.doc_history) {
+            for (var i = 1; i <= tvheadend.doc_history.length; i++) {
+                var p = tvheadend.doc_history[i-1];
+                if (!history)
+                    history = '## ' + _('Last Help Pages') + '\n\n';
+                history += '' + i + '. [' + p[1] + '](' + p[0] + ')\n';
+            }
+            history = parse(history);
+            if (history)
+                history += '<hr/>';
+        }
         
-        var text = '<div>';
-        if (tvheadend.docs_toc)
-            text += '<div class="hts-doc-toc">' + tvheadend.docs_toc + '</div>';
-        text += '<div class="hts-doc-text">' + micromarkdown.parse(mdtext, 0, linkfcn) + '</div>';
+        var bodyid = Ext.id();
+        var text = '<div id="' + bodyid + '">';
+        if (tvheadend.docs_toc || history)
+            text += '<div class="hts-doc-toc">' + history + tvheadend.docs_toc + '</div>';
+        text += '<div class="hts-doc-text">' + parse(mdtext) + '</div>';
         text += '</div>';
 
         var content = new Ext.Panel({
@@ -124,6 +158,38 @@ tvheadend.mdhelp = function(pagename) {
             html: text
         });
 
+        var doresize = function(win) {
+            var aw = Ext.lib.Dom.getViewWidth();
+            var ah = Ext.lib.Dom.getViewHeight();
+            var size = win.getSize();
+            var osize = [size.width, size.height];
+            var pos = win.getPosition();
+            var opos = pos;
+
+            if (pos[0] > -9000) {
+                if (pos[0] + size.width > aw)
+                    pos[0] = Math.max(0, aw - size.width);
+                if (pos[0] + size.width > aw)
+                    size.width = aw;
+                if (pos[1] + size.height > ah)
+                    pos[1] = Math.max(0, ah - size.height);
+                if (pos[1] + size.height > ah)
+                    size.height = ah;
+                if (pos != opos || osize[0] != size.width || osize[1] != size.height) {
+                  win.setPosition(pos);
+                  win.setSize(size);
+                }
+            }
+
+            var dom = win.getEl().dom;
+            var el = dom.querySelectorAll("img");
+            var maxwidth = '97%';
+            if (size.width >= 350)
+                maxwidth = '' + (size.width - 290) + 'px';
+            for (var i = 0; i < el.length; i++)
+                el[i].style['max-width'] = maxwidth;
+        }
+
         var win = new Ext.Window({
             title: _('Help for') + ' ' + title,
             iconCls: 'help',
@@ -139,10 +205,35 @@ tvheadend.mdhelp = function(pagename) {
                         if (page)
                             tvheadend.mdhelp(page);
                     });
+                },
+                afterrender: function(win) {
+                    doresize(win);
+                },
+                resize: function(win, width, height) {
+                    doresize(win);
+                },
+                destroy: function(win) {
+                    if (win == tvheadend.doc_win)
+                        tvheadend.doc_win = null;
+                    Ext.EventManager.removeResizeListener(doresize, this);
                 }
             },
         });
+        var aw = Ext.lib.Dom.getViewWidth();
+        var ah = Ext.lib.Dom.getViewHeight();
+        if (aw > 400) aw -= 50;
+        if (aw > 500) aw -= 50;
+        if (aw > 800) aw -= 100;
+        if (ah > 400) ah -= 50;
+        if (ah > 500) ah -= 50;
+        if (ah > 800) ah -= 100;
+        win.setSize(aw, ah);
+        Ext.EventManager.onWindowResize(function() { doresize(this); }, win, [true]);
         win.show();
+        tvheadend.doc_history = [[pagename, title]].concat(tvheadend.doc_history);
+        if (tvheadend.doc_history.length > 5)
+           tvheadend.doc_history.pop();
+        tvheadend.doc_win = win;
     }
 
     Ext.Ajax.request({
@@ -152,7 +243,7 @@ tvheadend.mdhelp = function(pagename) {
                 Ext.Ajax.request({
                     url: 'markdown/toc',
                     success: function(result_toc) {
-                        tvheadend.docs_toc = micromarkdown.parse(result_toc.responseText, 0, linkfcn);
+                        tvheadend.docs_toc = parse(result_toc.responseText);
                         fcn(result);
                     }
                 });
index 79266d8aa8e9b72ed85bb1635049febb94f2fc80..e3f0adfcaf9adf5e6826e70c7cb396b835728687 100644 (file)
@@ -102,7 +102,7 @@ tvheadend.wizard_start = function(page) {
         if (icon)
             c += '<img class="x-wizard-icon" src="' + icon + '"/>';
         if (text)
-            text = micromarkdown.parse(text);
+            text = marked(text);
         c += '<div class="x-wizard-description">' + text + '</div>';
         var p = new Ext.Panel({
             width: 570,
index f585161ad07061489f4c7ac325ff0b5df49ac981..f96779cfc6e676c6e1896d4850c59f6810f8741c 100755 (executable)
@@ -83,6 +83,7 @@ class TVH_C_Renderer(Renderer):
         xlink.append(o)
         if t.find(']') >= 0 and (t.endswith(')') or t.endswith(') ')):
           d += xfound and self.get_lang(r) or self.get_nolang(r)
+          r = ''
           xfound = 0
           d += ''.join(xlink)
           xlink = []
@@ -126,11 +127,11 @@ class TVH_C_Renderer(Renderer):
 
   def hrule(self):
     if DEBUG: debug('hrule')
-    return self.get_nolang('---') + '\n'
+    return '\n' + self.get_nolang('---') + '\n'
 
   def header(self, text, level, raw=None):
     if DEBUG: debug('header[%d]: ' % level + repr(text))
-    return '\n' + self.get_nolang('#'*(level+1)) + text + '\n'
+    return '\n' + self.get_nolang('#'*(level+1) + ' ') + text + '\n'
 
   def paragraph(self, text):
     if DEBUG: debug('paragraph: ' + repr(text))
@@ -202,6 +203,7 @@ class TVH_C_Renderer(Renderer):
     self.link(src, title, text, image=True)
 
   def table(self, header, body):
+    if DEBUG: debug('table: ' + repr(header) + ' ' + repr(body))
     hrows = []
     while header:
       header, type, t = self.get_block(header)
@@ -247,41 +249,46 @@ class TVH_C_Renderer(Renderer):
         if 'align' in col.flags:
           align[i] = col.flags['align'][0]
         i += 1
-    r = ''
+    r = '\n'
     for row in hrows:
       i = 0
       for col in row:
         if i > 0:
           r += self.get_nolang(' | ')
-        r += col.text.ljust(colmax[i])
+        r += col.text
         i += 1
-      r += self.get_nolang('\n')
+      r += '\n'
     for i in range(colscount):
       if i > 0:
         r += self.get_nolang(' | ')
       if align[i] == 'c':
-        r += self.get_nolang(':' + '-'.ljust(colmax[i]-2, '-') + ':')
+        r += self.get_nolang(':---:')
       elif align[i] == 'l':
-        r += self.get_nolang(':' + '-'.ljust(colmax[i]-1, '-'))
+        r += self.get_nolang(':----')
       elif align[i] == 'r':
-        r += self.get_nolang('-'.ljust(colmax[i]-1, '-') + ':')
+        r += self.get_nolang('----:')
       else:
-        r += self.get_nolang('-'.ljust(colmax[i], '-'))
-    r += self.get_nolang('\n')
+        r += self.get_nolang('-----')
+    r += '\n'
     for row in brows:
       i = 0
       for col in row:
         if i > 0:
           r += self.get_nolang(' | ')
-        r += col.text.ljust(colmax[i])
+        r += col.text
         i += 1
-      r += self.get_nolang('\n')
+      r += '\n'
     return r
 
   def table_row(self, content):
-    return self.get_nolang('r' + str(len(content)) + ':') + content
+    if DEBUG: debug('table_row: ' + repr(content))
+    return 'r' + str(len(content)) + ':' + content
 
   def table_cell(self, content, **flags):
+    if DEBUG: debug('table_cell: ' + repr(content) + ' ' + repr(flags))
+    # dirty fix for inline images
+    if content.startswith('x1:!_1:['):
+      content = '_2:![' + content[8:]
     content = content.replace('\n', ' ')
     r = ''
     for fl in flags: