[dokuwiki] Enhanced full-text search function

  • From: Kazutaka Miyasaka <kazmiya@xxxxxxxxx>
  • To: dokuwiki@xxxxxxxxxxxxx
  • Date: Sun, 20 Sep 2009 23:09:28 +0900

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

Other related posts: