Minor fixes for XHTML compliance
[pfb-moodle.git] / admin / lang.php
blobf74fbad33feb549479a8d893864efd521baef6fd
1 <?PHP // $Id$
2 /**
3 * Display the admin/language menu and process strings translation.
4 */
6 require_once('../config.php');
7 require_once($CFG->libdir.'/adminlib.php');
8 $adminroot = admin_get_root();
9 admin_externalpage_setup('langedit', $adminroot);
11 define('LANG_SUBMIT_REPEAT', 1); // repeat displaying submit button?
12 define('LANG_SUBMIT_REPEAT_EVERY', 20); // if so, after how many lines?
13 define('LANG_DISPLAY_MISSING_LINKS', 1); // display "go to first/next missing string" links?
14 define('LANG_DEFAULT_FILE', ''); // default file to translate. Empty allowed
15 define('LANG_LINK_MISSING_STRINGS', 1); // create links from "missing" page to "compare" page?
16 define('LANG_DEFAULT_USELOCAL', 0); // should *_utf8_local be used by default?
17 define('LANG_MISSING_TEXT_MAX_LEN', 60); // maximum length of the missing text to display
18 define('LANG_KEEP_ORPHANS', 1); // keep orphaned strings (i.e. strings w/o English reference)
20 $mode = optional_param('mode', '', PARAM_ALPHA);
21 $currentfile = optional_param('currentfile', LANG_DEFAULT_FILE, PARAM_FILE);
22 $uselocal = optional_param('uselocal', -1, PARAM_INT);
24 if ($uselocal == -1) {
25 if (isset($SESSION->langtranslateintolocal)) {
26 $uselocal = $SESSION->langtranslateintolocal;
27 } else {
28 $uselocal = LANG_DEFAULT_USELOCAL;
30 } else {
31 $SESSION->langtranslateintolocal = $uselocal;
34 $strlanguage = get_string("language");
35 $strcurrentlanguage = get_string("currentlanguage");
36 $strmissingstrings = get_string("missingstrings");
37 $streditstrings = get_string("editstrings", 'admin');
38 $stredithelpdocs = get_string("edithelpdocs", 'admin');
39 $strthislanguage = get_string("thislanguage");
40 $strgotofirst = get_string('gotofirst','admin');
41 $strfilestoredin = get_string('filestoredin', 'admin');
42 $strfilestoredinhelp = get_string('filestoredinhelp', 'admin');
43 $strswitchlang = get_string('switchlang', 'admin');
44 $strchoosefiletoedit = get_string('choosefiletoedit', 'admin');
45 $streditennotallowed = get_string('langnoeditenglish', 'admin');
46 $strfilecreated = get_string('filecreated', 'admin');
47 $strprev = get_string('previous');
48 $strnext = get_string('next');
51 $currentlang = current_language();
53 switch ($mode) {
54 case "missing":
55 // Missing array keys are not bugs here but missing strings
56 error_reporting(E_ALL ^ E_NOTICE);
57 $navigation = "<a href=\"lang.php\">$strlanguage</a> -> $strmissingstrings";
58 $title = $strmissingstrings;
59 $button = '<form '.$CFG->frametarget.' method="get" action="'.$CFG->wwwroot.'/'.$CFG->admin.'/lang.php">'.
60 '<div>'.
61 '<input type="hidden" name="mode" value="compare" />'.
62 '<input type="submit" value="'.$streditstrings.'" /></div></form>';
63 break;
64 case "compare":
65 $navigation = "<a href=\"lang.php\">$strlanguage</a> -> $streditstrings";
66 $title = $streditstrings;
67 $button = '<form '.$CFG->frametarget.' method="get" action="'.$CFG->wwwroot.'/'.$CFG->admin.'/lang.php">'.
68 '<div>'.
69 '<input type="hidden" name="mode" value="missing" />'.
70 '<input type="submit" value="'.$strmissingstrings.'" /></div></form>';
71 break;
72 default:
73 $title = $strlanguage;
74 $navigation = $strlanguage;
75 $button = '';
76 break;
80 admin_externalpage_print_header($adminroot);
82 if (!$mode) {
83 print_box_start();
84 $currlang = current_language();
85 $langs = get_list_of_languages();
86 popup_form ("$CFG->wwwroot/$CFG->admin/lang.php?lang=", $langs, "chooselang", $currlang, "", "", "", false, 'self', $strcurrentlanguage.':');
87 print_heading("<a href=\"lang.php?mode=missing\">$strmissingstrings</a>");
88 print_heading("<a href=\"lang.php?mode=compare\">$streditstrings</a>");
89 print_heading("<a href=\"langdoc.php\">$stredithelpdocs</a>");
90 print_box_end();
91 admin_externalpage_print_footer($adminroot);
92 exit;
95 // Get a list of all the root files in the English directory
97 $langbase = $CFG->dataroot . '/lang';
98 $enlangdir = "$CFG->dirroot/lang/en_utf8";
99 if ($currentlang == 'en_utf8') {
100 $langdir = $enlangdir;
101 } else {
102 $langdir = "$langbase/$currentlang";
104 $locallangdir = "$langbase/{$currentlang}_local";
106 if (! $stringfiles = get_directory_list($enlangdir, "", false)) {
107 error("Could not find English language pack!");
110 foreach ($stringfiles as $key => $file) {
111 if (substr($file, -4) != ".php") { //Avoid non php files to be showed
112 unset($stringfiles[$key]);
114 if ($file == "langconfig.php") { //Avoid langconfig.php to be showed
115 unset($stringfiles[$key]);
119 if ($mode == "missing") {
120 if (!file_exists($langdir)) {
121 error ('to edit this language pack, you need to put it in '.$CFG->dataroot.'/lang');
124 // Following variables store the HTML output to be echo-ed
125 $m = '';
126 $o = '';
128 // For each file, check that a counterpart exists, then check all the strings
129 foreach ($stringfiles as $file) {
130 unset($string);
131 include("$enlangdir/$file");
132 $enstring = $string;
134 ksort($enstring);
136 unset($string);
138 if (file_exists("$langdir/$file")) {
139 include("$langdir/$file");
140 $fileismissing = 0;
141 } else {
142 $fileismissing = 1;
143 // notify(get_string("filemissing", "", "$langdir/$file"));
144 $o .= '<div class="notifyproblem">'.get_string("filemissing", "", "$langdir/$file").'</div><br />';
145 $string = array();
148 $missingcounter = 0;
150 $first = true;
151 foreach ($enstring as $key => $value) {
152 if (empty($string[$key]) and $string[$key] != "0") { //bug fix 4735 mits
153 $value = htmlspecialchars($value);
154 $value = str_replace("$"."a", "\\$"."a", $value);
155 $value = str_replace("%%","%",$value);
156 if ($first) {
157 $m .= "<a href=\"lang.php?mode=missing#$file\">$file";
158 $m .= $fileismissing ? '*' : '';
159 $m .= '</a> &nbsp; ';
160 $o .= "<p><a name=\"$file\"></a><b>".get_string("stringsnotset","","$langdir/$file")."</b></p><pre>";
161 $first = false;
162 $somethingfound = true;
164 $missingcounter++;
165 if (LANG_LINK_MISSING_STRINGS) {
166 $missinglinkstart = "<a href=\"lang.php?mode=compare&amp;currentfile=$file#missing$missingcounter\">";
167 $missinglinkend = '</a>';
168 } else {
169 $missinglinkstart = '';
170 $missinglinkend = '';
172 if (strlen($value) > LANG_MISSING_TEXT_MAX_LEN) {
173 $value = lang_xhtml_save_substr($value, 0, LANG_MISSING_TEXT_MAX_LEN) . ' ...'; // MDL-8852
175 $o .= "$"."string['".$missinglinkstart.$key.$missinglinkend."'] = \"$value\";<br />";
178 if (!$first) {
179 $o .= '</pre><hr />';
183 if ($m <> '') {
184 print_box($m, 'filenames');
186 echo $o;
188 if (! $files = get_directory_list("$CFG->dirroot/lang/en_utf8/help", "CVS")) {
189 error("Could not find English language help files!");
192 foreach ($files as $filekey => $file) { // check all the help files.
193 if (!file_exists("$langdir/help/$file")) {
194 echo "<p><font color=\"red\">".get_string("filemissing", "", "$langdir/help/$file")."</font></p>";
195 $somethingfound = true;
196 continue;
200 if (! $files = get_directory_list("$CFG->dirroot/lang/en_utf8/docs", "CVS")) {
201 error("Could not find English language docs files!");
203 foreach ($files as $filekey => $file) { // check all the docs files.
204 if (!file_exists("$langdir/docs/$file")) {
205 echo "<p><font color=\"red\">".get_string("filemissing", "", "$langdir/docs/$file")."</font></p>";
206 $somethingfound = true;
207 continue;
211 if (!empty($somethingfound)) {
212 print_continue("lang.php");
213 } else {
214 notice(get_string("languagegood"), "lang.php", '', $adminroot);
217 } else if ($mode == "compare") {
219 if (!file_exists($langbase) ){
220 if (!lang_make_directory($langbase) ){
221 error('ERROR: Could not create base lang directory ' . $langbase);
222 } else {
223 echo '<div class="notifysuccess">Created directory '.
224 $langbase .'</div>'."<br />\n";
227 if (!$uselocal && !file_exists($langdir)) {
228 if (!lang_make_directory($langdir)) {
229 error('ERROR: Could not create directory '.$langdir);
230 } else {
231 echo '<div class="notifysuccess">Created directory '.
232 $langdir .'</div>'."<br />\n";
235 if ($uselocal && !file_exists($locallangdir)) {
236 if (!lang_make_directory($locallangdir)) {
237 echo '<div class="notifyproblem">ERROR: Could not create directory '.
238 $locallangdir .'</div>'."<br />\n";
239 $uselocal = 0;
240 } else {
241 echo '<div class="notifysuccess">Created directory '.
242 $locallangdir .'</div>'."<br />\n";
246 if (isset($_POST['currentfile'])){ // Save a file
247 if (!confirm_sesskey()) {
248 error(get_string('confirmsesskeybad', 'error'));
251 $newstrings = array();
253 foreach ($_POST as $postkey => $postval) {
254 $stringkey = lang_file_string_key($postkey);
255 $newstrings[$stringkey] = $postval;
258 unset($newstrings['currentfile']);
260 if ($uselocal) {
261 include("$langdir/$currentfile");
262 if (isset($string)) {
263 $packstring = $string;
264 } else {
265 $packstring = array();
267 unset($string);
268 $saveinto = $locallangdir;
269 } else {
270 $packstring = array();
271 $saveinto = $langdir;
274 if (lang_save_file($saveinto, $currentfile, $newstrings, $uselocal, $packstring)) {
275 notify(get_string("changessaved")." ($saveinto/$currentfile)", "green");
276 } else {
277 error("Could not save the file '$saveinto/$currentfile'!", "lang.php?mode=compare&amp;currentfile=$currentfile");
279 unset($packstring);
282 print_heading_with_help($streditstrings, "langedit");
284 print_box_start('generalbox editstrings');
285 foreach ($stringfiles as $file) {
286 if ($file == $currentfile) {
287 echo "<b>$file</b> &nbsp; ";
288 } else {
289 echo "<a href=\"lang.php?mode=compare&amp;currentfile=$file\">$file</a> &nbsp; ";
292 print_box_end();
294 print_heading("<a href=\"lang.php?mode=missing\">$strmissingstrings</a>", "center", 4); // one-click way back
296 print_box_start();
297 echo $strfilestoredin;
298 echo $uselocal ? "{$currentlang}_local" : $currentlang;
299 helpbutton('langswitchstorage', $strfilestoredinhelp, 'moodle');
301 echo '<form '.$CFG->frametarget.' method="get" action="'.$CFG->wwwroot.'/'.$CFG->admin.'/lang.php">'.
302 '<div>'.
303 '<input type="hidden" name="mode" value="compare" />'.
304 '<input type="hidden" name="currentfile" value="'.$currentfile.'" />'.
305 '<input type="hidden" name="uselocal" value="'.(1 - $uselocal % 2).'" />'.
306 '<input type="submit" value="'.$strswitchlang.'" />'.
307 '</div></form>';
308 print_box_end();
310 if ($currentfile <> '') {
311 $saveto = $uselocal ? $locallangdir : $langdir;
312 error_reporting(0);
313 if (!file_exists("$saveto/$currentfile")) {
314 if (!@touch("$saveto/$currentfile")) {
315 print_heading(get_string("filemissing", "", "$saveto/$currentfile"), '', 4, 'error');
316 } else {
317 print_heading($strfilecreated, '', 4, 'notifysuccess');
320 if ($currentlang == "en_utf8" && !$uselocal) {
321 $editable = false;
322 print_heading($streditennotallowed, '', 4);
323 } elseif ($f = fopen("$saveto/$currentfile","r+")) {
324 $editable = true;
325 fclose($f);
326 } else {
327 $editable = false;
328 echo "<p><font size=\"1\">".get_string("makeeditable", "", "$saveto/$currentfile")."</font></p>";
330 error_reporting($CFG->debug);
332 print_heading("$currentfile", "", 4);
333 if (LANG_DISPLAY_MISSING_LINKS && $editable) {
334 print_heading('<a href="#missing1">'.$strgotofirst.'</a>', "", 4);
337 unset($string);
338 include("$enlangdir/$currentfile");
339 $enstring = $string;
340 if ($currentlang != 'en' and $currentfile == 'moodle.php') {
341 $enstring['thislanguage'] = "<< TRANSLATORS: Specify the name of your language here. If possible use Unicode Numeric Character References >>";
342 $enstring['thischarset'] = "<< TRANSLATORS: Charset encoding - always use utf-8 >>";
343 $enstring['thisdirection'] = "<< TRANSLATORS: This string specifies the direction of your text, either left-to-right or right-to-left. Insert either 'ltr' or 'rtl' here. >>";
344 $enstring['parentlanguage'] = "<< TRANSLATORS: If your language has a Parent Language that Moodle should use when strings are missing from your language pack, then specify the code for it here. If you leave this blank then English will be used. Example: nl >>";
346 ksort($enstring);
348 unset($string);
350 @include("$locallangdir/$currentfile");
351 $localstring = isset($string) ? $string : array();
352 unset($string);
354 @include("$langdir/$currentfile");
356 if ($editable) {
357 echo "<form id=\"$currentfile\" action=\"lang.php\" method=\"post\">";
358 echo '<div>';
360 echo "<table summary=\"\" width=\"100%\" class=\"translator\">";
361 $linescounter = 0;
362 $missingcounter = 0;
363 foreach ($enstring as $key => $envalue) {
364 $linescounter++ ;
365 if (LANG_SUBMIT_REPEAT && $editable && $linescounter % LANG_SUBMIT_REPEAT_EVERY == 0) {
366 echo '<tr><td>&nbsp;</td><td><br />';
367 echo ' <input type="submit" name="update" value="'.get_string('savechanges').': '.$currentfile.'" />';
368 echo '<br />&nbsp;</td></tr>';
370 $envalue = nl2br(htmlspecialchars($envalue));
371 $envalue = preg_replace('/(\$a\-\&gt;[a-zA-Z0-9]*|\$a)/', '<b>$0</b>', $envalue); // Make variables bold.
372 $envalue = str_replace("%%","%",$envalue);
373 $envalue = str_replace("\\","",$envalue); // Delete all slashes
375 echo "\n\n".'<tr class="';
376 if ($linescounter % 2 == 0) {
377 echo 'r0';
378 } else {
379 echo 'r1';
381 echo '">';
382 echo '<td dir="ltr" lang="en">';
383 echo '<span class="stren">'.$envalue.'</span>';
384 echo '<br />'."\n";
385 echo '<span class="strkey">'.$key.'</span>';
386 echo '</td>'."\n";
388 // Missing array keys are not bugs here but missing strings
389 error_reporting(E_ALL ^ E_NOTICE);
390 if ($uselocal) {
391 $value = lang_fix_value_from_file($localstring[$key]);
392 $value2 = lang_fix_value_from_file($string[$key]);
393 if ($value == '') {
394 $value = $value2;
396 } else {
397 $value = lang_fix_value_from_file($string[$key]);
398 $value2 = lang_fix_value_from_file($localstring[$key]);
400 error_reporting($CFG->debug);
402 // Color highlighting:
403 // red #ef6868 - translation missing in both system and local pack
404 // yellow #feff7f - translation missing in system pack but is translated in local
405 // green #AAFFAA - translation present in both system and local but is different
406 if (!$value) {
407 if (!$value2) {
408 $cellcolour = 'class="bothmissing"';
409 } else {
410 $cellcolour = 'class="mastermissing"';
412 $missingcounter++;
413 if (LANG_DISPLAY_MISSING_LINKS) {
414 $missingtarget = '<a name="missing'.$missingcounter.'"></a>';
415 $missingnext = '<a href="#missing'.($missingcounter+1).'">'.
416 '<img src="' . $CFG->pixpath . '/t/down.gif" class="iconsmall" alt="'.$strnext.'" /></a>';
417 $missingprev = '<a href="#missing'.($missingcounter-1).'">'.
418 '<img src="' . $CFG->pixpath . '/t/up.gif" class="iconsmall" alt="'.$strprev.'" /></a>';
419 } else {
420 $missingtarget = '';
421 $missingnext = '';
422 $missingprev = '';
424 } else {
425 if ($value <> $value2 && $value2 <> '') {
426 $cellcolour = 'class="localdifferent"';
427 } else {
428 $cellcolour = '';
430 $missingtarget = '';
431 $missingnext = '';
432 $missingprev = '';
435 if ($editable) {
436 echo '<td '.$cellcolour.' valign="top">'. $missingprev . $missingtarget."\n";
437 if (isset($string[$key])) {
438 $valuelen = strlen($value);
439 } else {
440 $valuelen = strlen($envalue);
442 $cols=40;
443 if (strstr($value, "\r") or strstr($value, "\n") or $valuelen > $cols) {
444 $rows = ceil($valuelen / $cols);
445 echo '<textarea name="stringXXX'.lang_form_string_key($key).'" cols="'.$cols.'" rows="'.$rows.'">'.$value.'</textarea>'."\n";
446 } else {
447 if ($valuelen) {
448 $cols = $valuelen + 5;
450 echo '<input type="text" name="stringXXX'.lang_form_string_key($key).'" value="'.$value.'" size="'.$cols.'" />';
452 if ($value2 <> '' && $value <> $value2) {
453 echo '<br /><span style="font-size:small">'.$value2.'</span>';
455 echo $missingnext . '</td>';
457 } else {
458 echo '<td bgcolor="'.$cellcolour.'" valign="top">'.$value.'</td>';
460 echo '</tr>'."\n";
462 if ($editable) {
463 echo '<tr><td>&nbsp;</td><td><br />';
464 echo '<input type="hidden" name="sesskey" value="'.$USER->sesskey.'" />';
465 echo ' <input type="hidden" name="currentfile" value="'.$currentfile.'" />';
466 echo ' <input type="hidden" name="mode" value="compare" />';
467 echo ' <input type="submit" name="update" value="'.get_string('savechanges').': '.$currentfile.'" />';
468 echo '</td></tr>';
470 echo '</table>';
471 echo '</div>';
472 echo '</form>';
474 } else {
475 // no $currentfile specified
476 print_heading($strchoosefiletoedit, "", 4);
480 admin_externalpage_print_footer($adminroot);
482 //////////////////////////////////////////////////////////////////////
485 * Save language translation file.
487 * Thanks to Petri Asikainen for the original version of code
488 * used to save language files.
490 * @uses $CFG
491 * @uses $USER
492 * @param string $path Full pathname to the directory to use
493 * @param string $file File to overwrite
494 * @param array $strings Array of strings to write
495 * @param bool $local Should *_local version be saved?
496 * @param array $packstrings Array of default langpack strings (needed if $local)
497 * @return bool Created successfully?
499 function lang_save_file($path, $file, $strings, $local, $packstrings) {
500 global $CFG, $USER;
501 if (LANG_KEEP_ORPHANS) {
502 // let us load the current content of the file
503 unset($string);
504 @include("$path/$file");
505 if (isset($string)) {
506 $orphans = $string;
507 unset($string);
508 } else {
509 $orphans = array();
512 // let us rewrite the file
513 if (!$f = @fopen("$path/$file","w")) {
514 return false;
517 fwrite($f, "<?PHP // \$Id\$ \n");
518 fwrite($f, " // $file - created with Moodle $CFG->release ($CFG->version)\n");
519 if ($local) {
520 fwrite($f, " // local modifications from $CFG->wwwroot\n");
522 fwrite($f, "\n\n");
523 ksort($strings);
524 foreach ($strings as $key => $value) {
525 @list($id, $stringname) = explode('XXX',$key);
526 $value = lang_fix_value_before_save($value);
527 if ($id == "string" and $value != ""){
528 if ((!$local) || (lang_fix_value_from_file($packstrings[$stringname]) <> lang_fix_value_from_file($value))) {
529 fwrite($f,"\$string['$stringname'] = '$value';\n");
530 if (LANG_KEEP_ORPHANS && isset($orphans[$stringname])) {
531 unset($orphans[$stringname]);
536 if (LANG_KEEP_ORPHANS) {
537 // let us add orphaned strings, i.e. already translated strings without the English referential source
538 foreach ($orphans as $key => $value) {
539 fwrite($f,"\$string['$key'] = '".lang_fix_value_before_save($value)."'; // ORPHANED\n");
542 fwrite($f,"\n?>\n");
543 fclose($f);
544 return true;
548 * Fix value of the translated string after it is load from the file.
550 * These modifications are typically necessary to work with the same string coming from two sources.
551 * We need to compare the content of these sources and we want to have e.g. "This string\r\n"
552 * to be the same as " This string\n".
554 * @param string $value Original string from the file
555 * @return string Fixed value
557 function lang_fix_value_from_file($value='') {
558 $value = str_replace("\r","",$value); // Bad character caused by Windows
559 $value = preg_replace("/\n{3,}/", "\n\n", $value); // Collapse runs of blank lines
560 $value = trim($value); // Delete leading/trailing white space
561 $value = str_replace("\\","",$value); // Delete all slashes
562 $value = str_replace("%%","%",$value);
563 $value = str_replace("<","&lt;",$value);
564 $value = str_replace(">","&gt;",$value);
565 $value = str_replace('"',"&quot;",$value);
566 return $value;
570 * Fix value of the translated string before it is saved into the file
572 * @uses $CFG
573 * @param string $value Raw string to be saved into the lang pack
574 * @return string Fixed value
576 function lang_fix_value_before_save($value='') {
577 global $CFG;
578 if ($CFG->lang != "zh_hk" and $CFG->lang != "zh_tw") { // Some MB languages include backslash bytes
579 $value = str_replace("\\","",$value); // Delete all slashes
581 $value = str_replace("'", "\\'", $value); // Add slashes for '
582 $value = str_replace('"', "\\\"", $value); // Add slashes for "
583 $value = str_replace("%","%%",$value); // Escape % characters
584 $value = str_replace("\r", "",$value); // Remove linefeed characters
585 $value = trim($value); // Delete leading/trailing white space
586 return $value;
590 * Try and create a new language directory.
592 * @uses $CFG
593 * @param string $directory full path to the directory under $langbase
594 * @return string|false Returns full path to directory if successful, false if not
596 function lang_make_directory($dir, $shownotices=true) {
597 global $CFG;
598 umask(0000);
599 if (! file_exists($dir)) {
600 if (! @mkdir($dir, $CFG->directorypermissions)) {
601 return false;
603 //@chmod($dir, $CFG->directorypermissions); // Just in case mkdir didn't do it
605 return $dir;
609 * Return the string key name for use in HTML form.
611 * Required because '.' in form input names get replaced by '_' by PHP.
613 * @param string $keyfromfile The key name containing '.'
614 * @return string The key name without '.'
616 function lang_form_string_key($keyfromfile) {
617 return str_replace('.', '##46#', $keyfromfile); /// Derived from &#46, the ascii value for a period.
621 * Return the string key name for use in file.
623 * Required because '.' in form input names get replaced by '_' by PHP.
625 * @param string $keyfromfile The key name without '.'
626 * @return string The key name containing '.'
628 function lang_file_string_key($keyfromform) {
629 return str_replace('##46#', '.', $keyfromform);
633 * Return the substring of the string and take care of XHTML compliance.
635 * There was a problem with pure substr() which could possibly produce XHTML parsing error:
636 * substr('Marks &amp; Spencer', 0, 9) -> 'Marks &am' ... is not XHTML compliance
637 * This function takes care of these cases. Fixes MDL-8852.
639 * Thanks to kovacsendre, the author of the function at http://php.net/substr
641 * @param string $str The original string
642 * @param int $start Start position in the $value string
643 * @param int $length Optional length of the returned substring
644 * @return string The substring as returned by substr() with XHTML compliance
645 * @todo Seems the function does not work with negative $start together with $length being set
647 function lang_xhtml_save_substr($str, $start, $length = NULL) {
648 if ($length === 0) {
649 //stop wasting our time ;)
650 return "";
653 //check if we can simply use the built-in functions
654 if (strpos($str, '&') === false) {
655 // No entities. Use built-in functions
656 if ($length === NULL) {
657 return substr($str, $start);
658 } else {
659 return substr($str, $start, $length);
663 // create our array of characters and html entities
664 $chars = preg_split('/(&[^;\s]+;)|/', $str, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE);
665 $html_length = count($chars);
667 // check if we can predict the return value and save some processing time, i.e.:
668 // input string was empty OR
669 // $start is longer than the input string OR
670 // all characters would be omitted
671 if (($html_length === 0) or ($start >= $html_length) or (isset($length) and ($length <= -$html_length))) {
672 return '';
675 //calculate start position
676 if ($start >= 0) {
677 $real_start = $chars[$start][1];
678 } else {
679 //start'th character from the end of string
680 $start = max($start,-$html_length);
681 $real_start = $chars[$html_length+$start][1];
684 if (!isset($length)) {
685 // no $length argument passed, return all remaining characters
686 return substr($str, $real_start);
687 } elseif ($length > 0) {
688 // copy $length chars
689 if ($start+$length >= $html_length) {
690 // return all remaining characters
691 return substr($str, $real_start);
692 } else {
693 //return $length characters
694 return substr($str, $real_start, $chars[max($start,0)+$length][1] - $real_start);
696 } else {
697 //negative $length. Omit $length characters from end
698 return substr($str, $real_start, $chars[$html_length+$length][1] - $real_start);