Hi all, This is my first post here. I've made a patch attached which enhances DokuWiki full-text search function. This patch provides: 1. Sophisticated search query syntax (OR, grouping, etc.) 2. Better search experience in Asian language Could you please check this patch? Merging to the DokuWiki core would be nice, but changes are big. I had to rewrite ft_queryParser and ft_pageSearch functions completely. The details are below: --- #1. Sophisticated search query syntax (OR, grouping, etc.) By applying this patch, you can use the following expressions in your search query: Words: include -exclude Phrases: "phrase to be included" -"phrase you want to exclude" [*NEW*] Namespaces: @include:namespace (or ns:include:namespace) ^exclude:namespace (or -ns:exclude:namespace) Groups: () [*NEW*] -() [*NEW*] Operators: and (default operator: you can always omit this) or (lower precedence than 'and') [*NEW*] e.g. A query [ aa "bb cc" @dd:ee ] means "search pages which contain a word 'aa', a phrase 'bb cc' and are within a namespace 'dd:ee'". This query is equivalent to [ -(-aa or -"bb cc" or -ns:dd:ee) ] as long as you don't mind hit counts. Now you don't have to include words or phrases to hit. A query which consists only of a "namespace" term will work. e.g. Searching [ @wiki ] returns a list of pages which are within the "wiki" namespace. This may be the answer to Kay Roesler's problem. //www.freelists.org/post/dokuwiki/enhance-search-with-namespace-name --- #2. Better search experience in Asian language The core idea is expressed in my asiansearch plugin's page. Asian Search Plugin http://www.dokuwiki.org/plugin:asiansearch Furthermore, thanks to the newly implemented -"phrase to be excluded" syntax, a proper handling of -unwantedword for Asian language has been achieved. --- Thanks, -- Kazutaka Miyasaka <kazmiya@xxxxxxxxx>
Sun Sep 20 21:11:16 JST 2009 Kazutaka Miyasaka <kazmiya@xxxxxxxxx> * enhanced full-text search function - better search experience in Asian language - sophisticated search query syntax (OR, grouping, etc.) New patches: [enhanced full-text search function Kazutaka Miyasaka <kazmiya@xxxxxxxxx>**20090920121116 Ignore-this: cb05f50ca4de12e1cdf3a6cfb0e1b8bc - better search experience in Asian language - sophisticated search query syntax (OR, grouping, etc.) ] { hunk ./inc/fulltext.php 28 return trigger_event('SEARCH_QUERY_FULLPAGE', $data, '_ft_pageSearch'); } -function _ft_pageSearch(&$data){ - // split out original parameters - $query = $data['query']; - $highlight =& $data['highlight']; hunk ./inc/fulltext.php 29 - $q = ft_queryParser($query); - - $highlight = array(); +/** + * Returns a list of matching documents for the given query + * + * @author Andreas Gohr <andi@xxxxxxxxxxxxxx> + * @author Kazutaka Miyasaka <kazmiya@xxxxxxxxx> + */ +function _ft_pageSearch(&$data) { + // parse the given query + $q = ft_queryParser($data['query']); + $data['highlight'] = $q['highlight']; hunk ./inc/fulltext.php 40 - // remember for hilighting later - foreach($q['words'] as $wrd){ - $highlight[] = str_replace('*','',$wrd); - } + if (empty($q['parsed_ary'])) return array(); // lookup all words found in the query hunk ./inc/fulltext.php 43 - $words = array_merge($q['and'],$q['not']); - if(!count($words)) return array(); - $result = idx_lookup($words); - if(!count($result)) return array(); - - // merge search results with query - foreach($q['and'] as $pos => $w){ - $q['and'][$pos] = $result[$w]; - } - // create a list of unwanted docs - $not = array(); - foreach($q['not'] as $pos => $w){ - $not = array_merge($not,array_keys($result[$w])); - } - - // combine and-words - if(count($q['and']) > 1){ - $docs = ft_resultCombine($q['and']); - }else{ - $docs = $q['and'][0]; - } - if(!count($docs)) return array(); - - // create a list of hidden pages in the result - $hidden = array(); - $hidden = array_filter(array_keys($docs),'isHiddenPage'); - $not = array_merge($not,$hidden); + $lookup = idx_lookup($q['words']); hunk ./inc/fulltext.php 45 - // filter unmatched namespaces - if(!empty($q['ns'])) { - $pattern = implode('|^',$q['ns']); - foreach($docs as $key => $val) { - if(!preg_match('/^'.$pattern.'/',$key)) { - unset($docs[$key]); - } - } - } - - // filter unwanted namespaces - if(!empty($q['notns'])) { - $pattern = implode('|^',$q['notns']); - foreach($docs as $key => $val) { - if(preg_match('/^'.$pattern.'/',$key)) { - unset($docs[$key]); - } - } + // get all pages in this dokuwiki site (!: includes nonexistent pages) + $pages_all = array(); + foreach (idx_getIndex('page', '') as $id) { + $pages_all[trim($id)] = 0; // base: 0 hit } hunk ./inc/fulltext.php 51 - // remove negative matches - foreach($not as $n){ - unset($docs[$n]); - } - - if(!count($docs)) return array(); - // handle phrases - if(count($q['phrases'])){ - $q['phrases'] = array_map('utf8_strtolower',$q['phrases']); - // use this for higlighting later: - $highlight = array_merge($highlight,$q['phrases']); - $q['phrases'] = array_map('preg_quote_cb',$q['phrases']); - // check the source of all documents for the exact phrases - foreach(array_keys($docs) as $id){ - $text = utf8_strtolower(rawWiki($id)); - foreach($q['phrases'] as $phrase){ - if(!preg_match('/'.$phrase.'/usi',$text)){ - unset($docs[$id]); // no hit - remove - break; + // process the query + $stack = array(); + foreach ($q['parsed_ary'] as $token) { + switch (substr($token, 0, 3)) { + case 'W+:': + case 'W-:': // word + $word = substr($token, 3); + $stack[] = (array) $lookup[$word]; + break; + case 'P_:': // phrase + $phrase = substr($token, 3); + // since phrases are always parsed as ((W1)(W2)...(P)), + // the end($stack) always points the pages that contain + // all words in this phrase + $pages = end($stack); + $pages_matched = array(); + foreach(array_keys($pages) as $id){ + $text = utf8_strtolower(rawWiki($id)); + if (strpos($text, $phrase) !== false) { + $pages_matched[$id] = 0; // phrase: always 0 hit + } } hunk ./inc/fulltext.php 73 - } + $stack[] = $pages_matched; + break; + case 'N_:': // namespace + $ns = substr($token, 3); + $pages_matched = array(); + foreach (array_keys($pages_all) as $id) { + if (strpos($id, $ns) === 0) { + $pages_matched[$id] = 0; // namespace: always 0 hit + } + } + $stack[] = $pages_matched; + break; + case 'AND': // and operation + list($pages1, $pages2) = array_splice($stack, -2); + $stack[] = ft_resultCombine(array($pages1, $pages2)); + break; + case 'OR': // or operation + list($pages1, $pages2) = array_splice($stack, -2); + $stack[] = ft_resultUnite(array($pages1, $pages2)); + break; + case 'NOT': // not operation (unary) + $pages = array_pop($stack); + $stack[] = ft_resultComplement(array($pages_all, $pages)); + break; } } hunk ./inc/fulltext.php 99 + $docs = array_pop($stack); hunk ./inc/fulltext.php 101 - if(!count($docs)) return array(); + if (empty($docs)) return array(); hunk ./inc/fulltext.php 103 - // check ACL permissions - foreach(array_keys($docs) as $doc){ - if(auth_quickaclcheck($doc) < AUTH_READ){ - unset($docs[$doc]); + // check: settings, acls, existence + foreach (array_keys($docs) as $id) { + if (isHiddenPage($id) || auth_quickaclcheck($id) < AUTH_READ || !page_exists($id, '', false)) { + unset($docs[$id]); } } hunk ./inc/fulltext.php 110 - if(!count($docs)) return array(); - - // if there are any hits left, sort them by count + // sort docs by count arsort($docs); return $docs; hunk ./inc/fulltext.php 391 } /** - * Builds an array of search words from a query + * Unites found documents and sum up their scores + * + * based upon ft_resultCombine() function + * + * @param array $args An array of page arrays + * @author Kazutaka Miyasaka <kazmiya@xxxxxxxxx> + */ +function ft_resultUnite($args) { + $array_count = count($args); + if ($array_count === 1) { + return $args[0]; + } + + $result = $args[0]; + for ($i = 1; $i !== $array_count; $i++) { + foreach (array_keys($args[$i]) as $id) { + $result[$id] += $args[$i][$id]; + } + } + return $result; +} + +/** + * Computes the difference of documents using page id for comparison + * + * nearly identical to PHP5's array_diff_key() + * + * @param array $args An array of page arrays + * @author Kazutaka Miyasaka <kazmiya@xxxxxxxxx> + */ +function ft_resultComplement($args) { + $array_count = count($args); + if ($array_count === 1) { + return $args[0]; + } + + $result = $args[0]; + foreach (array_keys($result) as $id) { + for ($i = 1; $i !== $array_count; $i++) { + if (isset($args[$i][$id])) unset($result[$id]); + } + } + return $result; +} + +/** + * Parses a search query and builds an array of search formulas * hunk ./inc/fulltext.php 439 - * @todo support OR and parenthesises? + * @author Andreas Gohr <andi@xxxxxxxxxxxxxx> + * @author Kazutaka Miyasaka <kazmiya@xxxxxxxxx> */ function ft_queryParser($query){ global $conf; hunk ./inc/fulltext.php 444 - $swfile = DOKU_INC.'inc/lang/'.$conf['lang'].'/stopwords.txt'; - if(@file_exists($swfile)){ - $stopwords = file($swfile); - }else{ - $stopwords = array(); - } + $swfile = DOKU_INC.'inc/lang/'.$conf['lang'].'/stopwords.txt'; + $stopwords = @file_exists($swfile) ? file($swfile) : array(); hunk ./inc/fulltext.php 447 - $q = array(); - $q['query'] = $query; - $q['ns'] = array(); - $q['notns'] = array(); - $q['phrases'] = array(); - $q['words'] = array(); - $q['and'] = array(); - $q['not'] = array(); + /** + * parse a search query and transform it into intermediate representation + * + * in a search query, you can use the following expressions: + * + * words: + * include + * -exclude + * phrases: + * "phrase to be included" + * -"phrase you want to exclude" + * namespaces: + * @include:namespace (or ns:include:namespace) + * ^exclude:namespace (or -ns:exclude:namespace) + * groups: + * () + * -() + * operators: + * and ('and' is the default operator: you can always omit this) + * or (lower precedence than 'and') + * + * e.g. a query [ aa "bb cc" @dd:ee ] means "search pages which contain + * a word 'aa', a phrase 'bb cc' and are within a namespace 'dd:ee'". + * this query is equivalent to [ -(-aa or -"bb cc" or -ns:dd:ee) ] + * as long as you don't mind hit counts. + * + * intermediate representation consists of the following parts: + * + * ( ) - group + * AND - logical and + * OR - logical or + * NOT - logical not + * W+: - word (needs to be highlighted) + * W-: - word (no need to highlight) + * P_: - phrase + * N_: - namespace + */ + $parsed_query = ''; + $parens_level = 0; + $terms = preg_split('/(-?".*?")/u', utf8_strtolower($query), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + + foreach ($terms as $term) { + $parsed = ''; + if (preg_match('/^(-?)"(.+)"$/u', $term, $matches)) { + // phrase-include and phrase-exclude + $not = $matches[1] ? 'NOT' : ''; + $parsed = $not.ft_termParser($matches[2], $stopwords, false, true); + } else { + // fix incomplete phrase + $term = str_replace('"', ' ', $term); + + // fix parentheses + $term = str_replace(')' , ' ) ', $term); + $term = str_replace('(' , ' ( ', $term); + $term = str_replace('- (', ' -(', $term); hunk ./inc/fulltext.php 503 - // handle phrase searches - while(preg_match('/"(.*?)"/',$query,$match)){ - $q['phrases'][] = $match[1]; - $q['and'] = array_merge($q['and'], idx_tokenizer($match[0],$stopwords)); - $query = preg_replace('/"(.*?)"/','',$query,1); + // treat ideographic spaces (U+3000) as search term separators + // FIXME: some more separators? + $term = preg_replace('/[ \x{3000}]+/u', ' ', $term); + $term = trim($term); + if ($term === '') continue; + + $tokens = explode(' ', $term); + foreach ($tokens as $token) { + if ($token === '(') { + // parenthesis-include-open + $parsed .= '('; + ++$parens_level; + } elseif ($token === '-(') { + // parenthesis-exclude-open + $parsed .= 'NOT('; + ++$parens_level; + } elseif ($token === ')') { + // parenthesis-any-close + if ($parens_level === 0) continue; + $parsed .= ')'; + $parens_level--; + } elseif ($token === 'and') { + // logical-and (do nothing) + } elseif ($token === 'or') { + // logical-or + $parsed .= 'OR'; + } elseif (preg_match('/^(?:\^|-ns:)(.+)$/u', $token, $matches)) { + // namespace-exclude + $parsed .= 'NOT(N_:'.$matches[1].')'; + } elseif (preg_match('/^(?:@|ns:)(.+)$/u', $token, $matches)) { + // namespace-include + $parsed .= '(N_:'.$matches[1].')'; + } elseif (preg_match('/^-(.+)$/', $token, $matches)) { + // word-exclude + $parsed .= 'NOT('.ft_termParser($matches[1], $stopwords).')'; + } else { + // word-include + $parsed .= ft_termParser($token, $stopwords); + } + } + } + $parsed_query .= $parsed; } hunk ./inc/fulltext.php 547 - $words = explode(' ',$query); - foreach($words as $w){ - if($w{0} == '-'){ - $token = idx_tokenizer($w,$stopwords,true); - if(count($token)) $q['not'] = array_merge($q['not'],$token); - } else if ($w{0} == '@') { // Namespace to search? - $w = substr($w,1); - $q['ns'] = array_merge($q['ns'],(array)$w); - } else if ($w{0} == '^') { // Namespace not to search? - $w = substr($w,1); - $q['notns'] = array_merge($q['notns'],(array)$w); - }else{ - // asian "words" need to be searched as phrases - if(@preg_match_all('/(('.IDX_ASIAN.')+)/u',$w,$matches)){ - $q['phrases'] = array_merge($q['phrases'],$matches[1]); + // cleanup (very sensitive) + $parsed_query .= str_repeat(')', $parens_level); + do { + $parsed_query_old = $parsed_query; + $parsed_query = preg_replace('/(NOT)?\(\)/u', '', $parsed_query); + } while ($parsed_query !== $parsed_query_old); + $parsed_query = preg_replace('/(NOT|OR)+\)/u', ')' , $parsed_query); + $parsed_query = preg_replace('/(OR)+/u' , 'OR' , $parsed_query); + $parsed_query = preg_replace('/\(OR/u' , '(' , $parsed_query); + $parsed_query = preg_replace('/^OR|OR$/u' , '' , $parsed_query); + $parsed_query = preg_replace('/\)(NOT)?\(/u' , ')AND$1(', $parsed_query); + + /** + * convert infix notation string into postfix (Reverse Polish notation) array + * by Shunting-yard algorithm + * + * see: http://en.wikipedia.org/wiki/Reverse_Polish_notation + * see: http://en.wikipedia.org/wiki/Shunting-yard_algorithm + */ + $parsed_ary = array(); + $ope_stack = array(); + $ope_precedence = array(')' => 1, 'OR' => 2, 'AND' => 3, 'NOT' => 4, '(' => 5); + $ope_regex = '/([()]|OR|AND|NOT)/u'; hunk ./inc/fulltext.php 571 + $tokens = preg_split($ope_regex, $parsed_query, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + foreach ($tokens as $token) { + if (preg_match($ope_regex, $token)) { + // operator + $last_ope = end($ope_stack); + while ($ope_precedence[$token] <= $ope_precedence[$last_ope] && $last_ope != '(') { + $parsed_ary[] = array_pop($ope_stack); + $last_ope = end($ope_stack); } hunk ./inc/fulltext.php 580 - $token = idx_tokenizer($w,$stopwords,true); - if(count($token)){ - $q['and'] = array_merge($q['and'],$token); - $q['words'] = array_merge($q['words'],$token); + if ($token == ')') { + array_pop($ope_stack); // this array_pop always deletes '(' + } else { + $ope_stack[] = $token; } hunk ./inc/fulltext.php 585 + } else { + // operand + $token_decoded = str_replace(array('OP', 'CP'), array('(', ')'), $token); + $parsed_ary[] = $token_decoded; + } + } + $parsed_ary = array_values(array_merge($parsed_ary, array_reverse($ope_stack))); + + // cleanup: each double "NOT" in RPN array actually does nothing + $parsed_ary_count = count($parsed_ary); + for ($i = 1; $i < $parsed_ary_count; ++$i) { + if ($parsed_ary[$i] === 'NOT' && $parsed_ary[$i - 1] === 'NOT') { + unset($parsed_ary[$i], $parsed_ary[$i - 1]); + } + } + $parsed_ary = array_values($parsed_ary); + + // build return value + $q = array(); + $q['query'] = $query; + $q['parsed_str'] = $parsed_query; + $q['parsed_ary'] = $parsed_ary; + + foreach ($q['parsed_ary'] as $token) { + if ($token[2] !== ':') continue; + $body = substr($token, 3); + + switch (substr($token, 0, 3)) { + case 'N_:': + $q['ns'][] = $body; // for backward compatibility + break; + case 'W-:': + $q['words'][] = $body; + break; + case 'W+:': + $q['words'][] = $body; + $q['highlight'][] = str_replace('*', '', $body); + break; + case 'P_:': + $q['phrases'][] = $body; + $q['highlight'][] = str_replace('*', '', $body); + break; } } hunk ./inc/fulltext.php 629 + foreach (array('words', 'phrases', 'highlight', 'ns') as $key) { + $q[$key] = empty($q[$key]) ? array() : array_values(array_unique($q[$key])); + } + + // keep backward compatibility (to some extent) + // this part can be deleted if no plugins use ft_queryParser() directly + $q['and'] = $q['words']; + $q['not'] = array(); // difficult to set: imagine [ aaa -(bbb -ccc) ] + $q['notns'] = array(); // same as above return $q; } hunk ./inc/fulltext.php 642 +/** + * Transforms given search term into intermediate representation + * + * This function is used in ft_queryParser() and not for general purpose use. + * + * @author Kazutaka Miyasaka <kazmiya@xxxxxxxxx> + */ +function ft_termParser($term, &$stopwords, $consider_asian = true, $phrase_mode = false) { + $parsed = ''; + if ($consider_asian) { + // successive asian characters need to be searched as a phrase + $words = preg_split('/('.IDX_ASIAN.'+)/u', $term, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + foreach ($words as $word) { + if (preg_match('/'.IDX_ASIAN.'/u', $word)) $phrase_mode = true; + $parsed .= ft_termParser($word, $stopwords, false, $phrase_mode); + } + } else { + $term_noparen = str_replace(array('(', ')'), ' ', $term); + $words = idx_tokenizer($term_noparen, $stopwords, true); + + // W+: needs to be highlighted, W-: no need to highlight + if (empty($words)) { + $parsed = '()'; // important: do not remove + } elseif ($words[0] === $term) { + $parsed = '(W+:'.$words[0].')'; + } elseif ($phrase_mode) { + $term_encoded = str_replace(array('(', ')'), array('OP', 'CP'), $term); + $parsed = '((W-:'.implode(')(W-:', $words).')(P_:'.$term_encoded.'))'; + } else { + $parsed = '((W+:'.implode(')(W+:', $words).'))'; + } + } + return $parsed; +} + //Setup VIM: ex: et ts=4 enc=utf-8 : hunk ./inc/html.php 308 //check if search is restricted to namespace if(preg_match('/@([^@]*)/',$QUERY,$match)) { $id = cleanID($match[1]); - if(empty($id)) { - print '<div class="nothing">'.$lang['nothingfound'].'</div>'; - flush(); - return; - } } else { $id = cleanID($QUERY); } hunk ./inc/html.php 323 //do quick pagesearch $data = array(); - $data = ft_pageLookup($id); + if($id) $data = ft_pageLookup($id); if(count($data)){ print '<div class="search_quickresult">'; print '<h3>'.$lang['quickhits'].':</h3>'; hunk ./inc/html.php 353 foreach($data as $id => $cnt){ print '<div class="search_result">'; print html_wikilink(':'.$id,useHeading('navigation')?NULL:$id,$regex); - print ': <span class="search_cnt">'.$cnt.' '.$lang['hits'].'</span><br />'; - if($num < 15){ // create snippets for the first number of matches only #FIXME add to conf ? - print '<div class="search_snippet">'.ft_snippet($id,$regex).'</div>'; + if($cnt !== 0){ + print ': <span class="search_cnt">'.$cnt.' '.$lang['hits'].'</span><br />'; + if($num < 15){ // create snippets for the first number of matches only #FIXME add to conf ? + print '<div class="search_snippet">'.ft_snippet($id,$regex).'</div>'; + } + $num++; } print '</div>'; flush(); hunk ./inc/html.php 362 - $num++; } }else{ print '<div class="nothing">'.$lang['nothingfound'].'</div>'; } Context: [fixed event handler attachment Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090918101358 Ignore-this: 9ec0aa658bf73175401e4282663b7f68 ] [Polish language update slawkens <slawkens@xxxxxxxxx>**20090915180536 Ignore-this: e0052320dd2335219102095f84af8551 ] [Romanian language update N3o <n30@xxxxxxxxxxxxxxxx>**20090915180324 Ignore-this: 5935ce3731aab09e699f6d94879ee4e ] [Portuguese language update André Neves <drakferion@xxxxxxxxx>**20090911132038 Ignore-this: c8b07435c4583624e414ceab874537c5 ] [Romanian language update N3o <n30@xxxxxxxxxxxxxxxx>**20090911131322 Ignore-this: f00ec0a348996f0b4157499fd29ac0c ] [Esperanto language update Robert Bogenschneider <robog@xxxxxx>**20090911131158 Ignore-this: bf0e731014fc37f2ea8e6300a282310 ] [Show toolbar for editable textarea only Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090906110042 Ignore-this: 7f9e82fb2c7e67d4b42ea6ec2d7bd7c2 ] [One click revert for managers Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090911081833 Ignore-this: e3c9b5f941b2f1aa83ca375861203a2f This patch adds another button for users with the $conf['manager'] role when viewing an old revision. It allows them to revert to this revision with a single click. ] [set memory limit and unify php call in CLI apps Elan Ruusamäe <glen@xxxxxxxx>**20090904211555 Ignore-this: 1132d10ee32a2a68ddc1929c428e708 - short open tag shouldn't be needed anymore - CLI memory limits a too low usually ] [new headline icons for the editor toolbar matthiasgrimm@xxxxxxxxxxxxxxxxxxxxx**20090904164002 The old icons weren't very clear and confused many people. This set of icons describe more clearly what the buttons will do. Furthermore the sequence of the buttons changed to put the most used bottons in front. ] [Handle relative redirects correctly FS#1741 Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090904152257 Ignore-this: a85fdaa1c3aae0315a5f2a51ccbde5a0 Some servers (or scripts) do not send full qualified URLs in the Location header on redirects. ] [fixed backlink button (missing break) Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090904100956 Ignore-this: f5092496dd6b976f4fc1573cc1dc5053 ] [gracefully handle missing groups in auth:ad Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090902181259 Ignore-this: 98bfcf5fc6f786038562b0abbccbc6a2 ] [TAG develsnap 2009-09-01 Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090831230002] [do not prepend colon via javascript in media manager Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090830130823 Ignore-this: 67ae7589474461a1279720865f0d18b9 Just moves the prepending of the colon (for absolute namespaces) from the javascript to the PHP backend code - makes reusing the javascript for non local files easier. ] [added MEDIAMANAGER_CONTENT_OUTPUT event Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090830111438 Ignore-this: 1742cf72bee0a1ac1898109ba5afc962 ] [Added support for multipart/form-data in HTTPClient Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090830101808 Ignore-this: ce1342ac66bd276efc7791ff69a025a3 ] [replaced two search_* funcs with calls to search_universal Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090828080240 Ignore-this: c22ff5dcffaf279b6c4397893d5e82af ] [added class for headline picker Andreas Gohr <gohr@xxxxxxxxxxxx>**20090827142929 Ignore-this: 6aee01f1e872490512480ff8cac566be ] [removed obsolete internal link toolbar button Andreas Gohr <gohr@xxxxxxxxxxxx>**20090827124504 Ignore-this: 62264fd057c80fb8fa70f53481f2875b ] [language string change Andreas Gohr <gohr@xxxxxxxxxxxx>**20090827115449 Ignore-this: 8442785eb2ef884001e9f70e361b5415 ] [select sample in tb_formatln() Andreas Gohr <gohr@xxxxxxxxxxxx>**20090827115144 Ignore-this: 85ca1838ed81f69512d07d8504f6673f Note: development is part of ICKE 2.0 project http://www.icke-projekt.de ] [Fixed IE compatibility for recent JavaScript changes Andreas Gohr <gohr@xxxxxxxxxxxx>**20090827113438 Ignore-this: 62d43ad8ce4d6c506839a0da4a8ec40 Note: development is part of ICKE 2.0 project http://www.icke-projekt.de ] [More Link wizard cleanup Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090814114056 Ignore-this: 100b66fbe26d82dfd6cffba751cf6992 ] [fix scrolling on keyboard select in LinkWizard Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090814105344 Ignore-this: 831a3252b5cb7c3f8658c377f60c0a95 ] [added missing images Andreas Gohr <gohr@xxxxxxxxxxxx>**20090814100605 Ignore-this: aad736c38ba3e5070502a73843c8b64d ] [small JS fix Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090814092553 Ignore-this: 42bc05343dabfa0b7cb7b14b9ba61834 ] [simplify JavaScript loading Andreas Gohr <gohr@xxxxxxxxxxxx>**20090812194007 Ignore-this: 7637977e042ed8ba7e9e9097f9e9f03f This patch removes the differences between the JavaScript loaded in edit and view modes. * increases the amount of JavaScript that is loaded initially * decreases the number of requests * only one cache for all javascript * all javascript is available in view mode The last point is the most important as it makes a lot of functionality available to plugins working in the view mode. The discussion plugin now can reuse the toolbar code for example. Note: development is part of ICKE 2.0 project http://www.icke-projekt.de ] [Language file cleanups for JS changes Andreas Gohr <gohr@xxxxxxxxxxxx>**20090812191138 Ignore-this: 7c8f68f29f52bc1d33fdb76ba98d2307 Removed unused string, move another string to the [js] subarray. ] [make dragged objects stylable via CSS Andreas Gohr <gohr@xxxxxxxxxxxx>**20090812180344 Ignore-this: ae47b532b80d10868e82e0ccc5c963d1 A DOM object that is dragged through the new drag Object gets the ondrag assigned. note: development was part of the ICKE 2.0 project see http://www.icke-projekt.de for info ] [Link Wizard added Andreas Gohr <gohr@xxxxxxxxxxxx>**20090812102302 Ignore-this: c15561aa909f921f7845576378851b93 This adds a new link wizard to the toolbar which helps users to find the page the want to link to. Pages can be found by a simple page name search or by browsing the existing namespaces. This is the first checkin. Some cleanup and MSIE compatibility checks remain. note: development was part of the ICKE 2.0 project see http://www.icke-projekt.de for info ] [Script lib for draggable DOM objects Andreas Gohr <gohr@xxxxxxxxxxxx>**20090812102055 Ignore-this: 907af01f2757cc494d2c54d8e4d7b9d1 This adds a simple object that can be attached to positioned DOM objects to make them draggable. This is useful for inplace dialogs. note: development was part of the ICKE 2.0 project see http://www.icke-projekt.de for info ] [universal callback for search() Andreas Gohr <gohr@xxxxxxxxxxxx>**20090812101653 Ignore-this: 4d786345ea9bfb19fb6f8af9348f5248 This patch adds a callback for use with the search() function, that is flexible enough to replace many of the other specialized callbacks and opens up more possibilties to plugin authors without the need to write new callbacks. Existing callbacks need to be reexamined and rewritten to wrap around this callback instead. note: development was part of the ICKE 2.0 project see http://www.icke-projekt.de for info ] [some cleanup in the IE selection handling Andreas Gohr <gohr@xxxxxxxxxxxx>**20090806093833 Ignore-this: 5a6b527fbf3f2ffc79e3ceef11552763 ] [fixes another IE weirdnes when handling list items Andreas Gohr <gohr@xxxxxxxxxxxx>**20090805123523 Ignore-this: d5a0f9671af3607796332a1afcc8de79 Fixes a problem where list mode couldn't easily be left by removing a bullet. Hitting enter would readd a bullet always. note: development was part of the ICKE 2.0 project see http://www.icke-projekt.de for info ] [Some text selection workarounds for MSIE Andreas Gohr <gohr@xxxxxxxxxxxx>**20090804150501 Ignore-this: b4a14bbf96712ec9ce9011e172f2af81 This patch solves some problems with reading the cursor positions and text selection on MSIE. note: development was part of the ICKE 2.0 project see http://www.icke-projekt.de for info ] [improved list handling Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090804095707 Ignore-this: 2e4f3fbfb28917ee66cf3e1925c806d3 This patch adds multiple enhancements to handling lists and indented code blocks in the editor. 1. Pressing enter when in a list item or code block will keep the indention and adds a new list point 2. Pressing space at the start of a list item will indent the item to the next level 3. Pressing bckspace at the start of a list item will outdent the item to the previous level or delete the list bullet when you are at the 1st level already 4. A new type of formatting button called formatln is added. It applies formatting to several lines. It's used for the list buttons currently and makes it possible to convert mutiple lines to a list This enhncement are currently only tested in Firefox are most likely to break IE compatibility. A compatibility patch will be submitted later note: development was part of the ICKE 2.0 project see http://www.icke-projekt.de for info ] [reshow $QUERY in search form instead of super global Andreas Gohr <gohr@xxxxxxxxxxxx>**20090826113901] [allow disabling of nosmblink warning via JavaScript Andreas Gohr <gohr@xxxxxxxxxxxx>**20090825143507 Just add LANG['nosmblinks'] = ''; to conf/userscript.js ] [Javascript variable explicitly declared Samuele Tognini <samuele@xxxxxxxxxxxxxxxxxxx>**20090818154140 Ignore-this: 6cd522b1fa6d768ac7735c30fac3e1df ] [French language update Erik Pedersen <erik.pedersen@xxxxxxx>**20090811185014 Ignore-this: 3c16a0a7483f35e9026c7e292c926453 ] [Esperanto language update Erik Pedersen <erik.pedersen@xxxxxxx>**20090811184929 Ignore-this: 1454880f75d932f8f2c11c5ec3e1895f ] [Norwegian language update Erik Pedersen <erik.pedersen@xxxxxxx>**20090811184843 Ignore-this: 9de87e69b85c8fbc67c79d53c5e7592b ] [Persian language update Mohammad Reza Shoaei <shoaei@xxxxxxxxx>**20090809195453 Ignore-this: de3bd855c542ea27ecd54eee64e5c873 ] [Show media namespaces in ACL manager Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090807094607 Ignore-this: b46799f7d65081eaa364ecaab8a2c7f9 Namespaces that only exist within the media directory are now merged with the page namespaces in tree explorer of the namespace manager ] [do not rerender metadata for pages without abstract forever FS#1701 Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090802132535 Ignore-this: 8ee7f4c5e4ef4f3e66f959dc9bb0c235 ] [fixed too strict trim (again) and missing class on code by indenting Anika Henke <anika@xxxxxxxxxxxxxxx>**20090802120528] [Use the server port in DOKU_COOKIE when securecookie is defined FS#1664 Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090801222159 Ignore-this: de9ef30fc53fbfc1caa74b55f97290a5 This should avoid problems on portbased virtual hosts. This patch might log you out ;-) ] [Spanish language update Marvin Ortega <maty1206@xxxxxxxxxxxxxxx>**20090801221608 Ignore-this: d40854cfc017fc8ac1a5da4437c32360 ] [Esperanto language update Erik Pedersen <erik.pedersen@xxxxxxx>**20090801221500 Ignore-this: bfb0044138af78c5d3167be96474117a ] [Italian language update robocap <robocap1@xxxxxxxxx>**20090731115716 Ignore-this: 537c0d5ff5488726782a8bda21f6e48d ] [TAG develsnap 2009-08-01 Andreas Gohr <andi@xxxxxxxxxxxxxx>**20090731230001] Patch bundle hash: e80a22af05300ff1b46dc6c6a46d0707c01005f6