From 14f6bfee16d71d620ca36614796898f915112407 Mon Sep 17 00:00:00 2001 From: Simon Li Date: Mon, 26 Sep 2016 22:19:42 +0100 Subject: [PATCH 1/5] Add non-private idr config --- .../idr-playbooks/files/IDR-OMERO-52-omero.j2 | 51 +++++++++++++++++ .../idr-playbooks/group_vars/omero-hosts.yml | 22 +++++++ .../idr-playbooks/group_vars/proxy-hosts.yml | 57 +++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 ansible/idr-playbooks/files/IDR-OMERO-52-omero.j2 create mode 100644 ansible/idr-playbooks/group_vars/omero-hosts.yml create mode 100644 ansible/idr-playbooks/group_vars/proxy-hosts.yml diff --git a/ansible/idr-playbooks/files/IDR-OMERO-52-omero.j2 b/ansible/idr-playbooks/files/IDR-OMERO-52-omero.j2 new file mode 100644 index 000000000..672219146 --- /dev/null +++ b/ansible/idr-playbooks/files/IDR-OMERO-52-omero.j2 @@ -0,0 +1,51 @@ +config set omero.db.poolsize 25 +config set omero.jvmcfg.heap_size.blitz 24G +# public user doesn;t share omero session between visitors +#config set omero.sessions.timeout 3600000 +config set omero.policy.binary_access "+read,+write,-image,-plate" + +# Prevent retrieval of uncached BF data +config set omero.pixeldata.fail_if_missing true + +# web +config set omero.web.application_server "wsgi-tcp" +config set omero.web.application_server.max_requests 300 +config set omero.web.wsgi_timeout 30 +#config set omero.web.wsgi_args -- '--forwarded-allow-ips=YOUR_IP' + +config set omero.web.use_x_forwarded_host true +# The config generated by omero web wsgi contains X-Forwarded-Proto +# Override it in the front-end proxy by checking a different header +config set omero.web.secure_proxy_ssl_header '["HTTP_X_FORWARDED_PROTO_OMERO_WEB", "https"]' + +# TODO: Enable prefixes (requires additional nginx changes for redirects) +#config set omero.web.prefix /omero +#config set omero.web.static_url /omero/static/ + +# public user +config set omero.web.public.enabled True +config set omero.web.public.server_id 1 +config set omero.web.public.user public +config set omero.web.public.password {{ idr_secret_omero_web_public_password | default("") }} +config set omero.web.public.url_filter ^/(webadmin/myphoto/|webclient/(?!(action|annotate_(file|tags|comment|rating|map)|script_ui|ome_tiff|figure_script))|webgateway/(?!(archived_files|download_as))) + +# Group/User drop down menu +config set omero.client.ui.menu.dropdown.colleagues.enabled false +config set omero.client.ui.menu.dropdown.everyone.label "Demo data" +config set omero.client.ui.menu.dropdown.leaders.enabled false + + +config set omero.web.login_redirect '{"redirect": ["webindex"], "viewname": "load_template", "args": ["userdata"], "query_string": "experimenter=-1"}' + + +# session +config set omero.web.session_cookie_name sessionid_demo +config set omero.web.session_cookie_age 3600 + +# redis +config set omero.web.session_engine "django.contrib.sessions.backends.cache" +config set omero.web.caches '{"default": {"BACKEND": "redis_cache.RedisCache","LOCATION": "127.0.0.1:6379"}}' + +# social media +config set omero.web.sharing.twitter '{"omero": "@openmicroscopy"}' +config set omero.web.sharing.opengraph '{"omero": "Open Microscopy"}' diff --git a/ansible/idr-playbooks/group_vars/omero-hosts.yml b/ansible/idr-playbooks/group_vars/omero-hosts.yml new file mode 100644 index 000000000..a142dfc4c --- /dev/null +++ b/ansible/idr-playbooks/group_vars/omero-hosts.yml @@ -0,0 +1,22 @@ +omero_dbuser: omero +omero_dbname: idr +omero_dbpassword: omero +omero_datadir: /data/OMERO + +# TODO: change this to a lower system uid +omero_system_uid: "{{ idr_secret_omero_system_uid | default(999) }}" + +omero_web_runtime_redis: True +omero_release: latest +omero_omego_additional_args: "--downloadurl https://downloads.openmicroscopy.org/idr" + +# Disable database backups +omero_database_backupdir: +omero_upgrade: True +# Recursively chown data dir (this may take a very long time) +#omero_datadir_chown: True + +omero_prestart_file: "{{ playbook_dir }}/files/IDR-OMERO-52-omero.j2" + +omero_logmonitor_slack_token: "{{ idr_secret_omero_logmonitor_slack_token | default(None) }}" +omero_logmonitor_slack_channel: "#logs" diff --git a/ansible/idr-playbooks/group_vars/proxy-hosts.yml b/ansible/idr-playbooks/group_vars/proxy-hosts.yml new file mode 100644 index 000000000..c7a11d782 --- /dev/null +++ b/ansible/idr-playbooks/group_vars/proxy-hosts.yml @@ -0,0 +1,57 @@ +# nginx-proxy SSL +#nginx_proxy_ssl_certificate_source_path: ~/ansible-files/idr-fullchain.pem +#nginx_proxy_ssl_certificate_key_source_path: ~/ansible-files/idr-privkey.pem +# The config generated by omero web wsgi contains X-Forwarded-Proto +# Override it in the front-end proxy by setting a different header +nginx_proxy_forward_scheme_header: X-Forwarded-Proto-Omero-Web +nginx_proxy_cachebuster_port: 9000 + +# Order is important +nginx_proxy_redirect_map: +- match: default + dest: / + +- match: /mito/webclient/?show=screen-1 + dest: /webclient/?show=screen-1101 +- match: /mito/webclient/?show=screen-701 + dest: /webclient/?show=screen-1302 +- match: /mito/webclient/?show=well-324 + dest: /webclient/?show=well-771034 + +- match: /tara/webclient/?show=screen-151 + dest: /webclient/?show=screen-1201 +- match: /tara/webclient/?show=plate-362 + dest: /webclient/?show=plate-4751 + +- match: /pgpc/webclient/?show=screen-101 + dest: /webclient/?show=screen-1151 +- match: /pgpc/webclient/?show=run-2 + dest: /webclient/?show=plate-4101 + +- match: ~/mito + dest: /webclient/?show=screen-1101 +- match: ~/tara + dest: /webclient/?show=screen-1201 +- match: ~/pgpc + dest: /webclient/?show=screen-1151 + +nginx_proxy_redirect_map_locations: +# TODO: change to 301 when we're happy +- location: "~ ^/(mito|tara|pgpc)($|/)" + code: 302 + +# "= /" has higher priority than "/" in the proxy config +nginx_proxy_direct_locations: +# TODO: change to 301 when we're happy +- location: "= /webclient" + redirect302: /webclient/userdata/?experimenter=-1 +- location: "= /" + redirect302: /about +- location: "^~ /about" + index: idr.html + alias: /srv/www/html + +#nginx_proxy_block_locations: +#- "^~ /login" + +nginx_proxy_set_header_host: 'idr-demo.openmicroscopy.org' From 16f5fc97d15448d0570f9f8362d1da8f5e63a087 Mon Sep 17 00:00:00 2001 From: Simon Li Date: Mon, 26 Sep 2016 22:24:00 +0100 Subject: [PATCH 2/5] Move idr html files into public repo --- .../files/html/Markdown.Converter.js | 1622 ++++++++++++ .../files/html/Markdown.Editor.js | 2301 +++++++++++++++++ .../files/html/Markdown.Sanitizer.js | 108 + ansible/idr-playbooks/files/html/bbsrc.png | Bin 0 -> 13450 bytes .../files/html/eurobioimaging_logo.gif | Bin 0 -> 5558 bytes ansible/idr-playbooks/files/html/h2020.png | Bin 0 -> 15440 bytes ansible/idr-playbooks/files/html/idr.html | 104 + ansible/idr-playbooks/files/html/markdown.md | 88 + .../idr-playbooks/files/html/ome-logo-200.png | Bin 0 -> 6254 bytes .../idr-playbooks/files/html/take_a_look.png | Bin 0 -> 15277 bytes ansible/idr-playbooks/idr-local-files.yml | 2 +- 11 files changed, 4224 insertions(+), 1 deletion(-) create mode 100644 ansible/idr-playbooks/files/html/Markdown.Converter.js create mode 100644 ansible/idr-playbooks/files/html/Markdown.Editor.js create mode 100644 ansible/idr-playbooks/files/html/Markdown.Sanitizer.js create mode 100644 ansible/idr-playbooks/files/html/bbsrc.png create mode 100644 ansible/idr-playbooks/files/html/eurobioimaging_logo.gif create mode 100644 ansible/idr-playbooks/files/html/h2020.png create mode 100644 ansible/idr-playbooks/files/html/idr.html create mode 100644 ansible/idr-playbooks/files/html/markdown.md create mode 100644 ansible/idr-playbooks/files/html/ome-logo-200.png create mode 100644 ansible/idr-playbooks/files/html/take_a_look.png diff --git a/ansible/idr-playbooks/files/html/Markdown.Converter.js b/ansible/idr-playbooks/files/html/Markdown.Converter.js new file mode 100644 index 000000000..66fdedc7c --- /dev/null +++ b/ansible/idr-playbooks/files/html/Markdown.Converter.js @@ -0,0 +1,1622 @@ +"use strict"; +var Markdown; + +if (typeof exports === "object" && typeof require === "function") // we're in a CommonJS (e.g. Node.js) module + Markdown = exports; +else + Markdown = {}; + +// The following text is included for historical reasons, but should +// be taken with a pinch of salt; it's not all true anymore. + +// +// Wherever possible, Showdown is a straight, line-by-line port +// of the Perl version of Markdown. +// +// This is not a normal parser design; it's basically just a +// series of string substitutions. It's hard to read and +// maintain this way, but keeping Showdown close to the original +// design makes it easier to port new features. +// +// More importantly, Showdown behaves like markdown.pl in most +// edge cases. So web applications can do client-side preview +// in Javascript, and then build identical HTML on the server. +// +// This port needs the new RegExp functionality of ECMA 262, +// 3rd Edition (i.e. Javascript 1.5). Most modern web browsers +// should do fine. Even with the new regular expression features, +// We do a lot of work to emulate Perl's regex functionality. +// The tricky changes in this file mostly have the "attacklab:" +// label. Major or self-explanatory changes don't. +// +// Smart diff tools like Araxis Merge will be able to match up +// this file with markdown.pl in a useful way. A little tweaking +// helps: in a copy of markdown.pl, replace "#" with "//" and +// replace "$text" with "text". Be sure to ignore whitespace +// and line endings. +// + + +// +// Usage: +// +// var text = "Markdown *rocks*."; +// +// var converter = new Markdown.Converter(); +// var html = converter.makeHtml(text); +// +// alert(html); +// +// Note: move the sample code to the bottom of this +// file before uncommenting it. +// + +(function () { + + function identity(x) { return x; } + function returnFalse(x) { return false; } + + function HookCollection() { } + + HookCollection.prototype = { + + chain: function (hookname, func) { + var original = this[hookname]; + if (!original) + throw new Error("unknown hook " + hookname); + + if (original === identity) + this[hookname] = func; + else + this[hookname] = function (text) { + var args = Array.prototype.slice.call(arguments, 0); + args[0] = original.apply(null, args); + return func.apply(null, args); + }; + }, + set: function (hookname, func) { + if (!this[hookname]) + throw new Error("unknown hook " + hookname); + this[hookname] = func; + }, + addNoop: function (hookname) { + this[hookname] = identity; + }, + addFalse: function (hookname) { + this[hookname] = returnFalse; + } + }; + + Markdown.HookCollection = HookCollection; + + // g_urls and g_titles allow arbitrary user-entered strings as keys. This + // caused an exception (and hence stopped the rendering) when the user entered + // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this + // (since no builtin property starts with "s_"). See + // http://meta.stackexchange.com/questions/64655/strange-wmd-bug + // (granted, switching from Array() to Object() alone would have left only __proto__ + // to be a problem) + function SaveHash() { } + SaveHash.prototype = { + set: function (key, value) { + this["s_" + key] = value; + }, + get: function (key) { + return this["s_" + key]; + } + }; + + Markdown.Converter = function (OPTIONS) { + var pluginHooks = this.hooks = new HookCollection(); + + // given a URL that was encountered by itself (without markup), should return the link text that's to be given to this link + pluginHooks.addNoop("plainLinkText"); + + // called with the orignal text as given to makeHtml. The result of this plugin hook is the actual markdown source that will be cooked + pluginHooks.addNoop("preConversion"); + + // called with the text once all normalizations have been completed (tabs to spaces, line endings, etc.), but before any conversions have + pluginHooks.addNoop("postNormalization"); + + // Called with the text before / after creating block elements like code blocks and lists. Note that this is called recursively + // with inner content, e.g. it's called with the full text, and then only with the content of a blockquote. The inner + // call will receive outdented text. + pluginHooks.addNoop("preBlockGamut"); + pluginHooks.addNoop("postBlockGamut"); + + // called with the text of a single block element before / after the span-level conversions (bold, code spans, etc.) have been made + pluginHooks.addNoop("preSpanGamut"); + pluginHooks.addNoop("postSpanGamut"); + + // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml + pluginHooks.addNoop("postConversion"); + + // + // Private state of the converter instance: + // + + // Global hashes, used by various utility routines + var g_urls; + var g_titles; + var g_html_blocks; + + // Used to track when we're inside an ordered or unordered list + // (see _ProcessListItems() for details): + var g_list_level; + + OPTIONS = OPTIONS || {}; + var asciify = identity, deasciify = identity; + if (OPTIONS.nonAsciiLetters) { + + /* In JavaScript regular expressions, \w only denotes [a-zA-Z0-9_]. + * That's why there's inconsistent handling e.g. with intra-word bolding + * of Japanese words. That's why we do the following if OPTIONS.nonAsciiLetters + * is true: + * + * Before doing bold and italics, we find every instance + * of a unicode word character in the Markdown source that is not + * matched by \w, and the letter "Q". We take the character's code point + * and encode it in base 51, using the "digits" + * + * A, B, ..., P, R, ..., Y, Z, a, b, ..., y, z + * + * delimiting it with "Q" on both sides. For example, the source + * + * > In Chinese, the smurfs are called 藍精靈, meaning "blue spirits". + * + * turns into + * + * > In Chinese, the smurfs are called QNIhQQMOIQQOuUQ, meaning "blue spirits". + * + * Since everything that is a letter in Unicode is now a letter (or + * several letters) in ASCII, \w and \b should always do the right thing. + * + * After the bold/italic conversion, we decode again; since "Q" was encoded + * alongside all non-ascii characters (as "QBfQ"), and the conversion + * will not generate "Q", the only instances of that letter should be our + * encoded characters. And since the conversion will not break words, the + * "Q...Q" should all still be in one piece. + * + * We're using "Q" as the delimiter because it's probably one of the + * rarest characters, and also because I can't think of any special behavior + * that would ever be triggered by this letter (to use a silly example, if we + * delimited with "H" on the left and "P" on the right, then "Ψ" would be + * encoded as "HTTP", which may cause special behavior). The latter would not + * actually be a huge issue for bold/italic, but may be if we later use it + * in other places as well. + * */ + (function () { + var lettersThatJavaScriptDoesNotKnowAndQ = /[Q\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376-\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0523\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0621-\u064a\u0660-\u0669\u066e-\u066f\u0671-\u06d3\u06d5\u06e5-\u06e6\u06ee-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07c0-\u07ea\u07f4-\u07f5\u07fa\u0904-\u0939\u093d\u0950\u0958-\u0961\u0966-\u096f\u0971-\u0972\u097b-\u097f\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc-\u09dd\u09df-\u09e1\u09e6-\u09f1\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a66-\u0a6f\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0-\u0ae1\u0ae6-\u0aef\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b66-\u0b6f\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0be6-\u0bef\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58-\u0c59\u0c60-\u0c61\u0c66-\u0c6f\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0-\u0ce1\u0ce6-\u0cef\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d3d\u0d60-\u0d61\u0d66-\u0d6f\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32-\u0e33\u0e40-\u0e46\u0e50-\u0e59\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0ed0-\u0ed9\u0edc-\u0edd\u0f00\u0f20-\u0f29\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8b\u1000-\u102a\u103f-\u1049\u1050-\u1055\u105a-\u105d\u1061\u1065-\u1066\u106e-\u1070\u1075-\u1081\u108e\u1090-\u1099\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1159\u115f-\u11a2\u11a8-\u11f9\u1200-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u1676\u1681-\u169a\u16a0-\u16ea\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u17e0-\u17e9\u1810-\u1819\u1820-\u1877\u1880-\u18a8\u18aa\u1900-\u191c\u1946-\u196d\u1970-\u1974\u1980-\u19a9\u19c1-\u19c7\u19d0-\u19d9\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b50-\u1b59\u1b83-\u1ba0\u1bae-\u1bb9\u1c00-\u1c23\u1c40-\u1c49\u1c4d-\u1c7d\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u203f-\u2040\u2054\u2071\u207f\u2090-\u2094\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2183-\u2184\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2c6f\u2c71-\u2c7d\u2c80-\u2ce4\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3006\u3031-\u3035\u303b-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31b7\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fc3\ua000-\ua48c\ua500-\ua60c\ua610-\ua62b\ua640-\ua65f\ua662-\ua66e\ua67f-\ua697\ua717-\ua71f\ua722-\ua788\ua78b-\ua78c\ua7fb-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8d0-\ua8d9\ua900-\ua925\ua930-\ua946\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa50-\uaa59\uac00-\ud7a3\uf900-\ufa2d\ufa30-\ufa6a\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe33-\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]/g; + var cp_Q = "Q".charCodeAt(0); + var cp_A = "A".charCodeAt(0); + var cp_Z = "Z".charCodeAt(0); + var dist_Za = "a".charCodeAt(0) - cp_Z - 1; + + asciify = function(text) { + return text.replace(lettersThatJavaScriptDoesNotKnowAndQ, function (m) { + var c = m.charCodeAt(0); + var s = ""; + var v; + while (c > 0) { + v = (c % 51) + cp_A; + if (v >= cp_Q) + v++; + if (v > cp_Z) + v += dist_Za; + s = String.fromCharCode(v) + s; + c = c / 51 | 0; + } + return "Q" + s + "Q"; + }) + }; + + deasciify = function(text) { + return text.replace(/Q([A-PR-Za-z]{1,3})Q/g, function (m, s) { + var c = 0; + var v; + for (var i = 0; i < s.length; i++) { + v = s.charCodeAt(i); + if (v > cp_Z) + v -= dist_Za; + if (v > cp_Q) + v--; + v -= cp_A; + c = (c * 51) + v; + } + return String.fromCharCode(c); + }) + } + })(); + } + + var _DoItalicsAndBold = OPTIONS.asteriskIntraWordEmphasis ? _DoItalicsAndBold_AllowIntrawordWithAsterisk : _DoItalicsAndBoldStrict; + + this.makeHtml = function (text) { + + // + // Main function. The order in which other subs are called here is + // essential. Link and image substitutions need to happen before + // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the + // and tags get encoded. + // + + // This will only happen if makeHtml on the same converter instance is called from a plugin hook. + // Don't do that. + if (g_urls) + throw new Error("Recursive call to converter.makeHtml"); + + // Create the private state objects. + g_urls = new SaveHash(); + g_titles = new SaveHash(); + g_html_blocks = []; + g_list_level = 0; + + text = pluginHooks.preConversion(text); + + // attacklab: Replace ~ with ~T + // This lets us use tilde as an escape char to avoid md5 hashes + // The choice of character is arbitray; anything that isn't + // magic in Markdown will work. + text = text.replace(/~/g, "~T"); + + // attacklab: Replace $ with ~D + // RegExp interprets $ as a special character + // when it's in a replacement string + text = text.replace(/\$/g, "~D"); + + // Standardize line endings + text = text.replace(/\r\n/g, "\n"); // DOS to Unix + text = text.replace(/\r/g, "\n"); // Mac to Unix + + // Make sure text begins and ends with a couple of newlines: + text = "\n\n" + text + "\n\n"; + + // Convert all tabs to spaces. + text = _Detab(text); + + // Strip any lines consisting only of spaces and tabs. + // This makes subsequent regexen easier to write, because we can + // match consecutive blank lines with /\n+/ instead of something + // contorted like /[ \t]*\n+/ . + text = text.replace(/^[ \t]+$/mg, ""); + + text = pluginHooks.postNormalization(text); + + // Turn block-level HTML blocks into hash entries + text = _HashHTMLBlocks(text); + + // Strip link definitions, store in hashes. + text = _StripLinkDefinitions(text); + + text = _RunBlockGamut(text); + + text = _UnescapeSpecialChars(text); + + // attacklab: Restore dollar signs + text = text.replace(/~D/g, "$$"); + + // attacklab: Restore tildes + text = text.replace(/~T/g, "~"); + + text = pluginHooks.postConversion(text); + + g_html_blocks = g_titles = g_urls = null; + + return text; + }; + + function _StripLinkDefinitions(text) { + // + // Strips link definitions from text, stores the URLs and titles in + // hash references. + // + + // Link defs are in the form: ^[id]: url "optional title" + + /* + text = text.replace(/ + ^[ ]{0,3}\[([^\[\]]+)\]: // id = $1 attacklab: g_tab_width - 1 + [ \t]* + \n? // maybe *one* newline + [ \t]* + ? // url = $2 + (?=\s|$) // lookahead for whitespace instead of the lookbehind removed below + [ \t]* + \n? // maybe one newline + [ \t]* + ( // (potential) title = $3 + (\n*) // any lines skipped = $4 attacklab: lookbehind removed + [ \t]+ + ["(] + (.+?) // title = $5 + [")] + [ \t]* + )? // title is optional + (\n+) // subsequent newlines = $6, capturing because they must be put back if the potential title isn't an actual title + /gm, function(){...}); + */ + + text = text.replace(/^[ ]{0,3}\[([^\[\]]+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(\n+)/gm, + function (wholeMatch, m1, m2, m3, m4, m5, m6) { + m1 = m1.toLowerCase(); + g_urls.set(m1, _EncodeAmpsAndAngles(m2)); // Link IDs are case-insensitive + if (m4) { + // Oops, found blank lines, so it's not a title. + // Put back the parenthetical statement we stole. + return m3 + m6; + } else if (m5) { + g_titles.set(m1, m5.replace(/"/g, """)); + } + + // Completely remove the definition from the text + return ""; + } + ); + + return text; + } + + function _HashHTMLBlocks(text) { + + // Hashify HTML blocks: + // We only want to do this for block-level HTML tags, such as headers, + // lists, and tables. That's because we still want to wrap

s around + // "paragraphs" that are wrapped in non-block-level tags, such as anchors, + // phrase emphasis, and spans. The list of tags we're looking for is + // hard-coded: + var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del" + var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math" + + // First, look for nested blocks, e.g.: + //

+ //
+ // tags for inner block must be indented. + //
+ //
+ // + // The outermost tags must start at the left margin for this to match, and + // the inner nested divs must be indented. + // We need to do this before the next, more liberal match, because the next + // match will start at the first `
` and stop at the first `
`. + + // attacklab: This regex can be expensive when it fails. + + /* + text = text.replace(/ + ( // save in $1 + ^ // start of line (with /m) + <($block_tags_a) // start tag = $2 + \b // word break + // attacklab: hack around khtml/pcre bug... + [^\r]*?\n // any number of lines, minimally matching + // the matching end tag + [ \t]* // trailing spaces/tabs + (?=\n+) // followed by a newline + ) // attacklab: there are sentinel newlines at end of document + /gm,function(){...}}; + */ + text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm, hashMatch); + + // + // Now match more liberally, simply from `\n` to `\n` + // + + /* + text = text.replace(/ + ( // save in $1 + ^ // start of line (with /m) + <($block_tags_b) // start tag = $2 + \b // word break + // attacklab: hack around khtml/pcre bug... + [^\r]*? // any number of lines, minimally matching + .* // the matching end tag + [ \t]* // trailing spaces/tabs + (?=\n+) // followed by a newline + ) // attacklab: there are sentinel newlines at end of document + /gm,function(){...}}; + */ + text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm, hashMatch); + + // Special case just for
. It was easier to make a special case than + // to make the other regex more complicated. + + /* + text = text.replace(/ + \n // Starting after a blank line + [ ]{0,3} + ( // save in $1 + (<(hr) // start tag = $2 + \b // word break + ([^<>])*? + \/?>) // the matching end tag + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashMatch); + */ + text = text.replace(/\n[ ]{0,3}((<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, hashMatch); + + // Special case for standalone HTML comments: + + /* + text = text.replace(/ + \n\n // Starting after a blank line + [ ]{0,3} // attacklab: g_tab_width - 1 + ( // save in $1 + -]|-[^>])(?:[^-]|-[^-])*)--) // see http://www.w3.org/TR/html-markup/syntax.html#comments and http://meta.stackexchange.com/q/95256 + > + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashMatch); + */ + text = text.replace(/\n\n[ ]{0,3}(-]|-[^>])(?:[^-]|-[^-])*)--)>[ \t]*(?=\n{2,}))/g, hashMatch); + + // PHP and ASP-style processor instructions ( and <%...%>) + + /* + text = text.replace(/ + (?: + \n\n // Starting after a blank line + ) + ( // save in $1 + [ ]{0,3} // attacklab: g_tab_width - 1 + (?: + <([?%]) // $2 + [^\r]*? + \2> + ) + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashMatch); + */ + text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, hashMatch); + + return text; + } + + function hashBlock(text) { + text = text.replace(/(^\n+|\n+$)/g, ""); + // Replace the element text with a marker ("~KxK" where x is its key) + return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n"; + } + + function hashMatch(wholeMatch, m1) { + return hashBlock(m1); + } + + var blockGamutHookCallback = function (t) { return _RunBlockGamut(t); } + + function _RunBlockGamut(text, doNotUnhash, doNotCreateParagraphs) { + // + // These are all the transformations that form block-level + // tags like paragraphs, headers, and list items. + // + + text = pluginHooks.preBlockGamut(text, blockGamutHookCallback); + + text = _DoHeaders(text); + + // Do Horizontal Rules: + var replacement = "
\n"; + text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, replacement); + text = text.replace(/^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$/gm, replacement); + text = text.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm, replacement); + + text = _DoLists(text); + text = _DoCodeBlocks(text); + text = _DoBlockQuotes(text); + + text = pluginHooks.postBlockGamut(text, blockGamutHookCallback); + + // We already ran _HashHTMLBlocks() before, in Markdown(), but that + // was to escape raw HTML in the original Markdown source. This time, + // we're escaping the markup we've just created, so that we don't wrap + //

tags around block-level tags. + text = _HashHTMLBlocks(text); + + text = _FormParagraphs(text, doNotUnhash, doNotCreateParagraphs); + + return text; + } + + function _RunSpanGamut(text) { + // + // These are all the transformations that occur *within* block-level + // tags like paragraphs, headers, and list items. + // + + text = pluginHooks.preSpanGamut(text); + + text = _DoCodeSpans(text); + text = _EscapeSpecialCharsWithinTagAttributes(text); + text = _EncodeBackslashEscapes(text); + + // Process anchor and image tags. Images must come first, + // because ![foo][f] looks like an anchor. + text = _DoImages(text); + text = _DoAnchors(text); + + // Make links out of things like `` + // Must come after _DoAnchors(), because you can use < and > + // delimiters in inline links like [this](). + text = _DoAutoLinks(text); + + text = text.replace(/~P/g, "://"); // put in place to prevent autolinking; reset now + + text = _EncodeAmpsAndAngles(text); + text = _DoItalicsAndBold(text); + + // Do hard breaks: + text = text.replace(/ +\n/g, "
\n"); + + text = pluginHooks.postSpanGamut(text); + + return text; + } + + function _EscapeSpecialCharsWithinTagAttributes(text) { + // + // Within tags -- meaning between < and > -- encode [\ ` * _] so they + // don't conflict with their use in Markdown for code, italics and strong. + // + + // Build a regex to find HTML tags and comments. See Friedl's + // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. + + // SE: changed the comment part of the regex + + var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi; + + text = text.replace(regex, function (wholeMatch) { + var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`"); + tag = escapeCharacters(tag, wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"); // also escape slashes in comments to prevent autolinking there -- http://meta.stackexchange.com/questions/95987 + return tag; + }); + + return text; + } + + function _DoAnchors(text) { + + if (text.indexOf("[") === -1) + return text; + + // + // Turn Markdown link shortcuts into XHTML
tags. + // + // + // First, handle reference-style links: [link text] [id] + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ( + (?: + \[[^\]]*\] // allow brackets nested one level + | + [^\[] // or anything else + )* + ) + \] + + [ ]? // one optional space + (?:\n[ ]*)? // one optional newline followed by spaces + + \[ + (.*?) // id = $3 + \] + ) + ()()()() // pad remaining backreferences + /g, writeAnchorTag); + */ + text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag); + + // + // Next, inline-style links: [link text](url "optional title") + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ( + (?: + \[[^\]]*\] // allow brackets nested one level + | + [^\[\]] // or anything else + )* + ) + \] + \( // literal paren + [ \t]* + () // no id, so leave $3 empty + ? + [ \t]* + ( // $5 + (['"]) // quote char = $6 + (.*?) // Title = $7 + \6 // matching quote + [ \t]* // ignore any spaces/tabs between closing quote and ) + )? // title is optional + \) + ) + /g, writeAnchorTag); + */ + + text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag); + + // + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link test][1] + // or [link test](/foo) + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ([^\[\]]+) // link text = $2; can't contain '[' or ']' + \] + ) + ()()()()() // pad rest of backreferences + /g, writeAnchorTag); + */ + text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); + + return text; + } + + function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) { + if (m7 == undefined) m7 = ""; + var whole_match = m1; + var link_text = m2.replace(/:\/\//g, "~P"); // to prevent auto-linking withing the link. will be converted back after the auto-linker runs + var link_id = m3.toLowerCase(); + var url = m4; + var title = m7; + + if (url == "") { + if (link_id == "") { + // lower-case and turn embedded newlines into spaces + link_id = link_text.toLowerCase().replace(/ ?\n/g, " "); + } + url = "#" + link_id; + + if (g_urls.get(link_id) != undefined) { + url = g_urls.get(link_id); + if (g_titles.get(link_id) != undefined) { + title = g_titles.get(link_id); + } + } + else { + if (whole_match.search(/\(\s*\)$/m) > -1) { + // Special case for explicit empty url + url = ""; + } else { + return whole_match; + } + } + } + url = attributeSafeUrl(url); + + var result = ""; + + return result; + } + + function _DoImages(text) { + + if (text.indexOf("![") === -1) + return text; + + // + // Turn Markdown image shortcuts into tags. + // + + // + // First, handle reference-style labeled images: ![alt text][id] + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + !\[ + (.*?) // alt text = $2 + \] + + [ ]? // one optional space + (?:\n[ ]*)? // one optional newline followed by spaces + + \[ + (.*?) // id = $3 + \] + ) + ()()()() // pad rest of backreferences + /g, writeImageTag); + */ + text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag); + + // + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + + /* + text = text.replace(/ + ( // wrap whole match in $1 + !\[ + (.*?) // alt text = $2 + \] + \s? // One optional whitespace character + \( // literal paren + [ \t]* + () // no id, so leave $3 empty + ? // src url = $4 + [ \t]* + ( // $5 + (['"]) // quote char = $6 + (.*?) // title = $7 + \6 // matching quote + [ \t]* + )? // title is optional + \) + ) + /g, writeImageTag); + */ + text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag); + + return text; + } + + function attributeEncode(text) { + // unconditionally replace angle brackets here -- what ends up in an attribute (e.g. alt or title) + // never makes sense to have verbatim HTML in it (and the sanitizer would totally break it) + return text.replace(/>/g, ">").replace(/" + _RunSpanGamut(m1) + "\n\n"; } + ); + + text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, + function (matchFound, m1) { return "

" + _RunSpanGamut(m1) + "

\n\n"; } + ); + + // atx-style headers: + // # Header 1 + // ## Header 2 + // ## Header 2 with closing hashes ## + // ... + // ###### Header 6 + // + + /* + text = text.replace(/ + ^(\#{1,6}) // $1 = string of #'s + [ \t]* + (.+?) // $2 = Header text + [ \t]* + \#* // optional closing #'s (not counted) + \n+ + /gm, function() {...}); + */ + + text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, + function (wholeMatch, m1, m2) { + var h_level = m1.length; + return "" + _RunSpanGamut(m2) + "\n\n"; + } + ); + + return text; + } + + function _DoLists(text, isInsideParagraphlessListItem) { + // + // Form HTML ordered (numbered) and unordered (bulleted) lists. + // + + // attacklab: add sentinel to hack around khtml/safari bug: + // http://bugs.webkit.org/show_bug.cgi?id=11231 + text += "~0"; + + // Re-usable pattern to match any entirel ul or ol list: + + /* + var whole_list = / + ( // $1 = whole list + ( // $2 + [ ]{0,3} // attacklab: g_tab_width - 1 + ([*+-]|\d+[.]) // $3 = first list item marker + [ \t]+ + ) + [^\r]+? + ( // $4 + ~0 // sentinel for workaround; should be $ + | + \n{2,} + (?=\S) + (?! // Negative lookahead for another list item marker + [ \t]* + (?:[*+-]|\d+[.])[ \t]+ + ) + ) + ) + /g + */ + var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; + if (g_list_level) { + text = text.replace(whole_list, function (wholeMatch, m1, m2) { + var list = m1; + var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol"; + var first_number; + if (list_type === "ol") + first_number = parseInt(m2, 10) + + var result = _ProcessListItems(list, list_type, isInsideParagraphlessListItem); + + // Trim any trailing whitespace, to put the closing `` + // up on the preceding line, to get it past the current stupid + // HTML block parser. This is a hack to work around the terrible + // hack that is the HTML block parser. + result = result.replace(/\s+$/, ""); + var opening = "<" + list_type; + if (first_number && first_number !== 1) + opening += " start=\"" + first_number + "\""; + result = opening + ">" + result + "\n"; + return result; + }); + } else { + whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; + text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) { + var runup = m1; + var list = m2; + + var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol"; + + var first_number; + if (list_type === "ol") + first_number = parseInt(m3, 10) + + var result = _ProcessListItems(list, list_type); + var opening = "<" + list_type; + if (first_number && first_number !== 1) + opening += " start=\"" + first_number + "\""; + + result = runup + opening + ">\n" + result + "\n"; + return result; + }); + } + + // attacklab: strip sentinel + text = text.replace(/~0/, ""); + + return text; + } + + var _listItemMarkers = { ol: "\\d+[.]", ul: "[*+-]" }; + + function _ProcessListItems(list_str, list_type, isInsideParagraphlessListItem) { + // + // Process the contents of a single ordered or unordered list, splitting it + // into individual list items. + // + // list_type is either "ul" or "ol". + + // The $g_list_level global keeps track of when we're inside a list. + // Each time we enter a list, we increment it; when we leave a list, + // we decrement. If it's zero, we're not in a list anymore. + // + // We do this because when we're not inside a list, we want to treat + // something like this: + // + // I recommend upgrading to version + // 8. Oops, now this line is treated + // as a sub-list. + // + // As a single paragraph, despite the fact that the second line starts + // with a digit-period-space sequence. + // + // Whereas when we're inside a list (or sub-list), that line will be + // treated as the start of a sub-list. What a kludge, huh? This is + // an aspect of Markdown's syntax that's hard to parse perfectly + // without resorting to mind-reading. Perhaps the solution is to + // change the syntax rules such that sub-lists must start with a + // starting cardinal number; e.g. "1." or "a.". + + g_list_level++; + + // trim trailing blank lines: + list_str = list_str.replace(/\n{2,}$/, "\n"); + + // attacklab: add sentinel to emulate \z + list_str += "~0"; + + // In the original attacklab showdown, list_type was not given to this function, and anything + // that matched /[*+-]|\d+[.]/ would just create the next
  • , causing this mismatch: + // + // Markdown rendered by WMD rendered by MarkdownSharp + // ------------------------------------------------------------------ + // 1. first 1. first 1. first + // 2. second 2. second 2. second + // - third 3. third * third + // + // We changed this to behave identical to MarkdownSharp. This is the constructed RegEx, + // with {MARKER} being one of \d+[.] or [*+-], depending on list_type: + + /* + list_str = list_str.replace(/ + (^[ \t]*) // leading whitespace = $1 + ({MARKER}) [ \t]+ // list marker = $2 + ([^\r]+? // list item text = $3 + (\n+) + ) + (?= + (~0 | \2 ({MARKER}) [ \t]+) + ) + /gm, function(){...}); + */ + + var marker = _listItemMarkers[list_type]; + var re = new RegExp("(^[ \\t]*)(" + marker + ")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1(" + marker + ")[ \\t]+))", "gm"); + var last_item_had_a_double_newline = false; + list_str = list_str.replace(re, + function (wholeMatch, m1, m2, m3) { + var item = m3; + var leading_space = m1; + var ends_with_double_newline = /\n\n$/.test(item); + var contains_double_newline = ends_with_double_newline || item.search(/\n{2,}/) > -1; + + var loose = contains_double_newline || last_item_had_a_double_newline; + item = _RunBlockGamut(_Outdent(item), /* doNotUnhash = */true, /* doNotCreateParagraphs = */ !loose); + + last_item_had_a_double_newline = ends_with_double_newline; + return "
  • " + item + "
  • \n"; + } + ); + + // attacklab: strip sentinel + list_str = list_str.replace(/~0/g, ""); + + g_list_level--; + return list_str; + } + + function _DoCodeBlocks(text) { + // + // Process Markdown `
    ` blocks.
    +            //  
    +
    +            /*
    +            text = text.replace(/
    +                (?:\n\n|^)
    +                (                               // $1 = the code block -- one or more lines, starting with a space/tab
    +                    (?:
    +                        (?:[ ]{4}|\t)           // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
    +                        .*\n+
    +                    )+
    +                )
    +                (\n*[ ]{0,3}[^ \t\n]|(?=~0))    // attacklab: g_tab_width
    +            /g ,function(){...});
    +            */
    +
    +            // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
    +            text += "~0";
    +
    +            text = text.replace(/(?:\n\n|^\n?)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
    +                function (wholeMatch, m1, m2) {
    +                    var codeblock = m1;
    +                    var nextChar = m2;
    +
    +                    codeblock = _EncodeCode(_Outdent(codeblock));
    +                    codeblock = _Detab(codeblock);
    +                    codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
    +                    codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
    +
    +                    codeblock = "
    " + codeblock + "\n
    "; + + return "\n\n" + codeblock + "\n\n" + nextChar; + } + ); + + // attacklab: strip sentinel + text = text.replace(/~0/, ""); + + return text; + } + + function _DoCodeSpans(text) { + // + // * Backtick quotes are used for spans. + // + // * You can use multiple backticks as the delimiters if you want to + // include literal backticks in the code span. So, this input: + // + // Just type ``foo `bar` baz`` at the prompt. + // + // Will translate to: + // + //

    Just type foo `bar` baz at the prompt.

    + // + // There's no arbitrary limit to the number of backticks you + // can use as delimters. If you need three consecutive backticks + // in your code, use four for delimiters, etc. + // + // * You can use spaces to get literal backticks at the edges: + // + // ... type `` `bar` `` ... + // + // Turns to: + // + // ... type `bar` ... + // + + /* + text = text.replace(/ + (^|[^\\`]) // Character before opening ` can't be a backslash or backtick + (`+) // $2 = Opening run of ` + (?!`) // and no more backticks -- match the full run + ( // $3 = The code block + [^\r]*? + [^`] // attacklab: work around lack of lookbehind + ) + \2 // Matching closer + (?!`) + /gm, function(){...}); + */ + + text = text.replace(/(^|[^\\`])(`+)(?!`)([^\r]*?[^`])\2(?!`)/gm, + function (wholeMatch, m1, m2, m3, m4) { + var c = m3; + c = c.replace(/^([ \t]*)/g, ""); // leading whitespace + c = c.replace(/[ \t]*$/g, ""); // trailing whitespace + c = _EncodeCode(c); + c = c.replace(/:\/\//g, "~P"); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. Will be converted back after the auto-linker runs. + return m1 + "" + c + ""; + } + ); + + return text; + } + + function _EncodeCode(text) { + // + // Encode/escape certain characters inside Markdown code runs. + // The point is that in code, these characters are literals, + // and lose their special Markdown meanings. + // + // Encode all ampersands; HTML entities are not + // entities within a Markdown code span. + text = text.replace(/&/g, "&"); + + // Do the angle bracket song and dance: + text = text.replace(//g, ">"); + + // Now, escape characters that are magic in Markdown: + text = escapeCharacters(text, "\*_{}[]\\", false); + + // jj the line above breaks this: + //--- + + //* Item + + // 1. Subitem + + // special char: * + //--- + + return text; + } + + function _DoItalicsAndBoldStrict(text) { + + if (text.indexOf("*") === -1 && text.indexOf("_") === - 1) + return text; + + text = asciify(text); + + // must go first: + + // (^|[\W_]) Start with a non-letter or beginning of string. Store in \1. + // (?:(?!\1)|(?=^)) Either the next character is *not* the same as the previous, + // or we started at the end of the string (in which case the previous + // group had zero width, so we're still there). Because the next + // character is the marker, this means that if there are e.g. multiple + // underscores in a row, we can only match the left-most ones (which + // prevents foo___bar__ from getting bolded) + // (\*|_) The marker character itself, asterisk or underscore. Store in \2. + // \2 The marker again, since bold needs two. + // (?=\S) The first bolded character cannot be a space. + // ([^\r]*?\S) The actual bolded string. At least one character, and it cannot *end* + // with a space either. Note that like in many other places, [^\r] is + // just a workaround for JS' lack of single-line regexes; it's equivalent + // to a . in an /s regex, because the string cannot contain any \r (they + // are removed in the normalizing step). + // \2\2 The marker character, twice -- end of bold. + // (?!\2) Not followed by another marker character (ensuring that we match the + // rightmost two in a longer row)... + // (?=[\W_]|$) ...but by any other non-word character or the end of string. + text = text.replace(/(^|[\W_])(?:(?!\1)|(?=^))(\*|_)\2(?=\S)([^\r]*?\S)\2\2(?!\2)(?=[\W_]|$)/g, + "$1$3"); + + // This is almost identical to the regex, except 1) there's obviously just one marker + // character, and 2) the italicized string cannot contain the marker character. + text = text.replace(/(^|[\W_])(?:(?!\1)|(?=^))(\*|_)(?=\S)((?:(?!\2)[^\r])*?\S)\2(?!\2)(?=[\W_]|$)/g, + "$1$3"); + + return deasciify(text); + } + + function _DoItalicsAndBold_AllowIntrawordWithAsterisk(text) { + + if (text.indexOf("*") === -1 && text.indexOf("_") === - 1) + return text; + + text = asciify(text); + + // must go first: + // (?=[^\r][*_]|[*_]) Optimization only, to find potentially relevant text portions faster. Minimally slower in Chrome, but much faster in IE. + // ( Store in \1. This is the last character before the delimiter + // ^ Either we're at the start of the string (i.e. there is no last character)... + // | ... or we allow one of the following: + // (?= (lookahead; we're not capturing this, just listing legal possibilities) + // \W__ If the delimiter is __, then this last character must be non-word non-underscore (extra-word emphasis only) + // | + // (?!\*)[\W_]\*\* If the delimiter is **, then this last character can be non-word non-asterisk (extra-word emphasis)... + // | + // \w\*\*\w ...or it can be word/underscore, but only if the first bolded character is such a character as well (intra-word emphasis) + // ) + // [^\r] actually capture the character (can't use `.` since it could be \n) + // ) + // (\*\*|__) Store in \2: the actual delimiter + // (?!\2) not followed by the delimiter again (at most one more asterisk/underscore is allowed) + // (?=\S) the first bolded character can't be a space + // ( Store in \3: the bolded string + // + // (?:| Look at all bolded characters except for the last one. Either that's empty, meaning only a single character was bolded... + // [^\r]*? ... otherwise take arbitrary characters, minimally matching; that's all bolded characters except for the last *two* + // (?!\2) the last two characters cannot be the delimiter itself (because that would mean four underscores/asterisks in a row) + // [^\r] capture the next-to-last bolded character + // ) + // (?= lookahead at the very last bolded char and what comes after + // \S_ for underscore-bolding, it can be any non-space + // | + // \w for asterisk-bolding (otherwise the previous alternative would've matched, since \w implies \S), either the last char is word/underscore... + // | + // \S\*\*(?:[\W_]|$) ... or it's any other non-space, but in that case the character *after* the delimiter may not be a word character + // ) + // . actually capture the last character (can use `.` this time because the lookahead ensures \S in all cases) + // ) + // (?= lookahead; list the legal possibilities for the closing delimiter and its following character + // __(?:\W|$) for underscore-bolding, the following character (if any) must be non-word non-underscore + // | + // \*\*(?:[^*]|$) for asterisk-bolding, any non-asterisk is allowed (note we already ensured above that it's not a word character if the last bolded character wasn't one) + // ) + // \2 actually capture the closing delimiter (and make sure that it matches the opening one) + + text = text.replace(/(?=[^\r][*_]|[*_])(^|(?=\W__|(?!\*)[\W_]\*\*|\w\*\*\w)[^\r])(\*\*|__)(?!\2)(?=\S)((?:|[^\r]*?(?!\2)[^\r])(?=\S_|\w|\S\*\*(?:[\W_]|$)).)(?=__(?:\W|$)|\*\*(?:[^*]|$))\2/g, + "$1$3"); + + // now : + // (?=[^\r][*_]|[*_]) Optimization, see above. + // ( Store in \1. This is the last character before the delimiter + // ^ Either we're at the start of the string (i.e. there is no last character)... + // | ... or we allow one of the following: + // (?= (lookahead; we're not capturing this, just listing legal possibilities) + // \W_ If the delimiter is _, then this last character must be non-word non-underscore (extra-word emphasis only) + // | + // (?!\*) otherwise, we list two possiblities for * as the delimiter; in either case, the last characters cannot be an asterisk itself + // (?: + // [\W_]\* this last character can be non-word (extra-word emphasis)... + // | + // \D\*(?=\w)\D ...or it can be word (otherwise the first alternative would've matched), but only if + // a) the first italicized character is such a character as well (intra-word emphasis), and + // b) neither character on either side of the asterisk is a digit + // ) + // ) + // [^\r] actually capture the character (can't use `.` since it could be \n) + // ) + // (\*|_) Store in \2: the actual delimiter + // (?!\2\2\2) not followed by more than two more instances of the delimiter + // (?=\S) the first italicized character can't be a space + // ( Store in \3: the italicized string + // (?:(?!\2)[^\r])*? arbitrary characters except for the delimiter itself, minimally matching + // (?= lookahead at the very last italicized char and what comes after + // [^\s_]_ for underscore-italicizing, it can be any non-space non-underscore + // | + // (?=\w)\D\*\D for asterisk-italicizing, either the last char is word/underscore *and* neither character on either side of the asterisk is a digit... + // | + // [^\s*]\*(?:[\W_]|$) ... or that last char is any other non-space non-asterisk, but then the character after the delimiter (if any) must be non-word + // ) + // . actually capture the last character (can use `.` this time because the lookahead ensures \S in all cases) + // ) + // (?= lookahead; list the legal possibilities for the closing delimiter and its following character + // _(?:\W|$) for underscore-italicizing, the following character (if any) must be non-word non-underscore + // | + // \*(?:[^*]|$) for asterisk-italicizing, any non-asterisk is allowed; all other restrictions have already been ensured in the previous lookahead + // ) + // \2 actually capture the closing delimiter (and make sure that it matches the opening one) + + text = text.replace(/(?=[^\r][*_]|[*_])(^|(?=\W_|(?!\*)(?:[\W_]\*|\D\*(?=\w)\D))[^\r])(\*|_)(?!\2\2\2)(?=\S)((?:(?!\2)[^\r])*?(?=[^\s_]_|(?=\w)\D\*\D|[^\s*]\*(?:[\W_]|$)).)(?=_(?:\W|$)|\*(?:[^*]|$))\2/g, + "$1$3"); + + return deasciify(text); + } + + + function _DoBlockQuotes(text) { + + /* + text = text.replace(/ + ( // Wrap whole match in $1 + ( + ^[ \t]*>[ \t]? // '>' at the start of a line + .+\n // rest of the first line + (.+\n)* // subsequent consecutive lines + \n* // blanks + )+ + ) + /gm, function(){...}); + */ + + text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, + function (wholeMatch, m1) { + var bq = m1; + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + + bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting + + // attacklab: clean up hack + bq = bq.replace(/~0/g, ""); + + bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines + bq = _RunBlockGamut(bq); // recurse + + bq = bq.replace(/(^|\n)/g, "$1 "); + // These leading spaces screw with
     content, so we need to fix that:
    +                    bq = bq.replace(
    +                            /(\s*
    [^\r]+?<\/pre>)/gm,
    +                        function (wholeMatch, m1) {
    +                            var pre = m1;
    +                            // attacklab: hack around Konqueror 3.5.4 bug:
    +                            pre = pre.replace(/^  /mg, "~0");
    +                            pre = pre.replace(/~0/g, "");
    +                            return pre;
    +                        });
    +
    +                    return hashBlock("
    \n" + bq + "\n
    "); + } + ); + return text; + } + + function _FormParagraphs(text, doNotUnhash, doNotCreateParagraphs) { + // + // Params: + // $text - string to process with html

    tags + // + + // Strip leading and trailing lines: + text = text.replace(/^\n+/g, ""); + text = text.replace(/\n+$/g, ""); + + var grafs = text.split(/\n{2,}/g); + var grafsOut = []; + + var markerRe = /~K(\d+)K/; + + // + // Wrap

    tags. + // + var end = grafs.length; + for (var i = 0; i < end; i++) { + var str = grafs[i]; + + // if this is an HTML marker, copy it + if (markerRe.test(str)) { + grafsOut.push(str); + } + else if (/\S/.test(str)) { + str = _RunSpanGamut(str); + str = str.replace(/^([ \t]*)/g, doNotCreateParagraphs ? "" : "

    "); + if (!doNotCreateParagraphs) + str += "

    " + grafsOut.push(str); + } + + } + // + // Unhashify HTML blocks + // + if (!doNotUnhash) { + end = grafsOut.length; + for (var i = 0; i < end; i++) { + var foundAny = true; + while (foundAny) { // we may need several runs, since the data may be nested + foundAny = false; + grafsOut[i] = grafsOut[i].replace(/~K(\d+)K/g, function (wholeMatch, id) { + foundAny = true; + return g_html_blocks[id]; + }); + } + } + } + return grafsOut.join("\n\n"); + } + + function _EncodeAmpsAndAngles(text) { + // Smart processing for ampersands and angle brackets that need to be encoded. + + // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: + // http://bumppo.net/projects/amputator/ + text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&"); + + // Encode naked <'s + text = text.replace(/<(?![a-z\/?!]|~D)/gi, "<"); + + return text; + } + + function _EncodeBackslashEscapes(text) { + // + // Parameter: String. + // Returns: The string, with after processing the following backslash + // escape sequences. + // + + // attacklab: The polite way to do this is with the new + // escapeCharacters() function: + // + // text = escapeCharacters(text,"\\",true); + // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); + // + // ...but we're sidestepping its use of the (slow) RegExp constructor + // as an optimization for Firefox. This function gets called a LOT. + + text = text.replace(/\\(\\)/g, escapeCharacters_callback); + text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback); + return text; + } + + var charInsideUrl = "[-A-Z0-9+&@#/%?=~_|[\\]()!:,.;]", + charEndingUrl = "[-A-Z0-9+&@#/%=~_|[\\])]", + autoLinkRegex = new RegExp("(=\"|<)?\\b(https?|ftp)(://" + charInsideUrl + "*" + charEndingUrl + ")(?=$|\\W)", "gi"), + endCharRegex = new RegExp(charEndingUrl, "i"); + + function handleTrailingParens(wholeMatch, lookbehind, protocol, link) { + if (lookbehind) + return wholeMatch; + if (link.charAt(link.length - 1) !== ")") + return "<" + protocol + link + ">"; + var parens = link.match(/[()]/g); + var level = 0; + for (var i = 0; i < parens.length; i++) { + if (parens[i] === "(") { + if (level <= 0) + level = 1; + else + level++; + } + else { + level--; + } + } + var tail = ""; + if (level < 0) { + var re = new RegExp("\\){1," + (-level) + "}$"); + link = link.replace(re, function (trailingParens) { + tail = trailingParens; + return ""; + }); + } + if (tail) { + var lastChar = link.charAt(link.length - 1); + if (!endCharRegex.test(lastChar)) { + tail = lastChar + tail; + link = link.substr(0, link.length - 1); + } + } + return "<" + protocol + link + ">" + tail; + } + + function _DoAutoLinks(text) { + + // note that at this point, all other URL in the text are already hyperlinked as
    + // *except* for the case + + // automatically add < and > around unadorned raw hyperlinks + // must be preceded by a non-word character (and not by =" or <) and followed by non-word/EOF character + // simulating the lookbehind in a consuming way is okay here, since a URL can neither and with a " nor + // with a <, so there is no risk of overlapping matches. + text = text.replace(autoLinkRegex, handleTrailingParens); + + // autolink anything like + + + var replacer = function (wholematch, m1) { + var url = attributeSafeUrl(m1); + + return "" + pluginHooks.plainLinkText(m1) + ""; + }; + text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer); + + // Email addresses: + /* + text = text.replace(/ + < + (?:mailto:)? + ( + [-.\w]+ + \@ + [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ + ) + > + /gi, _DoAutoLinks_callback()); + */ + + /* disabling email autolinking, since we don't do that on the server, either + text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, + function(wholeMatch,m1) { + return _EncodeEmailAddress( _UnescapeSpecialChars(m1) ); + } + ); + */ + return text; + } + + function _UnescapeSpecialChars(text) { + // + // Swap back in all the special characters we've hidden. + // + text = text.replace(/~E(\d+)E/g, + function (wholeMatch, m1) { + var charCodeToReplace = parseInt(m1); + return String.fromCharCode(charCodeToReplace); + } + ); + return text; + } + + function _Outdent(text) { + // + // Remove one level of line-leading tabs or spaces + // + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + + text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width + + // attacklab: clean up hack + text = text.replace(/~0/g, "") + + return text; + } + + function _Detab(text) { + if (!/\t/.test(text)) + return text; + + var spaces = [" ", " ", " ", " "], + skew = 0, + v; + + return text.replace(/[\n\t]/g, function (match, offset) { + if (match === "\n") { + skew = offset + 1; + return match; + } + v = (offset - skew) % 4; + skew = offset + 1; + return spaces[v]; + }); + } + + // + // attacklab: Utility functions + // + + function attributeSafeUrl(url) { + url = attributeEncode(url); + url = escapeCharacters(url, "*_:()[]") + return url; + } + + function escapeCharacters(text, charsToEscape, afterBackslash) { + // First we have to escape the escape characters so that + // we can build a character class out of them + var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])"; + + if (afterBackslash) { + regexString = "\\\\" + regexString; + } + + var regex = new RegExp(regexString, "g"); + text = text.replace(regex, escapeCharacters_callback); + + return text; + } + + + function escapeCharacters_callback(wholeMatch, m1) { + var charCodeToEscape = m1.charCodeAt(0); + return "~E" + charCodeToEscape + "E"; + } + + }; // end of the Markdown.Converter constructor + +})(); \ No newline at end of file diff --git a/ansible/idr-playbooks/files/html/Markdown.Editor.js b/ansible/idr-playbooks/files/html/Markdown.Editor.js new file mode 100644 index 000000000..2f71291fc --- /dev/null +++ b/ansible/idr-playbooks/files/html/Markdown.Editor.js @@ -0,0 +1,2301 @@ +// needs Markdown.Converter.js at the moment + +(function () { + + var util = {}, + position = {}, + ui = {}, + doc = window.document, + re = window.RegExp, + nav = window.navigator, + SETTINGS = { lineLength: 72 }, + + // Used to work around some browser bugs where we can't use feature testing. + uaSniffed = { + isIE: /msie/.test(nav.userAgent.toLowerCase()), + isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()), + isOpera: /opera/.test(nav.userAgent.toLowerCase()) + }; + + var defaultsStrings = { + bold: "Strong Ctrl+B", + boldexample: "strong text", + + italic: "Emphasis Ctrl+I", + italicexample: "emphasized text", + + link: "Hyperlink Ctrl+L", + linkdescription: "enter link description here", + linkdialog: "

    Insert Hyperlink

    http://example.com/ \"optional title\"

    ", + + quote: "Blockquote
    Ctrl+Q", + quoteexample: "Blockquote", + + code: "Code Sample
     Ctrl+K",
    +        codeexample: "enter code here",
    +
    +        image: "Image  Ctrl+G",
    +        imagedescription: "enter image description here",
    +        imagedialog: "

    Insert Image

    http://example.com/images/diagram.jpg \"optional title\"

    Need
    free image hosting?

    ", + + olist: "Numbered List
      Ctrl+O", + ulist: "Bulleted List
        Ctrl+U", + litem: "List item", + + heading: "Heading

        /

        Ctrl+H", + headingexample: "Heading", + + hr: "Horizontal Rule
        Ctrl+R", + + undo: "Undo - Ctrl+Z", + redo: "Redo - Ctrl+Y", + redomac: "Redo - Ctrl+Shift+Z", + + help: "Markdown Editing Help" + }; + + + // ------------------------------------------------------------------- + // YOUR CHANGES GO HERE + // + // I've tried to localize the things you are likely to change to + // this area. + // ------------------------------------------------------------------- + + // The default text that appears in the dialog input box when entering + // links. + var imageDefaultText = "http://"; + var linkDefaultText = "http://"; + + // ------------------------------------------------------------------- + // END OF YOUR CHANGES + // ------------------------------------------------------------------- + + // options, if given, can have the following properties: + // options.helpButton = { handler: yourEventHandler } + // options.strings = { italicexample: "slanted text" } + // `yourEventHandler` is the click handler for the help button. + // If `options.helpButton` isn't given, not help button is created. + // `options.strings` can have any or all of the same properties as + // `defaultStrings` above, so you can just override some string displayed + // to the user on a case-by-case basis, or translate all strings to + // a different language. + // + // For backwards compatibility reasons, the `options` argument can also + // be just the `helpButton` object, and `strings.help` can also be set via + // `helpButton.title`. This should be considered legacy. + // + // The constructed editor object has the methods: + // - getConverter() returns the markdown converter object that was passed to the constructor + // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op. + // - refreshPreview() forces the preview to be updated. This method is only available after run() was called. + Markdown.Editor = function (markdownConverter, idPostfix, options) { + + options = options || {}; + + if (typeof options.handler === "function") { //backwards compatible behavior + options = { helpButton: options }; + } + options.strings = options.strings || {}; + if (options.helpButton) { + options.strings.help = options.strings.help || options.helpButton.title; + } + var getString = function (identifier) { return options.strings[identifier] || defaultsStrings[identifier]; } + + idPostfix = idPostfix || ""; + + var hooks = this.hooks = new Markdown.HookCollection(); + hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed + hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text + hooks.addFalse("insertImageDialog"); /* called with one parameter: a callback to be called with the URL of the image. If the application creates + * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen + * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used. + */ + + this.getConverter = function () { return markdownConverter; } + + var that = this, + panels; + + this.run = function () { + if (panels) + return; // already initialized + + panels = new PanelCollection(idPostfix); + var commandManager = new CommandManager(hooks, getString, markdownConverter); + var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); }); + var undoManager, uiManager; + + if (!/\?noundo/.test(doc.location.href)) { + undoManager = new UndoManager(function () { + previewManager.refresh(); + if (uiManager) // not available on the first call + uiManager.setUndoRedoButtonStates(); + }, panels); + this.textOperation = function (f) { + undoManager.setCommandMode(); + f(); + that.refreshPreview(); + } + } + + uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, options.helpButton, getString); + uiManager.setUndoRedoButtonStates(); + + var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); }; + + forceRefresh(); + }; + + } + + // before: contains all the text in the input box BEFORE the selection. + // after: contains all the text in the input box AFTER the selection. + function Chunks() { } + + // startRegex: a regular expression to find the start tag + // endRegex: a regular expresssion to find the end tag + Chunks.prototype.findTags = function (startRegex, endRegex) { + + var chunkObj = this; + var regex; + + if (startRegex) { + + regex = util.extendRegExp(startRegex, "", "$"); + + this.before = this.before.replace(regex, + function (match) { + chunkObj.startTag = chunkObj.startTag + match; + return ""; + }); + + regex = util.extendRegExp(startRegex, "^", ""); + + this.selection = this.selection.replace(regex, + function (match) { + chunkObj.startTag = chunkObj.startTag + match; + return ""; + }); + } + + if (endRegex) { + + regex = util.extendRegExp(endRegex, "", "$"); + + this.selection = this.selection.replace(regex, + function (match) { + chunkObj.endTag = match + chunkObj.endTag; + return ""; + }); + + regex = util.extendRegExp(endRegex, "^", ""); + + this.after = this.after.replace(regex, + function (match) { + chunkObj.endTag = match + chunkObj.endTag; + return ""; + }); + } + }; + + // If remove is false, the whitespace is transferred + // to the before/after regions. + // + // If remove is true, the whitespace disappears. + Chunks.prototype.trimWhitespace = function (remove) { + var beforeReplacer, afterReplacer, that = this; + if (remove) { + beforeReplacer = afterReplacer = ""; + } else { + beforeReplacer = function (s) { that.before += s; return ""; } + afterReplacer = function (s) { that.after = s + that.after; return ""; } + } + + this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer); + }; + + + Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) { + + if (nLinesBefore === undefined) { + nLinesBefore = 1; + } + + if (nLinesAfter === undefined) { + nLinesAfter = 1; + } + + nLinesBefore++; + nLinesAfter++; + + var regexText; + var replacementText; + + // chrome bug ... documented at: http://meta.stackexchange.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985 + if (navigator.userAgent.match(/Chrome/)) { + "X".match(/()./); + } + + this.selection = this.selection.replace(/(^\n*)/, ""); + + this.startTag = this.startTag + re.$1; + + this.selection = this.selection.replace(/(\n*$)/, ""); + this.endTag = this.endTag + re.$1; + this.startTag = this.startTag.replace(/(^\n*)/, ""); + this.before = this.before + re.$1; + this.endTag = this.endTag.replace(/(\n*$)/, ""); + this.after = this.after + re.$1; + + if (this.before) { + + regexText = replacementText = ""; + + while (nLinesBefore--) { + regexText += "\\n?"; + replacementText += "\n"; + } + + if (findExtraNewlines) { + regexText = "\\n*"; + } + this.before = this.before.replace(new re(regexText + "$", ""), replacementText); + } + + if (this.after) { + + regexText = replacementText = ""; + + while (nLinesAfter--) { + regexText += "\\n?"; + replacementText += "\n"; + } + if (findExtraNewlines) { + regexText = "\\n*"; + } + + this.after = this.after.replace(new re(regexText, ""), replacementText); + } + }; + + // end of Chunks + + // A collection of the important regions on the page. + // Cached so we don't have to keep traversing the DOM. + // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around + // this issue: + // Internet explorer has problems with CSS sprite buttons that use HTML + // lists. When you click on the background image "button", IE will + // select the non-existent link text and discard the selection in the + // textarea. The solution to this is to cache the textarea selection + // on the button's mousedown event and set a flag. In the part of the + // code where we need to grab the selection, we check for the flag + // and, if it's set, use the cached area instead of querying the + // textarea. + // + // This ONLY affects Internet Explorer (tested on versions 6, 7 + // and 8) and ONLY on button clicks. Keyboard shortcuts work + // normally since the focus never leaves the textarea. + function PanelCollection(postfix) { + this.buttonBar = doc.getElementById("wmd-button-bar" + postfix); + this.preview = doc.getElementById("wmd-preview" + postfix); + this.input = doc.getElementById("wmd-input" + postfix); + }; + + // Returns true if the DOM element is visible, false if it's hidden. + // Checks if display is anything other than none. + util.isVisible = function (elem) { + + if (window.getComputedStyle) { + // Most browsers + return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none"; + } + else if (elem.currentStyle) { + // IE + return elem.currentStyle["display"] !== "none"; + } + }; + + + // Adds a listener callback to a DOM element which is fired on a specified + // event. + util.addEvent = function (elem, event, listener) { + if (elem.attachEvent) { + // IE only. The "on" is mandatory. + elem.attachEvent("on" + event, listener); + } + else { + // Other browsers. + elem.addEventListener(event, listener, false); + } + }; + + + // Removes a listener callback from a DOM element which is fired on a specified + // event. + util.removeEvent = function (elem, event, listener) { + if (elem.detachEvent) { + // IE only. The "on" is mandatory. + elem.detachEvent("on" + event, listener); + } + else { + // Other browsers. + elem.removeEventListener(event, listener, false); + } + }; + + // Converts \r\n and \r to \n. + util.fixEolChars = function (text) { + text = text.replace(/\r\n/g, "\n"); + text = text.replace(/\r/g, "\n"); + return text; + }; + + // Extends a regular expression. Returns a new RegExp + // using pre + regex + post as the expression. + // Used in a few functions where we have a base + // expression and we want to pre- or append some + // conditions to it (e.g. adding "$" to the end). + // The flags are unchanged. + // + // regex is a RegExp, pre and post are strings. + util.extendRegExp = function (regex, pre, post) { + + if (pre === null || pre === undefined) { + pre = ""; + } + if (post === null || post === undefined) { + post = ""; + } + + var pattern = regex.toString(); + var flags; + + // Replace the flags with empty space and store them. + pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) { + flags = flagsPart; + return ""; + }); + + // Remove the slash delimiters on the regular expression. + pattern = pattern.replace(/(^\/|\/$)/g, ""); + pattern = pre + pattern + post; + + return new re(pattern, flags); + } + + // UNFINISHED + // The assignment in the while loop makes jslint cranky. + // I'll change it to a better loop later. + position.getTop = function (elem, isInner) { + var result = elem.offsetTop; + if (!isInner) { + while (elem = elem.offsetParent) { + result += elem.offsetTop; + } + } + return result; + }; + + position.getHeight = function (elem) { + return elem.offsetHeight || elem.scrollHeight; + }; + + position.getWidth = function (elem) { + return elem.offsetWidth || elem.scrollWidth; + }; + + position.getPageSize = function () { + + var scrollWidth, scrollHeight; + var innerWidth, innerHeight; + + // It's not very clear which blocks work with which browsers. + if (self.innerHeight && self.scrollMaxY) { + scrollWidth = doc.body.scrollWidth; + scrollHeight = self.innerHeight + self.scrollMaxY; + } + else if (doc.body.scrollHeight > doc.body.offsetHeight) { + scrollWidth = doc.body.scrollWidth; + scrollHeight = doc.body.scrollHeight; + } + else { + scrollWidth = doc.body.offsetWidth; + scrollHeight = doc.body.offsetHeight; + } + + if (self.innerHeight) { + // Non-IE browser + innerWidth = self.innerWidth; + innerHeight = self.innerHeight; + } + else if (doc.documentElement && doc.documentElement.clientHeight) { + // Some versions of IE (IE 6 w/ a DOCTYPE declaration) + innerWidth = doc.documentElement.clientWidth; + innerHeight = doc.documentElement.clientHeight; + } + else if (doc.body) { + // Other versions of IE + innerWidth = doc.body.clientWidth; + innerHeight = doc.body.clientHeight; + } + + var maxWidth = Math.max(scrollWidth, innerWidth); + var maxHeight = Math.max(scrollHeight, innerHeight); + return [maxWidth, maxHeight, innerWidth, innerHeight]; + }; + + // Handles pushing and popping TextareaStates for undo/redo commands. + // I should rename the stack variables to list. + function UndoManager(callback, panels) { + + var undoObj = this; + var undoStack = []; // A stack of undo states + var stackPtr = 0; // The index of the current state + var mode = "none"; + var lastState; // The last state + var timer; // The setTimeout handle for cancelling the timer + var inputStateObj; + + // Set the mode for later logic steps. + var setMode = function (newMode, noSave) { + if (mode != newMode) { + mode = newMode; + if (!noSave) { + saveState(); + } + } + + if (!uaSniffed.isIE || mode != "moving") { + timer = setTimeout(refreshState, 1); + } + else { + inputStateObj = null; + } + }; + + var refreshState = function (isInitialState) { + inputStateObj = new TextareaState(panels, isInitialState); + timer = undefined; + }; + + this.setCommandMode = function () { + mode = "command"; + saveState(); + timer = setTimeout(refreshState, 0); + }; + + this.canUndo = function () { + return stackPtr > 1; + }; + + this.canRedo = function () { + if (undoStack[stackPtr + 1]) { + return true; + } + return false; + }; + + // Removes the last state and restores it. + this.undo = function () { + + if (undoObj.canUndo()) { + if (lastState) { + // What about setting state -1 to null or checking for undefined? + lastState.restore(); + lastState = null; + } + else { + undoStack[stackPtr] = new TextareaState(panels); + undoStack[--stackPtr].restore(); + + if (callback) { + callback(); + } + } + } + + mode = "none"; + panels.input.focus(); + refreshState(); + }; + + // Redo an action. + this.redo = function () { + + if (undoObj.canRedo()) { + + undoStack[++stackPtr].restore(); + + if (callback) { + callback(); + } + } + + mode = "none"; + panels.input.focus(); + refreshState(); + }; + + // Push the input area state to the stack. + var saveState = function () { + var currState = inputStateObj || new TextareaState(panels); + + if (!currState) { + return false; + } + if (mode == "moving") { + if (!lastState) { + lastState = currState; + } + return; + } + if (lastState) { + if (undoStack[stackPtr - 1].text != lastState.text) { + undoStack[stackPtr++] = lastState; + } + lastState = null; + } + undoStack[stackPtr++] = currState; + undoStack[stackPtr + 1] = null; + if (callback) { + callback(); + } + }; + + var handleCtrlYZ = function (event) { + + var handled = false; + + if ((event.ctrlKey || event.metaKey) && !event.altKey) { + + // IE and Opera do not support charCode. + var keyCode = event.charCode || event.keyCode; + var keyCodeChar = String.fromCharCode(keyCode); + + switch (keyCodeChar.toLowerCase()) { + + case "y": + undoObj.redo(); + handled = true; + break; + + case "z": + if (!event.shiftKey) { + undoObj.undo(); + } + else { + undoObj.redo(); + } + handled = true; + break; + } + } + + if (handled) { + if (event.preventDefault) { + event.preventDefault(); + } + if (window.event) { + window.event.returnValue = false; + } + return; + } + }; + + // Set the mode depending on what is going on in the input area. + var handleModeChange = function (event) { + + if (!event.ctrlKey && !event.metaKey) { + + var keyCode = event.keyCode; + + if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) { + // 33 - 40: page up/dn and arrow keys + // 63232 - 63235: page up/dn and arrow keys on safari + setMode("moving"); + } + else if (keyCode == 8 || keyCode == 46 || keyCode == 127) { + // 8: backspace + // 46: delete + // 127: delete + setMode("deleting"); + } + else if (keyCode == 13) { + // 13: Enter + setMode("newlines"); + } + else if (keyCode == 27) { + // 27: escape + setMode("escape"); + } + else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) { + // 16-20 are shift, etc. + // 91: left window key + // I think this might be a little messed up since there are + // a lot of nonprinting keys above 20. + setMode("typing"); + } + } + }; + + var setEventHandlers = function () { + util.addEvent(panels.input, "keypress", function (event) { + // keyCode 89: y + // keyCode 90: z + if ((event.ctrlKey || event.metaKey) && !event.altKey && (event.keyCode == 89 || event.keyCode == 90)) { + event.preventDefault(); + } + }); + + var handlePaste = function () { + if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) { + if (timer == undefined) { + mode = "paste"; + saveState(); + refreshState(); + } + } + }; + + util.addEvent(panels.input, "keydown", handleCtrlYZ); + util.addEvent(panels.input, "keydown", handleModeChange); + util.addEvent(panels.input, "mousedown", function () { + setMode("moving"); + }); + + panels.input.onpaste = handlePaste; + panels.input.ondrop = handlePaste; + }; + + var init = function () { + setEventHandlers(); + refreshState(true); + saveState(); + }; + + init(); + } + + // end of UndoManager + + // The input textarea state/contents. + // This is used to implement undo/redo by the undo manager. + function TextareaState(panels, isInitialState) { + + // Aliases + var stateObj = this; + var inputArea = panels.input; + this.init = function () { + if (!util.isVisible(inputArea)) { + return; + } + if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box + return; + } + + this.setInputAreaSelectionStartEnd(); + this.scrollTop = inputArea.scrollTop; + if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) { + this.text = inputArea.value; + } + + } + + // Sets the selected text in the input box after we've performed an + // operation. + this.setInputAreaSelection = function () { + + if (!util.isVisible(inputArea)) { + return; + } + + if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) { + + inputArea.focus(); + inputArea.selectionStart = stateObj.start; + inputArea.selectionEnd = stateObj.end; + inputArea.scrollTop = stateObj.scrollTop; + } + else if (doc.selection) { + + if (doc.activeElement && doc.activeElement !== inputArea) { + return; + } + + inputArea.focus(); + var range = inputArea.createTextRange(); + range.moveStart("character", -inputArea.value.length); + range.moveEnd("character", -inputArea.value.length); + range.moveEnd("character", stateObj.end); + range.moveStart("character", stateObj.start); + range.select(); + } + }; + + this.setInputAreaSelectionStartEnd = function () { + + if (!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) { + + stateObj.start = inputArea.selectionStart; + stateObj.end = inputArea.selectionEnd; + } + else if (doc.selection) { + + stateObj.text = util.fixEolChars(inputArea.value); + + // IE loses the selection in the textarea when buttons are + // clicked. On IE we cache the selection. Here, if something is cached, + // we take it. + var range = panels.ieCachedRange || doc.selection.createRange(); + + var fixedRange = util.fixEolChars(range.text); + var marker = "\x07"; + var markedRange = marker + fixedRange + marker; + range.text = markedRange; + var inputText = util.fixEolChars(inputArea.value); + + range.moveStart("character", -markedRange.length); + range.text = fixedRange; + + stateObj.start = inputText.indexOf(marker); + stateObj.end = inputText.lastIndexOf(marker) - marker.length; + + var len = stateObj.text.length - util.fixEolChars(inputArea.value).length; + + if (len) { + range.moveStart("character", -fixedRange.length); + while (len--) { + fixedRange += "\n"; + stateObj.end += 1; + } + range.text = fixedRange; + } + + if (panels.ieCachedRange) + stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange + + panels.ieCachedRange = null; + + this.setInputAreaSelection(); + } + }; + + // Restore this state into the input area. + this.restore = function () { + + if (stateObj.text != undefined && stateObj.text != inputArea.value) { + inputArea.value = stateObj.text; + } + this.setInputAreaSelection(); + inputArea.scrollTop = stateObj.scrollTop; + }; + + // Gets a collection of HTML chunks from the inptut textarea. + this.getChunks = function () { + + var chunk = new Chunks(); + chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start)); + chunk.startTag = ""; + chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end)); + chunk.endTag = ""; + chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end)); + chunk.scrollTop = stateObj.scrollTop; + + return chunk; + }; + + // Sets the TextareaState properties given a chunk of markdown. + this.setChunks = function (chunk) { + + chunk.before = chunk.before + chunk.startTag; + chunk.after = chunk.endTag + chunk.after; + + this.start = chunk.before.length; + this.end = chunk.before.length + chunk.selection.length; + this.text = chunk.before + chunk.selection + chunk.after; + this.scrollTop = chunk.scrollTop; + }; + this.init(); + }; + + function PreviewManager(converter, panels, previewRefreshCallback) { + + var managerObj = this; + var timeout; + var elapsedTime; + var oldInputText; + var maxDelay = 3000; + var startType = "delayed"; // The other legal value is "manual" + + // Adds event listeners to elements + var setupEvents = function (inputElem, listener) { + + util.addEvent(inputElem, "input", listener); + inputElem.onpaste = listener; + inputElem.ondrop = listener; + + util.addEvent(inputElem, "keypress", listener); + util.addEvent(inputElem, "keydown", listener); + }; + + var getDocScrollTop = function () { + + var result = 0; + + if (window.innerHeight) { + result = window.pageYOffset; + } + else + if (doc.documentElement && doc.documentElement.scrollTop) { + result = doc.documentElement.scrollTop; + } + else + if (doc.body) { + result = doc.body.scrollTop; + } + + return result; + }; + + var makePreviewHtml = function () { + + // If there is no registered preview panel + // there is nothing to do. + if (!panels.preview) + return; + + + var text = panels.input.value; + if (text && text == oldInputText) { + return; // Input text hasn't changed. + } + else { + oldInputText = text; + } + + var prevTime = new Date().getTime(); + + text = converter.makeHtml(text); + + // Calculate the processing time of the HTML creation. + // It's used as the delay time in the event listener. + var currTime = new Date().getTime(); + elapsedTime = currTime - prevTime; + + pushPreviewHtml(text); + }; + + // setTimeout is already used. Used as an event listener. + var applyTimeout = function () { + + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + + if (startType !== "manual") { + + var delay = 0; + + if (startType === "delayed") { + delay = elapsedTime; + } + + if (delay > maxDelay) { + delay = maxDelay; + } + timeout = setTimeout(makePreviewHtml, delay); + } + }; + + var getScaleFactor = function (panel) { + if (panel.scrollHeight <= panel.clientHeight) { + return 1; + } + return panel.scrollTop / (panel.scrollHeight - panel.clientHeight); + }; + + var setPanelScrollTops = function () { + if (panels.preview) { + panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview); + } + }; + + this.refresh = function (requiresRefresh) { + + if (requiresRefresh) { + oldInputText = ""; + makePreviewHtml(); + } + else { + applyTimeout(); + } + }; + + this.processingTime = function () { + return elapsedTime; + }; + + var isFirstTimeFilled = true; + + // IE doesn't let you use innerHTML if the element is contained somewhere in a table + // (which is the case for inline editing) -- in that case, detach the element, set the + // value, and reattach. Yes, that *is* ridiculous. + var ieSafePreviewSet = function (text) { + var preview = panels.preview; + var parent = preview.parentNode; + var sibling = preview.nextSibling; + parent.removeChild(preview); + preview.innerHTML = text; + if (!sibling) + parent.appendChild(preview); + else + parent.insertBefore(preview, sibling); + } + + var nonSuckyBrowserPreviewSet = function (text) { + panels.preview.innerHTML = text; + } + + var previewSetter; + + var previewSet = function (text) { + if (previewSetter) + return previewSetter(text); + + try { + nonSuckyBrowserPreviewSet(text); + previewSetter = nonSuckyBrowserPreviewSet; + } catch (e) { + previewSetter = ieSafePreviewSet; + previewSetter(text); + } + }; + + var pushPreviewHtml = function (text) { + + var emptyTop = position.getTop(panels.input) - getDocScrollTop(); + + if (panels.preview) { + previewSet(text); + previewRefreshCallback(); + } + + setPanelScrollTops(); + + if (isFirstTimeFilled) { + isFirstTimeFilled = false; + return; + } + + var fullTop = position.getTop(panels.input) - getDocScrollTop(); + + if (uaSniffed.isIE) { + setTimeout(function () { + window.scrollBy(0, fullTop - emptyTop); + }, 0); + } + else { + window.scrollBy(0, fullTop - emptyTop); + } + }; + + var init = function () { + + setupEvents(panels.input, applyTimeout); + makePreviewHtml(); + + if (panels.preview) { + panels.preview.scrollTop = 0; + } + }; + + init(); + }; + + // Creates the background behind the hyperlink text entry box. + // And download dialog + // Most of this has been moved to CSS but the div creation and + // browser-specific hacks remain here. + ui.createBackground = function () { + + var background = doc.createElement("div"), + style = background.style; + + background.className = "wmd-prompt-background"; + + style.position = "absolute"; + style.top = "0"; + + style.zIndex = "1000"; + + if (uaSniffed.isIE) { + style.filter = "alpha(opacity=50)"; + } + else { + style.opacity = "0.5"; + } + + var pageSize = position.getPageSize(); + style.height = pageSize[1] + "px"; + + if (uaSniffed.isIE) { + style.left = doc.documentElement.scrollLeft; + style.width = doc.documentElement.clientWidth; + } + else { + style.left = "0"; + style.width = "100%"; + } + + doc.body.appendChild(background); + return background; + }; + + // This simulates a modal dialog box and asks for the URL when you + // click the hyperlink or image buttons. + // + // text: The html for the input box. + // defaultInputText: The default value that appears in the input box. + // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel. + // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel + // was chosen). + ui.prompt = function (text, defaultInputText, callback) { + + // These variables need to be declared at this level since they are used + // in multiple functions. + var dialog; // The dialog box. + var input; // The text box where you enter the hyperlink. + + + if (defaultInputText === undefined) { + defaultInputText = ""; + } + + // Used as a keydown event handler. Esc dismisses the prompt. + // Key code 27 is ESC. + var checkEscape = function (key) { + var code = (key.charCode || key.keyCode); + if (code === 27) { + if (key.stopPropagation) key.stopPropagation(); + close(true); + return false; + } + }; + + // Dismisses the hyperlink input box. + // isCancel is true if we don't care about the input text. + // isCancel is false if we are going to keep the text. + var close = function (isCancel) { + util.removeEvent(doc.body, "keyup", checkEscape); + var text = input.value; + + if (isCancel) { + text = null; + } + else { + // Fixes common pasting errors. + text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://'); + if (!/^(?:https?|ftp):\/\//.test(text)) + text = 'http://' + text; + } + + dialog.parentNode.removeChild(dialog); + + callback(text); + return false; + }; + + + + // Create the text input box form/window. + var createDialog = function () { + + // The main dialog box. + dialog = doc.createElement("div"); + dialog.className = "wmd-prompt-dialog"; + dialog.style.padding = "10px;"; + dialog.style.position = "fixed"; + dialog.style.width = "400px"; + dialog.style.zIndex = "1001"; + + // The dialog text. + var question = doc.createElement("div"); + question.innerHTML = text; + question.style.padding = "5px"; + dialog.appendChild(question); + + // The web form container for the text box and buttons. + var form = doc.createElement("form"), + style = form.style; + form.onsubmit = function () { return close(false); }; + style.padding = "0"; + style.margin = "0"; + style.cssFloat = "left"; + style.width = "100%"; + style.textAlign = "center"; + style.position = "relative"; + dialog.appendChild(form); + + // The input text box + input = doc.createElement("input"); + input.type = "text"; + input.value = defaultInputText; + style = input.style; + style.display = "block"; + style.width = "80%"; + style.marginLeft = style.marginRight = "auto"; + form.appendChild(input); + + // The ok button + var okButton = doc.createElement("input"); + okButton.type = "button"; + okButton.onclick = function () { return close(false); }; + okButton.value = "OK"; + style = okButton.style; + style.margin = "10px"; + style.display = "inline"; + style.width = "7em"; + + + // The cancel button + var cancelButton = doc.createElement("input"); + cancelButton.type = "button"; + cancelButton.onclick = function () { return close(true); }; + cancelButton.value = "Cancel"; + style = cancelButton.style; + style.margin = "10px"; + style.display = "inline"; + style.width = "7em"; + + form.appendChild(okButton); + form.appendChild(cancelButton); + + util.addEvent(doc.body, "keyup", checkEscape); + dialog.style.top = "50%"; + dialog.style.left = "50%"; + dialog.style.display = "block"; + if (uaSniffed.isIE_5or6) { + dialog.style.position = "absolute"; + dialog.style.top = doc.documentElement.scrollTop + 200 + "px"; + dialog.style.left = "50%"; + } + doc.body.appendChild(dialog); + + // This has to be done AFTER adding the dialog to the form if you + // want it to be centered. + dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px"; + dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px"; + + }; + + // Why is this in a zero-length timeout? + // Is it working around a browser bug? + setTimeout(function () { + + createDialog(); + + var defTextLen = defaultInputText.length; + if (input.selectionStart !== undefined) { + input.selectionStart = 0; + input.selectionEnd = defTextLen; + } + else if (input.createTextRange) { + var range = input.createTextRange(); + range.collapse(false); + range.moveStart("character", -defTextLen); + range.moveEnd("character", defTextLen); + range.select(); + } + + input.focus(); + }, 0); + }; + + function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions, getString) { + + var inputBox = panels.input, + buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements. + + makeSpritedButtonRow(); + + var keyEvent = "keydown"; + if (uaSniffed.isOpera) { + keyEvent = "keypress"; + } + + util.addEvent(inputBox, keyEvent, function (key) { + + // Check to see if we have a button key and, if so execute the callback. + if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) { + + var keyCode = key.charCode || key.keyCode; + var keyCodeStr = String.fromCharCode(keyCode).toLowerCase(); + + switch (keyCodeStr) { + case "b": + doClick(buttons.bold); + break; + case "i": + doClick(buttons.italic); + break; + case "l": + doClick(buttons.link); + break; + case "q": + doClick(buttons.quote); + break; + case "k": + doClick(buttons.code); + break; + case "g": + doClick(buttons.image); + break; + case "o": + doClick(buttons.olist); + break; + case "u": + doClick(buttons.ulist); + break; + case "h": + doClick(buttons.heading); + break; + case "r": + doClick(buttons.hr); + break; + case "y": + doClick(buttons.redo); + break; + case "z": + if (key.shiftKey) { + doClick(buttons.redo); + } + else { + doClick(buttons.undo); + } + break; + default: + return; + } + + + if (key.preventDefault) { + key.preventDefault(); + } + + if (window.event) { + window.event.returnValue = false; + } + } + }); + + // Auto-indent on shift-enter + util.addEvent(inputBox, "keyup", function (key) { + if (key.shiftKey && !key.ctrlKey && !key.metaKey) { + var keyCode = key.charCode || key.keyCode; + // Character 13 is Enter + if (keyCode === 13) { + var fakeButton = {}; + fakeButton.textOp = bindCommand("doAutoindent"); + doClick(fakeButton); + } + } + }); + + // special handler because IE clears the context of the textbox on ESC + if (uaSniffed.isIE) { + util.addEvent(inputBox, "keydown", function (key) { + var code = key.keyCode; + if (code === 27) { + return false; + } + }); + } + + + // Perform the button's action. + function doClick(button) { + + inputBox.focus(); + + if (button.textOp) { + + if (undoManager) { + undoManager.setCommandMode(); + } + + var state = new TextareaState(panels); + + if (!state) { + return; + } + + var chunks = state.getChunks(); + + // Some commands launch a "modal" prompt dialog. Javascript + // can't really make a modal dialog box and the WMD code + // will continue to execute while the dialog is displayed. + // This prevents the dialog pattern I'm used to and means + // I can't do something like this: + // + // var link = CreateLinkDialog(); + // makeMarkdownLink(link); + // + // Instead of this straightforward method of handling a + // dialog I have to pass any code which would execute + // after the dialog is dismissed (e.g. link creation) + // in a function parameter. + // + // Yes this is awkward and I think it sucks, but there's + // no real workaround. Only the image and link code + // create dialogs and require the function pointers. + var fixupInputArea = function () { + + inputBox.focus(); + + if (chunks) { + state.setChunks(chunks); + } + + state.restore(); + previewManager.refresh(); + }; + + var noCleanup = button.textOp(chunks, fixupInputArea); + + if (!noCleanup) { + fixupInputArea(); + } + + } + + if (button.execute) { + button.execute(undoManager); + } + }; + + function setupButton(button, isEnabled) { + + var normalYShift = "0px"; + var disabledYShift = "-20px"; + var highlightYShift = "-40px"; + var image = button.getElementsByTagName("span")[0]; + if (isEnabled) { + image.style.backgroundPosition = button.XShift + " " + normalYShift; + button.onmouseover = function () { + image.style.backgroundPosition = this.XShift + " " + highlightYShift; + }; + + button.onmouseout = function () { + image.style.backgroundPosition = this.XShift + " " + normalYShift; + }; + + // IE tries to select the background image "button" text (it's + // implemented in a list item) so we have to cache the selection + // on mousedown. + if (uaSniffed.isIE) { + button.onmousedown = function () { + if (doc.activeElement && doc.activeElement !== panels.input) { // we're not even in the input box, so there's no selection + return; + } + panels.ieCachedRange = document.selection.createRange(); + panels.ieCachedScrollTop = panels.input.scrollTop; + }; + } + + if (!button.isHelp) { + button.onclick = function () { + if (this.onmouseout) { + this.onmouseout(); + } + doClick(this); + return false; + } + } + } + else { + image.style.backgroundPosition = button.XShift + " " + disabledYShift; + button.onmouseover = button.onmouseout = button.onclick = function () { }; + } + } + + function bindCommand(method) { + if (typeof method === "string") + method = commandManager[method]; + return function () { method.apply(commandManager, arguments); } + } + + function makeSpritedButtonRow() { + + var buttonBar = panels.buttonBar; + + var normalYShift = "0px"; + var disabledYShift = "-20px"; + var highlightYShift = "-40px"; + + var buttonRow = document.createElement("ul"); + buttonRow.id = "wmd-button-row" + postfix; + buttonRow.className = 'wmd-button-row'; + buttonRow = buttonBar.appendChild(buttonRow); + var xPosition = 0; + var makeButton = function (id, title, XShift, textOp) { + var button = document.createElement("li"); + button.className = "wmd-button"; + button.style.left = xPosition + "px"; + xPosition += 25; + var buttonImage = document.createElement("span"); + button.id = id + postfix; + button.appendChild(buttonImage); + button.title = title; + button.XShift = XShift; + if (textOp) + button.textOp = textOp; + setupButton(button, true); + buttonRow.appendChild(button); + return button; + }; + var makeSpacer = function (num) { + var spacer = document.createElement("li"); + spacer.className = "wmd-spacer wmd-spacer" + num; + spacer.id = "wmd-spacer" + num + postfix; + buttonRow.appendChild(spacer); + xPosition += 25; + } + + buttons.bold = makeButton("wmd-bold-button", getString("bold"), "0px", bindCommand("doBold")); + buttons.italic = makeButton("wmd-italic-button", getString("italic"), "-20px", bindCommand("doItalic")); + makeSpacer(1); + buttons.link = makeButton("wmd-link-button", getString("link"), "-40px", bindCommand(function (chunk, postProcessing) { + return this.doLinkOrImage(chunk, postProcessing, false); + })); + buttons.quote = makeButton("wmd-quote-button", getString("quote"), "-60px", bindCommand("doBlockquote")); + buttons.code = makeButton("wmd-code-button", getString("code"), "-80px", bindCommand("doCode")); + buttons.image = makeButton("wmd-image-button", getString("image"), "-100px", bindCommand(function (chunk, postProcessing) { + return this.doLinkOrImage(chunk, postProcessing, true); + })); + makeSpacer(2); + buttons.olist = makeButton("wmd-olist-button", getString("olist"), "-120px", bindCommand(function (chunk, postProcessing) { + this.doList(chunk, postProcessing, true); + })); + buttons.ulist = makeButton("wmd-ulist-button", getString("ulist"), "-140px", bindCommand(function (chunk, postProcessing) { + this.doList(chunk, postProcessing, false); + })); + buttons.heading = makeButton("wmd-heading-button", getString("heading"), "-160px", bindCommand("doHeading")); + buttons.hr = makeButton("wmd-hr-button", getString("hr"), "-180px", bindCommand("doHorizontalRule")); + makeSpacer(3); + buttons.undo = makeButton("wmd-undo-button", getString("undo"), "-200px", null); + buttons.undo.execute = function (manager) { if (manager) manager.undo(); }; + + var redoTitle = /win/.test(nav.platform.toLowerCase()) ? + getString("redo") : + getString("redomac"); // mac and other non-Windows platforms + + buttons.redo = makeButton("wmd-redo-button", redoTitle, "-220px", null); + buttons.redo.execute = function (manager) { if (manager) manager.redo(); }; + + if (helpOptions) { + var helpButton = document.createElement("li"); + var helpButtonImage = document.createElement("span"); + helpButton.appendChild(helpButtonImage); + helpButton.className = "wmd-button wmd-help-button"; + helpButton.id = "wmd-help-button" + postfix; + helpButton.XShift = "-240px"; + helpButton.isHelp = true; + helpButton.style.right = "0px"; + helpButton.title = getString("help"); + helpButton.onclick = helpOptions.handler; + + setupButton(helpButton, true); + buttonRow.appendChild(helpButton); + buttons.help = helpButton; + } + + setUndoRedoButtonStates(); + } + + function setUndoRedoButtonStates() { + if (undoManager) { + setupButton(buttons.undo, undoManager.canUndo()); + setupButton(buttons.redo, undoManager.canRedo()); + } + }; + + this.setUndoRedoButtonStates = setUndoRedoButtonStates; + + } + + function CommandManager(pluginHooks, getString, converter) { + this.hooks = pluginHooks; + this.getString = getString; + this.converter = converter; + } + + var commandProto = CommandManager.prototype; + + // The markdown symbols - 4 spaces = code, > = blockquote, etc. + commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)"; + + // Remove markdown symbols from the chunk selection. + commandProto.unwrap = function (chunk) { + var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g"); + chunk.selection = chunk.selection.replace(txt, "$1 $2"); + }; + + commandProto.wrap = function (chunk, len) { + this.unwrap(chunk); + var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"), + that = this; + + chunk.selection = chunk.selection.replace(regex, function (line, marked) { + if (new re("^" + that.prefixes, "").test(line)) { + return line; + } + return marked + "\n"; + }); + + chunk.selection = chunk.selection.replace(/\s+$/, ""); + }; + + commandProto.doBold = function (chunk, postProcessing) { + return this.doBorI(chunk, postProcessing, 2, this.getString("boldexample")); + }; + + commandProto.doItalic = function (chunk, postProcessing) { + return this.doBorI(chunk, postProcessing, 1, this.getString("italicexample")); + }; + + // chunk: The selected region that will be enclosed with */** + // nStars: 1 for italics, 2 for bold + // insertText: If you just click the button without highlighting text, this gets inserted + commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) { + + // Get rid of whitespace and fixup newlines. + chunk.trimWhitespace(); + chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n"); + + // Look for stars before and after. Is the chunk already marked up? + // note that these regex matches cannot fail + var starsBefore = /(\**$)/.exec(chunk.before)[0]; + var starsAfter = /(^\**)/.exec(chunk.after)[0]; + + var prevStars = Math.min(starsBefore.length, starsAfter.length); + + // Remove stars if we have to since the button acts as a toggle. + if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) { + chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), ""); + chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), ""); + } + else if (!chunk.selection && starsAfter) { + // It's not really clear why this code is necessary. It just moves + // some arbitrary stuff around. + chunk.after = chunk.after.replace(/^([*_]*)/, ""); + chunk.before = chunk.before.replace(/(\s?)$/, ""); + var whitespace = re.$1; + chunk.before = chunk.before + starsAfter + whitespace; + } + else { + + // In most cases, if you don't have any selected text and click the button + // you'll get a selected, marked up region with the default text inserted. + if (!chunk.selection && !starsAfter) { + chunk.selection = insertText; + } + + // Add the true markup. + var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ? + chunk.before = chunk.before + markup; + chunk.after = markup + chunk.after; + } + + return; + }; + + commandProto.stripLinkDefs = function (text, defsToAdd) { + + text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm, + function (totalMatch, id, link, newlines, title) { + defsToAdd[id] = totalMatch.replace(/\s*$/, ""); + if (newlines) { + // Strip the title and return that separately. + defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, ""); + return newlines + title; + } + return ""; + }); + + return text; + }; + + commandProto.addLinkDef = function (chunk, linkDef) { + + var refNumber = 0; // The current reference number + var defsToAdd = {}; // + // Start with a clean slate by removing all previous link definitions. + chunk.before = this.stripLinkDefs(chunk.before, defsToAdd); + chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd); + chunk.after = this.stripLinkDefs(chunk.after, defsToAdd); + + var defs = ""; + var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g; + + // The above regex, used to update [foo][13] references after renumbering, + // is much too liberal; it can catch things that are not actually parsed + // as references (notably: code). It's impossible to know which matches are + // real references without performing a markdown conversion, so that's what + // we do. All matches are replaced with a unique reference number, which is + // given a unique link. The uniquifier in both cases is the character offset + // of the match inside the source string. The modified version is then sent + // through the Markdown renderer. Because link reference are stripped during + // rendering, the unique link is present in the rendered version if and only + // if the match at its offset was in fact rendered as a link or image. + var complete = chunk.before + chunk.selection + chunk.after; + var rendered = this.converter.makeHtml(complete); + var testlink = "http://this-is-a-real-link.biz/"; + + // If our fake link appears in the rendered version *before* we have added it, + // this probably means you're a Meta Stack Exchange user who is deliberately + // trying to break this feature. You can still break this workaround if you + // attach a plugin to the converter that sometimes (!) inserts this link. In + // that case, consider yourself unsupported. + while (rendered.indexOf(testlink) != -1) + testlink += "nicetry/"; + + var fakedefs = "\n\n"; + + // the regex is tested on the (up to) three chunks separately, and on substrings, + // so in order to have the correct offsets to check against okayToModify(), we + // have to keep track of how many characters are in the original source before + // the substring that we're looking at. Note that doLinkOrImage aligns the selection + // on potential brackets, so there should be no major breakage from the chunk + // separation. + var skippedChars = 0; + + var uniquified = complete.replace(regex, function uniquify(wholeMatch, before, inner, afterInner, id, end, offset) { + skippedChars += offset; + fakedefs += " [" + skippedChars + "]: " + testlink + skippedChars + "/unicorn\n"; + skippedChars += before.length; + inner = inner.replace(regex, uniquify); + skippedChars -= before.length; + var result = before + inner + afterInner + skippedChars + end; + skippedChars -= offset; + return result; + }); + + rendered = this.converter.makeHtml(uniquified + fakedefs); + + var okayToModify = function(offset) { + return rendered.indexOf(testlink + offset + "/unicorn") !== -1; + } + + var addDefNumber = function (def) { + refNumber++; + def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:"); + defs += "\n" + def; + }; + + // note that + // a) the recursive call to getLink cannot go infinite, because by definition + // of regex, inner is always a proper substring of wholeMatch, and + // b) more than one level of nesting is neither supported by the regex + // nor making a lot of sense (the only use case for nesting is a linked image) + var getLink = function (wholeMatch, before, inner, afterInner, id, end, offset) { + if (!okayToModify(skippedChars + offset)) + return wholeMatch; + skippedChars += offset + before.length; + inner = inner.replace(regex, getLink); + skippedChars -= offset + before.length; + if (defsToAdd[id]) { + addDefNumber(defsToAdd[id]); + return before + inner + afterInner + refNumber + end; + } + return wholeMatch; + }; + + var len = chunk.before.length; + chunk.before = chunk.before.replace(regex, getLink); + skippedChars += len; + + len = chunk.selection.length; + if (linkDef) { + addDefNumber(linkDef); + } + else { + chunk.selection = chunk.selection.replace(regex, getLink); + } + skippedChars += len; + + var refOut = refNumber; + + chunk.after = chunk.after.replace(regex, getLink); + + if (chunk.after) { + chunk.after = chunk.after.replace(/\n*$/, ""); + } + if (!chunk.after) { + chunk.selection = chunk.selection.replace(/\n*$/, ""); + } + + chunk.after += "\n\n" + defs; + + return refOut; + }; + + // takes the line as entered into the add link/as image dialog and makes + // sure the URL and the optinal title are "nice". + function properlyEncoded(linkdef) { + return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) { + + var inQueryString = false; + + // Having `[^\w\d-./]` in there is just a shortcut that lets us skip + // the most common characters in URLs. Replacing that it with `.` would not change + // the result, because encodeURI returns those characters unchanged, but it + // would mean lots of unnecessary replacement calls. Having `[` and `]` in that + // section as well means we do *not* enocde square brackets. These characters are + // a strange beast in URLs, but if anything, this causes URLs to be more readable, + // and we leave it to the browser to make sure that these links are handled without + // problems. + link = link.replace(/%(?:[\da-fA-F]{2})|\?|\+|[^\w\d-./[\]]/g, function (match) { + // Valid percent encoding. Could just return it as is, but we follow RFC3986 + // Section 2.1 which says "For consistency, URI producers and normalizers + // should use uppercase hexadecimal digits for all percent-encodings." + // Note that we also handle (illegal) stand-alone percent characters by + // replacing them with "%25" + if (match.length === 3 && match.charAt(0) == "%") { + return match.toUpperCase(); + } + switch (match) { + case "?": + inQueryString = true; + return "?"; + break; + + // In the query string, a plus and a space are identical -- normalize. + // Not strictly necessary, but identical behavior to the previous version + // of this function. + case "+": + if (inQueryString) + return "%20"; + break; + } + return encodeURI(match); + }) + + if (title) { + title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, ""); + title = title.replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(//g, ">"); + } + return title ? link + ' "' + title + '"' : link; + }); + } + + commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) { + + chunk.trimWhitespace(); + chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/); + var background; + + if (chunk.endTag.length > 1 && chunk.startTag.length > 0) { + + chunk.startTag = chunk.startTag.replace(/!?\[/, ""); + chunk.endTag = ""; + this.addLinkDef(chunk, null); + + } + else { + + // We're moving start and end tag back into the selection, since (as we're in the else block) we're not + // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the + // link text. linkEnteredCallback takes care of escaping any brackets. + chunk.selection = chunk.startTag + chunk.selection + chunk.endTag; + chunk.startTag = chunk.endTag = ""; + + if (/\n\n/.test(chunk.selection)) { + this.addLinkDef(chunk, null); + return; + } + var that = this; + // The function to be executed when you enter a link and press OK or Cancel. + // Marks up the link and adds the ref. + var linkEnteredCallback = function (link) { + + background.parentNode.removeChild(background); + + if (link !== null) { + // ( $1 + // [^\\] anything that's not a backslash + // (?:\\\\)* an even number (this includes zero) of backslashes + // ) + // (?= followed by + // [[\]] an opening or closing bracket + // ) + // + // In other words, a non-escaped bracket. These have to be escaped now to make sure they + // don't count as the end of the link or similar. + // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets), + // the bracket in one match may be the "not a backslash" character in the next match, so it + // should not be consumed by the first match. + // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the + // start of the string, so this also works if the selection begins with a bracket. We cannot solve + // this by anchoring with ^, because in the case that the selection starts with two brackets, this + // would mean a zero-width match at the start. Since zero-width matches advance the string position, + // the first bracket could then not act as the "not a backslash" for the second. + chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1); + + var linkDef = " [999]: " + properlyEncoded(link); + + var num = that.addLinkDef(chunk, linkDef); + chunk.startTag = isImage ? "![" : "["; + chunk.endTag = "][" + num + "]"; + + if (!chunk.selection) { + if (isImage) { + chunk.selection = that.getString("imagedescription"); + } + else { + chunk.selection = that.getString("linkdescription"); + } + } + } + postProcessing(); + }; + + background = ui.createBackground(); + + if (isImage) { + if (!this.hooks.insertImageDialog(linkEnteredCallback)) + ui.prompt(this.getString("imagedialog"), imageDefaultText, linkEnteredCallback); + } + else { + ui.prompt(this.getString("linkdialog"), linkDefaultText, linkEnteredCallback); + } + return true; + } + }; + + // When making a list, hitting shift-enter will put your cursor on the next line + // at the current indent level. + commandProto.doAutoindent = function (chunk, postProcessing) { + + var commandMgr = this, + fakeSelection = false; + + chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n"); + chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n"); + chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n"); + + // There's no selection, end the cursor wasn't at the end of the line: + // The user wants to split the current list item / code line / blockquote line + // (for the latter it doesn't really matter) in two. Temporarily select the + // (rest of the) line to achieve this. + if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) { + chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) { + chunk.selection = wholeMatch; + return ""; + }); + fakeSelection = true; + } + + if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) { + if (commandMgr.doList) { + commandMgr.doList(chunk); + } + } + if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) { + if (commandMgr.doBlockquote) { + commandMgr.doBlockquote(chunk); + } + } + if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { + if (commandMgr.doCode) { + commandMgr.doCode(chunk); + } + } + + if (fakeSelection) { + chunk.after = chunk.selection + chunk.after; + chunk.selection = ""; + } + }; + + commandProto.doBlockquote = function (chunk, postProcessing) { + + chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/, + function (totalMatch, newlinesBefore, text, newlinesAfter) { + chunk.before += newlinesBefore; + chunk.after = newlinesAfter + chunk.after; + return text; + }); + + chunk.before = chunk.before.replace(/(>[ \t]*)$/, + function (totalMatch, blankLine) { + chunk.selection = blankLine + chunk.selection; + return ""; + }); + + chunk.selection = chunk.selection.replace(/^(\s|>)+$/, ""); + chunk.selection = chunk.selection || this.getString("quoteexample"); + + // The original code uses a regular expression to find out how much of the + // text *directly before* the selection already was a blockquote: + + /* + if (chunk.before) { + chunk.before = chunk.before.replace(/\n?$/, "\n"); + } + chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/, + function (totalMatch) { + chunk.startTag = totalMatch; + return ""; + }); + */ + + // This comes down to: + // Go backwards as many lines a possible, such that each line + // a) starts with ">", or + // b) is almost empty, except for whitespace, or + // c) is preceeded by an unbroken chain of non-empty lines + // leading up to a line that starts with ">" and at least one more character + // and in addition + // d) at least one line fulfills a) + // + // Since this is essentially a backwards-moving regex, it's susceptible to + // catstrophic backtracking and can cause the browser to hang; + // see e.g. http://meta.stackexchange.com/questions/9807. + // + // Hence we replaced this by a simple state machine that just goes through the + // lines and checks for a), b), and c). + + var match = "", + leftOver = "", + line; + if (chunk.before) { + var lines = chunk.before.replace(/\n$/, "").split("\n"); + var inChain = false; + for (var i = 0; i < lines.length; i++) { + var good = false; + line = lines[i]; + inChain = inChain && line.length > 0; // c) any non-empty line continues the chain + if (/^>/.test(line)) { // a) + good = true; + if (!inChain && line.length > 1) // c) any line that starts with ">" and has at least one more character starts the chain + inChain = true; + } else if (/^[ \t]*$/.test(line)) { // b) + good = true; + } else { + good = inChain; // c) the line is not empty and does not start with ">", so it matches if and only if we're in the chain + } + if (good) { + match += line + "\n"; + } else { + leftOver += match + line; + match = "\n"; + } + } + if (!/(^|\n)>/.test(match)) { // d) + leftOver += match; + match = ""; + } + } + + chunk.startTag = match; + chunk.before = leftOver; + + // end of change + + if (chunk.after) { + chunk.after = chunk.after.replace(/^\n?/, "\n"); + } + + chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/, + function (totalMatch) { + chunk.endTag = totalMatch; + return ""; + } + ); + + var replaceBlanksInTags = function (useBracket) { + + var replacement = useBracket ? "> " : ""; + + if (chunk.startTag) { + chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/, + function (totalMatch, markdown) { + return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; + }); + } + if (chunk.endTag) { + chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/, + function (totalMatch, markdown) { + return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; + }); + } + }; + + if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) { + this.wrap(chunk, SETTINGS.lineLength - 2); + chunk.selection = chunk.selection.replace(/^/gm, "> "); + replaceBlanksInTags(true); + chunk.skipLines(); + } else { + chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, ""); + this.unwrap(chunk); + replaceBlanksInTags(false); + + if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) { + chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n"); + } + + if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) { + chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n"); + } + } + + chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection); + + if (!/\n/.test(chunk.selection)) { + chunk.selection = chunk.selection.replace(/^(> *)/, + function (wholeMatch, blanks) { + chunk.startTag += blanks; + return ""; + }); + } + }; + + commandProto.doCode = function (chunk, postProcessing) { + + var hasTextBefore = /\S[ ]*$/.test(chunk.before); + var hasTextAfter = /^[ ]*\S/.test(chunk.after); + + // Use 'four space' markdown if the selection is on its own + // line or is multiline. + if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) { + + chunk.before = chunk.before.replace(/[ ]{4}$/, + function (totalMatch) { + chunk.selection = totalMatch + chunk.selection; + return ""; + }); + + var nLinesBack = 1; + var nLinesForward = 1; + + if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { + nLinesBack = 0; + } + if (/^\n(\t|[ ]{4,})/.test(chunk.after)) { + nLinesForward = 0; + } + + chunk.skipLines(nLinesBack, nLinesForward); + + if (!chunk.selection) { + chunk.startTag = " "; + chunk.selection = this.getString("codeexample"); + } + else { + if (/^[ ]{0,3}\S/m.test(chunk.selection)) { + if (/\n/.test(chunk.selection)) + chunk.selection = chunk.selection.replace(/^/gm, " "); + else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior + chunk.before += " "; + } + else { + chunk.selection = chunk.selection.replace(/^(?:[ ]{4}|[ ]{0,3}\t)/gm, ""); + } + } + } + else { + // Use backticks (`) to delimit the code block. + + chunk.trimWhitespace(); + chunk.findTags(/`/, /`/); + + if (!chunk.startTag && !chunk.endTag) { + chunk.startTag = chunk.endTag = "`"; + if (!chunk.selection) { + chunk.selection = this.getString("codeexample"); + } + } + else if (chunk.endTag && !chunk.startTag) { + chunk.before += chunk.endTag; + chunk.endTag = ""; + } + else { + chunk.startTag = chunk.endTag = ""; + } + } + }; + + commandProto.doList = function (chunk, postProcessing, isNumberedList) { + + // These are identical except at the very beginning and end. + // Should probably use the regex extension function to make this clearer. + var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/; + var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/; + + // The default bullet is a dash but others are possible. + // This has nothing to do with the particular HTML bullet, + // it's just a markdown bullet. + var bullet = "-"; + + // The number in a numbered list. + var num = 1; + + // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list. + var getItemPrefix = function () { + var prefix; + if (isNumberedList) { + prefix = " " + num + ". "; + num++; + } + else { + prefix = " " + bullet + " "; + } + return prefix; + }; + + // Fixes the prefixes of the other list items. + var getPrefixedItem = function (itemText) { + + // The numbering flag is unset when called by autoindent. + if (isNumberedList === undefined) { + isNumberedList = /^\s*\d/.test(itemText); + } + + // Renumber/bullet the list element. + itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm, + function (_) { + return getItemPrefix(); + }); + + return itemText; + }; + + chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null); + + if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) { + chunk.before += chunk.startTag; + chunk.startTag = ""; + } + + if (chunk.startTag) { + + var hasDigits = /\d+[.]/.test(chunk.startTag); + chunk.startTag = ""; + chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n"); + this.unwrap(chunk); + chunk.skipLines(); + + if (hasDigits) { + // Have to renumber the bullet points if this is a numbered list. + chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem); + } + if (isNumberedList == hasDigits) { + return; + } + } + + var nLinesUp = 1; + + chunk.before = chunk.before.replace(previousItemsRegex, + function (itemText) { + if (/^\s*([*+-])/.test(itemText)) { + bullet = re.$1; + } + nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; + return getPrefixedItem(itemText); + }); + + if (!chunk.selection) { + chunk.selection = this.getString("litem"); + } + + var prefix = getItemPrefix(); + + var nLinesDown = 1; + + chunk.after = chunk.after.replace(nextItemsRegex, + function (itemText) { + nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; + return getPrefixedItem(itemText); + }); + + chunk.trimWhitespace(true); + chunk.skipLines(nLinesUp, nLinesDown, true); + chunk.startTag = prefix; + var spaces = prefix.replace(/./g, " "); + this.wrap(chunk, SETTINGS.lineLength - spaces.length); + chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces); + + }; + + commandProto.doHeading = function (chunk, postProcessing) { + + // Remove leading/trailing whitespace and reduce internal spaces to single spaces. + chunk.selection = chunk.selection.replace(/\s+/g, " "); + chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, ""); + + // If we clicked the button with no selected text, we just + // make a level 2 hash header around some default text. + if (!chunk.selection) { + chunk.startTag = "## "; + chunk.selection = this.getString("headingexample"); + chunk.endTag = " ##"; + return; + } + + var headerLevel = 0; // The existing header level of the selected text. + + // Remove any existing hash heading markdown and save the header level. + chunk.findTags(/#+[ ]*/, /[ ]*#+/); + if (/#+/.test(chunk.startTag)) { + headerLevel = re.lastMatch.length; + } + chunk.startTag = chunk.endTag = ""; + + // Try to get the current header level by looking for - and = in the line + // below the selection. + chunk.findTags(null, /\s?(-+|=+)/); + if (/=+/.test(chunk.endTag)) { + headerLevel = 1; + } + if (/-+/.test(chunk.endTag)) { + headerLevel = 2; + } + + // Skip to the next line so we can create the header markdown. + chunk.startTag = chunk.endTag = ""; + chunk.skipLines(1, 1); + + // We make a level 2 header if there is no current header. + // If there is a header level, we substract one from the header level. + // If it's already a level 1 header, it's removed. + var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1; + + if (headerLevelToCreate > 0) { + + // The button only creates level 1 and 2 underline headers. + // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner? + var headerChar = headerLevelToCreate >= 2 ? "-" : "="; + var len = chunk.selection.length; + if (len > SETTINGS.lineLength) { + len = SETTINGS.lineLength; + } + chunk.endTag = "\n"; + while (len--) { + chunk.endTag += headerChar; + } + } + }; + + commandProto.doHorizontalRule = function (chunk, postProcessing) { + chunk.startTag = "----------\n"; + chunk.selection = ""; + chunk.skipLines(2, 1, true); + } + + +})(); \ No newline at end of file diff --git a/ansible/idr-playbooks/files/html/Markdown.Sanitizer.js b/ansible/idr-playbooks/files/html/Markdown.Sanitizer.js new file mode 100644 index 000000000..73f5d7467 --- /dev/null +++ b/ansible/idr-playbooks/files/html/Markdown.Sanitizer.js @@ -0,0 +1,108 @@ +(function () { + var output, Converter; + if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module + output = exports; + Converter = require("./Markdown.Converter").Converter; + } else { + output = window.Markdown; + Converter = output.Converter; + } + + output.getSanitizingConverter = function () { + var converter = new Converter(); + converter.hooks.chain("postConversion", sanitizeHtml); + converter.hooks.chain("postConversion", balanceTags); + return converter; + } + + function sanitizeHtml(html) { + return html.replace(/<[^>]*>?/gi, sanitizeTag); + } + + // (tags that can be opened/closed) | (tags that stand alone) + var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol(?: start="\d+")?|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i; + // | + var a_white = /^(]+")?\s?>|<\/a>)$/i; + + // ]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i; + + function sanitizeTag(tag) { + if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white)) + return tag; + else + return ""; + } + + /// + /// attempt to balance HTML tags in the html string + /// by removing any unmatched opening or closing tags + /// IMPORTANT: we *assume* HTML has *already* been + /// sanitized and is safe/sane before balancing! + /// + /// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593 + /// + function balanceTags(html) { + + if (html == "") + return ""; + + var re = /<\/?\w+[^>]*(\s|$|>)/g; + // convert everything to lower case; this makes + // our case insensitive comparisons easier + var tags = html.toLowerCase().match(re); + + // no HTML tags present? nothing to do; exit now + var tagcount = (tags || []).length; + if (tagcount == 0) + return html; + + var tagname, tag; + var ignoredtags = "



      • "; + var match; + var tagpaired = []; + var tagremove = []; + var needsRemoval = false; + + // loop through matched tags in forward order + for (var ctag = 0; ctag < tagcount; ctag++) { + tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1"); + // skip any already paired tags + // and skip tags in our ignore list; assume they're self-closed + if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1) + continue; + + tag = tags[ctag]; + match = -1; + + if (!/^<\//.test(tag)) { + // this is an opening tag + // search forwards (next tags), look for closing tags + for (var ntag = ctag + 1; ntag < tagcount; ntag++) { + if (!tagpaired[ntag] && tags[ntag] == "") { + match = ntag; + break; + } + } + } + + if (match == -1) + needsRemoval = tagremove[ctag] = true; // mark for removal + else + tagpaired[match] = true; // mark paired + } + + if (!needsRemoval) + return html; + + // delete all orphaned tags from the string + + var ctag = 0; + html = html.replace(re, function (match) { + var res = tagremove[ctag] ? "" : match; + ctag++; + return res; + }); + return html; + } +})(); diff --git a/ansible/idr-playbooks/files/html/bbsrc.png b/ansible/idr-playbooks/files/html/bbsrc.png new file mode 100644 index 0000000000000000000000000000000000000000..ee84fc0bec9ea236c0f0e5cf6aa76c5ddd4a8aaf GIT binary patch literal 13450 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 YX}=(U+tEqCe{TOviOGxB3L6If4?bqmjQ{`u literal 0 HcmV?d00001 diff --git a/ansible/idr-playbooks/files/html/eurobioimaging_logo.gif b/ansible/idr-playbooks/files/html/eurobioimaging_logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..ff05a74cbc75942f577123f6c34f05d363b5bb15 GIT binary patch literal 5558 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 E2SUM_5dZ)H literal 0 HcmV?d00001 diff --git a/ansible/idr-playbooks/files/html/h2020.png b/ansible/idr-playbooks/files/html/h2020.png new file mode 100644 index 0000000000000000000000000000000000000000..55a335f7bba128c21c226069302edd297f34beb3 GIT binary patch literal 15440 zcmV-WJg>uvP)4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH z9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK zVkc9?T=n|PIo~X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1 zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#mZ8eu=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7 zqW-CFs9&fT)ZaU5gc&=gBz-DaCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaER00009a7bBm000*f000*f0cW4mQUCxq(n&-?RCocE zoC&yORdw%I&C?y~&OOo-4GlC6Po@~-fD?m2P%wFpplA{WHE2GQ81<<@eEFQBiAJBs zfB`YlD1rhS5D}b-qK!0Q(=@l~+kNM;Zq0Sx@4t4{?QUq_m$&adRp;z+?KSVU_oHRVq!eEKbQHH`Z5 z°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
      • zVV$tb0%tD{U@m5)tzguA(<(DLo%3J z)`2m^ATUJ}Uql+jpXrPK#3V6AE>#yMkVb_Ne$O5DQXUk{z;?<`4wr6cCJ~6{7WJkT zI7i7uc$oy(0%8Q&{A53ff#{h5Cb1r3jga5V0NDRX=0FiHF1;iQx3*ekt7GFL?4j7z zxmf0hC0shgaWhUe$E6+MLurCKo_GVPiM3!r#2!9Gq?i!4Ux!!{EbZbl7cdII0}H-E zu^Ys)?F_R>RTqlvRAOq1eCJ_`o&A-&}#Yc69BX{#4>D&+-Ka233u zt~~Yt@x`=w?lE5F%kUattBKaZac-8F7;URlwCRF52JnG;U=DU&bs{U3<qqL%7Fef%g!?7K3O9nuEKp zMPOKI2e}7)I5VUQ{X03L#Yv|G4rI>VEfJyR+91t(wjY4jA%$!BH5Q~kxf%vR6UYyM zpp9z9s%73K;20R*6F88ADSNlTCAFsa6R~Z23mVA?J_g8QBw25)Jw(+RMSKpv>6AQ0 zH&jO|51^^5B)cbnL9n9>Kw7_bvxG|Kae9eNYX2kEm9qZfA{rr&+J;-KY?}#ACE``i zcIy2H{06kl;2?9u_|DDMA-0f>&p!l|dl#TJ2?1)H9n$6>@{=hst~7kSZUmN{`+ zOQt~kBemr!-}B6wcPBun&WY$n9SK2Audh&Kocr&YeVmHsDBcLWuiY>~f^c?EQF&;E{sM<%rTNeXrJ7BS0kd2Q21HS{|lxA>e6vq~g12Z&Y;C(bU zy#9qdEit$~kzN<2bL(TN>}KpSN5aLOaCL)-Sp&(h*Y-2e0l-8xHpltKP->zHCJwP> zcEVx})!3P(rtA4qM4G4=qH_i9AR?gj=}*s;aT`ZkU{3E-zpJ30O^A^cv80s`qh9nM zN6KV}lbnFg7j^Q8l>!?%bq<;qc`>HQ(o_V|UWuf#4l{^RdO0Jg-&<%B^cvHiKX6uP zQ(<{rF5x(0>r3neRtbaG2-q}P9_x+k7LmS21ERM|K!w!7+5L%i}ULDU(q*II%)-&AttW;6O1N#o)_UotK$*^ZVp8zi z(mGF>A3;!H7M7POylVLtwZ;T<*o%qTgVxy1bdJFgL{m$u2(>7ar+9?U4>an$`IJI3@NJgZ z80%uGx;VZMla}T*m6lT_R4pf&@KYKb{?J*71|jguGGogh0;buid4MDcqh%b|GSXjT z^-^OQR!1Au(HJs^{=;1mkVQ8D=W#lGjOtZun|mutvMXgJuvB!^W5n;{_z`*! zfdYToC5JPY!8z+HqA}<-0TV0`O4q4le|HjLXwX-UBWW$PMva3dVumIvRX@t%3;}bB za4l9nO~n9w?wSY};{@RcAF|gXaC>u4H5`}Hq}==DU8{z6-d_2kw2d3e97$?N+iV09J8pTL!7c&+xNDb};HllkZ3R8@jgQrc~8*6;c&S#RLKh0;`-PVk@n*V=AGH za#hj%(Fp$FSz;cw=p;0OM=dcIQnO@h`6Za7l_b?ERyIkrheGHnO%Bx|^U&Qr=$=NF zh^81c%dTKU#Kx~l@$yX)U!#CYX;PMCeliDI8R%V&8fHRTstyyhU<#QLy}#LFtuKE@ zOEyuPQ`Yn?4Iw;eX(!F0abwDmHXSG?jGC(BsjEK$)xskc(g|LNi8Hv?xbg|K|L&7pnXnq+ z5uoDr7EwSmnR6LcpiF#Hs*J zXQ(Ptn#Hk{XRi2$5Ru|wjfl~JVG@`Ky_XrsmgX=gdSyNd%zDSN!YOY#qS{q-M#ro) z@e!O*Fnfb>L~PAJIJV3brjR{%hHyVY4B~9Sw+I4It`TJp0`O|%cvzV{`TAFR=W;Bn zLOVa)@+nOmy41I89?j0j5)+WzIT1b?ADVz^GWCt7P;+qVS{dhACp+TS7bR0`al?)o zO{UrURd`aWP9a`7C$d#_?4e#~msI+|_y*c0)zU7|TIyQ_YjYj|>8&eGISi&F<9O`8 zT$H+$DG?$7l0T_j*;LJ6>Pl?|v`m6-wI&YlR!Tbw@f|-!ed;IwkWS+JDZ<-w!YXx$ ziLKOnTw}>OY9{z$4#CKLw&DuRfk1a+GIauF{Oo!b5*6(XkWzf1%d%SZiCmLiuzgZH8j$`guB$n?-jD?y0W z_b;So6%E!>Q_Lk~DtHvUbTw7bMeA}+C5Gor$OrP5F+^I9a`?7+(9K6o`v91OG;8rX zQ97M|tZ+ES5#U-tJAHT+)Jf1!lW!Ee!>He5Xz|{p=IOt}LJY;Dcux2>LnvGE zurn?XfgwU?5->OSp%S2gXM{|SX|u`C4zSS&#^ofEDSHP%G6DMG$q-4DN>QhpjF?3# z973Z`-9Exut_58GNC&CDx1c|y9tQKJBf*sf+t1#o`x1=BB=tjEVi3WvzO~fT#F`DM zWTaB&1)so?3lYSVvfcWn6uqM$Caf#20kb5~3Jv1;`qmLO1%eW&OeZYTEjXJ(IP()& zib43&P98fBsWgf^%z!U#=%I2dN*nl(bHNB=D*etuh!fD4pqA#O#GPT?w~6Yu-L+k) z=j_34U}zYPdb${_&G_mI9FP?>DBUVaI67RWLeFb|ho{g_`@ZuPSRi~V&+lmFjHBkd z+L=1*y#q=MS3(Yq_AFFGj6hFrdlWR1HegBWBchht2#^NwW=Ol% z$BH6)9T2Ld-(r#`(8(f&P}Q&3VcFWki47|kg--RG2RZ=DudWc>5x&!e!v#ale=LK2 zg7`3fhd`I_&cW%jmu}l(Dlxi`g%*r8-e$fdQ$ns2>qE?otF>RHs60Ucc{HZ-^1laA=%Xi+z8zhC5LJB6H(b2&RZ) z1y&x6&sfqhT1DX{pk0Inq?72OV2~P-ISe?E-xKE0aE%~Qk&{bRHCmyJOrMJpdBTBK zW`bJ`C2a}TU1*rOGS!&2{^$8TA1!spp-eanpP`+qyEbwd!W@*>Ne(9?XlX8nmU;E} zI~Y$wAs?(GuHME~h(i}T_UCqQ4m`k!2^d2SUeBoA)h7war^HYIh zj=BO_z+cD2NJD5w5){W*2S*@T3?W9-K^mP|l(zAZen14DYNs!Kgrt6SJtMXA9N%8Z zo$|n^7>%2DyH4AHZ;?r5VJ={U3=jEJtQbs+7m-9$nJD)7R)*G#!7vF8C=7shI`${; zQ(XrY#sIDk53j=rKDq`!{9X`AyXaf(gHz;>)C<}Md+DY^$d|7m6T82T&{iWfBzk?=YH#(M^h&8cA-! z~;z>36-7r;5pQh&!?q3+7dWc`WGFYNodIgH3w$|Aqt#JNPQXa zK6?w2^STgKaMO{hKnWk(VQ}@v;|j34fxs%1`%fFHc)12qws^>^BMBmco&sa(4&+kF zfuh8dPVeZ3s(x-l_g+WKMyY%K+6kP?s)Gmtg=~n07MPd7T(aXjKcPb}I(=2IYST%K zAQ;u3451LCd;alB0-=?UGl=7;9a@TxUQSInSXse8>^#P|Bp!3E1#W^#ss86tUoXsF-DUlb`6fH>4>{XDHOWe=g#9L zT2bIQic&GxiGo+W&0K&*4)&#i!>hzEQ6QNEpTENTDh@9)-UZJo`w3xRV@-4o|oXIP;*E;1pAWbKy^>NOkn0 zujZm82GpFS9kc^1d}~|X1(y0c0_T1Md*=M08oT+4@&5~rI7KvgblsQ$0000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IDR + + + + + + + + + + + +
        + +
        + + +
        + +
        +
        + + + +
        + + + + + + +
        diff --git a/ansible/idr-playbooks/files/html/markdown.md b/ansible/idr-playbooks/files/html/markdown.md new file mode 100644 index 000000000..8ee0d1258 --- /dev/null +++ b/ansible/idr-playbooks/files/html/markdown.md @@ -0,0 +1,88 @@ +#Image Data Repository + +## Overview + +Welcome to the Image Data Repository (IDR). This online, public resource seeks to +store, integrate and serve image datasets from published scientific studies. +We have collected and are continuing to receive existing and newly created +“reference” image datasets that are valuable resources for a broad community +of users, either because they will be frequently accessed and cited or because +they can serve as a basis for re-analysis and the development of new +computational tools. + + +[![data link](/about/take_a_look.png "Take a look at the data")](/webclient/userdata/?experimenter=-1) + +## Goal + +The IDR aims to make datasets that have never previously been accessible +publicly available, allowing the community to search, view, mine and even +process and analyze large complex multidimensional life sciences image data. +Sharing data promotes the validation of experimental methods and scientific +conclusions, the comparison with new data obtained by scientists around the +world, and allows the possibility of data reuse by developers of new analysis +and processing tools. + +All datasets have been annotated with author-supplied metadata (e.g., +annotations, defined regions, feature vectors and ontological annotations) +which are all stored and available for browsing. All metadata are also +available through the OMERO API. + +## Technology + +IDR uses OME’s +[Bio-Formats](http://www.openmicroscopy.org/site/products/bio-formats) and +[OMERO](http://www.openmicroscopy.org/site/products/omero) tools to read, +manage and serve data, and provide links to EMBL-EBI’s molecular and +structural resources. The resource is built upon EMBL-EBI’s OpenStack-based +Embassy infrastructure. All tools used for reading and converting metadata +associated with each dataset are available and all scripts used to build the +infrastructure are maintained and available. We will be extending the system +to enable computational re-analysis of the data. + +## Examples + +[Datasets in human cells](/webclient/?show=well-45407), +[Drosophila](/webclient/?show=well-547609), +and +[fungi](/webclient/?show=well-590686) are +included. The full +[Mitocheck dataset](/webclient/?show=well-771034) and a +comprehensive +[chemical screen in human cells](/webclient/?show=plate-4101) are +included. Imaging data from +[Tara Oceans](/webclient/?show=plate-4751), +a global survey of plankton and other marine organisms is also included. + +Wherever possible, functional annotations (e.g., “increased peripheral +actin") and experimental components have been converted to defined terms in +the [EFO](http://www.ebi.ac.uk/ols/ontologies/efo), +[CMPO](http://www.ebi.ac.uk/ols/ontologies/cmpo) or other +ontologies, always in collaboration with the data submitters +([see example](/webclient/?show=image-109846)). >80% of the functional +annotations have links to defined, published controlled vocabularies. + + +[![Plate8_Actinome1](/webgateway/render_thumbnail/122770/96/ "Plate8_Actinome1 [Well O02 Field #1]")](/webclient/?show=image-122770) +[![toret-adhesionB](/webgateway/render_thumbnail/928607/96/ "Secondary_001a [Well C05 Field #1]")](/webclient/?show=image-928607) +[![graml-sysgroA](/webgateway/render_thumbnail/1230008/96/ "JL_120731_S6A [Well F-7; Field #1]")](/webclient/?show=image-1230008) +[![mitocheck](/webgateway/render_thumbnail/1484759/96/ "LT0002_02 [Well E2, Field 1]")](/webclient/?show=image-1484759) + +## Funding and Project Participants + +This project is funded by the BBSRC (BB/M018423/1) and is a collaboration +between the OME Consortium at Dundee (www.openmicroscopy.org), EMBL-EBI and +the University of Cambridge. Further development of the IDR UI is funded by +Euro-BioImaging Prep Phase II award (Horizon2020 Ref:688945). The IDR Project +PIs are [Jason Swedlow (Dundee)](https://www.openmicroscopy.org/site/about/development-teams/jason), +[Alvis Brazma (EMBL-EBI)](http://www.ebi.ac.uk/about/people/alvis-brazma) and +[Rafael Carazo-Salas (Cambridge)](http://www.gen.cam.ac.uk/research-groups/carazo-salas). + +## Current Status + +Currently the IDR contains 35 Tb of multi-dimensional image data consisting of +26 million individual planes from 20 different imaging studies. Data comes +from studies on human, Drosophila, budding and fission yeast, and mouse cells. +Imaging modalities included so far include high content screens and +histopathology, super-resolution and 3D-SIM datasets. + diff --git a/ansible/idr-playbooks/files/html/ome-logo-200.png b/ansible/idr-playbooks/files/html/ome-logo-200.png new file mode 100644 index 0000000000000000000000000000000000000000..417a36ab74d780b1987c1c59739e040c5519518b GIT binary patch literal 6254 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 literal 0 HcmV?d00001 diff --git a/ansible/idr-playbooks/idr-local-files.yml b/ansible/idr-playbooks/idr-local-files.yml index b7fa90e8c..157a4e07b 100644 --- a/ansible/idr-playbooks/idr-local-files.yml +++ b/ansible/idr-playbooks/idr-local-files.yml @@ -25,7 +25,7 @@ #synchronize: copy: dest: /srv/www/html/ - src: "{{ inventory_dir }}/files/html/" + src: "{{ playbook_dir }}/files/html/" #delete: yes #owner: no #recursive: yes From d73805eaf69a3a5762cb110ac6777e4965d01733 Mon Sep 17 00:00:00 2001 From: Simon Li Date: Tue, 27 Sep 2016 11:32:46 +0100 Subject: [PATCH 3/5] Default to an omero uid that probably wont already exist --- ansible/idr-playbooks/group_vars/omero-hosts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/idr-playbooks/group_vars/omero-hosts.yml b/ansible/idr-playbooks/group_vars/omero-hosts.yml index a142dfc4c..abd814a3f 100644 --- a/ansible/idr-playbooks/group_vars/omero-hosts.yml +++ b/ansible/idr-playbooks/group_vars/omero-hosts.yml @@ -4,7 +4,7 @@ omero_dbpassword: omero omero_datadir: /data/OMERO # TODO: change this to a lower system uid -omero_system_uid: "{{ idr_secret_omero_system_uid | default(999) }}" +omero_system_uid: "{{ idr_secret_omero_system_uid | default(546) }}" omero_web_runtime_redis: True omero_release: latest From 5d4ddb1073bddd7c133b02411bd9b1c4a770fe8f Mon Sep 17 00:00:00 2001 From: Simon Li Date: Tue, 27 Sep 2016 12:32:49 +0100 Subject: [PATCH 4/5] Set IDR omero_release 0.0.7-rc1 --- ansible/idr-playbooks/group_vars/omero-hosts.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ansible/idr-playbooks/group_vars/omero-hosts.yml b/ansible/idr-playbooks/group_vars/omero-hosts.yml index abd814a3f..70996a47c 100644 --- a/ansible/idr-playbooks/group_vars/omero-hosts.yml +++ b/ansible/idr-playbooks/group_vars/omero-hosts.yml @@ -7,7 +7,8 @@ omero_datadir: /data/OMERO omero_system_uid: "{{ idr_secret_omero_system_uid | default(546) }}" omero_web_runtime_redis: True -omero_release: latest +# This refers to a custom IDR release URL, not an official OMERO version +omero_release: "0.0.7-rc1" omero_omego_additional_args: "--downloadurl https://downloads.openmicroscopy.org/idr" # Disable database backups From a524b745bacfc507d221f548eaa97ea8cbdac019 Mon Sep 17 00:00:00 2001 From: Simon Li Date: Tue, 27 Sep 2016 14:17:19 +0000 Subject: [PATCH 5/5] idr-playbooks/idr.yml includes all runnable public playbooks --- ansible/idr-playbooks/idr.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 ansible/idr-playbooks/idr.yml diff --git a/ansible/idr-playbooks/idr.yml b/ansible/idr-playbooks/idr.yml new file mode 100644 index 000000000..178cfb134 --- /dev/null +++ b/ansible/idr-playbooks/idr.yml @@ -0,0 +1,5 @@ +# Runs all public playbooks for setting up the IDR infrastructure +# The remaining playbooks require additional private configuration +- include: idr-local-users.yml +- include: idr-omero.yml +- include: idr-local-files.yml