From 512546ebfb7cd7e78de7ed12a1e6bf72485e2e36 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Mon, 4 Apr 2016 18:18:56 +0200 Subject: [PATCH] mdhelp: many changes, it's starting to be useful - 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 | 2 +- docs/markdown/epg.md | 224 +++++ src/webui/doc_md.c | 4 +- src/webui/static/app/acleditor.js | 4 +- src/webui/static/app/epg.js | 2 +- src/webui/static/app/ext.css | 31 +- src/webui/static/app/marked.js | 1285 +++++++++++++++++++++++++ src/webui/static/app/micromarkdown.js | 437 --------- src/webui/static/app/tvheadend.js | 117 ++- src/webui/static/app/wizard.js | 2 +- support/doc/md_to_c.py | 33 +- 11 files changed, 1669 insertions(+), 472 deletions(-) create mode 100644 docs/markdown/epg.md create mode 100644 src/webui/static/app/marked.js delete mode 100644 src/webui/static/app/micromarkdown.js diff --git a/Makefile.webui b/Makefile.webui index c83cab3e4..83102509e 100644 --- a/Makefile.webui +++ b/Makefile.webui @@ -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 index 000000000..2b7396007 --- /dev/null +++ b/docs/markdown/epg.md @@ -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) diff --git a/src/webui/doc_md.c b/src/webui/doc_md.c index d2a588299..4c4b14dfc 100644 --- a/src/webui/doc_md.c +++ b/src/webui/doc_md.c @@ -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); diff --git a/src/webui/static/app/acleditor.js b/src/webui/static/app/acleditor.js index d2da5c559..60757dccc 100644 --- a/src/webui/static/app/acleditor.js +++ b/src/webui/static/app/acleditor.js @@ -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'); } }); }; diff --git a/src/webui/static/app/epg.js b/src/webui/static/app/epg.js index 9b3aa702c..b66a6d94e 100644 --- a/src/webui/static/app/epg.js +++ b/src/webui/static/app/epg.js @@ -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'); } } ]; diff --git a/src/webui/static/app/ext.css b/src/webui/static/app/ext.css index f119dd8cb..f9462b1ed 100644 --- a/src/webui/static/app/ext.css +++ b/src/webui/static/app/ext.css @@ -733,6 +733,10 @@ 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 { @@ -827,14 +831,23 @@ .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; } @@ -844,18 +857,32 @@ 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; } @@ -868,7 +895,7 @@ .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 index 000000000..03251f3c5 --- /dev/null +++ b/src/webui/static/app/marked.js @@ -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: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\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', //) + ('closed', /<(tag)[\s\S]+?<\/\1>/) + ('closing', /])*?>/) + (/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: /^|^<\/?\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]+?(?=[\\?(?:\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 && /^/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 '
'
+      + (escaped ? code : escape(code, true))
+      + '\n
'; + } + + return '
'
+    + (escaped ? code : escape(code, true))
+    + '\n
\n'; +}; + +Renderer.prototype.blockquote = function(quote) { + return '
\n' + quote + '
\n'; +}; + +Renderer.prototype.html = function(html) { + return html; +}; + +Renderer.prototype.heading = function(text, level, raw) { + return '' + + text + + '\n'; +}; + +Renderer.prototype.hr = function() { + return this.options.xhtml ? '
\n' : '
\n'; +}; + +Renderer.prototype.list = function(body, ordered) { + var type = ordered ? 'ol' : 'ul'; + return '<' + type + '>\n' + body + '\n'; +}; + +Renderer.prototype.listitem = function(text) { + return '
  • ' + text + '
  • \n'; +}; + +Renderer.prototype.paragraph = function(text) { + return '

    ' + text + '

    \n'; +}; + +Renderer.prototype.table = function(header, body) { + return '\n' + + '\n' + + header + + '\n' + + '\n' + + body + + '\n' + + '
    \n'; +}; + +Renderer.prototype.tablerow = function(content) { + return '\n' + content + '\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 + '\n'; +}; + +// span level renderer +Renderer.prototype.strong = function(text) { + return '' + text + ''; +}; + +Renderer.prototype.em = function(text) { + return '' + text + ''; +}; + +Renderer.prototype.codespan = function(text) { + return '' + text + ''; +}; + +Renderer.prototype.br = function() { + return this.options.xhtml ? '
    ' : '
    '; +}; + +Renderer.prototype.del = function(text) { + return '' + text + ''; +}; + +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 = '
    '; + return out; +}; + +Renderer.prototype.image = function(href, title, text) { + var out = '' + text + '' : '>'; + 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, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +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 '

    An error occured:

    '
    +        + escape(e.message + '', true)
    +        + '
    '; + } + 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 index 92ef85f07..000000000 --- a/src/webui/static/app/micromarkdown.js +++ /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('$&', '$&'); - - /* code */ - while ((stra = micromarkdown.regexobject.code.exec(str)) !== null) { - crc32str = micromarkdown.crc32(stra[0]); - micromarkdown.codeblocks[crc32str] = '\n' + micromarkdown.htmlEncode(stra[1]).replace(/\n/gm, '
    ').replace(/\ /gm, ' ') + '
    \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], '' + stra[2] + '' + '\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 = '
      '; - } else { - repstr = '
        '; - } - 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 += '
          '; - helper1.push('
        '); - } else { - repstr += '
          '; - helper1.push('
        '); - } - status++; - casca++; - } - repstr += '
      1. ' + line[6] + '
      2. ' + '\n'; - } - } - while (casca > 0) { - repstr += '
    '; - casca--; - } - if ((stra[0].trim().substr(0, 1) === '*') || (stra[0].trim().substr(0, 1) === '-')) { - repstr += ''; - } else { - repstr += ''; - } - str = str.replace(stra[0], repstr + '\n'); - } - - /* tables */ - while ((stra = micromarkdown.regexobject.tables.exec(str)) !== null) { - repstr = ''; - 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 = [''; - } - repstr += ''; - cel = [''; - for (j = 0; j < helper2.length; j++) { - repstr += cel[calign[j]] + helper2[j].trim() + ''; - } - repstr += '' + '\n'; - } - } - repstr += '
    ', '', '', '']; - for (i = 0; i < helper.length; i++) { - repstr += cel[calign[i]] + helper[i].trim() + '
    ', '', '', '']; - 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 += '
    '; - 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], '' + stra[2] + ''); - } else { - switch (stra[1].length) { - case 1: - repstr = ['', '']; - break; - case 2: - repstr = ['', '']; - break; - case 3: - repstr = ['', '']; - 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], '' + 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], '
    ' + stra[1] + '\n'); - } - } - while ((stra = micromarkdown.regexobject.mail.exec(str)) !== null) { - str = str.replace(stra[0], '' + stra[1] + ''); - } - while ((stra = micromarkdown.regexobject.url.exec(str)) !== null) { - repstr = stra[1]; - if (repstr.indexOf('://') === -1) { - repstr = 'http://' + repstr; - } - str = str.replace(stra[0], '' + repstr.replace(/(https:\/\/|http:\/\/|mailto:|ftp:\/\/)/gmi, '') + ''); - } - 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], '' + stra[1] + ''); - 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], '' + stra[1] + ''); - } - while ((stra = micromarkdown.regexobject.url2.exec(str)) !== null) { - repstr = stra[1]; - str = str.replace(stra[0], '' + repstr + ''); - } - - /* horizontal line */ - while ((stra = micromarkdown.regexobject.hr.exec(str)) !== null) { - str = str.replace(stra[0], '\n
    \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 = ''; - for (i = 0; i < helper1.length; i++) { - helper = helper1[i].split(helper2); - repstr += ''; - for (j = 0; j < helper.length; j++) { - repstr += ''; - } - repstr += ''; - } - repstr += '
    ' + micromarkdown.htmlEncode(helper[j]) + '
    '; - str = str.replace(stra[0], repstr); - } else { - str = str.replace(stra[0], '' + helper1.join('\n') + ''); - } - } else { - str = str.replace(stra[0], ''); - } - } - } - - str = str.replace(/ {2,}[\n]{1,}/gmi, '

    '); - - str = str.replace(/[\n]{2,}/gmi, '

    '); - - for(var index in micromarkdown.codeblocks) { - if(micromarkdown.codeblocks.hasOwnProperty(index)) { - str = str.replace('§§§' + index + '§§§', micromarkdown.codeblocks[index]); - } - } - str = str.replace('$&', '$&'); - - 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; -})); diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js index 88036615a..8d29cf527 100644 --- a/src/webui/static/app/tvheadend.js +++ b/src/webui/static/app/tvheadend.js @@ -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 = '' + text + ''; + } + 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 += '


    '; + } - var text = '
    '; - if (tvheadend.docs_toc) - text += '
    ' + tvheadend.docs_toc + '
    '; - text += '
    ' + micromarkdown.parse(mdtext, 0, linkfcn) + '
    '; + var bodyid = Ext.id(); + var text = '
    '; + if (tvheadend.docs_toc || history) + text += '
    ' + history + tvheadend.docs_toc + '
    '; + text += '
    ' + parse(mdtext) + '
    '; text += '
    '; 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); } }); diff --git a/src/webui/static/app/wizard.js b/src/webui/static/app/wizard.js index 79266d8aa..e3f0adfca 100644 --- a/src/webui/static/app/wizard.js +++ b/src/webui/static/app/wizard.js @@ -102,7 +102,7 @@ tvheadend.wizard_start = function(page) { if (icon) c += ''; if (text) - text = micromarkdown.parse(text); + text = marked(text); c += '
    ' + text + '
    '; var p = new Ext.Panel({ width: 570, diff --git a/support/doc/md_to_c.py b/support/doc/md_to_c.py index f585161ad..f96779cfc 100755 --- a/support/doc/md_to_c.py +++ b/support/doc/md_to_c.py @@ -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: -- 2.47.3