From d1bf009071aa8ec875242918a26bf212bba424c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Fri, 2 Nov 2012 14:06:48 +0100 Subject: [PATCH] add Mediawiki support --- src/node/handler/ExportHandler.js | 21 ++ src/node/hooks/express/importexport.js | 2 +- src/node/utils/ExportMediaWiki.js | 351 +++++++++++++++++++++++++ src/static/css/pad.css | 3 + src/static/js/pad_impexp.js | 1 + src/templates/pad.html | 1 + src/templates/timeslider.html | 1 + 7 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 src/node/utils/ExportMediaWiki.js diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index 1b7fcc26d..5632cbeac 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js @@ -21,6 +21,7 @@ var ERR = require("async-stacktrace"); var exporthtml = require("../utils/ExportHtml"); var exportdokuwiki = require("../utils/ExportDokuWiki"); +var exportmediawiki = require("../utils/ExportMediaWiki"); var padManager = require("../db/PadManager"); var async = require("async"); var fs = require("fs"); @@ -85,6 +86,26 @@ exports.doExport = function(req, res, padId, type) if(err && err != "stop") throw err; }); } + else if(type == 'mediawiki') + { + var randNum; + var srcFile, destFile; + + async.series([ + //render the mediawiki document + function(callback) + { + exportmediawiki.getPadMediaWikiDocument(padId, req.params.rev, function(err, mediawiki) + { + res.send(mediawiki); + callback("stop"); + }); + }, + ], function(err) + { + if(err && err != "stop") throw err; + }); + } else { var html; diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js index 9e78f34d7..997edd544 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.js @@ -5,7 +5,7 @@ var importHandler = require('../../handler/ImportHandler'); exports.expressCreateServer = function (hook_name, args, cb) { args.app.get('/p/:pad/:rev?/export/:type', function(req, res, next) { - var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"]; + var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki", "mediawiki"]; //send a 404 if we don't support this filetype if (types.indexOf(req.params.type) == -1) { next(); diff --git a/src/node/utils/ExportMediaWiki.js b/src/node/utils/ExportMediaWiki.js new file mode 100644 index 000000000..0afca4b1c --- /dev/null +++ b/src/node/utils/ExportMediaWiki.js @@ -0,0 +1,351 @@ +/** + * Copyright 2011 Adrian Lang + * 2012 Francois Boulogne + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var async = require("async"); + +var Changeset = require("ep_etherpad-lite/static/js/Changeset"); +var padManager = require("../db/PadManager"); + +function getPadMediaWiki(pad, revNum, callback) +{ + var atext = pad.atext; + var dokuwiki; + async.waterfall([ + // fetch revision atext + + + function (callback) + { + if (revNum != undefined) + { + pad.getInternalRevisionAText(revNum, function (err, revisionAtext) + { + atext = revisionAtext; + callback(err); + }); + } + else + { + callback(null); + } + }, + + // convert atext to dokuwiki text + + function (callback) + { + dokuwiki = getMediaWikiFromAtext(pad, atext); + callback(null); + }], + // run final callback + + + function (err) + { + callback(err, dokuwiki); + }); +} + +function getMediaWikiFromAtext(pad, atext) +{ + var apool = pad.apool(); + var textLines = atext.text.slice(0, -1).split('\n'); + var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + + var tags = ['==', '===', '\'\'\'', '\'\'', 'u>', 's>']; + var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; + var anumMap = {}; + + props.forEach(function (propName, i) + { + var propTrueNum = apool.putAttrib([propName, true], true); + if (propTrueNum >= 0) + { + anumMap[propTrueNum] = i; + } + }); + + function getLineMediaWiki(text, attribs) + { + var propVals = [false, false, false]; + var ENTER = 1; + var STAY = 2; + var LEAVE = 0; + + // Use order of tags (b/i/u) as order of nesting, for simplicity + // and decent nesting. For example, + // Just bold Bold and italics Just italics + // becomes + // Just bold Bold and italics Just italics + var taker = Changeset.stringIterator(text); + var assem = Changeset.stringAssembler(); + + function emitOpenTag(i) + { + if (tags[i].indexOf('>') !== -1) { + assem.append('<'); + } + assem.append(tags[i]); + } + + function emitCloseTag(i) + { + if (tags[i].indexOf('>') !== -1) { + assem.append(' bold, etc. + if (!propVals[i]) + { + propVals[i] = ENTER; + propChanged = true; + } + else + { + propVals[i] = STAY; + } + } + }); + for (var i = 0; i < propVals.length; i++) + { + if (propVals[i] === true) + { + propVals[i] = LEAVE; + propChanged = true; + } + else if (propVals[i] === STAY) + { + propVals[i] = true; // set it back + } + } + // now each member of propVal is in {false,LEAVE,ENTER,true} + // according to what happens at start of span + if (propChanged) + { + // leaving bold (e.g.) also leaves italics, etc. + var left = false; + for (var i = 0; i < propVals.length; i++) + { + var v = propVals[i]; + if (!left) + { + if (v === LEAVE) + { + left = true; + } + } + else + { + if (v === true) + { + propVals[i] = STAY; // tag will be closed and re-opened + } + } + } + + for (var i = propVals.length - 1; i >= 0; i--) + { + if (propVals[i] === LEAVE) + { + emitCloseTag(i); + propVals[i] = false; + } + else if (propVals[i] === STAY) + { + emitCloseTag(i); + } + } + for (var i = 0; i < propVals.length; i++) + { + if (propVals[i] === ENTER || propVals[i] === STAY) + { + emitOpenTag(i); + propVals[i] = true; + } + } + // propVals is now all {true,false} again + } // end if (propChanged) + var chars = o.chars; + if (o.lines) + { + chars--; // exclude newline at end of line, if present + } + var s = taker.take(chars); + + assem.append(_escapeMediaWiki(s)); + } // end iteration over spans in line + for (var i = propVals.length - 1; i >= 0; i--) + { + if (propVals[i]) + { + emitCloseTag(i); + propVals[i] = false; + } + } + } // end processNextChars + if (urls) + { + urls.forEach(function (urlData) + { + var startIndex = urlData[0]; + var url = urlData[1]; + var urlLength = url.length; + processNextChars(startIndex - idx); + assem.append('['); + + // Do not use processNextChars since a link does not contain syntax and + // needs no escaping + var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + urlLength)); + idx += urlLength; + assem.append(taker.take(iter.next().chars)); + + assem.append(']'); + }); + } + processNextChars(text.length - idx); + + return assem.toString() + "\n"; + } // end getLineMediaWiki + var pieces = []; + + for (var i = 0; i < textLines.length; i++) + { + var line = _analyzeLine(textLines[i], attribLines[i], apool); + var lineContent = getLineMediaWiki(line.text, line.aline); + + if (line.listLevel && lineContent) + { + if (line.listTypeName == "number") + { + pieces.push(new Array(line.listLevel + 1).join(' ') + '# '); + } else { + pieces.push(new Array(line.listLevel + 1).join(' ') + '* '); + } + } + pieces.push(lineContent); + } + + return pieces.join(''); +} + +function _analyzeLine(text, aline, apool) +{ + var line = {}; + + // identify list + var lineMarker = 0; + line.listLevel = 0; + if (aline) + { + var opIter = Changeset.opIterator(aline); + if (opIter.hasNext()) + { + var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool); + if (listType) + { + lineMarker = 1; + listType = /([a-z]+)([12345678])/.exec(listType); + if (listType) + { + line.listTypeName = listType[1]; + line.listLevel = Number(listType[2]); + } + } + } + } + if (lineMarker) + { + line.text = text.substring(1); + line.aline = Changeset.subattribution(aline, 1); + } + else + { + line.text = text; + line.aline = aline; + } + + return line; +} + +exports.getPadMediaWikiDocument = function (padId, revNum, callback) +{ + padManager.getPad(padId, function (err, pad) + { + if (err) + { + callback(err); + return; + } + + getPadMediaWiki(pad, revNum, callback); + }); +} + +function _escapeMediaWiki(s) +{ + s = s.replace(/(\/\/|\*\*|__)/g, '%%$1%%'); + return s; +} + +// copied from ACE +var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; +var _REGEX_SPACE = /\s/; +var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')'); +var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g'); + +// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...] + + +function _findURLs(text) +{ + _REGEX_URL.lastIndex = 0; + var urls = null; + var execResult; + while ((execResult = _REGEX_URL.exec(text))) + { + urls = (urls || []); + var startIndex = execResult.index; + var url = execResult[0]; + urls.push([startIndex, url]); + } + + return urls; +} diff --git a/src/static/css/pad.css b/src/static/css/pad.css index df9dde143..b97256d3f 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -571,6 +571,9 @@ table#otheruserstable { #exportdokuwiki { background-position: 0px -459px } +#exportmediawiki { + background-position: 0px -459px +} #importstatusball { display: none } diff --git a/src/static/js/pad_impexp.js b/src/static/js/pad_impexp.js index 08dd42934..adc345c77 100644 --- a/src/static/js/pad_impexp.js +++ b/src/static/js/pad_impexp.js @@ -218,6 +218,7 @@ var padimpexp = (function() $("#exporthtmla").attr("href", pad_root_path + "/export/html"); $("#exportplaina").attr("href", pad_root_path + "/export/txt"); $("#exportdokuwikia").attr("href", pad_root_path + "/export/dokuwiki"); + $("#exportmediawikia").attr("href", pad_root_path + "/export/mediawiki"); //hide stuff thats not avaible if abiword is disabled if(clientVars.abiwordAvailable == "no") diff --git a/src/templates/pad.html b/src/templates/pad.html index 425e476d8..f3be87dbf 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -222,6 +222,7 @@
PDF
OpenDocument
DokuWiki text
+
MediaWiki text
<% e.end_block(); %> diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index 469ddd94e..b94afe7ba 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -141,6 +141,7 @@
PDF
OpenDocument
DokuWiki text
+
MediaWiki text