├── print.less ├── images ├── a_left.png ├── a_center.png ├── a_right.png ├── add_table.png ├── col_left.png ├── col_right.png ├── dropdown.png ├── row_above.png ├── row_below.png ├── inputshadow.png ├── merge_down.png ├── merge_right.png ├── remove_col.png ├── remove_row.png ├── split_down.png ├── split_right.png ├── buttonshadow.png ├── text_heading.png ├── dragmarker_topright.png ├── buttonshadow_toggled.png ├── buttonshadow_toggled2.png ├── dragmarker_bottomleft.png └── dragmarker_bottomright.png ├── plugin.info.txt ├── style.less ├── .travis.yml ├── README.handsontable ├── less ├── jquery.handsontable.rowmove.less ├── jquery.handsontable.columnmove.less ├── editor.less ├── editbutton.less ├── contextmenu.less └── jquery.handsontable.full.less ├── script.js ├── script ├── editbutton.js ├── newtable.js ├── jquery.handsontable.rowmove.js ├── jquery.handsontable.columnmove.js ├── contextmenu.js ├── editor.js └── json2.js ├── lang ├── ko │ └── lang.php ├── ja │ └── lang.php ├── en │ └── lang.php ├── ru │ └── lang.php ├── de │ └── lang.php ├── tr │ └── lang.php ├── eo │ └── lang.php ├── nl │ └── lang.php └── fr │ └── lang.php ├── README ├── action ├── sectionjump.php ├── preprocess.php ├── newtable.php └── editor.php ├── _test ├── renderer_plugin_edittable_inverse.test.php ├── action_plugin_edittable_editor.test.php ├── renderer_plugin_edittable_json.test.php └── renderer_plugin_edittable_inverse.test.txt └── renderer ├── json.php └── inverse.php /print.less: -------------------------------------------------------------------------------- 1 | .dokuwiki div.editbutton_table { 2 | display: none !important; 3 | } -------------------------------------------------------------------------------- /images/a_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/a_left.png -------------------------------------------------------------------------------- /images/a_center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/a_center.png -------------------------------------------------------------------------------- /images/a_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/a_right.png -------------------------------------------------------------------------------- /images/add_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/add_table.png -------------------------------------------------------------------------------- /images/col_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/col_left.png -------------------------------------------------------------------------------- /images/col_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/col_right.png -------------------------------------------------------------------------------- /images/dropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/dropdown.png -------------------------------------------------------------------------------- /images/row_above.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/row_above.png -------------------------------------------------------------------------------- /images/row_below.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/row_below.png -------------------------------------------------------------------------------- /images/inputshadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/inputshadow.png -------------------------------------------------------------------------------- /images/merge_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/merge_down.png -------------------------------------------------------------------------------- /images/merge_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/merge_right.png -------------------------------------------------------------------------------- /images/remove_col.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/remove_col.png -------------------------------------------------------------------------------- /images/remove_row.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/remove_row.png -------------------------------------------------------------------------------- /images/split_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/split_down.png -------------------------------------------------------------------------------- /images/split_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/split_right.png -------------------------------------------------------------------------------- /images/buttonshadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/buttonshadow.png -------------------------------------------------------------------------------- /images/text_heading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/text_heading.png -------------------------------------------------------------------------------- /images/dragmarker_topright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/dragmarker_topright.png -------------------------------------------------------------------------------- /images/buttonshadow_toggled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/buttonshadow_toggled.png -------------------------------------------------------------------------------- /images/buttonshadow_toggled2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/buttonshadow_toggled2.png -------------------------------------------------------------------------------- /images/dragmarker_bottomleft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/dragmarker_bottomleft.png -------------------------------------------------------------------------------- /images/dragmarker_bottomright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamma/edittable/master/images/dragmarker_bottomright.png -------------------------------------------------------------------------------- /plugin.info.txt: -------------------------------------------------------------------------------- 1 | base edittable 2 | author Andreas Gohr 3 | email gohr@cosmocode.de 4 | date 2014-06-18 5 | name edittable plugin 6 | desc Provide a custom editor for tables 7 | url http://www.dokuwiki.org/plugin:edittable 8 | 9 | -------------------------------------------------------------------------------- /style.less: -------------------------------------------------------------------------------- 1 | @import "less/jquery.handsontable.full.less"; 2 | @import "less/jquery.handsontable.columnmove.less"; 3 | @import "less/jquery.handsontable.rowmove.less"; 4 | @import "less/editbutton.less"; 5 | @import "less/editor.less"; 6 | @import "less/contextmenu.less"; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - "5.5" 4 | - "5.4" 5 | - "5.3" 6 | env: 7 | - DOKUWIKI=master 8 | - DOKUWIKI=stable 9 | - DOKUWIKI=old-stable 10 | before_install: wget https://raw.github.com/splitbrain/dokuwiki-travis/master/travis.sh 11 | install: sh travis.sh 12 | script: cd _test && phpunit --stderr --group plugin_edittable 13 | -------------------------------------------------------------------------------- /README.handsontable: -------------------------------------------------------------------------------- 1 | The handsontable version used in this plugin is slightly modified and 2 | maintained at https://github.com/cosmocode/jquery-handsontable/tree/dokuwiki 3 | 4 | No modifications should be done on the handsontable.* files here! Changes 5 | need to be done in the source files in the above repository, be compiled there 6 | and copied over here. 7 | -------------------------------------------------------------------------------- /less/jquery.handsontable.rowmove.less: -------------------------------------------------------------------------------- 1 | .handsontable th .editTableRowMover { 2 | position: absolute; 3 | left: 0; 4 | top: 0; 5 | background-color: transparent; 6 | width: 50px; 7 | height: 5px; 8 | z-index: 999; 9 | cursor: move; 10 | 11 | &:hover, 12 | &.active { 13 | background-color: #88F; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /less/jquery.handsontable.columnmove.less: -------------------------------------------------------------------------------- 1 | .handsontable th .editTableColumnMover { 2 | position: absolute; 3 | left: 0; 4 | top: 0; 5 | background-color: transparent; 6 | width: 5px; 7 | height: 25px; 8 | z-index: 999; 9 | cursor: move; 10 | 11 | &:hover, 12 | &.active { 13 | background-color: #88F; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | /* DOKUWIKI:include_once script/json2.js */ 2 | /* DOKUWIKI:include_once script/jquery.handsontable.full.js */ 3 | /* DOKUWIKI:include script/jquery.handsontable.columnmove.js */ 4 | /* DOKUWIKI:include script/jquery.handsontable.rowmove.js */ 5 | 6 | /* DOKUWIKI:include script/contextmenu.js */ 7 | /* DOKUWIKI:include script/editor.js */ 8 | /* DOKUWIKI:include script/newtable.js */ 9 | /* DOKUWIKI:include script/editbutton.js */ 10 | -------------------------------------------------------------------------------- /less/editor.less: -------------------------------------------------------------------------------- 1 | #edittable__editor { 2 | margin-bottom: 1.4em; 3 | 4 | table { 5 | 6 | td.right { 7 | text-align: right; 8 | float: none; // fix incompatibility with some templates (like ICKE) 9 | } 10 | 11 | td.center { 12 | text-align: center; 13 | } 14 | 15 | td.header { 16 | font-weight: bold; 17 | background-color: @ini_background_alt; 18 | } 19 | 20 | } 21 | } 22 | 23 | // add a z-index to DokuWiki's toolbar pickers 24 | div.picker { 25 | z-index: 500; 26 | } -------------------------------------------------------------------------------- /script/editbutton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adjust the top margin and make buttons visible 3 | */ 4 | jQuery(function () { 5 | var $editbutton = jQuery('.dokuwiki div.editbutton_table'); 6 | if (!$editbutton.length) return; 7 | 8 | // unhide the buttons - we have JavaScript 9 | $editbutton.show(); 10 | 11 | // determine the bottom margin of the table above and remove it from our button 12 | var margin = 0; 13 | var $tablediv = $editbutton.prev('div.table'); 14 | if (!$tablediv.length) return; 15 | margin += parseFloat($tablediv.css('margin-bottom')); 16 | margin += parseFloat($tablediv.find('table').css('margin-bottom')); 17 | margin += 1; // for the border 18 | 19 | $editbutton.css('margin-top', margin * -1); 20 | }); -------------------------------------------------------------------------------- /lang/ko/lang.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['secedit_name'] = 'テーブル'; 9 | $lang['add_table'] = '新規テーブルの挿入'; 10 | $lang['js']['toggle_header'] = 'ヘッダー状態の切替'; 11 | $lang['js']['align_left'] = '左揃え'; 12 | $lang['js']['align_center'] = '中央揃え'; 13 | $lang['js']['align_right'] = '右揃え'; 14 | $lang['js']['confirmdeleterow'] = '本当に行を削除しますか?'; 15 | $lang['js']['confirmdeletecol'] = '本当に列を削除しますか?'; 16 | $lang['js']['row_above'] = '上に行を追加する'; 17 | $lang['js']['remove_row'] = '行を削除する'; 18 | $lang['js']['row_below'] = '下に行を追加する'; 19 | $lang['js']['col_left'] = '左に列を追加する'; 20 | $lang['js']['remove_col'] = '列を削除する'; 21 | $lang['js']['col_right'] = '右列を追加する'; 22 | $lang['js']['colspan_add'] = '横のセル結合を増やす'; 23 | $lang['js']['colspan_del'] = '横のセル結合を減らす'; 24 | $lang['js']['rowspan_add'] = '縦のセル結合を増やす'; 25 | $lang['js']['rowspan_del'] = '縦のセル結合を減らす'; 26 | -------------------------------------------------------------------------------- /lang/en/lang.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['secedit_name'] = 'Таблица'; 9 | $lang['add_table'] = 'Вставить новую таблицу'; 10 | $lang['js']['align_left'] = 'По левому краю'; 11 | $lang['js']['align_center'] = 'По центру'; 12 | $lang['js']['align_right'] = 'По правому краю'; 13 | $lang['js']['confirmdeleterow'] = 'Действительно удалить строку?'; 14 | $lang['js']['confirmdeletecol'] = 'Действительно удалить столбец?'; 15 | $lang['js']['row_above'] = 'Добавить строку сверху'; 16 | $lang['js']['remove_row'] = 'Удалить строку'; 17 | $lang['js']['row_below'] = 'Добавить строку ниже'; 18 | $lang['js']['col_left'] = 'Добавить столбец слева'; 19 | $lang['js']['remove_col'] = 'Удалить столбец'; 20 | $lang['js']['col_right'] = 'Объединить ячейку с правой ячейкой'; 21 | $lang['js']['colspan_add'] = 'Объединить ячейку с правой ячейкой'; 22 | $lang['js']['rowspan_add'] = 'Объединить ячейку с нижней ячейкой'; 23 | -------------------------------------------------------------------------------- /lang/de/lang.php: -------------------------------------------------------------------------------- 1 | , 18 | Adrian Lang 19 | 20 | This program is free software; you can redistribute it and/or modify 21 | it under the terms of the GNU General Public License as published by 22 | the Free Software Foundation; version 2 of the License 23 | 24 | This program is distributed in the hope that it will be useful, 25 | but WITHOUT ANY WARRANTY; without even the implied warranty of 26 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 27 | GNU General Public License for more details. 28 | 29 | See the COPYING file in your DokuWiki folder for details 30 | -------------------------------------------------------------------------------- /lang/tr/lang.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['secedit_name'] = 'Tablo'; 9 | $lang['add_table'] = 'Yeni bir tablo ekle'; 10 | $lang['js']['toggle_header'] = 'Başlık durumunu değiştir'; 11 | $lang['js']['align_left'] = 'Hücreyi sola hizala'; 12 | $lang['js']['align_center'] = 'Hücreyi ortala'; 13 | $lang['js']['align_right'] = 'Hücreyi sağa hizala'; 14 | $lang['js']['confirmdeleterow'] = 'Satır silinsin mi?'; 15 | $lang['js']['confirmdeletecol'] = 'Sütun silinsin mi?'; 16 | $lang['js']['row_above'] = 'Üste satır ekle'; 17 | $lang['js']['remove_row'] = 'Satırı sil'; 18 | $lang['js']['row_below'] = 'Alta satır ekle'; 19 | $lang['js']['col_left'] = 'Sola sütun ekle'; 20 | $lang['js']['remove_col'] = 'Sütunu sil'; 21 | $lang['js']['col_right'] = 'Sağa sütun ekle'; 22 | $lang['js']['colspan_add'] = 'Sütun kapsamını arttır'; 23 | $lang['js']['colspan_del'] = 'Sütun kapsamını azalt'; 24 | $lang['js']['rowspan_add'] = 'Satır kapsamını arttır'; 25 | $lang['js']['rowspan_del'] = 'Satır kapsamını azalt'; 26 | -------------------------------------------------------------------------------- /action/sectionjump.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | if(!defined('DOKU_INC')) die(); 9 | if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/'); 10 | 11 | class action_plugin_edittable_sectionjump extends DokuWiki_Action_Plugin { 12 | 13 | /** 14 | * Register its handlers with the DokuWiki's event controller 15 | */ 16 | function register(Doku_Event_Handler $controller) { 17 | $controller->register_hook('ACTION_SHOW_REDIRECT', 'BEFORE', $this, 'jump_to_section'); 18 | } 19 | 20 | /** 21 | * Jump after save to the section containing this table 22 | * 23 | * @param Doku_Event $event 24 | */ 25 | function jump_to_section($event) { 26 | global $INPUT; 27 | if(!$INPUT->has('edittable_data')) return; 28 | 29 | global $PRE; 30 | if(preg_match_all('/^\s*={2,}([^=\n]+)/m', $PRE, $match, PREG_SET_ORDER)) { 31 | $check = false; //Byref 32 | $match = array_pop($match); 33 | $event->data['fragment'] = sectionID($match[1], $check); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lang/eo/lang.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['secedit_name'] = 'Tabelo'; 9 | $lang['add_table'] = 'Enmeti novan tabelon'; 10 | $lang['js']['toggle_header'] = '(Mal)ŝalti la titollinion'; 11 | $lang['js']['align_left'] = 'Maldekstrigi ĉelon'; 12 | $lang['js']['align_center'] = 'Centrigi ĉelon'; 13 | $lang['js']['align_right'] = 'Dekstrigi ĉelon'; 14 | $lang['js']['confirmdeleterow'] = 'Ĉu vere forigi vicon?'; 15 | $lang['js']['confirmdeletecol'] = 'Ĉu vere forigi kolumnon?'; 16 | $lang['js']['row_above'] = 'Aldoni vicon supre'; 17 | $lang['js']['remove_row'] = 'Forigi vivon'; 18 | $lang['js']['row_below'] = 'Aldoni vicon sube'; 19 | $lang['js']['col_left'] = 'Aldoni kolumnon maldekstre'; 20 | $lang['js']['remove_col'] = 'Forigi kolumnon'; 21 | $lang['js']['col_right'] = 'Aldoni kolumnon dekstre'; 22 | $lang['js']['colspan_add'] = 'Kunfandi pli da kolumnoj'; 23 | $lang['js']['colspan_del'] = 'Kunfandi malpli da kolumnoj'; 24 | $lang['js']['rowspan_add'] = 'Kunfandi pli da vicoj'; 25 | $lang['js']['rowspan_del'] = 'Kunfandi malpli da vicoj'; 26 | -------------------------------------------------------------------------------- /lang/nl/lang.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['secedit_name'] = 'Tabel'; 9 | $lang['add_table'] = 'Een nieuwe tabel invoegen'; 10 | $lang['js']['toggle_header'] = 'Wissel tussen kop en tekst'; 11 | $lang['js']['align_left'] = 'Cel links uitlijnen'; 12 | $lang['js']['align_center'] = 'Cel gecentreerd uitlijnen'; 13 | $lang['js']['align_right'] = 'Cel rechts uitlijnen'; 14 | $lang['js']['confirmdeleterow'] = 'Rij echt verwijderen?'; 15 | $lang['js']['confirmdeletecol'] = 'Kolom echt verwijderen?'; 16 | $lang['js']['row_above'] = 'Rij invoegen hierboven'; 17 | $lang['js']['remove_row'] = 'Rij verwijderen'; 18 | $lang['js']['row_below'] = 'Rij invoegen hieronder'; 19 | $lang['js']['col_left'] = 'Links kolom invoegen'; 20 | $lang['js']['remove_col'] = 'Kolom verwijderen'; 21 | $lang['js']['col_right'] = 'Rechts kolom invoegen'; 22 | $lang['js']['colspan_add'] = 'Kolomsamenvoeging verlengen'; 23 | $lang['js']['colspan_del'] = 'Kolomsamenvoeging verkorten'; 24 | $lang['js']['rowspan_add'] = 'Rijsamenvoeging verlengen'; 25 | $lang['js']['rowspan_del'] = 'Rijsamenvoeging verkorten'; 26 | -------------------------------------------------------------------------------- /lang/fr/lang.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $lang['secedit_name'] = 'Tableau'; 9 | $lang['add_table'] = 'Insérer une nouvelle ligne.'; 10 | $lang['js']['toggle_header'] = 'dés(activer) entêtes'; 11 | $lang['js']['align_left'] = 'Alignement à gauche'; 12 | $lang['js']['align_center'] = 'Centrage'; 13 | $lang['js']['align_right'] = 'Alignement à droite'; 14 | $lang['js']['confirmdeleterow'] = 'Vraiment supprimer la ligne ?'; 15 | $lang['js']['confirmdeletecol'] = 'vraiment supprimer la colonne ?'; 16 | $lang['js']['row_above'] = 'Ajouter une ligne au dessus'; 17 | $lang['js']['remove_row'] = 'Supprimer la ligne'; 18 | $lang['js']['row_below'] = 'Ajouter une ligne en dessous'; 19 | $lang['js']['col_left'] = 'Ajouter une colonne à gauche'; 20 | $lang['js']['remove_col'] = 'Supprimer la colonne'; 21 | $lang['js']['col_right'] = 'Ajouter une colonne à droite'; 22 | $lang['js']['colspan_add'] = 'Agrandir la cellule horizontalement'; 23 | $lang['js']['colspan_del'] = 'Réduire la cellule horizontalement'; 24 | $lang['js']['rowspan_add'] = 'Augmenter la cellule verticalement'; 25 | $lang['js']['rowspan_del'] = 'Réduire la cellule verticalement'; 26 | -------------------------------------------------------------------------------- /action/preprocess.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | if(!defined('DOKU_INC')) die(); 9 | if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/'); 10 | 11 | /** 12 | * just intercepts ACTION_ACT_PREPROCESS and emits two new events 13 | * 14 | * We have two action components handling above event but need them to execute in a specific order. 15 | * That's currently not possible to guarantee, so we catch the event only once and emit two of our own 16 | * in the right order. Once DokuWiki supports a sort we can skip this. 17 | */ 18 | class action_plugin_edittable_preprocess extends DokuWiki_Action_Plugin { 19 | 20 | /** 21 | * Register its handlers with the DokuWiki's event controller 22 | */ 23 | function register(Doku_Event_Handler $controller) { 24 | // register preprocessing for accepting editor data 25 | $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_preprocess'); 26 | } 27 | 28 | /** 29 | * See class description for WTF we're doing here 30 | * 31 | * @param Doku_Event $event 32 | */ 33 | public function handle_preprocess(Doku_Event $event){ 34 | trigger_event('PLUGIN_EDITTABLE_PREPROCESS_EDITOR', $event->data); 35 | trigger_event('PLUGIN_EDITTABLE_PREPROCESS_NEWTABLE', $event->data); 36 | } 37 | } -------------------------------------------------------------------------------- /_test/renderer_plugin_edittable_inverse.test.php: -------------------------------------------------------------------------------- 1 | render($input); 13 | $this->assertEquals($input, $output); 14 | } 15 | 16 | function test_fullsyntax() { 17 | $input = io_readFile(dirname(__FILE__).'/'.basename(__FILE__, '.php').'.txt'); 18 | $this->assertTrue(strlen($input) > 1000); // make sure we got what we want 19 | $output = $this->render($input); 20 | 21 | $input = $this->noWS($input); 22 | $output = $this->noWS($output); 23 | $this->assertEquals($input, $output); 24 | } 25 | 26 | /** 27 | * reduce spaces and newlines to single occurances 28 | * 29 | * @param $text 30 | * @return mixed 31 | */ 32 | protected function noWS($text) { 33 | $text = preg_replace('/\n+/s', "\n", $text); 34 | $text = preg_replace('/ +/', ' ', $text); 35 | return $text; 36 | } 37 | 38 | /** 39 | * render the given text with the inverse renderer 40 | * 41 | * @param $text 42 | * @return string 43 | */ 44 | protected function render($text) { 45 | $instructions = p_get_instructions($text); 46 | $Renderer = new renderer_plugin_edittable_inverse(); 47 | 48 | foreach($instructions as $instruction) { 49 | // Execute the callback against the Renderer 50 | call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1]); 51 | } 52 | return $Renderer->doc; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /script/newtable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Add button action for your toolbar button 3 | * 4 | * @param {jQuery} $btn Button element to add the action to 5 | * @param {Array} props Associative array of button properties 6 | * @param {string} edid ID of the editor textarea 7 | * @return {string} If button should be appended return the id for in aria-controls, 8 | * otherwise an empty string 9 | */ 10 | function addBtnActionNewTable($btn, props, edid) { 11 | 12 | $btn.click(function () { 13 | var editform = jQuery('#dw__editform')[0]; 14 | var ed = jQuery('#' + edid)[0]; 15 | 16 | function addField(name, val) { 17 | var pos_field = document.createElement('textarea'); 18 | pos_field.name = 'edittable__new[' + name + ']'; 19 | pos_field.value = val; 20 | pos_field.style.display = 'none'; 21 | editform.appendChild(pos_field); 22 | } 23 | 24 | var sel; 25 | if (window.DWgetSelection) { 26 | sel = DWgetSelection(ed); 27 | } else { 28 | sel = getSelection(ed); 29 | } 30 | addField('pre', ed.value.substr(0, sel.start)); 31 | addField('text', ed.value.substr(sel.start, sel.end - sel.start)); 32 | addField('suf', ed.value.substr(sel.end)); 33 | 34 | // adora belle requires a range, even though we handle ranging ourselve here 35 | var range = document.createElement('input'); 36 | range.name = 'range'; 37 | range.value = '0-0'; 38 | range.type = 'hidden'; 39 | editform.appendChild(range); 40 | 41 | // Fake POST 42 | var editbutton = document.createElement('input'); 43 | editbutton.name = 'do[edit]'; 44 | editbutton.type = 'submit'; 45 | editbutton.style.display = 'none'; 46 | editform.appendChild(editbutton); 47 | // Prevent warning 48 | window.textChanged = false; 49 | editbutton.click(); 50 | 51 | }); 52 | return 'click'; 53 | } 54 | -------------------------------------------------------------------------------- /less/contextmenu.less: -------------------------------------------------------------------------------- 1 | .htContextMenu tbody { 2 | 3 | div { 4 | padding-left: 20px; 5 | background-position: center left; 6 | background-repeat: no-repeat; 7 | 8 | &.toggle_header { 9 | background-image: url('../plugins/edittable/images/text_heading.png'); 10 | } 11 | 12 | &.align_left { 13 | background-image: url('../plugins/edittable/images/a_left.png'); 14 | } 15 | 16 | &.align_center { 17 | background-image: url('../plugins/edittable/images/a_center.png'); 18 | } 19 | 20 | &.align_right { 21 | background-image: url('../plugins/edittable/images/a_right.png'); 22 | } 23 | 24 | &.row_above { 25 | background-image: url('../plugins/edittable/images/row_above.png'); 26 | } 27 | 28 | &.remove_row { 29 | background-image: url('../plugins/edittable/images/remove_row.png'); 30 | } 31 | 32 | &.row_below { 33 | background-image: url('../plugins/edittable/images/row_below.png'); 34 | } 35 | 36 | 37 | &.col_left { 38 | background-image: url('../plugins/edittable/images/col_left.png'); 39 | } 40 | 41 | &.remove_col { 42 | background-image: url('../plugins/edittable/images/remove_col.png'); 43 | } 44 | 45 | &.col_right { 46 | background-image: url('../plugins/edittable/images/col_right.png'); 47 | } 48 | 49 | &.colspan_add { 50 | background-image: url('../plugins/edittable/images/merge_right.png'); 51 | } 52 | 53 | &.colspan_del { 54 | background-image: url('../plugins/edittable/images/split_right.png'); 55 | } 56 | 57 | &.rowspan_add { 58 | background-image: url('../plugins/edittable/images/merge_down.png'); 59 | } 60 | 61 | &.rowspan_del { 62 | background-image: url('../plugins/edittable/images/split_down.png'); 63 | } 64 | } 65 | 66 | td.htSeparator div { 67 | padding-left: 0; 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /_test/action_plugin_edittable_editor.test.php: -------------------------------------------------------------------------------- 1 | 'left', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'th'), 22 | array('align' => 'center', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'th'), 23 | array('align' => 'right', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'th'), 24 | array('align' => 'left', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'th'), 25 | ), 26 | array( 27 | array('align' => 'left', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 28 | array('align' => 'left', 'colspan' => 2, 'rowspan' => 2, 'tag' => 'td'), 29 | array('hide' => true), 30 | array('align' => null, 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 31 | ), 32 | array( 33 | array('align' => 'left', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 34 | array('hide' => true), 35 | array('hide' => true), 36 | array('align' => null, 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 37 | ), 38 | array( 39 | array('align' => 'left', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 40 | array('align' => 'left', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 41 | array('align' => null, 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 42 | array('align' => null, 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 43 | ), 44 | ); 45 | 46 | $expect = <<build_table($data, $meta); 56 | 57 | $this->assertEquals($expect, $output); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /_test/renderer_plugin_edittable_json.test.php: -------------------------------------------------------------------------------- 1 | 'left', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'th'), 30 | array('align' => 'center', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'th'), 31 | array('align' => 'right', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'th'), 32 | array('align' => 'left', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'th'), 33 | ), 34 | array( 35 | array('align' => 'left', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 36 | array('align' => 'left', 'colspan' => 2, 'rowspan' => 2, 'tag' => 'td'), 37 | array('hide' => true, 'rowspan' => 1, 'colspan' => 1), 38 | array('align' => null, 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 39 | ), 40 | array( 41 | array('align' => 'left', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 42 | array('hide' => true, 'rowspan' => 1, 'colspan' => 1), 43 | array('hide' => true, 'rowspan' => 1, 'colspan' => 1), 44 | array('align' => null, 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 45 | ), 46 | array( 47 | array('align' => 'left', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 48 | array('align' => 'left', 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 49 | array('align' => null, 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 50 | array('align' => null, 'colspan' => 1, 'rowspan' => 1, 'tag' => 'td'), 51 | ), 52 | ); 53 | 54 | $renderer = $this->render($input); 55 | $json = new JSON(JSON_LOOSE_TYPE); 56 | 57 | $this->assertEquals($data, $json->decode($renderer->getDataJSON())); 58 | $this->assertEquals($meta, $json->decode($renderer->getMetaJSON())); 59 | } 60 | 61 | 62 | /** 63 | * render the given text with the JSON table renderer 64 | * 65 | * @param $text 66 | * @return renderer_plugin_edittable_json 67 | */ 68 | protected function render($text) { 69 | $instructions = p_get_instructions($text); 70 | $Renderer = new renderer_plugin_edittable_json(); 71 | 72 | foreach($instructions as $instruction) { 73 | // Execute the callback against the Renderer 74 | call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1]); 75 | } 76 | return $Renderer; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /action/newtable.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | if(!defined('DOKU_INC')) die(); 9 | if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/'); 10 | 11 | /** 12 | * Handles the inserting of a new table in a running edit session 13 | */ 14 | class action_plugin_edittable_newtable extends DokuWiki_Action_Plugin { 15 | 16 | /** 17 | * Register its handlers with the DokuWiki's event controller 18 | */ 19 | function register(Doku_Event_Handler $controller) { 20 | $controller->register_hook('TOOLBAR_DEFINE', 'AFTER', $this, 'toolbar'); 21 | 22 | //$controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_newtable'); 23 | $controller->register_hook('PLUGIN_EDITTABLE_PREPROCESS_NEWTABLE', 'BEFORE', $this, 'handle_newtable'); 24 | } 25 | 26 | /** 27 | * Add a button for inserting tables to the toolbar array 28 | * 29 | * @param Doku_Event $event 30 | */ 31 | function toolbar($event) { 32 | $event->data[] = array( 33 | 'title' => $this->getLang('add_table'), 34 | 'type' => 'NewTable', 35 | 'icon' => '../../plugins/edittable/images/add_table.png', 36 | 'block' => true 37 | ); 38 | } 39 | 40 | /** 41 | * Handle the click on the new table button in the toolbar 42 | * 43 | * @param Doku_Event $event 44 | */ 45 | function handle_newtable($event) { 46 | global $INPUT; 47 | global $TEXT; 48 | global $ACT; 49 | 50 | if(!$INPUT->post->has('edittable__new')) return; 51 | 52 | /* 53 | * $fields['pre'] has all data before the selection when the "Insert table" button was clicked 54 | * $fields['text'] has all data inside the selection when the "Insert table" button was clicked 55 | * $fields['suf'] has all data after the selection when the "Insert table" button was clicked 56 | * $TEXT has the table created by the editor (from action_plugin_edittable_editor::handle_table_post()) 57 | */ 58 | $fields = $INPUT->post->arr('edittable__new'); 59 | 60 | // clean the fields (undos formText()) and update the post and request arrays 61 | $fields['pre'] = cleanText($fields['pre']); 62 | $fields['text'] = cleanText($fields['text']); 63 | $fields['suf'] = cleanText($fields['suf']); 64 | $INPUT->post->set('edittable__new', $fields); 65 | 66 | 67 | $ACT = act_clean($ACT); 68 | switch($ACT){ 69 | case 'preview': 70 | // preview view of a table edit 71 | $INPUT->post->set('target', 'table'); 72 | break; 73 | case 'edit': 74 | // edit view of a table (first edit) 75 | $INPUT->post->set('target', 'table'); 76 | $TEXT = "^ ^ ^\n"; 77 | foreach(explode("\n", $fields['text']) as $line) { 78 | $TEXT .= "| $line | |\n"; 79 | } 80 | break; 81 | case 'draftdel': 82 | // not sure if/how this would happen, we restore all data and hand over to section edit 83 | $INPUT->post->set('target', 'section'); 84 | $TEXT = $fields['pre'].$fields['text'].$fields['suf']; 85 | $ACT = 'edit'; 86 | break; 87 | case 'save': 88 | // return to edit page 89 | $INPUT->post->set('target', 'section'); 90 | $TEXT = $fields['pre'].$TEXT.$fields['suf']; 91 | $ACT = 'edit'; 92 | break; 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /renderer/json.php: -------------------------------------------------------------------------------- 1 | 9 | * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 10 | */ 11 | 12 | // must be run within Dokuwiki 13 | if (!defined('DOKU_INC')) die(); 14 | 15 | require_once DOKU_PLUGIN . "/edittable/renderer/inverse.php"; 16 | 17 | class renderer_plugin_edittable_json extends renderer_plugin_edittable_inverse { 18 | /** @var array holds the data cells */ 19 | private $tdata = array(); 20 | /** @var array holds the cell meta data */ 21 | private $tmeta = array(); 22 | 23 | /** @var array holds the meta data of the current cell */ 24 | private $tmetacell = array(); 25 | 26 | /** @var int current row */ 27 | private $current_row = -1; 28 | 29 | /** @var int current column */ 30 | private $current_col = 0; 31 | 32 | /** 33 | * Returns the whole table data as two dimensional array 34 | * 35 | * @return array 36 | */ 37 | public function getDataJSON() { 38 | $json = new JSON(); 39 | return $json->encode($this->tdata); 40 | } 41 | 42 | /** 43 | * Returns meta data for all cells in a two dimensional array of arrays 44 | * 45 | * @return array 46 | */ 47 | public function getMetaJSON() { 48 | $json = new JSON(); 49 | return $json->encode($this->tmeta); 50 | } 51 | 52 | // renderer functions below 53 | 54 | function table_open($maxcols = null, $numrows = null, $pos = null) { 55 | // FIXME: is this needed somewhere? $this->_counter['table_begin_pos'] = strlen($this->doc); 56 | } 57 | 58 | function table_close($pos = null) { 59 | } 60 | 61 | function tablerow_open() { 62 | // move counters 63 | $this->current_row++; 64 | $this->current_col = 0; 65 | } 66 | 67 | function tablerow_close() { 68 | // resort just for better debug readability 69 | ksort($this->tdata[$this->current_row]); 70 | ksort($this->tmeta[$this->current_row]); 71 | } 72 | 73 | function tableheader_open($colspan = 1, $align = null, $rowspan = 1) { 74 | $this->_tablefield_open('th', $colspan, $align, $rowspan); 75 | } 76 | 77 | function tableheader_close() { 78 | $this->_tablefield_close(); 79 | } 80 | 81 | function tablecell_open($colspan = 1, $align = null, $rowspan = 1) { 82 | $this->_tablefield_open('td', $colspan, $align, $rowspan); 83 | } 84 | 85 | function tablecell_close() { 86 | $this->_tablefield_close(); 87 | } 88 | 89 | /** 90 | * Used for a opening THs and TDs 91 | * 92 | * @param $tag 93 | * @param $colspan 94 | * @param $align 95 | * @param $rowspan 96 | */ 97 | private function _tablefield_open($tag, $colspan, $align, $rowspan) { 98 | // skip cells that already exist - those are previous (span) cells! 99 | while(isset($this->tmeta[$this->current_row][$this->current_col])) { 100 | $this->current_col++; 101 | } 102 | 103 | // remember these, we use them when closing 104 | $this->tmetacell = array(); 105 | $this->tmetacell['tag'] = $tag; 106 | $this->tmetacell['colspan'] = $colspan; 107 | $this->tmetacell['rowspan'] = $rowspan; 108 | $this->tmetacell['align'] = $align; 109 | 110 | // empty $doc 111 | $this->doc = ''; 112 | } 113 | 114 | /** 115 | * Used for closing THs and TDs 116 | */ 117 | private function _tablefield_close() { 118 | // these have been set to the correct cell already 119 | $row = $this->current_row; 120 | $col = $this->current_col; 121 | 122 | $this->tdata[$row][$col] = trim(str_replace("\n", ' ', $this->doc)); // no newlines in table cells! 123 | $this->tmeta[$row][$col] = $this->tmetacell; // as remembered in the open call 124 | 125 | // now fill up missing span cells 126 | { 127 | $rowspan = $this->tmetacell['rowspan']; 128 | $colspan = $this->tmetacell['colspan']; 129 | 130 | for($c = 1; $c < $colspan; $c++) { 131 | // hide colspanned cell in same row 132 | $this->tmeta[$row][$col + $c]['hide'] = true; 133 | $this->tmeta[$row][$col + $c]['rowspan'] = 1; 134 | $this->tmeta[$row][$col + $c]['colspan'] = 1; 135 | $this->tdata[$row][$col + $c] = ''; 136 | 137 | // hide colspanned rows below if rowspan is in effect as well 138 | for($r = 1; $r < $rowspan; $r++) { 139 | $this->tmeta[$row + $r][$col + $c]['hide'] = true; 140 | $this->tmeta[$row + $r][$col + $c]['rowspan'] = 1; 141 | $this->tmeta[$row + $r][$col + $c]['colspan'] = 1; 142 | $this->tdata[$row + $r][$col + $c] = ''; 143 | } 144 | } 145 | 146 | // hide rowspanned columns 147 | for($r = 1; $r < $rowspan; $r++) { 148 | $this->tmeta[$row + $r][$col]['hide'] = true; 149 | $this->tmeta[$row + $r][$col]['rowspan'] = 1; 150 | $this->tmeta[$row + $r][$col]['colspan'] = 1; 151 | $this->tdata[$row + $r][$col] = ':::'; 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /script/jquery.handsontable.rowmove.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handle Row Moves on our Editor 3 | * 4 | * based on the example plugin that comes with Handsontable but simplified and adjusted to work on our 5 | * custom data/meta datastructure 6 | * 7 | * @author Andreas Gohr 8 | * @constructor 9 | */ 10 | function EditTableRowMove() { 11 | var pressed, 12 | startRow, 13 | endRow, 14 | startY, 15 | startOffset; 16 | 17 | var ghost = document.createElement('DIV'), 18 | ghostStyle = ghost.style; 19 | 20 | ghost.className = 'ghost'; 21 | ghostStyle.position = 'absolute'; 22 | ghostStyle.top = '25px'; 23 | ghostStyle.left = '50px'; 24 | ghostStyle.width = '10px'; 25 | ghostStyle.height = '10px'; 26 | ghostStyle.backgroundColor = '#CCC'; 27 | ghostStyle.opacity = 0.7; 28 | 29 | /** 30 | * Attach all the events 31 | */ 32 | var bindMoveRowEvents = function () { 33 | var instance = this; 34 | 35 | /** 36 | * during drag operation 37 | */ 38 | instance.rootElement.on('mousemove.editTableRowMove', function (e) { 39 | if (pressed) { 40 | ghostStyle.top = startOffset + e.pageY - startY + 6 + 'px'; 41 | if (ghostStyle.display === 'none') { 42 | ghostStyle.display = 'block'; 43 | } 44 | } 45 | }); 46 | 47 | /** 48 | * end of drag operation 49 | * 50 | * The row move is executed here 51 | */ 52 | instance.rootElement.on('mouseup.editTableRowMove', function () { 53 | if (pressed) { 54 | if (startRow < endRow) { 55 | endRow--; 56 | } 57 | if (instance.getSettings().colHeaders) { 58 | startRow--; 59 | endRow--; 60 | } 61 | jQuery('.editTableRowMover.active').removeClass('active'); 62 | pressed = false; 63 | ghostStyle.display = 'none'; 64 | 65 | // rows are off by one 66 | startRow += 1; 67 | endRow += 1; 68 | 69 | if (startRow == endRow) return; 70 | 71 | // if this row is part of a row span, do not move 72 | if (instance.raw.rowinfo[endRow].rowspan) return; 73 | 74 | // swap the rows 75 | instance.raw.data.splice(endRow, 0, instance.raw.data.splice(startRow, 1)[0]); 76 | instance.raw.meta.splice(endRow, 0, instance.raw.meta.splice(startRow, 1)[0]); 77 | 78 | instance.forceFullRender = true; 79 | instance.view.render(); //updates all 80 | } 81 | }); 82 | 83 | /** 84 | * start drag operation 85 | */ 86 | instance.rootElement.on('mousedown.editTableRowMove', '.editTableRowMover', function (e) { 87 | 88 | var mover = e.currentTarget; 89 | var TH = instance.view.wt.wtDom.closest(mover, 'TH'); 90 | startRow = instance.view.wt.wtDom.index(TH.parentNode) + instance.rowOffset(); 91 | endRow = startRow; 92 | pressed = true; 93 | startY = e.pageY; 94 | 95 | var TABLE = instance.$table[0]; 96 | TABLE.parentNode.appendChild(ghost); 97 | ghostStyle.height = instance.view.wt.wtDom.outerHeight(TH) + 'px'; 98 | ghostStyle.width = instance.view.wt.wtDom.outerWidth(TABLE) + 'px'; 99 | startOffset = parseInt(instance.view.wt.wtDom.offset(TH).top - instance.view.wt.wtDom.offset(TABLE).top, 10); 100 | ghostStyle.top = startOffset + 6 + 'px'; 101 | }); 102 | 103 | /** 104 | * on mouseover, show drag handle 105 | */ 106 | instance.rootElement.on('mouseenter.editTableRowMove', 'td, th', function () { 107 | if (pressed) { 108 | var active = instance.view.TBODY.querySelector('.editTableRowMover.active'); 109 | if (active) { 110 | instance.view.wt.wtDom.removeClass(active, 'active'); 111 | } 112 | endRow = instance.view.wt.wtDom.index(this.parentNode) + instance.rowOffset(); 113 | 114 | // if this row is part of a row span, do not move 115 | if (instance.raw.rowinfo[endRow].rowspan) return; 116 | 117 | var THs = instance.view.TBODY.querySelectorAll('th'); 118 | var mover = THs[endRow].querySelector('.editTableRowMover'); 119 | instance.view.wt.wtDom.addClass(mover, 'active'); 120 | } 121 | }); 122 | 123 | instance.addHook('afterDestroy', unbindMoveRowEvents); 124 | }; 125 | 126 | /** 127 | * Remove all the Events 128 | */ 129 | var unbindMoveRowEvents = function () { 130 | var instance = this; 131 | instance.rootElement.off('mouseup.editTableRowMove'); 132 | instance.rootElement.off('mousemove.editTableRowMove'); 133 | instance.rootElement.off('mousedown.editTableRowMove'); 134 | instance.rootElement.off('mouseenter.editTableRowMove'); 135 | }; 136 | 137 | /** 138 | * Initialize the plugin 139 | */ 140 | this.init = function () { 141 | var instance = this; 142 | var editTableRowMoveEnabled = !!(this.getSettings().editTableRowMove); 143 | 144 | if (editTableRowMoveEnabled) { 145 | instance.forceFullRender = true; 146 | bindMoveRowEvents.call(this); 147 | 148 | } else { 149 | unbindMoveRowEvents.call(this); 150 | } 151 | }; 152 | 153 | /** 154 | * Add a move handler to each row head 155 | * 156 | * @param row 157 | * @param TH 158 | */ 159 | this.getRowHeader = function (row, TH) { 160 | if (row > -1 && this.getSettings().editTableRowMove) { 161 | // if this row is part of a row span, do not add move handle 162 | if (this.raw.rowinfo[row].rowspan) return; 163 | 164 | var DIV = document.createElement('DIV'); 165 | DIV.className = 'editTableRowMover'; 166 | TH.firstChild.appendChild(DIV); 167 | } 168 | }; 169 | } 170 | 171 | var htEditTableRowMove = new EditTableRowMove(); 172 | 173 | Handsontable.PluginHooks.add('afterInit', function () { 174 | htEditTableRowMove.init.call(this); 175 | }); 176 | 177 | Handsontable.PluginHooks.add('afterGetRowHeader', htEditTableRowMove.getRowHeader); 178 | -------------------------------------------------------------------------------- /script/jquery.handsontable.columnmove.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handle Column Moves on our Editor 3 | * 4 | * based on the example plugin that comes with Handsontable but simplified and adjusted to work on our 5 | * custom data/meta datastructure 6 | * 7 | * @author Andreas Gohr 8 | * @constructor 9 | */ 10 | function EditTableColumnMove() { 11 | var pressed, 12 | startCol, 13 | endCol, 14 | startX, 15 | startOffset; 16 | 17 | var ghost = document.createElement('DIV'), 18 | ghostStyle = ghost.style; 19 | 20 | ghost.className = 'ghost'; 21 | ghostStyle.position = 'absolute'; 22 | ghostStyle.top = '25px'; 23 | ghostStyle.left = 0; 24 | ghostStyle.width = '10px'; 25 | ghostStyle.height = '10px'; 26 | ghostStyle.backgroundColor = '#CCC'; 27 | ghostStyle.opacity = 0.7; 28 | 29 | /** 30 | * Attach all the events 31 | */ 32 | var bindMoveColEvents = function () { 33 | var instance = this; 34 | 35 | /** 36 | * during drag operation 37 | */ 38 | instance.rootElement.on('mousemove.editTableColumnMove', function (e) { 39 | if (pressed) { 40 | ghostStyle.left = startOffset + e.pageX - startX + 6 + 'px'; 41 | if (ghostStyle.display === 'none') { 42 | ghostStyle.display = 'block'; 43 | } 44 | } 45 | }); 46 | 47 | /** 48 | * end of drag operation 49 | * 50 | * The column move is executed here 51 | */ 52 | instance.rootElement.on('mouseup.editTableColumnMove', function () { 53 | if (pressed) { 54 | if (startCol < endCol) { 55 | endCol--; 56 | } 57 | if (instance.getSettings().rowHeaders) { 58 | startCol--; 59 | endCol--; 60 | } 61 | jQuery('.editTableColumnMover.active').removeClass('active'); 62 | pressed = false; 63 | ghostStyle.display = 'none'; 64 | 65 | if (startCol == endCol) return; 66 | 67 | // if this row is part of a row span, do not move 68 | if (instance.raw.colinfo[endCol].colspan) return; 69 | 70 | // swap cols in each row 71 | for (var i = 0; i < instance.raw.data.length; i++) { 72 | instance.raw.data[i].splice(endCol, 0, instance.raw.data[i].splice(startCol, 1)[0]); 73 | instance.raw.meta[i].splice(endCol, 0, instance.raw.meta[i].splice(startCol, 1)[0]); 74 | } 75 | 76 | instance.forceFullRender = true; 77 | instance.view.render(); //updates all 78 | } 79 | }); 80 | 81 | /** 82 | * start drag operation 83 | */ 84 | instance.rootElement.on('mousedown.editTableColumnMove', '.editTableColumnMover', function (e) { 85 | 86 | var mover = e.currentTarget; 87 | var TH = instance.view.wt.wtDom.closest(mover, 'TH'); 88 | startCol = instance.view.wt.wtDom.index(TH) + instance.colOffset(); 89 | endCol = startCol; 90 | pressed = true; 91 | startX = e.pageX; 92 | 93 | var TABLE = instance.$table[0]; 94 | TABLE.parentNode.appendChild(ghost); 95 | ghostStyle.width = instance.view.wt.wtDom.outerWidth(TH) + 'px'; 96 | ghostStyle.height = instance.view.wt.wtDom.outerHeight(TABLE) + 'px'; 97 | startOffset = parseInt(instance.view.wt.wtDom.offset(TH).left - instance.view.wt.wtDom.offset(TABLE).left, 10); 98 | ghostStyle.left = startOffset + 6 + 'px'; 99 | }); 100 | 101 | /** 102 | * on mouseover, show drag handle 103 | */ 104 | instance.rootElement.on('mouseenter.editTableColumnMove', 'td, th', function () { 105 | if (pressed) { 106 | var active = instance.view.THEAD.querySelector('.editTableColumnMover.active'); 107 | if (active) { 108 | instance.view.wt.wtDom.removeClass(active, 'active'); 109 | } 110 | endCol = instance.view.wt.wtDom.index(this) + instance.colOffset(); 111 | 112 | // if this row is part of a row span, do not move 113 | if (instance.raw.colinfo[endCol - 1].colspan) return; 114 | 115 | var THs = instance.view.THEAD.querySelectorAll('th'); 116 | var mover = THs[endCol].querySelector('.editTableColumnMover'); 117 | instance.view.wt.wtDom.addClass(mover, 'active'); 118 | } 119 | }); 120 | 121 | instance.addHook('afterDestroy', unbindMoveColEvents); 122 | }; 123 | 124 | /** 125 | * Remove all the Events 126 | */ 127 | var unbindMoveColEvents = function () { 128 | var instance = this; 129 | instance.rootElement.off('mouseup.editTableColumnMove'); 130 | instance.rootElement.off('mousemove.editTableColumnMove'); 131 | instance.rootElement.off('mousedown.editTableColumnMove'); 132 | instance.rootElement.off('mouseenter.editTableColumnMove'); 133 | }; 134 | 135 | /** 136 | * Initialize the plugin 137 | */ 138 | this.init = function () { 139 | var instance = this; 140 | var editTableColMoveEnabled = !!(this.getSettings().editTableColumnMove); 141 | 142 | if (editTableColMoveEnabled) { 143 | instance.forceFullRender = true; 144 | bindMoveColEvents.call(this); 145 | 146 | } else { 147 | unbindMoveColEvents.call(this); 148 | } 149 | }; 150 | 151 | /** 152 | * Add a move handler to each column head 153 | * 154 | * @param col 155 | * @param TH 156 | */ 157 | this.getColHeader = function (col, TH) { 158 | if (this.getSettings().editTableColumnMove) { 159 | // if this col is part of a col span, do not add move handle 160 | if (this.raw.colinfo[col].colspan) return; 161 | 162 | var DIV = document.createElement('DIV'); 163 | DIV.className = 'editTableColumnMover'; 164 | TH.firstChild.appendChild(DIV); 165 | } 166 | }; 167 | } 168 | 169 | var htEditTableColumnMove = new EditTableColumnMove(); 170 | 171 | Handsontable.PluginHooks.add('afterInit', function () { 172 | htEditTableColumnMove.init.call(this); 173 | }); 174 | 175 | Handsontable.PluginHooks.add('afterGetColHeader', htEditTableColumnMove.getColHeader); 176 | -------------------------------------------------------------------------------- /action/editor.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Andreas Gohr 7 | */ 8 | 9 | if(!defined('DOKU_INC')) die(); 10 | if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/'); 11 | 12 | /** 13 | * handles all the editor related things 14 | * 15 | * like displaying the editor and adding custom edit buttons 16 | */ 17 | class action_plugin_edittable_editor extends DokuWiki_Action_Plugin { 18 | 19 | /** 20 | * Register its handlers with the DokuWiki's event controller 21 | */ 22 | function register(Doku_Event_Handler $controller) { 23 | // register custom edit buttons 24 | $controller->register_hook('HTML_SECEDIT_BUTTON', 'BEFORE', $this, 'secedit_button'); 25 | 26 | // register our editor 27 | $controller->register_hook('HTML_EDIT_FORMSELECTION', 'BEFORE', $this, 'editform'); 28 | 29 | // register preprocessing for accepting editor data 30 | // $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_table_post'); 31 | $controller->register_hook('PLUGIN_EDITTABLE_PREPROCESS_EDITOR', 'BEFORE', $this, 'handle_table_post'); 32 | } 33 | 34 | /** 35 | * Add a custom edit button under each table 36 | * 37 | * The target 'table' is provided by DokuWiki's XHTML core renderer in the table_close() method 38 | * 39 | * @param Doku_Event $event 40 | */ 41 | function secedit_button(Doku_Event $event) { 42 | if($event->data['target'] !== 'table') { 43 | return; 44 | } 45 | $event->data['name'] = $this->getLang('secedit_name'); 46 | } 47 | 48 | /** 49 | * Creates the actual Table Editor form 50 | * 51 | * @param Doku_Event $event 52 | */ 53 | function editform(Doku_Event $event) { 54 | global $TEXT; 55 | global $RANGE; 56 | 57 | if($event->data['target'] !== 'table') return; 58 | if(!$RANGE){ 59 | // section editing failed, use default editor instead 60 | $event->data['target'] = 'section'; 61 | return; 62 | } 63 | 64 | $event->stopPropagation(); 65 | $event->preventDefault(); 66 | 67 | /** @var renderer_plugin_edittable_json $Renderer our own renderer to convert table to array */ 68 | $Renderer = plugin_load('renderer', 'edittable_json', true); 69 | $instructions = p_get_instructions($TEXT); 70 | 71 | // Loop through the instructions 72 | foreach($instructions as $instruction) { 73 | // Execute the callback against the Renderer 74 | call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1]); 75 | } 76 | 77 | // output data and editor field 78 | 79 | /** @var Doku_Form $form */ 80 | $form =& $event->data['form']; 81 | 82 | // data for handsontable 83 | $form->addHidden('edittable_data', $Renderer->getDataJSON()); 84 | $form->addHidden('edittable_meta', $Renderer->getMetaJSON()); 85 | $form->addElement('
'); 86 | 87 | // FIXME add explanation here 88 | if(isset($_POST['edittable__new'])) { 89 | foreach($_POST['edittable__new'] as $k => $v) { 90 | $form->addHidden("edittable__new[$k]", $v); 91 | } 92 | } 93 | 94 | // set target and range to keep track during previews 95 | $form->addHidden('target', 'table'); 96 | $form->addHidden('range', $RANGE); 97 | } 98 | 99 | /** 100 | * Handles a POST from the table editor 101 | * 102 | * This function preprocesses a POST from the table editor and converts it to plain DokuWiki markup 103 | * 104 | * @author Andreas Gohr 105 | */ 106 | public function handle_table_post($event) { 107 | global $TEXT; 108 | global $INPUT; 109 | if(!$INPUT->post->has('edittable_data')) return; 110 | 111 | $json = new JSON(JSON_LOOSE_TYPE); 112 | $data = $json->decode($INPUT->post->str('edittable_data')); 113 | $meta = $json->decode($INPUT->post->str('edittable_meta')); 114 | 115 | $TEXT = $this->build_table($data, $meta); 116 | } 117 | 118 | /** 119 | * Create a DokuWiki table 120 | * 121 | * converts the table array to plain wiki markup text. pads the table so the markup is easy to read 122 | * 123 | * @param array $data table content for each cell 124 | * @param array $meta meta data for each cell 125 | * @return string 126 | */ 127 | public function build_table($data, $meta) { 128 | $table = ''; 129 | $rows = count($data); 130 | $cols = $rows ? count($data[0]) : 0; 131 | 132 | $colmax = $cols ? array_fill(0, $cols, 0) : array(); 133 | 134 | // find maximum column widths 135 | for($row = 0; $row < $rows; $row++) { 136 | for($col = 0; $col < $cols; $col++) { 137 | $len = utf8_strlen($data[$row][$col]); 138 | 139 | // alignment adds padding 140 | if($meta[$row][$col]['align'] == 'center') { 141 | $len += 4; 142 | } else { 143 | $len += 3; 144 | } 145 | 146 | // remember lenght 147 | $meta[$row][$col]['length'] = $len; 148 | 149 | if($len > $colmax[$col]) $colmax[$col] = $len; 150 | } 151 | } 152 | 153 | $last = '|'; // used to close the last cell 154 | for($row = 0; $row < $rows; $row++) { 155 | for($col = 0; $col < $cols; $col++) { 156 | 157 | // minimum padding according to alignment 158 | if($meta[$row][$col]['align'] == 'center') { 159 | $lpad = 2; 160 | $rpad = 2; 161 | } elseif($meta[$row][$col]['align'] == 'right') { 162 | $lpad = 2; 163 | $rpad = 1; 164 | } else { 165 | $lpad = 1; 166 | $rpad = 2; 167 | } 168 | 169 | // target width of this column 170 | $target = $colmax[$col]; 171 | 172 | // colspanned columns span all the cells 173 | for($i = 1; $i < $meta[$row][$col]['colspan']; $i++) { 174 | $target += $colmax[$col + $i]; 175 | } 176 | 177 | // copy colspans to rowspans below if any 178 | if($meta[$row][$col]['colspan'] > 1){ 179 | for($i = 1; $i < $meta[$row][$col]['rowspan']; $i++) { 180 | $meta[$row + $i][$col]['colspan'] = $meta[$row][$col]['colspan']; 181 | } 182 | } 183 | 184 | // how much padding needs to be added? 185 | $length = $meta[$row][$col]['length']; 186 | $addpad = $target - $length; 187 | 188 | // decide which side needs padding 189 | if($meta[$row][$col]['align'] == 'right') { 190 | $lpad += $addpad; 191 | } else { 192 | $rpad += $addpad; 193 | } 194 | 195 | // add the padding 196 | $cdata = $data[$row][$col]; 197 | if(!$meta[$row][$col]['hide'] || $cdata) { 198 | $cdata = str_pad('', $lpad).$cdata.str_pad('', $rpad); 199 | } 200 | 201 | // finally add the cell 202 | $last = ($meta[$row][$col]['tag'] == 'th') ? '^' : '|'; 203 | $table .= $last; 204 | $table .= $cdata; 205 | } 206 | 207 | // close the row 208 | $table .= "$last\n"; 209 | } 210 | 211 | return $table; 212 | } 213 | 214 | } -------------------------------------------------------------------------------- /less/jquery.handsontable.full.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Handsontable 0.10.1 3 | * Handsontable is a simple jQuery plugin for editable tables with basic copy-paste compatibility with Excel and Google Docs 4 | * 5 | * Copyright 2012, Marcin Warpechowski 6 | * Licensed under the MIT license. 7 | * http://handsontable.com/ 8 | * 9 | * Date: Mon Jan 20 2014 15:03:52 GMT+0100 (CET) 10 | */ 11 | 12 | .handsontable { 13 | position: relative; 14 | } 15 | 16 | .handsontable.htAutoColumnSize { 17 | visibility: hidden; 18 | left: 0; 19 | position: absolute; 20 | top: 0; 21 | } 22 | 23 | .handsontable table, 24 | .handsontable tbody, 25 | .handsontable thead, 26 | .handsontable td, 27 | .handsontable th, 28 | .handsontable div { 29 | box-sizing: content-box; 30 | -webkit-box-sizing: content-box; 31 | -moz-box-sizing: content-box; 32 | } 33 | 34 | .handsontable table.htCore { 35 | border-collapse: separate; 36 | /*it must be separate, otherwise there are offset miscalculations in WebKit: http://stackoverflow.com/questions/2655987/border-collapse-differences-in-ff-and-webkit*/ 37 | position: relative; 38 | /*this actually only changes appearance of user selection - does not make text unselectable 39 | -webkit-user-select: none; 40 | -khtml-user-select: none; 41 | -moz-user-select: none; 42 | -o-user-select: none; 43 | -ms-user-select: none; 44 | /*user-select: none; /*no browser supports unprefixed version*/ 45 | border-spacing: 0; 46 | margin: 0; 47 | border-width: 0; 48 | table-layout: fixed; 49 | width: 0; 50 | outline-width: 0; 51 | /* reset bootstrap table style. for more info see: https://github.com/warpech/jquery-handsontable/issues/224 */ 52 | max-width: none; 53 | max-height: none; 54 | } 55 | 56 | .handsontable col { 57 | width: 50px; 58 | } 59 | 60 | .handsontable col.rowHeader { 61 | width: 50px; 62 | } 63 | 64 | .handsontable th, 65 | .handsontable td { 66 | border-right: 1px solid #CCC; 67 | border-bottom: 1px solid #CCC; 68 | height: 22px; 69 | empty-cells: show; 70 | line-height: 21px; 71 | padding: 0; 72 | background-color: #FFF; 73 | vertical-align: top; 74 | overflow: hidden; 75 | outline-width: 0; 76 | white-space: pre-line; 77 | /* preserve new line character in cell */ 78 | } 79 | 80 | .handsontable td { 81 | padding: 0 4px 0 4px; 82 | /* top, bottom padding different than 0 is handled poorly by FF with HTML5 doctype */ 83 | } 84 | 85 | .handsontable td.htInvalid { 86 | -webkit-transition: background 0.75s ease; 87 | transition: background 0.75s ease; 88 | background-color: #ff4c42; 89 | } 90 | 91 | .handsontable th:last-child { 92 | /*Foundation framework fix*/ 93 | border-right: 1px solid #CCC; 94 | border-bottom: 1px solid #CCC; 95 | } 96 | 97 | .handsontable tr:first-child th.htNoFrame, 98 | .handsontable th:first-child.htNoFrame, 99 | .handsontable th.htNoFrame { 100 | border-left-width: 0; 101 | background-color: white; 102 | border-color: #FFF; 103 | } 104 | 105 | .handsontable th:first-child, 106 | .handsontable td:first-child, 107 | .handsontable .htNoFrame + th, 108 | .handsontable .htNoFrame + td { 109 | border-left: 1px solid #CCC; 110 | } 111 | 112 | .handsontable tr:first-child th, 113 | .handsontable tr:first-child td { 114 | border-top: 1px solid #CCC; 115 | } 116 | 117 | .handsontable thead tr:last-child th { 118 | border-bottom-width: 0; 119 | } 120 | 121 | .handsontable thead tr.lastChild th { 122 | border-bottom-width: 0; 123 | } 124 | 125 | .handsontable th { 126 | background-color: #EEE; 127 | color: #222; 128 | text-align: center; 129 | font-weight: normal; 130 | white-space: nowrap; 131 | } 132 | 133 | .handsontable thead th { 134 | padding: 0; 135 | } 136 | 137 | .handsontable th.active { 138 | background-color: #CCC; 139 | } 140 | 141 | .handsontable th .relative { 142 | position: relative; 143 | padding: 2px 4px; 144 | } 145 | 146 | /* plugins */ 147 | 148 | .handsontable .manualColumnMover { 149 | position: absolute; 150 | left: 0; 151 | top: 0; 152 | background-color: transparent; 153 | width: 5px; 154 | height: 25px; 155 | z-index: 999; 156 | cursor: move; 157 | } 158 | 159 | .handsontable th .manualColumnMover:hover, 160 | .handsontable th .manualColumnMover.active { 161 | background-color: #88F; 162 | } 163 | 164 | .handsontable .manualColumnResizer { 165 | position: absolute; 166 | top: 0; 167 | cursor: col-resize; 168 | } 169 | 170 | .handsontable .manualColumnResizerHandle { 171 | background-color: transparent; 172 | width: 5px; 173 | height: 25px; 174 | } 175 | 176 | .handsontable .manualColumnResizer:hover .manualColumnResizerHandle, 177 | .handsontable .manualColumnResizer.active .manualColumnResizerHandle { 178 | background-color: #AAB; 179 | } 180 | 181 | .handsontable .manualColumnResizerLine { 182 | position: absolute; 183 | right: 0; 184 | top: 0; 185 | background-color: #AAB; 186 | display: none; 187 | width: 0; 188 | border-right: 1px dashed #777; 189 | } 190 | 191 | .handsontable .manualColumnResizer.active .manualColumnResizerLine { 192 | display: block; 193 | } 194 | 195 | .handsontable .columnSorting:hover { 196 | text-decoration: underline; 197 | cursor: pointer; 198 | } 199 | 200 | /* border line */ 201 | 202 | .handsontable .wtBorder { 203 | position: absolute; 204 | font-size: 0; 205 | } 206 | 207 | .handsontable td.area { 208 | background-color: #EEF4FF; 209 | } 210 | 211 | /* fill handle */ 212 | 213 | .handsontable .wtBorder.corner { 214 | font-size: 0; 215 | cursor: crosshair; 216 | } 217 | 218 | .handsontable .htBorder.htFillBorder { 219 | background: red; 220 | width: 1px; 221 | height: 1px; 222 | } 223 | 224 | .handsontableInput { 225 | border: 2px solid #5292F7; 226 | outline-width: 0; 227 | margin: 0; 228 | padding: 1px 4px 0 2px; 229 | font-family: Arial, Helvetica, sans-serif; 230 | /*repeat from .handsontable (inherit doesn't work with IE<8) */ 231 | line-height: 1.3em; 232 | /*repeat from .handsontable (inherit doesn't work with IE<8) */ 233 | font-size: inherit; 234 | -webkit-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); 235 | box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); 236 | resize: none; 237 | /*below are needed to overwrite stuff added by jQuery UI Bootstrap theme*/ 238 | display: inline-block; 239 | color: #000; 240 | border-radius: 0; 241 | background-color: #FFF; 242 | /*overwrite styles potentionally made by a framework*/ 243 | } 244 | 245 | .handsontableInputHolder { 246 | position: absolute; 247 | top: 0; 248 | left: 0; 249 | z-index: 100; 250 | } 251 | 252 | .htSelectEditor { 253 | -webkit-appearance: menulist-button !important; 254 | position: absolute; 255 | } 256 | 257 | /* 258 | TextRenderer readOnly cell 259 | */ 260 | 261 | .handsontable .htDimmed { 262 | color: #777; 263 | } 264 | 265 | /* 266 | TextRenderer placeholder value 267 | */ 268 | 269 | .handsontable .htPlaceholder { 270 | color: #999; 271 | } 272 | 273 | /* 274 | AutocompleteRenderer down arrow 275 | */ 276 | 277 | .handsontable .htAutocompleteArrow { 278 | float: right; 279 | font-size: 10px; 280 | color: #EEE; 281 | cursor: default; 282 | width: 16px; 283 | text-align: center; 284 | } 285 | 286 | .handsontable td .htAutocompleteArrow:hover { 287 | color: #777; 288 | } 289 | 290 | /* 291 | CheckboxRenderer 292 | */ 293 | 294 | .handsontable .htCheckboxRendererInput.noValue { 295 | opacity: 0.5; 296 | } 297 | 298 | /* 299 | NumericRenderer 300 | */ 301 | 302 | .handsontable .htNumeric { 303 | text-align: right; 304 | } 305 | 306 | /*context menu rules*/ 307 | 308 | ul.context-menu-list { 309 | color: black; 310 | } 311 | 312 | ul.context-menu-list li { 313 | margin-bottom: 0; 314 | /*Foundation framework fix*/ 315 | } 316 | 317 | /** 318 | * dragdealer 319 | */ 320 | 321 | .handsontable .dragdealer { 322 | position: relative; 323 | width: 9px; 324 | height: 9px; 325 | background: #F8F8F8; 326 | border: 1px solid #DDD; 327 | } 328 | 329 | .handsontable .dragdealer .handle { 330 | position: absolute; 331 | width: 9px; 332 | height: 9px; 333 | background: #C5C5C5; 334 | } 335 | 336 | .handsontable .dragdealer .disabled { 337 | background: #898989; 338 | } 339 | 340 | /** 341 | * Handsontable in Handsontable 342 | */ 343 | 344 | .handsontable .handsontable .wtHider { 345 | padding: 0 0 5px 0; 346 | } 347 | 348 | .handsontable .handsontable table { 349 | -webkit-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); 350 | box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); 351 | } 352 | 353 | /** 354 | * Handsontable listbox theme 355 | */ 356 | 357 | .handsontable.listbox { 358 | margin: 0; 359 | } 360 | 361 | .handsontable.listbox table { 362 | border: 1px solid #ccc; 363 | border-collapse: separate; 364 | background: white; 365 | } 366 | 367 | .handsontable.listbox th, 368 | .handsontable.listbox tr:first-child th, 369 | .handsontable.listbox tr:last-child th, 370 | .handsontable.listbox tr:first-child td, 371 | .handsontable.listbox td { 372 | border-width: 0; 373 | } 374 | 375 | .handsontable.listbox th, 376 | .handsontable.listbox td { 377 | white-space: nowrap; 378 | text-overflow: ellipsis; 379 | } 380 | 381 | .handsontable.listbox td.htDimmed { 382 | cursor: default; 383 | color: inherit; 384 | font-style: inherit; 385 | } 386 | 387 | .handsontable.listbox .wtBorder { 388 | visibility: hidden; 389 | } 390 | 391 | .handsontable.listbox tr td.current, 392 | .handsontable.listbox tr:hover td { 393 | background: #eee; 394 | } 395 | 396 | .htContextMenu { 397 | display: none; 398 | position: absolute; 399 | } 400 | 401 | .htContextMenu table.htCore { 402 | outline: 1px solid #bbb; 403 | } 404 | 405 | .htContextMenu .wtBorder { 406 | visibility: hidden; 407 | } 408 | 409 | .htContextMenu table tbody tr td { 410 | background: white; 411 | border-width: 0; 412 | padding: 4px 6px 0px 6px; 413 | cursor: pointer; 414 | overflow: hidden; 415 | white-space: nowrap; 416 | text-overflow: ellipsis; 417 | } 418 | 419 | .htContextMenu table tbody tr td:first-child { 420 | border: 0; 421 | } 422 | 423 | .htContextMenu table tbody tr td.htDimmed{ 424 | font-style: normal; 425 | color: #323232; 426 | } 427 | 428 | .htContextMenu table tbody tr td.current{ 429 | background: rgb(233, 233, 233); 430 | } 431 | 432 | .htContextMenu table tbody tr td.htSeparator { 433 | border-top: 1px solid #bbb; 434 | height: 0; 435 | padding: 0; 436 | } 437 | 438 | .htContextMenu table tbody tr td.htDisabled { 439 | color: #999; 440 | } 441 | 442 | .htContextMenu table tbody tr td.htDisabled:hover { 443 | background: white; 444 | color: #999; 445 | cursor: default; 446 | } 447 | 448 | /*WalkontableDebugOverlay*/ 449 | 450 | .wtDebugHidden { 451 | display: none; 452 | } 453 | 454 | .wtDebugVisible { 455 | display: block; 456 | -webkit-animation-duration: 0.5s; 457 | -webkit-animation-name: wtFadeInFromNone; 458 | animation-duration: 0.5s; 459 | animation-name: wtFadeInFromNone; 460 | } 461 | 462 | @keyframes wtFadeInFromNone { 463 | 0% { 464 | display: none; 465 | opacity: 0; 466 | } 467 | 468 | 1% { 469 | display: block; 470 | opacity: 0; 471 | } 472 | 473 | 100% { 474 | display: block; 475 | opacity: 1; 476 | } 477 | } 478 | 479 | @-webkit-keyframes wtFadeInFromNone { 480 | 0% { 481 | display: none; 482 | opacity: 0; 483 | } 484 | 485 | 1% { 486 | display: block; 487 | opacity: 0; 488 | } 489 | 490 | 100% { 491 | display: block; 492 | opacity: 1; 493 | } 494 | } -------------------------------------------------------------------------------- /script/contextmenu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines our own contextMenu with custom callbacks 3 | * 4 | * @param data array 5 | * @param meta array 6 | * @returns object 7 | */ 8 | function getEditTableContextMenu(data, meta) { 9 | return { 10 | items: { 11 | toggle_header: { 12 | name: LANG.plugins.edittable.toggle_header, 13 | callback: function (key, selection) { 14 | var col = selection.start.col(); 15 | var row = selection.start.row(); 16 | 17 | if (meta[row][col].tag && meta[row][col].tag === 'th') { 18 | meta[row][col].tag = 'td'; 19 | } else { 20 | meta[row][col].tag = 'th'; 21 | } 22 | this.render(); 23 | } 24 | }, 25 | align_left: { 26 | name: LANG.plugins.edittable.align_left, 27 | callback: function (key, selection) { 28 | var col = selection.start.col(); 29 | var row = selection.start.row(); 30 | meta[row][col].align = 'left'; 31 | this.render(); 32 | }, 33 | disabled: function () { 34 | var selection = this.getSelected(); 35 | var row = selection[0]; 36 | var col = selection[1]; 37 | return (!meta[row][col].align || meta[row][col].align === 'left'); 38 | } 39 | }, 40 | align_center: { 41 | name: LANG.plugins.edittable.align_center, 42 | callback: function (key, selection) { 43 | var col = selection.start.col(); 44 | var row = selection.start.row(); 45 | meta[row][col].align = 'center'; 46 | this.render(); 47 | }, 48 | disabled: function () { 49 | var selection = this.getSelected(); 50 | var row = selection[0]; 51 | var col = selection[1]; 52 | return (meta[row][col].align && meta[row][col].align === 'center'); 53 | } 54 | }, 55 | align_right: { 56 | name: LANG.plugins.edittable.align_right, 57 | callback: function (key, selection) { 58 | var col = selection.start.col(); 59 | var row = selection.start.row(); 60 | meta[row][col].align = 'right'; 61 | this.render(); 62 | }, 63 | disabled: function () { 64 | var selection = this.getSelected(); 65 | var row = selection[0]; 66 | var col = selection[1]; 67 | return (meta[row][col].align && meta[row][col].align === 'right'); 68 | } 69 | }, 70 | hsep1: '---------', 71 | row_above: { 72 | name: LANG.plugins.edittable.row_above 73 | }, 74 | remove_row: { 75 | name: LANG.plugins.edittable.remove_row, 76 | /** 77 | * The same as the default action, but with confirmation 78 | * 79 | * @param key 80 | * @param selection 81 | */ 82 | callback: function (key, selection) { 83 | if (window.confirm(LANG.plugins.edittable.confirmdeleterow)) { 84 | var amount = selection.end.row() - selection.start.row() + 1; 85 | this.alter("remove_row", selection.start.row(), amount); 86 | } 87 | }, 88 | /** 89 | * do not show when this is the last row 90 | */ 91 | disabled: function () { 92 | return (this.countRows() <= 1); 93 | } 94 | }, 95 | row_below: { 96 | name: LANG.plugins.edittable.row_below 97 | }, 98 | hsep2: '---------', 99 | col_left: { 100 | name: LANG.plugins.edittable.col_left 101 | }, 102 | remove_col: { 103 | name: LANG.plugins.edittable.remove_col, 104 | /** 105 | * The same as the default action, but with confirmation 106 | * 107 | * @param key 108 | * @param selection 109 | */ 110 | callback: function (key, selection) { 111 | if (window.confirm(LANG.plugins.edittable.confirmdeletecol)) { 112 | var amount = selection.end.col() - selection.start.col() + 1; 113 | this.alter("remove_col", selection.start.col(), amount); 114 | } 115 | }, 116 | /** 117 | * do not show when this is the last row 118 | */ 119 | disabled: function () { 120 | return (this.countCols() <= 1); 121 | } 122 | }, 123 | col_right: { 124 | name: LANG.plugins.edittable.col_right 125 | }, 126 | hsep3: '---------', 127 | colspan_add: { 128 | name: LANG.plugins.edittable.colspan_add, 129 | /** 130 | * Increase colspan and rerender the table 131 | * 132 | * @param key 133 | * @param selection 134 | */ 135 | callback: function (key, selection) { 136 | var col = selection.start.col(); 137 | var row = selection.start.row(); 138 | 139 | // increase rowspan by the colspan of the cell to the right 140 | meta[row][col].colspan += meta[row][col + 1].colspan; 141 | 142 | // copy over any data from the merged cells 143 | var colspan = meta[row][col].colspan; 144 | var rowspan = meta[row][col].rowspan; 145 | for (var i = 0; i < rowspan; i++) { 146 | if (data[row + i][col + colspan - 1 ] != ':::') { 147 | data[row][col] += ' ' + data[row + i][col + colspan - 1 ]; 148 | } 149 | } 150 | 151 | this.render(); 152 | }, 153 | /** 154 | * don't show when not enough space for colspan 155 | * 156 | * @returns {boolean} 157 | */ 158 | disabled: function () { 159 | var selection = this.getSelected(); 160 | var row = selection[0]; 161 | var col = selection[1]; 162 | var end = this.countCols(); 163 | 164 | var rowspan = meta[row][col].rowspan; 165 | var colspan = meta[row][col].colspan; 166 | 167 | // no cells to merge 168 | if ((col + colspan) >= end) return true; 169 | 170 | // don't merge into hidden or spanned cells 171 | for (var i = 0; i < rowspan; i++) { 172 | if (meta[row + i][col + colspan].hide) return true; 173 | if (meta[row + i][col + colspan].rowspan > 1) { 174 | // we allow merge with same rowspanned cell only 175 | return meta[row + i][col + colspan].rowspan != rowspan; 176 | } 177 | } 178 | 179 | return false; // merge is fine 180 | } 181 | }, 182 | colspan_del: { 183 | name: LANG.plugins.edittable.colspan_del, 184 | /** 185 | * Decrease colspan and rerender table 186 | * 187 | * @param key 188 | * @param selection 189 | */ 190 | callback: function (key, selection) { 191 | var col = selection.start.col(); 192 | var row = selection.start.row(); 193 | 194 | meta[row][col].colspan--; 195 | this.render(); 196 | }, 197 | /** 198 | * Make available only when colspan is set 199 | * 200 | * @returns {boolean} 201 | */ 202 | disabled: function () { 203 | var selection = this.getSelected(); 204 | var row = selection[0]; 205 | var col = selection[1]; 206 | 207 | return !(meta[row][col].colspan && meta[row][col].colspan > 1); 208 | } 209 | }, 210 | rowspan_add: { 211 | name: LANG.plugins.edittable.rowspan_add, 212 | /** 213 | * Increase rowspan and rerender the table 214 | * 215 | * @param key 216 | * @param selection 217 | */ 218 | callback: function (key, selection) { 219 | var col = selection.start.col(); 220 | var row = selection.start.row(); 221 | 222 | // increase rowspan by the rowspan of the cell below 223 | meta[row][col].rowspan += meta[row + 1][col].rowspan; 224 | 225 | // copy over any data from the merged cells 226 | var colspan = meta[row][col].colspan; 227 | var rowspan = meta[row][col].rowspan; 228 | for (var i = 0; i < colspan; i++) { 229 | if (data[row + rowspan - 1][col + i] != ':::') { 230 | data[row][col] += ' ' + data[row + rowspan - 1][col + i]; 231 | } 232 | } 233 | 234 | this.render(); 235 | }, 236 | /** 237 | * don't show when not enough space for rowspan 238 | * 239 | * @returns {boolean} 240 | */ 241 | disabled: function () { 242 | var selection = this.getSelected(); 243 | var row = selection[0]; 244 | var col = selection[1]; 245 | var end = this.countRows(); 246 | 247 | var rowspan = meta[row][col].rowspan; 248 | var colspan = meta[row][col].colspan; 249 | 250 | // no cells to merge 251 | if ((row + rowspan) >= end) return true; 252 | 253 | // don't merge into hidden or spanned cells 254 | for (var i = 0; i < colspan; i++) { 255 | if (meta[row + rowspan][col + i].hide) return true; 256 | if (meta[row + rowspan][col + i].colspan > 1) { 257 | // we allow merge with same colspanned cell only 258 | return meta[row + rowspan][col + i].colspan != colspan; 259 | } 260 | } 261 | 262 | return false; // merge is fine 263 | } 264 | }, 265 | rowspan_del: { 266 | name: LANG.plugins.edittable.rowspan_del, 267 | /** 268 | * Decrease colspan and rerender table 269 | * 270 | * @param key 271 | * @param selection 272 | */ 273 | callback: function (key, selection) { 274 | var col = selection.start.col(); 275 | var row = selection.start.row(); 276 | 277 | meta[row][col].rowspan--; 278 | this.render(); 279 | }, 280 | /** 281 | * Make available only when rowspan is set 282 | * 283 | * @returns {boolean} 284 | */ 285 | disabled: function () { 286 | var selection = this.getSelected(); 287 | var row = selection[0]; 288 | var col = selection[1]; 289 | 290 | return !(meta[row][col].rowspan && meta[row][col].rowspan > 1); 291 | } 292 | } 293 | 294 | } 295 | }; 296 | } -------------------------------------------------------------------------------- /script/editor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This configures the Handsontable Plugin 3 | */ 4 | jQuery(function () { 5 | var $container = jQuery('#edittable__editor'); 6 | if (!$container.length) return; 7 | 8 | var $form = jQuery('#dw__editform'); 9 | var $datafield = $form.find('input[name=edittable_data]'); 10 | var $metafield = $form.find('input[name=edittable_meta]'); 11 | 12 | var data = JSON.parse($datafield.val()); 13 | var meta = JSON.parse($metafield.val()); 14 | var lastselect = {row: 0, col: 0}; 15 | 16 | $container.handsontable({ 17 | data: data, 18 | startRows: 5, 19 | startCols: 5, 20 | colHeaders: true, 21 | rowHeaders: true, 22 | multiSelect: false, // until properly tested with col/row span 23 | fillHandle: false, // until properly tested with col/row span 24 | undo: false, // until properly tested with col/row span 25 | manualColumnResize: true, 26 | editTableColumnMove: true, // custom plugin 27 | editTableRowMove: true, // custom plugin 28 | outsideClickDeselects: false, 29 | contextMenu: getEditTableContextMenu(data, meta), 30 | 31 | /** 32 | * Attach pointers to our raw data structures in the instance 33 | */ 34 | afterLoadData: function () { 35 | var i; 36 | this.raw = { 37 | data: data, 38 | meta: meta, 39 | colinfo: [], 40 | rowinfo: [] 41 | }; 42 | for (i = 0; i < data.length; i++) this.raw.rowinfo[i] = {}; 43 | for (i = 0; i < data[0].length; i++) this.raw.colinfo[i] = {}; 44 | }, 45 | 46 | /** 47 | * initialize cell properties 48 | * 49 | * properties are stored in extra array 50 | * 51 | * @param row int 52 | * @param col int 53 | * @param prop string 54 | * @returns {*} 55 | */ 56 | cells: function (row, col, prop) { 57 | return meta[row][col]; 58 | }, 59 | 60 | /** 61 | * Custom cell renderer 62 | * 63 | * It handles all our custom meta attributes like alignments and rowspans 64 | * 65 | * @param instance 66 | * @param td 67 | * @param row 68 | * @param col 69 | * @param prop 70 | * @param value 71 | * @param cellProperties 72 | */ 73 | renderer: function (instance, td, row, col, prop, value, cellProperties) { 74 | // for some reason, neither cellProperties nor instance.getCellMeta() give the right data 75 | var cellMeta = meta[row][col]; 76 | var $td = jQuery(td); 77 | 78 | if (cellMeta.colspan) { 79 | $td.attr('colspan', cellMeta.colspan); 80 | } else { 81 | $td.removeAttr('colspan'); 82 | } 83 | 84 | if (cellMeta.rowspan) { 85 | $td.attr('rowspan', cellMeta.rowspan); 86 | } else { 87 | $td.removeAttr('rowspan'); 88 | } 89 | 90 | if (cellMeta.hide) { 91 | $td.hide(); 92 | } else { 93 | $td.show(); 94 | } 95 | 96 | if (cellMeta.align == 'right') { 97 | $td.addClass('right'); 98 | $td.removeClass('center'); 99 | } else if (cellMeta.align == 'center') { 100 | $td.addClass('center'); 101 | $td.removeClass('right'); 102 | } else { 103 | $td.removeClass('center'); 104 | $td.removeClass('right'); 105 | } 106 | 107 | if (cellMeta.tag == 'th') { 108 | $td.addClass('header'); 109 | } else { 110 | $td.removeClass('header'); 111 | } 112 | 113 | Handsontable.renderers.TextRenderer.apply(this, arguments); 114 | }, 115 | 116 | /** 117 | * Initialization after the Editor loaded 118 | */ 119 | afterInit: function () { 120 | // select first cell 121 | this.selectCell(0, 0); 122 | 123 | // we need an ID on the input field 124 | this.rootElement.find('textarea.handsontableInput').attr('id', 'handsontable__input'); 125 | 126 | // we're ready to intialize the toolbar now 127 | initToolbar('tool__bar', 'handsontable__input', window.toolbar, false); 128 | 129 | // we wrap DokuWiki's pasteText() here to get notified when the toolbar inserted something into our editor 130 | var original_pasteText = window.pasteText; 131 | window.pasteText = function (selection, text, opts) { 132 | original_pasteText(selection, text, opts); // do what pasteText does 133 | // trigger resize 134 | jQuery('#handsontable__input').data('AutoResizer').check(); 135 | }; 136 | window.pasteText = original_pasteText; 137 | }, 138 | 139 | /** 140 | * This recalculates the col and row spans and makes sure all correct cells are hidden 141 | * 142 | * @param forced bool 143 | */ 144 | beforeRender: function (forced) { 145 | var row, r, c, col, i; 146 | 147 | // reset row and column infos - we store spanning info there 148 | this.raw.rowinfo = []; 149 | this.raw.colinfo = []; 150 | for (i = 0; i < data.length; i++) this.raw.rowinfo[i] = {}; 151 | for (i = 0; i < data[0].length; i++) this.raw.colinfo[i] = {}; 152 | 153 | // unhide all cells 154 | for (row = 0; row < data.length; row++) { 155 | for (col = 0; col < data[0].length; col++) { 156 | if (meta[row][col].hide) { 157 | meta[row][col].hide = false; 158 | data[row][col] = ''; 159 | } 160 | 161 | // make sure no data cell is undefined/null 162 | if (!data[row][col]) data[row][col] = ''; 163 | } 164 | } 165 | 166 | // rehide needed cells 167 | for (row = 0; row < data.length; row++) { 168 | for (col = 0; col < data[0].length; col++) { 169 | var colspan = meta[row][col].colspan; 170 | var rowspan = meta[row][col].rowspan; 171 | 172 | for (c = 1; c < colspan; c++) { 173 | // does the colspan reach out of the table? decrease it 174 | if (!meta[row][col + c]) { 175 | meta[row][col].colspan--; 176 | continue; 177 | } 178 | 179 | // hide colspanned cell in same row 180 | meta[row][col + c].hide = true; 181 | meta[row][col + c].rowspan = 1; 182 | meta[row][col + c].colspan = 1; 183 | data[row][col + c] = ''; 184 | 185 | this.raw.colinfo[col].colspan = true; 186 | this.raw.colinfo[col + c].colspan = true; 187 | 188 | // hide colspanned rows below if rowspan is in effect as well 189 | for (r = 1; r < rowspan; r++) { 190 | // does the rowspan reach out of the table? decrease it 191 | if (!meta[row + r]) { 192 | meta[row][col].rowspan--; 193 | continue; 194 | } 195 | 196 | meta[row + r][col + c].hide = true; 197 | meta[row + r][col + c].rowspan = 1; 198 | meta[row + r][col + c].colspan = 1; 199 | data[row + r][col + c] = ''; 200 | 201 | this.raw.rowinfo[row + r].rowspan = true; 202 | } 203 | 204 | } 205 | 206 | // hide rowspanned columns 207 | rowspan = meta[row][col].rowspan; // might have changed above 208 | for (r = 1; r < rowspan; r++) { 209 | // does the rowspan reach out of the table? decrease it 210 | if (!meta[row + r]) { 211 | meta[row][col].rowspan--; 212 | continue; 213 | } 214 | 215 | meta[row + r][col].hide = true; 216 | meta[row + r][col].rowspan = 1; 217 | meta[row + r][col].colspan = 1; 218 | data[row + r][col] = ':::'; 219 | 220 | this.raw.rowinfo[row].rowspan = true; 221 | this.raw.rowinfo[row + r].rowspan = true; 222 | } 223 | } 224 | } 225 | 226 | // Store data and meta back in the form 227 | $datafield.val(JSON.stringify(data)); 228 | $metafield.val(JSON.stringify(meta)); 229 | }, 230 | 231 | /** 232 | * Disable key handling while the link wizard or any other dialog is visible 233 | * 234 | * @param e 235 | */ 236 | beforeKeyDown: function(e) { 237 | if(jQuery('.ui-dialog:visible').length) { 238 | e.stopImmediatePropagation(); 239 | e.preventDefault(); 240 | } 241 | }, 242 | 243 | /** 244 | * Update meta data array when rows are added 245 | * 246 | * @param index int 247 | * @param amount int 248 | */ 249 | afterCreateRow: function (index, amount) { 250 | for (var z = 0; z < amount; z++) { 251 | this.raw.rowinfo.splice(index, 0, [ 252 | {} 253 | ]); 254 | } 255 | 256 | var i; 257 | var cols = 1; // minimal number of cells 258 | if (data[0]) cols = data[0].length; 259 | 260 | // insert into meta array 261 | for (i = 0; i < amount; i++) { 262 | var newrow = []; 263 | for (i = 0; i < cols; i++) newrow.push({rowspan: 1, colspan: 1}); 264 | meta.splice(index, 0, newrow); 265 | } 266 | }, 267 | 268 | /** 269 | * Update meta data array when rows are removed 270 | * 271 | * @param index int 272 | * @param amount int 273 | */ 274 | afterRemoveRow: function (index, amount) { 275 | meta.splice(index, amount); 276 | }, 277 | 278 | /** 279 | * Update meta data array when columns are added 280 | * 281 | * @param index int 282 | * @param amount int 283 | */ 284 | afterCreateCol: function (index, amount) { 285 | for (var z = 0; z < amount; z++) { 286 | this.raw.colinfo.splice(index, 0, [ 287 | {} 288 | ]); 289 | } 290 | 291 | for (var row = 0; row < data.length; row++) { 292 | for (var i = 0; i < amount; i++) { 293 | meta[row].splice(index, 0, {rowspan: 1, colspan: 1}); 294 | } 295 | } 296 | }, 297 | 298 | /** 299 | * Update meta data array when columns are removed 300 | * 301 | * @param index int 302 | * @param amount int 303 | */ 304 | afterRemoveCol: function (index, amount) { 305 | for (var row = 0; row < data.length; row++) { 306 | meta[row].splice(index, amount); 307 | } 308 | }, 309 | 310 | /** 311 | * Skip hidden cells for selection 312 | * 313 | * @param r int 314 | * @param c int 315 | * @param r2 int 316 | * @param c2 int 317 | */ 318 | afterSelection: function (r, c, r2, c2) { 319 | if (meta[r][c].hide) { 320 | // user navigated into a hidden cell! we need to find the next selectable cell 321 | var x = 0; 322 | 323 | var v = r - lastselect.row; 324 | if (v > 0) v = 1; 325 | if (v < 0) v = -1; 326 | 327 | var h = c - lastselect.col; 328 | if (h > 0) h = 1; 329 | if (h < 0) h = -1; 330 | 331 | if (v !== 0) { 332 | x = r; 333 | // user navigated vertically 334 | do { 335 | x += v; 336 | if (!meta[x][c].hide) { 337 | // cell is selectable, do it 338 | this.selectCell(x, c); 339 | return; 340 | } 341 | 342 | } while (x > 0 && x < data.length); 343 | // found no suitable cell 344 | this.deselectCell(); 345 | } else if (h !== 0) { 346 | x = c; 347 | // user navigated horizontally 348 | do { 349 | x += h; 350 | if (!meta[r][x].hide) { 351 | // cell is selectable, do it 352 | this.selectCell(r, x); 353 | return; 354 | } 355 | 356 | } while (x > 0 && x < data[0].length); 357 | // found no suitable cell 358 | this.deselectCell(); 359 | } 360 | } else { 361 | // remember this selection 362 | lastselect.row = r; 363 | lastselect.col = c; 364 | } 365 | } 366 | }); 367 | 368 | }); -------------------------------------------------------------------------------- /script/json2.js: -------------------------------------------------------------------------------- 1 | /* 2 | json2.js 3 | 2013-05-26 4 | 5 | Public Domain. 6 | 7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 8 | 9 | See http://www.JSON.org/js.html 10 | 11 | 12 | This code should be minified before deployment. 13 | See http://javascript.crockford.com/jsmin.html 14 | 15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 16 | NOT CONTROL. 17 | 18 | 19 | This file creates a global JSON object containing two methods: stringify 20 | and parse. 21 | 22 | JSON.stringify(value, replacer, space) 23 | value any JavaScript value, usually an object or array. 24 | 25 | replacer an optional parameter that determines how object 26 | values are stringified for objects. It can be a 27 | function or an array of strings. 28 | 29 | space an optional parameter that specifies the indentation 30 | of nested structures. If it is omitted, the text will 31 | be packed without extra whitespace. If it is a number, 32 | it will specify the number of spaces to indent at each 33 | level. If it is a string (such as '\t' or ' '), 34 | it contains the characters used to indent at each level. 35 | 36 | This method produces a JSON text from a JavaScript value. 37 | 38 | When an object value is found, if the object contains a toJSON 39 | method, its toJSON method will be called and the result will be 40 | stringified. A toJSON method does not serialize: it returns the 41 | value represented by the name/value pair that should be serialized, 42 | or undefined if nothing should be serialized. The toJSON method 43 | will be passed the key associated with the value, and this will be 44 | bound to the value 45 | 46 | For example, this would serialize Dates as ISO strings. 47 | 48 | Date.prototype.toJSON = function (key) { 49 | function f(n) { 50 | // Format integers to have at least two digits. 51 | return n < 10 ? '0' + n : n; 52 | } 53 | 54 | return this.getUTCFullYear() + '-' + 55 | f(this.getUTCMonth() + 1) + '-' + 56 | f(this.getUTCDate()) + 'T' + 57 | f(this.getUTCHours()) + ':' + 58 | f(this.getUTCMinutes()) + ':' + 59 | f(this.getUTCSeconds()) + 'Z'; 60 | }; 61 | 62 | You can provide an optional replacer method. It will be passed the 63 | key and value of each member, with this bound to the containing 64 | object. The value that is returned from your method will be 65 | serialized. If your method returns undefined, then the member will 66 | be excluded from the serialization. 67 | 68 | If the replacer parameter is an array of strings, then it will be 69 | used to select the members to be serialized. It filters the results 70 | such that only members with keys listed in the replacer array are 71 | stringified. 72 | 73 | Values that do not have JSON representations, such as undefined or 74 | functions, will not be serialized. Such values in objects will be 75 | dropped; in arrays they will be replaced with null. You can use 76 | a replacer function to replace those with JSON values. 77 | JSON.stringify(undefined) returns undefined. 78 | 79 | The optional space parameter produces a stringification of the 80 | value that is filled with line breaks and indentation to make it 81 | easier to read. 82 | 83 | If the space parameter is a non-empty string, then that string will 84 | be used for indentation. If the space parameter is a number, then 85 | the indentation will be that many spaces. 86 | 87 | Example: 88 | 89 | text = JSON.stringify(['e', {pluribus: 'unum'}]); 90 | // text is '["e",{"pluribus":"unum"}]' 91 | 92 | 93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); 94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 95 | 96 | text = JSON.stringify([new Date()], function (key, value) { 97 | return this[key] instanceof Date ? 98 | 'Date(' + this[key] + ')' : value; 99 | }); 100 | // text is '["Date(---current time---)"]' 101 | 102 | 103 | JSON.parse(text, reviver) 104 | This method parses a JSON text to produce an object or array. 105 | It can throw a SyntaxError exception. 106 | 107 | The optional reviver parameter is a function that can filter and 108 | transform the results. It receives each of the keys and values, 109 | and its return value is used instead of the original value. 110 | If it returns what it received, then the structure is not modified. 111 | If it returns undefined then the member is deleted. 112 | 113 | Example: 114 | 115 | // Parse the text. Values that look like ISO date strings will 116 | // be converted to Date objects. 117 | 118 | myData = JSON.parse(text, function (key, value) { 119 | var a; 120 | if (typeof value === 'string') { 121 | a = 122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 123 | if (a) { 124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 125 | +a[5], +a[6])); 126 | } 127 | } 128 | return value; 129 | }); 130 | 131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { 132 | var d; 133 | if (typeof value === 'string' && 134 | value.slice(0, 5) === 'Date(' && 135 | value.slice(-1) === ')') { 136 | d = new Date(value.slice(5, -1)); 137 | if (d) { 138 | return d; 139 | } 140 | } 141 | return value; 142 | }); 143 | 144 | 145 | This is a reference implementation. You are free to copy, modify, or 146 | redistribute. 147 | */ 148 | 149 | /*jslint evil: true, regexp: true */ 150 | 151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, 152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 154 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 155 | test, toJSON, toString, valueOf 156 | */ 157 | 158 | 159 | // Create a JSON object only if one does not already exist. We create the 160 | // methods in a closure to avoid creating global variables. 161 | 162 | if (typeof JSON !== 'object') { 163 | JSON = {}; 164 | } 165 | 166 | (function () { 167 | 'use strict'; 168 | 169 | function f(n) { 170 | // Format integers to have at least two digits. 171 | return n < 10 ? '0' + n : n; 172 | } 173 | 174 | if (typeof Date.prototype.toJSON !== 'function') { 175 | 176 | Date.prototype.toJSON = function () { 177 | 178 | return isFinite(this.valueOf()) 179 | ? this.getUTCFullYear() + '-' + 180 | f(this.getUTCMonth() + 1) + '-' + 181 | f(this.getUTCDate()) + 'T' + 182 | f(this.getUTCHours()) + ':' + 183 | f(this.getUTCMinutes()) + ':' + 184 | f(this.getUTCSeconds()) + 'Z' 185 | : null; 186 | }; 187 | 188 | String.prototype.toJSON = 189 | Number.prototype.toJSON = 190 | Boolean.prototype.toJSON = function () { 191 | return this.valueOf(); 192 | }; 193 | } 194 | 195 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 196 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 197 | gap, 198 | indent, 199 | meta = { // table of character substitutions 200 | '\b': '\\b', 201 | '\t': '\\t', 202 | '\n': '\\n', 203 | '\f': '\\f', 204 | '\r': '\\r', 205 | '"' : '\\"', 206 | '\\': '\\\\' 207 | }, 208 | rep; 209 | 210 | 211 | function quote(string) { 212 | 213 | // If the string contains no control characters, no quote characters, and no 214 | // backslash characters, then we can safely slap some quotes around it. 215 | // Otherwise we must also replace the offending characters with safe escape 216 | // sequences. 217 | 218 | escapable.lastIndex = 0; 219 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) { 220 | var c = meta[a]; 221 | return typeof c === 'string' 222 | ? c 223 | : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 224 | }) + '"' : '"' + string + '"'; 225 | } 226 | 227 | 228 | function str(key, holder) { 229 | 230 | // Produce a string from holder[key]. 231 | 232 | var i, // The loop counter. 233 | k, // The member key. 234 | v, // The member value. 235 | length, 236 | mind = gap, 237 | partial, 238 | value = holder[key]; 239 | 240 | // If the value has a toJSON method, call it to obtain a replacement value. 241 | 242 | if (value && typeof value === 'object' && 243 | typeof value.toJSON === 'function') { 244 | value = value.toJSON(key); 245 | } 246 | 247 | // If we were called with a replacer function, then call the replacer to 248 | // obtain a replacement value. 249 | 250 | if (typeof rep === 'function') { 251 | value = rep.call(holder, key, value); 252 | } 253 | 254 | // What happens next depends on the value's type. 255 | 256 | switch (typeof value) { 257 | case 'string': 258 | return quote(value); 259 | 260 | case 'number': 261 | 262 | // JSON numbers must be finite. Encode non-finite numbers as null. 263 | 264 | return isFinite(value) ? String(value) : 'null'; 265 | 266 | case 'boolean': 267 | case 'null': 268 | 269 | // If the value is a boolean or null, convert it to a string. Note: 270 | // typeof null does not produce 'null'. The case is included here in 271 | // the remote chance that this gets fixed someday. 272 | 273 | return String(value); 274 | 275 | // If the type is 'object', we might be dealing with an object or an array or 276 | // null. 277 | 278 | case 'object': 279 | 280 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 281 | // so watch out for that case. 282 | 283 | if (!value) { 284 | return 'null'; 285 | } 286 | 287 | // Make an array to hold the partial results of stringifying this object value. 288 | 289 | gap += indent; 290 | partial = []; 291 | 292 | // Is the value an array? 293 | 294 | if (Object.prototype.toString.apply(value) === '[object Array]') { 295 | 296 | // The value is an array. Stringify every element. Use null as a placeholder 297 | // for non-JSON values. 298 | 299 | length = value.length; 300 | for (i = 0; i < length; i += 1) { 301 | partial[i] = str(i, value) || 'null'; 302 | } 303 | 304 | // Join all of the elements together, separated with commas, and wrap them in 305 | // brackets. 306 | 307 | v = partial.length === 0 308 | ? '[]' 309 | : gap 310 | ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' 311 | : '[' + partial.join(',') + ']'; 312 | gap = mind; 313 | return v; 314 | } 315 | 316 | // If the replacer is an array, use it to select the members to be stringified. 317 | 318 | if (rep && typeof rep === 'object') { 319 | length = rep.length; 320 | for (i = 0; i < length; i += 1) { 321 | if (typeof rep[i] === 'string') { 322 | k = rep[i]; 323 | v = str(k, value); 324 | if (v) { 325 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 326 | } 327 | } 328 | } 329 | } else { 330 | 331 | // Otherwise, iterate through all of the keys in the object. 332 | 333 | for (k in value) { 334 | if (Object.prototype.hasOwnProperty.call(value, k)) { 335 | v = str(k, value); 336 | if (v) { 337 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 338 | } 339 | } 340 | } 341 | } 342 | 343 | // Join all of the member texts together, separated with commas, 344 | // and wrap them in braces. 345 | 346 | v = partial.length === 0 347 | ? '{}' 348 | : gap 349 | ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' 350 | : '{' + partial.join(',') + '}'; 351 | gap = mind; 352 | return v; 353 | } 354 | } 355 | 356 | // If the JSON object does not yet have a stringify method, give it one. 357 | 358 | if (typeof JSON.stringify !== 'function') { 359 | JSON.stringify = function (value, replacer, space) { 360 | 361 | // The stringify method takes a value and an optional replacer, and an optional 362 | // space parameter, and returns a JSON text. The replacer can be a function 363 | // that can replace values, or an array of strings that will select the keys. 364 | // A default replacer method can be provided. Use of the space parameter can 365 | // produce text that is more easily readable. 366 | 367 | var i; 368 | gap = ''; 369 | indent = ''; 370 | 371 | // If the space parameter is a number, make an indent string containing that 372 | // many spaces. 373 | 374 | if (typeof space === 'number') { 375 | for (i = 0; i < space; i += 1) { 376 | indent += ' '; 377 | } 378 | 379 | // If the space parameter is a string, it will be used as the indent string. 380 | 381 | } else if (typeof space === 'string') { 382 | indent = space; 383 | } 384 | 385 | // If there is a replacer, it must be a function or an array. 386 | // Otherwise, throw an error. 387 | 388 | rep = replacer; 389 | if (replacer && typeof replacer !== 'function' && 390 | (typeof replacer !== 'object' || 391 | typeof replacer.length !== 'number')) { 392 | throw new Error('JSON.stringify'); 393 | } 394 | 395 | // Make a fake root object containing our value under the key of ''. 396 | // Return the result of stringifying the value. 397 | 398 | return str('', {'': value}); 399 | }; 400 | } 401 | 402 | 403 | // If the JSON object does not yet have a parse method, give it one. 404 | 405 | if (typeof JSON.parse !== 'function') { 406 | JSON.parse = function (text, reviver) { 407 | 408 | // The parse method takes a text and an optional reviver function, and returns 409 | // a JavaScript value if the text is a valid JSON text. 410 | 411 | var j; 412 | 413 | function walk(holder, key) { 414 | 415 | // The walk method is used to recursively walk the resulting structure so 416 | // that modifications can be made. 417 | 418 | var k, v, value = holder[key]; 419 | if (value && typeof value === 'object') { 420 | for (k in value) { 421 | if (Object.prototype.hasOwnProperty.call(value, k)) { 422 | v = walk(value, k); 423 | if (v !== undefined) { 424 | value[k] = v; 425 | } else { 426 | delete value[k]; 427 | } 428 | } 429 | } 430 | } 431 | return reviver.call(holder, key, value); 432 | } 433 | 434 | 435 | // Parsing happens in four stages. In the first stage, we replace certain 436 | // Unicode characters with escape sequences. JavaScript handles many characters 437 | // incorrectly, either silently deleting them, or treating them as line endings. 438 | 439 | text = String(text); 440 | cx.lastIndex = 0; 441 | if (cx.test(text)) { 442 | text = text.replace(cx, function (a) { 443 | return '\\u' + 444 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 445 | }); 446 | } 447 | 448 | // In the second stage, we run the text against regular expressions that look 449 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 450 | // because they can cause invocation, and '=' because it can cause mutation. 451 | // But just to be safe, we want to reject all unexpected forms. 452 | 453 | // We split the second stage into 4 regexp operations in order to work around 454 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 455 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 456 | // replace all simple value tokens with ']' characters. Third, we delete all 457 | // open brackets that follow a colon or comma or that begin the text. Finally, 458 | // we look to see that the remaining characters are only whitespace or ']' or 459 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 460 | 461 | if (/^[\],:{}\s]*$/ 462 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') 463 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') 464 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 465 | 466 | // In the third stage we use the eval function to compile the text into a 467 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 468 | // in JavaScript: it can begin a block or an object literal. We wrap the text 469 | // in parens to eliminate the ambiguity. 470 | 471 | j = eval('(' + text + ')'); 472 | 473 | // In the optional fourth stage, we recursively walk the new structure, passing 474 | // each name/value pair to a reviver function for possible transformation. 475 | 476 | return typeof reviver === 'function' 477 | ? walk({'': j}, '') 478 | : j; 479 | } 480 | 481 | // If the text is not JSON parseable, then a SyntaxError is thrown. 482 | 483 | throw new SyntaxError('JSON.parse'); 484 | }; 485 | } 486 | }()); 487 | -------------------------------------------------------------------------------- /_test/renderer_plugin_edittable_inverse.test.txt: -------------------------------------------------------------------------------- 1 | ====== Formatting Syntax ====== 2 | 3 | 4 | ABOUT THIS FILE: This file is basically a copy of wiki:syntax. The unit test will render it with the reverse renderer which should produce the same file again (some liberties in whitespace handling given). Abiguities in DokuWiki's syntax have been removed in this test file. Instead a few more test cases have been added. 5 | 6 | 7 | 8 | 9 | [[doku>DokuWiki]] supports some simple markup language, which tries to make the datafiles to be as readable as possible. This page contains all possible syntax you may use when editing the pages. Simply have a look at the source of this page by pressing "Edit this page". If you want to try something, just use the [[playground:playground|playground]] page. The simpler markup is easily accessible via [[doku>toolbar|quickbuttons]], too. 10 | 11 | ===== Basic Text Formatting ===== 12 | 13 | DokuWiki supports **bold**, //italic//, __underlined__ and ''monospaced'' texts. Of course you can **__//''combine''//__** all these. 14 | 15 | DokuWiki supports **bold**, //italic//, __underlined__ and ''monospaced'' texts. 16 | Of course you can **__//''combine''//__** all these. 17 | 18 | You can use subscript and superscript, too. 19 | 20 | You can use subscript and superscript, too. 21 | 22 | You can mark something as deleted as well. 23 | 24 | You can mark something as deleted as well. 25 | 26 | **Paragraphs** are created from blank lines. If you want to **force a newline** without a paragraph, you can use two backslashes followed by a whitespace or the end of line. 27 | 28 | This is some text with some linebreaks\\ Note that the two backslashes are only recognized at the end of a line\\ or followed by\\ a whitespace \\this happens without it. 29 | 30 | This is some text with some linebreaks\\ Note that the 31 | two backslashes are only recognized at the end of a line\\ 32 | or followed by\\ a whitespace \\this happens without it. 33 | 34 | You should use forced newlines only if really needed. 35 | 36 | ===== Links ===== 37 | 38 | DokuWiki supports multiple ways of creating links. 39 | 40 | ==== External ==== 41 | 42 | External links are recognized automagically: http://www.google.com or simply www.google.com - You can set the link text as well: [[http://www.google.com|This Link points to google]]. Email addresses like this one: are recognized, too. 43 | 44 | DokuWiki supports multiple ways of creating links. External links are recognized 45 | automagically: http://www.google.com or simply www.google.com - You can set 46 | link text as well: [[http://www.google.com|This Link points to google]]. Email 47 | addresses like this one: are recognized, too. 48 | 49 | This is an external link with no title: [[http://www.google.com|]] 50 | 51 | ==== Internal ==== 52 | 53 | Internal links are created by using square brackets. You can either just give a [[pagename]] or use an additional [[pagename|link text]]. 54 | 55 | Internal links are created by using square brackets. You can either just give 56 | a [[pagename]] or use an additional [[pagename|link text]]. 57 | 58 | [[doku>pagename|Wiki pagenames]] are converted to lowercase automatically, special characters are not allowed. 59 | 60 | You can use [[some:namespaces]] by using a colon in the pagename. 61 | 62 | You can use [[some:namespaces]] by using a colon in the pagename. 63 | 64 | For details about namespaces see [[doku>namespaces]]. 65 | 66 | Linking to a specific section is possible, too. Just add the section name behind a hash character as known from HTML. This links to [[syntax#internal|this Section]]. 67 | 68 | This links to [[syntax#internal|this Section]]. 69 | 70 | Notes: 71 | 72 | * Links to [[syntax|existing pages]] are shown in a different style from [[nonexisting]] ones. 73 | * DokuWiki does not use [[wp>CamelCase]] to automatically create links by default, but this behavior can be enabled in the [[doku>config]] file. Hint: If DokuWiki is a link, then it's enabled. 74 | * When a section's heading is changed, its bookmark changes, too. So don't rely on section linking too much. 75 | 76 | ==== Interwiki ==== 77 | 78 | DokuWiki supports [[doku>Interwiki]] links. These are quick links to other Wikis. For example this is a link to Wikipedia's page about Wikis: [[wp>Wiki]]. 79 | 80 | DokuWiki supports [[doku>Interwiki]] links. These are quick links to other Wikis. 81 | For example this is a link to Wikipedia's page about Wikis: [[wp>Wiki]]. 82 | 83 | ==== Windows Shares ==== 84 | 85 | Windows shares like [[\\server\share|this]] are recognized, too. Please note that these only make sense in a homogeneous user group like a corporate [[wp>Intranet]]. 86 | 87 | Windows Shares like [[\\server\share|this]] are recognized, too. 88 | 89 | Notes: 90 | 91 | * For security reasons direct browsing of windows shares only works in Microsoft Internet Explorer per default (and only in the "local zone"). 92 | * For Mozilla and Firefox it can be enabled through different workaround mentioned in the [[http://kb.mozillazine.org/Links_to_local_pages_do_not_work|Mozilla Knowledge Base]]. However, there will still be a JavaScript warning about trying to open a Windows Share. To remove this warning (for all users), put the following line in ''conf/userscript.js'': 93 | 94 | LANG.nosmblinks = ''; 95 | 96 | ==== Image Links ==== 97 | 98 | You can also use an image to link to another internal or external page by combining the syntax for links and [[#images_and_other_files|images]] (see below) like this: 99 | 100 | [[http://www.php.net|{{wiki:dokuwiki-128.png}}]] 101 | 102 | [[http://www.php.net|{{wiki:dokuwiki-128.png}}]] 103 | 104 | Please note: The image formatting is the only formatting syntax accepted in link names. 105 | 106 | The whole [[#images_and_other_files|image]] and [[#links|link]] syntax is supported (including image resizing, internal and external images and URLs and interwiki links). 107 | 108 | ===== Footnotes ===== 109 | 110 | You can add footnotes ((This is a footnote)) by using double parentheses. 111 | 112 | You can add footnotes ((This is a footnote)) by using double parentheses. 113 | 114 | ===== Sectioning ===== 115 | 116 | You can use up to five different levels of headlines to structure your content. If you have more than three headlines, a table of contents is generated automatically -- this can be disabled by including the string ''%%~~NOTOC~~%%'' in the document. 117 | 118 | ==== Headline Level 3 ==== 119 | === Headline Level 4 === 120 | == Headline Level 5 == 121 | 122 | ==== Headline Level 3 ==== 123 | === Headline Level 4 === 124 | == Headline Level 5 == 125 | 126 | By using four or more dashes, you can make a horizontal line: 127 | 128 | ---- 129 | 130 | ===== Images and Other Files ===== 131 | 132 | You can include external and internal [[doku>images]] with curly brackets. Optionally you can specify the size of them. 133 | 134 | Real size: {{wiki:dokuwiki-128.png}} 135 | 136 | Resize to given width: {{wiki:dokuwiki-128.png?50}} 137 | 138 | Resize to given width and height((when the aspect ratio of the given width and height doesn't match that of the image, it will be cropped to the new ratio before resizing)): {{wiki:dokuwiki-128.png?200x50}} 139 | 140 | Resized external image: {{http://de3.php.net/images/php.gif?200x50}} 141 | 142 | Real size: {{wiki:dokuwiki-128.png}} 143 | Resize to given width: {{wiki:dokuwiki-128.png?50}} 144 | Resize to given width and height: {{wiki:dokuwiki-128.png?200x50}} 145 | Resized external image: {{http://de3.php.net/images/php.gif?200x50}} 146 | 147 | 148 | By using left or right whitespaces you can choose the alignment. 149 | 150 | {{ wiki:dokuwiki-128.png}} 151 | 152 | {{wiki:dokuwiki-128.png }} 153 | 154 | {{ wiki:dokuwiki-128.png }} 155 | 156 | {{ wiki:dokuwiki-128.png}} 157 | {{wiki:dokuwiki-128.png }} 158 | {{ wiki:dokuwiki-128.png }} 159 | 160 | Of course, you can add a title (displayed as a tooltip by most browsers), too. 161 | 162 | {{ wiki:dokuwiki-128.png |This is the caption}} 163 | 164 | {{ wiki:dokuwiki-128.png |This is the caption}} 165 | 166 | If you specify a filename (external or internal) that is not an image (''gif, jpeg, png''), then it will be displayed as a link instead. 167 | 168 | For linking an image to another page see [[#Image Links]] above. 169 | 170 | ===== Lists ===== 171 | 172 | Dokuwiki supports ordered and unordered lists. To create a list item, indent your text by two spaces and use a ''*'' for unordered lists or a ''-'' for ordered ones. 173 | 174 | * This is a list 175 | * The second item 176 | * You may have different levels 177 | * Another item 178 | 179 | - The same list but ordered 180 | - Another item 181 | - Just use indention for deeper levels 182 | - That's it 183 | 184 | 185 | * This is a list 186 | * The second item 187 | * You may have different levels 188 | * Another item 189 | 190 | - The same list but ordered 191 | - Another item 192 | - Just use indention for deeper levels 193 | - That's it 194 | 195 | 196 | Also take a look at the [[doku>faq:lists|FAQ on list items]]. 197 | 198 | ===== Text Conversions ===== 199 | 200 | DokuWiki can convert certain pre-defined characters or strings into images or other text or HTML. 201 | 202 | The text to image conversion is mainly done for smileys. And the text to HTML conversion is used for typography replacements, but can be configured to use other HTML as well. 203 | 204 | ==== Text to Image Conversions ==== 205 | 206 | DokuWiki converts commonly used [[wp>emoticon]]s to their graphical equivalents. Those [[doku>Smileys]] and other images can be configured and extended. Here is an overview of Smileys included in DokuWiki: 207 | 208 | * 8-) %% 8-) %% 209 | * 8-O %% 8-O %% 210 | * :-( %% :-( %% 211 | * :-) %% :-) %% 212 | * =) %% =) %% 213 | * :-/ %% :-/ %% 214 | * :-\ %% :-\ %% 215 | * :-? %% :-? %% 216 | * :-D %% :-D %% 217 | * :-P %% :-P %% 218 | * :-O %% :-O %% 219 | * :-X %% :-X %% 220 | * :-| %% :-| %% 221 | * ;-) %% ;-) %% 222 | * ^_^ %% ^_^ %% 223 | * :?: %% :?: %% 224 | * :!: %% :!: %% 225 | * LOL %% LOL %% 226 | * FIXME %% FIXME %% 227 | * DELETEME %% DELETEME %% 228 | 229 | ==== Text to HTML Conversions ==== 230 | 231 | Typography: [[DokuWiki]] can convert simple text characters to their typographically correct entities. Here is an example of recognized characters. 232 | 233 | -> <- <-> => <= <=> >> << -- --- 640x480 (c) (tm) (r) 234 | "He thought 'It's a man's world'..." 235 | 236 | 237 | -> <- <-> => <= <=> >> << -- --- 640x480 (c) (tm) (r) 238 | "He thought 'It's a man's world'..." 239 | 240 | 241 | The same can be done to produce any kind of HTML, it just needs to be added to the [[doku>entities|pattern file]]. 242 | 243 | There are three exceptions which do not come from that pattern file: multiplication entity (640x480), 'single' and "double quotes". They can be turned off through a [[doku>config:typography|config option]]. 244 | 245 | ===== Quoting ===== 246 | 247 | Some times you want to mark some text to show it's a reply or comment. You can use the following syntax: 248 | 249 | I think we should do it 250 | 251 | > No we shouldn't 252 | 253 | >> Well, I say we should 254 | 255 | > Really? 256 | 257 | >> Yes! 258 | 259 | >>> Then lets do it! 260 | 261 | I think we should do it 262 | 263 | > No we shouldn't 264 | 265 | >> Well, I say we should 266 | 267 | > Really? 268 | 269 | >> Yes! 270 | 271 | >>> Then lets do it! 272 | 273 | ===== Tables ===== 274 | 275 | DokuWiki supports a simple syntax to create tables. 276 | 277 | ^ Heading 1 ^ Heading 2 ^ Heading 3 ^ 278 | | Row 1 Col 1 | Row 1 Col 2 | Row 1 Col 3 | 279 | | Row 2 Col 1 | some colspan (note the double pipe) || 280 | | Row 3 Col 1 | Row 3 Col 2 | Row 3 Col 3 | 281 | 282 | Table rows have to start and end with a ''|'' for normal rows or a ''^'' for headers. 283 | 284 | ^ Heading 1 ^ Heading 2 ^ Heading 3 ^ 285 | | Row 1 Col 1 | Row 1 Col 2 | Row 1 Col 3 | 286 | | Row 2 Col 1 | some colspan (note the double pipe) || 287 | | Row 3 Col 1 | Row 3 Col 2 | Row 3 Col 3 | 288 | 289 | To connect cells horizontally, just make the next cell completely empty as shown above. Be sure to have always the same amount of cell separators! 290 | 291 | Vertical tableheaders are possible, too. 292 | 293 | | ^ Heading 1 ^ Heading 2 ^ 294 | ^ Heading 3 | Row 1 Col 2 | Row 1 Col 3 | 295 | ^ Heading 4 | no colspan this time | | 296 | ^ Heading 5 | Row 2 Col 2 | Row 2 Col 3 | 297 | 298 | As you can see, it's the cell separator before a cell which decides about the formatting: 299 | 300 | | ^ Heading 1 ^ Heading 2 ^ 301 | ^ Heading 3 | Row 1 Col 2 | Row 1 Col 3 | 302 | ^ Heading 4 | no colspan this time | | 303 | ^ Heading 5 | Row 2 Col 2 | Row 2 Col 3 | 304 | 305 | You can have rowspans (vertically connected cells) by adding '':::'' into the cells below the one to which they should connect. 306 | 307 | ^ Heading 1 ^ Heading 2 ^ Heading 3 ^ 308 | | Row 1 Col 1 | this cell spans vertically | Row 1 Col 3 | 309 | | Row 2 Col 1 | ::: | Row 2 Col 3 | 310 | | Row 3 Col 1 | ::: | Row 2 Col 3 | 311 | 312 | Apart from the rowspan syntax those cells should not contain anything else. 313 | 314 | ^ Heading 1 ^ Heading 2 ^ Heading 3 ^ 315 | | Row 1 Col 1 | this cell spans vertically | Row 1 Col 3 | 316 | | Row 2 Col 1 | ::: | Row 2 Col 3 | 317 | | Row 3 Col 1 | ::: | Row 2 Col 3 | 318 | 319 | You can align the table contents, too. Just add at least two whitespaces at the opposite end of your text: Add two spaces on the left to align right, two spaces on the right to align left and two spaces at least at both ends for centered text. 320 | 321 | ^ Table with alignment ^^^ 322 | | right | center | left | 323 | | left | right | center | 324 | | xxxxxxxxxxxxx | xxxxxxxxxxxxx | xxxxxxxxxxxxx | 325 | 326 | This is how it looks in the source: 327 | 328 | ^ Table with alignment ^^^ 329 | | right | center | left | 330 | | left | right | center | 331 | | xxxxxxxxxxxxx | xxxxxxxxxxxxx | xxxxxxxxxxxxx | 332 | 333 | Note: Vertical alignment is not supported. 334 | 335 | ===== No Formatting ===== 336 | 337 | If you need to display text exactly like it is typed (without any formatting), enclose the area either with ''%%%%'' tags or even simpler, with double percent signs ''%%''. 338 | 339 | 340 | This is some text which contains addresses like this: http://www.splitbrain.org and **formatting**, but nothing is done with it. 341 | 342 | The same is true for %%//__this__ text// with a smiley ;-)%%. 343 | 344 | 345 | This is some text which contains addresses like this: http://www.splitbrain.org and **formatting**, but nothing is done with it. 346 | 347 | The same is true for %%//__this__ text// with a smiley ;-)%%. 348 | 349 | ===== Code Blocks ===== 350 | 351 | You can include code blocks into your documents by either indenting them by at least two spaces (like used for the previous examples) or by using the tags ''%%%%'' or ''%%%%''. 352 | 353 | This is text is indented by two spaces. 354 | 355 | 356 | This is preformatted code all spaces are preserved: like <-this 357 | 358 | 359 | 360 | This is pretty much the same, but you could use it to show that you quoted a file. 361 | 362 | 363 | Those blocks were created by this source: 364 | 365 | This is text is indented by two spaces. 366 | 367 | 368 | This is preformatted code all spaces are preserved: like <-this 369 | 370 | 371 | 372 | This is pretty much the same, but you could use it to show that you quoted a file. 373 | 374 | 375 | ==== Syntax Highlighting ==== 376 | 377 | [[wiki:DokuWiki]] can highlight sourcecode, which makes it easier to read. It uses the [[http://qbnz.com/highlighter/|GeSHi]] Generic Syntax Highlighter -- so any language supported by GeSHi is supported. The syntax uses the same code and file blocks described in the previous section, but this time the name of the language syntax to be highlighted is included inside the tag, e.g. ''%%%%'' or ''%%%%''. 378 | 379 | 380 | /** 381 | * The HelloWorldApp class implements an application that 382 | * simply displays "Hello World!" to the standard output. 383 | */ 384 | class HelloWorldApp { 385 | public static void main(String[] args) { 386 | System.out.println("Hello World!"); //Display the string. 387 | } 388 | } 389 | 390 | 391 | The following language strings are currently recognized: //4cs, 6502acme, 6502kickass, 6502tasm, 68000devpac, abap, actionscript-french, actionscript, actionscript3, ada, algol68, apache, applescript, asm, asp, autoconf, autohotkey, autoit, avisynth, awk, bascomavr, bash, basic4gl, bf, bibtex, blitzbasic, bnf, boo, c, c_loadrunner, c_mac, caddcl, cadlisp, cfdg, cfm, chaiscript, cil, clojure, cmake, cobol, coffeescript, cpp, cpp-qt, csharp, css, cuesheet, d, dcs, delphi, diff, div, dos, dot, e, epc, ecmascript, eiffel, email, erlang, euphoria, f1, falcon, fo, fortran, freebasic, fsharp, gambas, genero, genie, gdb, glsl, gml, gnuplot, go, groovy, gettext, gwbasic, haskell, hicest, hq9plus, html, html5, icon, idl, ini, inno, intercal, io, j, java5, java, javascript, jquery, kixtart, klonec, klonecpp, latex, lb, lisp, llvm, locobasic, logtalk, lolcode, lotusformulas, lotusscript, lscript, lsl2, lua, m68k, magiksf, make, mapbasic, matlab, mirc, modula2, modula3, mmix, mpasm, mxml, mysql, newlisp, nsis, oberon2, objc, objeck, ocaml-brief, ocaml, oobas, oracle8, oracle11, oxygene, oz, pascal, pcre, perl, perl6, per, pf, php-brief, php, pike, pic16, pixelbender, pli, plsql, postgresql, povray, powerbuilder, powershell, proftpd, progress, prolog, properties, providex, purebasic, pycon, python, q, qbasic, rails, rebol, reg, robots, rpmspec, rsplus, ruby, sas, scala, scheme, scilab, sdlbasic, smalltalk, smarty, sql, systemverilog, tcl, teraterm, text, thinbasic, tsql, typoscript, unicon, uscript, vala, vbnet, vb, verilog, vhdl, vim, visualfoxpro, visualprolog, whitespace, winbatch, whois, xbasic, xml, xorg_conf, xpp, yaml, z80, zxbasic// 392 | 393 | ==== Downloadable Code Blocks ==== 394 | 395 | When you use the ''%%%%'' or ''%%%%'' syntax as above, you might want to make the shown code available for download as well. You can do this by specifying a file name after language code like this: 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | If you don't want any highlighting but want a downloadable file, specify a dash (''-'') as the language code: ''%%%%''. 408 | 409 | 410 | ===== Embedding HTML and PHP ===== 411 | 412 | You can embed raw HTML or PHP code into your documents by using the ''%%%%'' or ''%%%%'' tags. (Use uppercase tags if you need to enclose block level elements.) 413 | 414 | HTML example: 415 | 416 | 417 | 418 | This is some inline HTML 419 | 420 | 421 |

And this is some block HTML

422 | 423 |
424 | 425 | 426 | This is some inline HTML 427 | 428 | 429 |

And this is some block HTML

430 | 431 | 432 | PHP example: 433 | 434 | 435 | 436 | echo 'A logo generated by PHP:'; 437 | echo 'PHP Logo !'; 438 | echo '(generated inline HTML)'; 439 | 440 | 441 | echo ''; 442 | echo ''; 443 | echo '
The same, but inside a block level element:PHP Logo !
'; 444 |
445 |
446 | 447 | 448 | echo 'A logo generated by PHP:'; 449 | echo 'PHP Logo !'; 450 | echo '(inline HTML)'; 451 | 452 | 453 | echo ''; 454 | echo ''; 455 | echo '
The same, but inside a block level element:PHP Logo !
'; 456 |
457 | 458 | **Please Note**: HTML and PHP embedding is disabled by default in the configuration. If disabled, the code is displayed instead of executed. 459 | 460 | ===== RSS/ATOM Feed Aggregation ===== 461 | [[DokuWiki]] can integrate data from external XML feeds. For parsing the XML feeds, [[http://simplepie.org/|SimplePie]] is used. All formats understood by SimplePie can be used in DokuWiki as well. You can influence the rendering by multiple additional space separated parameters: 462 | 463 | ^ Parameter ^ Description ^ 464 | | any number | will be used as maximum number items to show, defaults to 8 | 465 | | reverse | display the last items in the feed first | 466 | | author | show item authors names | 467 | | date | show item dates | 468 | | description | show the item description. If [[doku>config:htmlok|HTML]] is disabled all tags will be stripped | 469 | | //n//[dhm] | refresh period, where d=days, h=hours, m=minutes. (e.g. 12h = 12 hours). | 470 | 471 | The refresh period defaults to 4 hours. Any value below 10 minutes will be treated as 10 minutes. [[wiki:DokuWiki]] will generally try to supply a cached version of a page, obviously this is inappropriate when the page contains dynamic external content. The parameter tells [[wiki:DokuWiki]] to re-render the page if it is more than //refresh period// since the page was last rendered. 472 | 473 | **Example:** 474 | 475 | {{rss>http://slashdot.org/index.rss 5 author date 1h}} 476 | 477 | {{rss>http://slashdot.org/index.rss 5 author date 1h}} 478 | 479 | 480 | ===== Control Macros ===== 481 | 482 | Some syntax influences how DokuWiki renders a page without creating any output it self. The following control macros are availble: 483 | 484 | ^ Macro ^ Description ^ 485 | | %%~~NOTOC~~%% | If this macro is found on the page, no table of contents will be created | 486 | | %%~~NOCACHE~~%% | DokuWiki caches all output by default. Sometimes this might not be wanted (eg. when the %%%% syntax above is used), adding this macro will force DokuWiki to rerender a page on every call | 487 | 488 | ===== Syntax Plugins ===== 489 | 490 | DokuWiki's syntax can be extended by [[doku>plugins|Plugins]]. How the installed plugins are used is described on their appropriate description pages. The following syntax plugins are available in this particular DokuWiki installation: 491 | 492 | ~~INFO:syntaxplugins~~ 493 | 494 | EOF -------------------------------------------------------------------------------- /renderer/inverse.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | // must be run within Dokuwiki 9 | if(!defined('DOKU_INC')) die(); 10 | 11 | require_once DOKU_INC.'inc/parser/renderer.php'; 12 | 13 | class renderer_plugin_edittable_inverse extends Doku_Renderer { 14 | /** @var string will contain the whole document */ 15 | public $doc = ''; 16 | 17 | // bunch of internal state variables 18 | private $prepend_not_block = ''; 19 | private $_key = 0; 20 | private $_pos = 0; 21 | private $_ownspan = 0; 22 | private $previous_block = false; 23 | private $_row = 0; 24 | private $_rowspans = array(); 25 | private $_table = array(); 26 | private $_liststack = array(); 27 | private $quotelvl = 0; 28 | private $_extlinkparser = null; 29 | 30 | function getFormat() { 31 | return 'wiki'; 32 | } 33 | 34 | function document_start() { 35 | } 36 | 37 | function document_end() { 38 | $this->block(); 39 | $this->doc = rtrim($this->doc); 40 | } 41 | 42 | function header($text, $level, $pos) { 43 | $this->block(); 44 | if(!$text) return; //skip empty headlines 45 | 46 | // write the header 47 | $markup = str_repeat('=', 7 - $level); 48 | $this->doc .= "$markup $text $markup".DOKU_LF; 49 | } 50 | 51 | function section_open($level) { 52 | $this->block(); 53 | # $this->doc .= DOKU_LF; 54 | } 55 | 56 | function section_close() { 57 | $this->block(); 58 | $this->doc .= DOKU_LF; 59 | } 60 | 61 | // FIXME this did something compllicated with surrounding whitespaces. Why? 62 | function cdata($text) { 63 | if(strlen($text) === 0) { 64 | $this->not_block(); 65 | return; 66 | } 67 | 68 | // if(!$this->previous_block && trim(substr($text, 0, 1)) === '' && trim($text) !== '') { 69 | // $this->doc .= ' '; 70 | // } 71 | $this->not_block(); 72 | 73 | // if(trim(substr($text, -1, 1)) === '' && trim($text) !== '') { 74 | // $this->prepend_not_block = ' '; 75 | // } 76 | // $this->doc .= trim($text); 77 | 78 | $this->doc .= $text; 79 | } 80 | 81 | function p_close() { 82 | $this->block(); 83 | if($this->quotelvl === 0) { 84 | $this->doc = rtrim($this->doc, DOKU_LF).DOKU_LF.DOKU_LF; 85 | } 86 | } 87 | 88 | function p_open() { 89 | $this->block(); 90 | if(strlen($this->doc) > 0 && substr($this->doc, 1, -1) !== DOKU_LF) { 91 | $this->doc .= DOKU_LF.DOKU_LF; 92 | } 93 | $this->doc .= str_repeat('>', $this->quotelvl); 94 | } 95 | 96 | function linebreak() { 97 | $this->not_block(); 98 | $this->doc .= '\\\\ '; 99 | } 100 | 101 | function hr() { 102 | $this->block(); 103 | $this->doc .= '----'; 104 | } 105 | 106 | function block() { 107 | if(isset($this->prepend_not_block)) { 108 | unset($this->prepend_not_block); 109 | } 110 | $this->previous_block = true; 111 | } 112 | 113 | function not_block() { 114 | if(isset($this->prepend_not_block)) { 115 | $this->doc .= $this->prepend_not_block; 116 | unset($this->prepend_not_block); 117 | } 118 | $this->previous_block = false; 119 | } 120 | 121 | function strong_open() { 122 | $this->not_block(); 123 | $this->doc .= '**'; 124 | } 125 | 126 | function strong_close() { 127 | $this->not_block(); 128 | $this->doc .= '**'; 129 | } 130 | 131 | function emphasis_open() { 132 | $this->not_block(); 133 | $this->doc .= '//'; 134 | } 135 | 136 | function emphasis_close() { 137 | $this->not_block(); 138 | $this->doc .= '//'; 139 | } 140 | 141 | function underline_open() { 142 | $this->not_block(); 143 | $this->doc .= '__'; 144 | } 145 | 146 | function underline_close() { 147 | $this->not_block(); 148 | $this->doc .= '__'; 149 | } 150 | 151 | function monospace_open() { 152 | $this->not_block(); 153 | $this->doc .= "''"; 154 | } 155 | 156 | function monospace_close() { 157 | $this->not_block(); 158 | $this->doc .= "''"; 159 | } 160 | 161 | function subscript_open() { 162 | $this->not_block(); 163 | $this->doc .= ''; 164 | } 165 | 166 | function subscript_close() { 167 | $this->not_block(); 168 | $this->doc .= ''; 169 | } 170 | 171 | function superscript_open() { 172 | $this->not_block(); 173 | $this->doc .= ''; 174 | } 175 | 176 | function superscript_close() { 177 | $this->not_block(); 178 | $this->doc .= ''; 179 | } 180 | 181 | function deleted_open() { 182 | $this->not_block(); 183 | $this->doc .= ''; 184 | } 185 | 186 | function deleted_close() { 187 | $this->not_block(); 188 | $this->doc .= ''; 189 | } 190 | 191 | function footnote_open() { 192 | $this->not_block(); 193 | $this->doc .= '(('; 194 | } 195 | 196 | function footnote_close() { 197 | $this->not_block(); 198 | $this->doc .= '))'; 199 | } 200 | 201 | function listu_open() { 202 | $this->block(); 203 | if(!isset($this->_liststack)) { 204 | $this->_liststack = array(); 205 | } 206 | if(count($this->_liststack) === 0) { 207 | $this->doc .= DOKU_LF; 208 | } 209 | $this->_liststack[] = '*'; 210 | } 211 | 212 | function listu_close() { 213 | $this->block(); 214 | array_pop($this->_liststack); 215 | if(count($this->_liststack) === 0) { 216 | $this->doc .= DOKU_LF; 217 | } 218 | } 219 | 220 | function listo_open() { 221 | $this->block(); 222 | if(!isset($this->_liststack)) { 223 | $this->_liststack = array(); 224 | } 225 | if(count($this->_liststack) === 0) { 226 | $this->doc .= DOKU_LF; 227 | } 228 | $this->_liststack[] = '-'; 229 | } 230 | 231 | function listo_close() { 232 | $this->block(); 233 | array_pop($this->_liststack); 234 | if(count($this->_liststack) === 0) { 235 | $this->doc .= DOKU_LF; 236 | } 237 | } 238 | 239 | function listitem_open($level) { 240 | $this->block(); 241 | $this->doc .= str_repeat(' ', $level * 2).end($this->_liststack).' '; 242 | } 243 | 244 | function listcontent_close() { 245 | $this->block(); 246 | $this->doc .= DOKU_LF; 247 | } 248 | 249 | function unformatted($text) { 250 | $this->not_block(); 251 | if(strpos($text, '%%') !== false) { 252 | $this->doc .= "$text"; 253 | } elseif($text{0} == "\n") { 254 | $this->doc .= "$text"; 255 | } else { 256 | $this->doc .= "%%$text%%"; 257 | } 258 | } 259 | 260 | function php($text, $wrapper = 'code') { 261 | $this->not_block(); 262 | $this->doc .= "$text"; 263 | } 264 | 265 | function phpblock($text) { 266 | $this->block(); 267 | $this->doc .= "$text"; 268 | } 269 | 270 | function html($text, $wrapper = 'code') { 271 | $this->not_block(); 272 | $this->doc .= "$text"; 273 | } 274 | 275 | function htmlblock($text) { 276 | $this->block(); 277 | $this->doc .= "$text"; 278 | } 279 | 280 | function quote_open() { 281 | $this->block(); 282 | if(substr($this->doc, -(++$this->quotelvl)) === DOKU_LF.str_repeat('>', $this->quotelvl - 1)) { 283 | $this->doc .= '>'; 284 | } else { 285 | $this->doc .= DOKU_LF.str_repeat('>', $this->quotelvl); 286 | } 287 | $this->prepend_not_block = ' '; 288 | } 289 | 290 | function quote_close() { 291 | $this->block(); 292 | $this->quotelvl--; 293 | if(strrpos($this->doc, DOKU_LF) === strlen($this->doc) - 1) { 294 | return; 295 | } 296 | $this->doc .= DOKU_LF.DOKU_LF; 297 | } 298 | 299 | function preformatted($text) { 300 | $this->block(); 301 | $this->doc .= preg_replace('/^/m', ' ', $text).DOKU_LF; 302 | } 303 | 304 | function file($text, $language = null, $filename = null) { 305 | $this->_highlight('file', $text, $language, $filename); 306 | } 307 | 308 | function code($text, $language = null, $filename = null) { 309 | $this->_highlight('code', $text, $language, $filename); 310 | } 311 | 312 | function _highlight($type, $text, $language = null, $filename = null) { 313 | if( $this->previous_block ) $this->doc .= "\n"; 314 | 315 | $this->block(); 316 | $this->doc .= "<$type"; 317 | if($language != null) { 318 | $this->doc .= " $language"; 319 | } 320 | if($filename != null) { 321 | $this->doc .= " $filename"; 322 | } 323 | $this->doc .= ">"; 324 | $this->doc .= $text; 325 | if($text{0} == "\n") $this->doc .= "\n"; 326 | $this->doc .= ""; 327 | } 328 | 329 | function acronym($acronym) { 330 | $this->not_block(); 331 | $this->doc .= $acronym; 332 | } 333 | 334 | function smiley($smiley) { 335 | $this->not_block(); 336 | $this->doc .= $smiley; 337 | } 338 | 339 | function entity($entity) { 340 | $this->not_block(); 341 | $this->doc .= $entity; 342 | } 343 | 344 | function multiplyentity($x, $y) { 345 | $this->not_block(); 346 | $this->doc .= "{$x}x{$y}"; 347 | } 348 | 349 | function singlequoteopening() { 350 | $this->not_block(); 351 | $this->doc .= "'"; 352 | } 353 | 354 | function singlequoteclosing() { 355 | $this->not_block(); 356 | $this->doc .= "'"; 357 | } 358 | 359 | function apostrophe() { 360 | $this->not_block(); 361 | $this->doc .= "'"; 362 | } 363 | 364 | function doublequoteopening() { 365 | $this->not_block(); 366 | $this->doc .= '"'; 367 | } 368 | 369 | function doublequoteclosing() { 370 | $this->not_block(); 371 | $this->doc .= '"'; 372 | } 373 | 374 | /** 375 | */ 376 | function camelcaselink($link) { 377 | $this->not_block(); 378 | $this->doc .= $link; 379 | } 380 | 381 | function locallink($hash, $name = null) { 382 | $this->not_block(); 383 | $this->doc .= "[[#$hash"; 384 | if($name !== null) { 385 | $this->doc .= '|'; 386 | $this->_echoLinkTitle($name); 387 | } 388 | $this->doc .= ']]'; 389 | } 390 | 391 | function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') { 392 | $this->not_block(); 393 | $this->doc .= "[[$id"; 394 | if($name !== null) { 395 | $this->doc .= '|'; 396 | $this->_echoLinkTitle($name); 397 | } 398 | $this->doc .= ']]'; 399 | } 400 | 401 | /** 402 | * Handle external Links 403 | * 404 | * @author Andreas Gohr 405 | * @param $url 406 | * @param null $name 407 | */ 408 | function externallink($url, $name = null) { 409 | $this->not_block(); 410 | 411 | /* 412 | * When $name is null it might have been a match of an URL that was in the text without 413 | * any link syntax. These are recognized by a bunch of patterns in Doku_Parser_Mode_externallink. 414 | * We simply reuse these patterns here. However, since we don't parse the pattern through the Lexer, 415 | * no escaping is done on the patterns - this means we need a non-conflicting delimiter. I decided for 416 | * a single tick >>'<< which seems to work. Since the patterns contain wordboundaries they are matched 417 | * against the URL surrounded by spaces. 418 | */ 419 | if($name === null) { 420 | // get the patterns from the parser 421 | if(is_null($this->_extlinkparser)) { 422 | $this->_extlinkparser = new Doku_Parser_Mode_externallink(); 423 | $this->_extlinkparser->preConnect(); 424 | } 425 | 426 | // check if URL matches pattern 427 | foreach($this->_extlinkparser->patterns as $pattern) { 428 | if(preg_match("'$pattern'", " $url ")) { 429 | $this->doc .= $url; // gotcha! 430 | return; 431 | } 432 | } 433 | } 434 | 435 | // still here? 436 | if($url === "http://$name" || $url === "ftp://$name") { 437 | // special case - www.* or ftp.* matching 438 | $this->doc .= $name; 439 | } else { 440 | // link syntax! definitively link syntax 441 | $this->doc .= "[[$url"; 442 | if(!is_null($name)) { 443 | // we do have a name! 444 | $this->doc .= '|'; 445 | $this->_echoLinkTitle($name); 446 | } 447 | $this->doc .= ']]'; 448 | } 449 | } 450 | 451 | function interwikilink($match, $name = null, $wikiName, $wikiUri) { 452 | $this->not_block(); 453 | $this->doc .= "[[$wikiName>$wikiUri"; 454 | if($name !== null) { 455 | $this->doc .= '|'; 456 | $this->_echoLinkTitle($name); 457 | } 458 | $this->doc .= ']]'; 459 | } 460 | 461 | function windowssharelink($url, $name = null) { 462 | $this->not_block(); 463 | $this->doc .= "[[$url"; 464 | if($name !== null) { 465 | $this->doc .= '|'; 466 | $this->_echoLinkTitle($name); 467 | } 468 | $this->doc .= "]]"; 469 | } 470 | 471 | function emaillink($address, $name = null) { 472 | $this->not_block(); 473 | if($name === null) { 474 | $this->doc .= "<$address>"; 475 | } else { 476 | $this->doc .= "[[$address|"; 477 | $this->_echoLinkTitle($name); 478 | $this->doc .= ']]'; 479 | } 480 | } 481 | 482 | function internalmedia($src, $title = null, $align = null, $width = null, 483 | $height = null, $cache = null, $linking = null) { 484 | $this->not_block(); 485 | $this->doc .= '{{'; 486 | if($align === 'center' || $align === 'right') { 487 | $this->doc .= ' '; 488 | } 489 | $this->doc .= $src; 490 | 491 | $params = array(); 492 | if($width !== null) { 493 | $params[0] = $width; 494 | if($height !== null) { 495 | $params[0] .= "x$height"; 496 | } 497 | } 498 | if($cache !== 'cache') { 499 | $params[] = $cache; 500 | } 501 | if($linking !== 'details') { 502 | $params[] = $linking; 503 | } 504 | if(count($params) > 0) { 505 | $this->doc .= '?'; 506 | } 507 | $this->doc .= join('&', $params); 508 | 509 | if($align === 'center' || $align === 'left') { 510 | $this->doc .= ' '; 511 | } 512 | if($title != null) { 513 | $this->doc .= "|$title"; 514 | } 515 | $this->doc .= '}}'; 516 | } 517 | 518 | function externalmedia($src, $title = null, $align = null, $width = null, 519 | $height = null, $cache = null, $linking = null) { 520 | $this->internalmedia($src, $title, $align, $width, $height, $cache, $linking); 521 | } 522 | 523 | /** 524 | * Renders an RSS feed 525 | * 526 | * @author Andreas Gohr 527 | */ 528 | function rss($url, $params) { 529 | $this->block(); 530 | $this->doc .= '{{rss>'.$url; 531 | $vals = array(); 532 | if($params['max'] !== 8) { 533 | $vals[] = $params['max']; 534 | } 535 | if($params['reverse']) { 536 | $vals[] = 'reverse'; 537 | } 538 | if($params['author']) { 539 | $vals[] = 'author'; 540 | } 541 | if($params['date']) { 542 | $vals[] = 'date'; 543 | } 544 | if($params['details']) { 545 | $vals[] = 'desc'; 546 | } 547 | if($params['refresh'] !== 14400) { 548 | $val = '10m'; 549 | foreach(array('d' => 86400, 'h' => 3600, 'm' => 60) as $p => $div) { 550 | $res = $params['refresh'] / $div; 551 | if($res === intval($res)) { 552 | $val = "$res$p"; 553 | break; 554 | } 555 | } 556 | $vals[] = $val; 557 | } 558 | if(count($vals) > 0) { 559 | $this->doc .= ' '.join(' ', $vals); 560 | } 561 | $this->doc .= '}}'; 562 | } 563 | 564 | function table_open($maxcols = null, $numrows = null, $pos = null) { 565 | $this->block(); 566 | $this->_table = array(); 567 | $this->_row = 0; 568 | $this->_rowspans = array(); 569 | } 570 | 571 | function table_close($pos = null) { 572 | $this->doc .= $this->_table_to_wikitext($this->_table); 573 | } 574 | 575 | function tablerow_open() { 576 | $this->block(); 577 | $this->_table[++$this->_row] = array(); 578 | $this->_key = 1; 579 | while(isset($this->_rowspans[$this->_key])) { 580 | --$this->_rowspans[$this->_key]; 581 | if($this->_rowspans[$this->_key] === 1) { 582 | unset($this->_rowspans[$this->_key]); 583 | } 584 | ++$this->_key; 585 | } 586 | } 587 | 588 | function tablerow_close() { 589 | $this->block(); 590 | } 591 | 592 | function tableheader_open($colspan = 1, $align = null, $rowspan = 1) { 593 | $this->_cellopen('th', $colspan, $align, $rowspan); 594 | } 595 | 596 | function _cellopen($tag, $colspan, $align, $rowspan) { 597 | $this->block(); 598 | $this->_table[$this->_row][$this->_key] = compact('tag', 'colspan', 'align', 'rowspan'); 599 | if($rowspan > 1) { 600 | $this->_rowspans[$this->_key] = $rowspan; 601 | $this->_ownspan = true; 602 | } 603 | $this->_pos = strlen($this->doc); 604 | } 605 | 606 | function tableheader_close() { 607 | $this->_cellclose(); 608 | } 609 | 610 | function _cellclose() { 611 | $this->block(); 612 | $this->_table[$this->_row][$this->_key]['text'] = trim(substr($this->doc, $this->_pos)); 613 | $this->doc = substr($this->doc, 0, $this->_pos); 614 | $this->_key += $this->_table[$this->_row][$this->_key]['colspan']; 615 | while(isset($this->_rowspans[$this->_key]) && !$this->_ownspan) { 616 | --$this->_rowspans[$this->_key]; 617 | if($this->_rowspans[$this->_key] === 1) { 618 | unset($this->_rowspans[$this->_key]); 619 | } 620 | ++$this->_key; 621 | } 622 | $this->_ownspan = false; 623 | } 624 | 625 | function tablecell_open($colspan = 1, $align = null, $rowspan = 1) { 626 | $this->_cellopen('td', $colspan, $align, $rowspan); 627 | } 628 | 629 | function tablecell_close() { 630 | $this->_cellclose(); 631 | } 632 | 633 | function plugin($name, $args, $state = '', $match = '') { 634 | $this->not_block(); 635 | // This will break for plugins which provide a catch-all render method 636 | // like the do or pagenavi plugins 637 | # $plugin =& plugin_load('syntax',$name); 638 | # if($plugin === null || !$plugin->render($this->getFormat(),$this,$args)) { 639 | $this->doc .= $match; 640 | # } 641 | } 642 | 643 | function _echoLinkTitle($title) { 644 | if(is_array($title)) { 645 | $this->internalmedia( 646 | $title['src'], 647 | $title['title'], 648 | $title['align'], 649 | $title['width'], 650 | $title['height'], 651 | $title['cache'], 652 | $title['linking'] 653 | ); 654 | } else { 655 | $this->doc .= $title; 656 | } 657 | } 658 | 659 | /** 660 | * Helper for table to wikitext conversion 661 | * 662 | * @author Adrian Lang 663 | * @param array $_table 664 | * @return string 665 | */ 666 | private function _table_to_wikitext($_table) { 667 | // Preprocess table for rowspan, make table 0-based. 668 | $table = array(); 669 | $keys = array_keys($_table); 670 | $start = array_pop($keys); 671 | foreach($_table as $i => $row) { 672 | $inorm = $i - $start; 673 | if(!isset($table[$inorm])) $table[$inorm] = array(); 674 | $nextkey = 0; 675 | foreach($row as $cell) { 676 | while(isset($table[$inorm][$nextkey])) { 677 | $nextkey++; 678 | } 679 | $nextkey += $cell['colspan'] - 1; 680 | $table[$inorm][$nextkey] = $cell; 681 | $rowspan = $cell['rowspan']; 682 | $i2 = $inorm + 1; 683 | while($rowspan-- > 1) { 684 | if(!isset($table[$i2])) $table[$i2] = array(); 685 | $nu_cell = $cell; 686 | $nu_cell['text'] = ':::'; 687 | $nu_cell['rowspan'] = 1; 688 | $table[$i2++][$nextkey] = $nu_cell; 689 | } 690 | } 691 | ksort($table[$inorm]); 692 | } 693 | 694 | // Get the max width for every column to do table prettyprinting. 695 | $m_width = array(); 696 | foreach($table as $row) { 697 | foreach($row as $n => $cell) { 698 | // Calculate cell width. 699 | $diff = (utf8_strlen($cell['text']) + $cell['colspan'] + 700 | ($cell['align'] === 'center' ? 3 : 2)); 701 | 702 | // Calculate current max width. 703 | $span = $cell['colspan']; 704 | while(--$span >= 0) { 705 | if(isset($m_width[$n - $span])) { 706 | $diff -= $m_width[$n - $span]; 707 | } 708 | } 709 | 710 | if($diff > 0) { 711 | // Just add the difference to all cols. 712 | while(++$span < $cell['colspan']) { 713 | $m_width[$n - $span] = (isset($m_width[$n - $span]) ? $m_width[$n - $span] : 0) + ceil($diff / $cell['colspan']); 714 | } 715 | } 716 | } 717 | } 718 | 719 | // Write the table. 720 | $types = array('th' => '^', 'td' => '|'); 721 | $str = ''; 722 | foreach($table as $row) { 723 | $pos = 0; 724 | foreach($row as $n => $cell) { 725 | $pos += utf8_strlen($cell['text']) + 1; 726 | $span = $cell['colspan']; 727 | $target = 0; 728 | while(--$span >= 0) { 729 | if(isset($m_width[$n - $span])) { 730 | $target += $m_width[$n - $span]; 731 | } 732 | } 733 | $pad = $target - utf8_strlen($cell['text']); 734 | $pos += $pad + ($cell['colspan'] - 1); 735 | switch($cell['align']) { 736 | case 'right': 737 | $lpad = $pad - 1; 738 | break; 739 | case 'left': 740 | case '': 741 | $lpad = 1; 742 | break; 743 | case 'center': 744 | $lpad = floor($pad / 2); 745 | break; 746 | } 747 | $str .= $types[$cell['tag']].str_repeat(' ', $lpad). 748 | $cell['text'].str_repeat(' ', $pad - $lpad). 749 | str_repeat($types[$cell['tag']], $cell['colspan'] - 1); 750 | } 751 | $str .= $types[$cell['tag']].DOKU_LF; 752 | } 753 | return $str; 754 | } 755 | } 756 | 757 | //Setup VIM: ex: et ts=4 enc=utf-8 : 758 | --------------------------------------------------------------------------------