public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $question = id(new PonderQuestionQuery())->setViewer($user)->withIDs(array($this->id))->requireCapabilities(array(PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT))->executeOne(); if (!$question) { return new Aphront404Response(); } $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs($question->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } else { $question = id(new PonderQuestion())->setStatus(PonderQuestionStatus::STATUS_OPEN)->setAuthorPHID($user->getPHID())->setVoteCount(0)->setAnswerCount(0)->setHeat(0.0); $v_projects = array(); } $v_title = $question->getTitle(); $v_content = $question->getContent(); $errors = array(); $e_title = true; if ($request->isFormPost()) { $v_title = $request->getStr('title'); $v_content = $request->getStr('content'); $v_projects = $request->getArr('projects'); $len = phutil_utf8_strlen($v_title); if ($len < 1) { $errors[] = pht('Title must not be empty.'); $e_title = pht('Required'); } else { if ($len > 255) { $errors[] = pht('Title is too long.'); $e_title = pht('Too Long'); } } if (!$errors) { $template = id(new PonderQuestionTransaction()); $xactions = array(); $xactions[] = id(clone $template)->setTransactionType(PonderQuestionTransaction::TYPE_TITLE)->setNewValue($v_title); $xactions[] = id(clone $template)->setTransactionType(PonderQuestionTransaction::TYPE_CONTENT)->setNewValue($v_content); $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PonderQuestionTransaction())->setTransactionType(PhabricatorTransactions::TYPE_EDGE)->setMetadataValue('edge:type', $proj_edge_type)->setNewValue(array('=' => array_fuse($v_projects))); $editor = id(new PonderQuestionEditor())->setActor($user)->setContentSourceFromRequest($request)->setContinueOnNoEffect(true); $editor->applyTransactions($question, $xactions); return id(new AphrontRedirectResponse())->setURI('/Q' . $question->getID()); } } $form = id(new AphrontFormView())->setUser($user)->appendChild(id(new AphrontFormTextControl())->setLabel(pht('Question'))->setName('title')->setValue($v_title)->setError($e_title))->appendChild(id(new PhabricatorRemarkupControl())->setUser($user)->setName('content')->setID('content')->setValue($v_content)->setLabel(pht('Description'))->setUser($user)); $form->appendControl(id(new AphrontFormTokenizerControl())->setLabel(pht('Projects'))->setName('projects')->setValue($v_projects)->setDatasource(new PhabricatorProjectDatasource())); $form->appendChild(id(new AphrontFormSubmitControl())->addCancelButton($this->getApplicationURI())->setValue(pht('Ask Away!'))); $preview = id(new PHUIRemarkupPreviewPanel())->setHeader(pht('Question Preview'))->setControlID('content')->setPreviewURI($this->getApplicationURI('preview/')); $form_box = id(new PHUIObjectBoxView())->setHeaderText(pht('Ask New Question'))->setFormErrors($errors)->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $id = $question->getID(); if ($id) { $crumbs->addTextCrumb("Q{$id}", "/Q{$id}"); $crumbs->addTextCrumb(pht('Edit')); } else { $crumbs->addTextCrumb(pht('Ask Question')); } return $this->buildApplicationPage(array($crumbs, $form_box, $preview), array('title' => pht('Ask New Question'))); }
public function testUTF8len() { $strings = array('' => 0, 'x' => 1, "�" => 1, "x東y" => 3, "xyz" => 3, 'quack' => 5); foreach ($strings as $str => $expect) { $this->assertEqual($expect, phutil_utf8_strlen($str), 'Length of ' . $str); } }
public function markupText($text, $children) { $text = trim($text); $lines = phutil_split_lines($text); if (count($lines) > 1) { $level = $lines[1][0] == '=' ? 1 : 2; $text = trim($lines[0]); } else { $level = 0; for ($ii = 0; $ii < min(5, strlen($text)); $ii++) { if ($text[$ii] == '=' || $text[$ii] == '#') { ++$level; } else { break; } } $text = trim($text, ' =#'); } $engine = $this->getEngine(); if ($engine->isTextMode()) { $char = $level == 1 ? '=' : '-'; return $text . "\n" . str_repeat($char, phutil_utf8_strlen($text)); } $use_anchors = $engine->getConfig('header.generate-toc'); $anchor = null; if ($use_anchors) { $anchor = $this->generateAnchor($level, $text); } $text = phutil_tag('h' . ($level + 1), array('class' => 'remarkup-header'), array($anchor, $this->applyRules($text))); return $text; }
public static function assertValidPublisherKey($value) { $length = phutil_utf8_strlen($value); if (!$length) { throw new Exception(pht('Publisher key "%s" is not valid: publisher keys are required.', $value)); } $max_length = 64; if ($length > $max_length) { throw new Exception(pht('Publisher key "%s" is not valid: publisher keys must not be ' . 'more than %s characters long.', $value, new PhutilNumber($max_length))); } if (!preg_match('/^[a-z]+\\z/', $value)) { throw new Exception(pht('Publisher key "%s" is not valid: publisher keys may only contain ' . 'lowercase latin letters.', $value)); } }
public static function assertValidVersionName($value) { $length = phutil_utf8_strlen($value); if (!$length) { throw new Exception(pht('Version name "%s" is not valid: version names are required.', $value)); } $max_length = 64; if ($length > $max_length) { throw new Exception(pht('Version name "%s" is not valid: version names must not be ' . 'more than %s characters long.', $value, new PhutilNumber($max_length))); } if (!preg_match('/^[A-Za-z0-9.-]+\\z/', $value)) { throw new Exception(pht('Version name "%s" is not valid: version names may only contain ' . 'latin letters, digits, periods, and hyphens.', $value)); } if (preg_match('/^[.-]|[.-]$/', $value)) { throw new Exception(pht('Version name "%s" is not valid: version names may not start or ' . 'end with a period or hyphen.', $value)); } }
protected function renderRemarkupTable(array $out_rows) { assert_instances_of($out_rows, 'array'); if ($this->getEngine()->isTextMode()) { $lengths = array(); foreach ($out_rows as $r => $row) { foreach ($row['content'] as $c => $cell) { $text = $this->getEngine()->restoreText($cell['content']); $lengths[$c][$r] = phutil_utf8_strlen($text); } } $max_lengths = array_map('max', $lengths); $out = array(); foreach ($out_rows as $r => $row) { $headings = false; foreach ($row['content'] as $c => $cell) { $length = $max_lengths[$c] - $lengths[$c][$r]; $out[] = '| ' . $cell['content'] . str_repeat(' ', $length) . ' '; if ($cell['type'] == 'th') { $headings = true; } } $out[] = "|\n"; if ($headings) { foreach ($row['content'] as $c => $cell) { $char = $cell['type'] == 'th' ? '-' : ' '; $out[] = '| ' . str_repeat($char, $max_lengths[$c]) . ' '; } $out[] = "|\n"; } } return rtrim(implode('', $out), "\n"); } $out = array(); $out[] = "\n"; foreach ($out_rows as $row) { $cells = array(); foreach ($row['content'] as $cell) { $cells[] = phutil_tag($cell['type'], array(), $cell['content']); } $out[] = phutil_tag($row['type'], array(), $cells); $out[] = "\n"; } return phutil_tag('table', array('class' => 'remarkup-table'), $out); }
private function handlePost() { $request = $this->getRequest(); $user = $request->getUser(); $errors = array(); $title = $request->getStr('title'); $content = $request->getStr('content'); // form validation if (phutil_utf8_strlen($title) < 1 || phutil_utf8_strlen($title) > 255) { $errors[] = "Please enter a title (1-255 characters)"; } if ($errors) { return $this->showForm($errors, $title, $content); } // no validation errors -> save it $content_source = PhabricatorContentSource::newForSource(PhabricatorContentSource::SOURCE_WEB, array('ip' => $request->getRemoteAddr())); $question = id(new PonderQuestion())->setTitle($title)->setContent($content)->setAuthorPHID($user->getPHID())->setContentSource($content_source)->setVoteCount(0)->setAnswerCount(0)->setHeat(0.0)->save(); PhabricatorSearchPonderIndexer::indexQuestion($question); return id(new AphrontRedirectResponse())->setURI('/Q' . $question->getID()); }
public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $question = id(new PonderQuestion())->setAuthorPHID($user->getPHID())->setVoteCount(0)->setAnswerCount(0)->setHeat(0.0); $errors = array(); $e_title = true; if ($request->isFormPost()) { $question->setTitle($request->getStr('title')); $question->setContent($request->getStr('content')); $len = phutil_utf8_strlen($question->getTitle()); if ($len < 1) { $errors[] = pht('Title must not be empty.'); $e_title = pht('Required'); } else { if ($len > 255) { $errors[] = pht('Title is too long.'); $e_title = pht('Too Long'); } } if (!$errors) { $content_source = PhabricatorContentSource::newForSource(PhabricatorContentSource::SOURCE_WEB, array('ip' => $request->getRemoteAddr())); $question->setContentSource($content_source); id(new PonderQuestionEditor())->setQuestion($question)->setUser($user)->save(); return id(new AphrontRedirectResponse())->setURI('/Q' . $question->getID()); } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView())->setTitle('Form Errors')->setErrors($errors); } $header = id(new PhabricatorHeaderView())->setHeader(pht('Ask Question')); $form = id(new AphrontFormView())->setUser($user)->setFlexible(true)->appendChild(id(new AphrontFormTextControl())->setLabel(pht('Question'))->setName('title')->setValue($question->getTitle())->setError($e_title))->appendChild(id(new PhabricatorRemarkupControl())->setName('content')->setID('content')->setValue($question->getContent())->setLabel(pht('Description')))->appendChild(id(new AphrontFormSubmitControl())->setValue('Ask Away!')); $preview = '<div class="aphront-panel-flush">' . '<div id="question-preview">' . '<span class="aphront-panel-preview-loading-text">' . pht('Loading question preview...') . '</span>' . '</div>' . '</div>'; Javelin::initBehavior('ponder-feedback-preview', array('uri' => '/ponder/question/preview/', 'content' => 'content', 'preview' => 'question-preview', 'question_id' => null)); $nav = $this->buildSideNavView($question); $nav->selectFilter($question->getID() ? null : 'question/ask'); $nav->appendChild(array($header, $error_view, $form, $preview)); return $this->buildApplicationPage($nav, array('device' => true, 'title' => 'Ask a Question')); }
protected function renderRemarkupTable(array $out_rows) { assert_instances_of($out_rows, 'array'); if ($this->getEngine()->isTextMode()) { $lengths = array(); foreach ($out_rows as $r => $row) { foreach ($row['content'] as $c => $cell) { $text = $this->getEngine()->restoreText($cell['content']); $lengths[$c][$r] = phutil_utf8_strlen($text); } } $max_lengths = array_map('max', $lengths); $out = array(); foreach ($out_rows as $r => $row) { $headings = false; foreach ($row['content'] as $c => $cell) { $length = $max_lengths[$c] - $lengths[$c][$r]; $out[] = '| ' . $cell['content'] . str_repeat(' ', $length) . ' '; if ($cell['type'] == 'th') { $headings = true; } } $out[] = "|\n"; if ($headings) { foreach ($row['content'] as $c => $cell) { $char = $cell['type'] == 'th' ? '-' : ' '; $out[] = '| ' . str_repeat($char, $max_lengths[$c]) . ' '; } $out[] = "|\n"; } } return rtrim(implode('', $out), "\n"); } if ($this->getEngine()->isHTMLMailMode()) { $table_attributes = array('style' => 'border-collapse: separate; border-spacing: 1px; background: #d3d3d3; margin: 12px 0;'); $cell_attributes = array('style' => 'background: #ffffff; padding: 3px 6px;'); } else { $table_attributes = array('class' => 'remarkup-table'); $cell_attributes = array(); } $out = array(); $out[] = "\n"; foreach ($out_rows as $row) { $cells = array(); foreach ($row['content'] as $cell) { $cells[] = phutil_tag($cell['type'], $cell_attributes, $cell['content']); } $out[] = phutil_tag($row['type'], array(), $cells); $out[] = "\n"; } return phutil_tag('table', $table_attributes, $out); }
/** * Check that text field input isn't longer than a specified length. * * A text field input is invalid if the length of the input is longer than a * specified length. This length can be determined by the space allotted in * the database, or given arbitrarily. * This method is intended to make implementing @{method:validateTransaction} * more convenient: * * $overdrawn = $this->validateIsTextFieldTooLong( * $object->getName(), * $xactions, * $field_length); * * This will return `true` if the net effect of the object and transactions * is a field that is too long. * * @param wild Current field value. * @param list<PhabricatorApplicationTransaction> Transactions editing the * field. * @param integer for maximum field length. * @return bool True if the field will be too long after edits. */ protected function validateIsTextFieldTooLong($field_value, array $xactions, $length) { if ($xactions) { $new_value_length = phutil_utf8_strlen(last($xactions)->getNewValue()); if ($new_value_length <= $length) { return false; } else { return true; } } $old_value_length = phutil_utf8_strlen($field_value); if ($old_value_length <= $length) { return false; } return true; }
public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); if ($id) { $question = id(new PonderQuestionQuery())->setViewer($viewer)->withIDs(array($id))->requireCapabilities(array(PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT))->executeOne(); if (!$question) { return new Aphront404Response(); } $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs($question->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); $is_new = false; } else { $is_new = true; $question = PonderQuestion::initializeNewQuestion($viewer); $v_projects = array(); } $v_title = $question->getTitle(); $v_content = $question->getContent(); $v_wiki = $question->getAnswerWiki(); $v_view = $question->getViewPolicy(); $v_space = $question->getSpacePHID(); $v_status = $question->getStatus(); $errors = array(); $e_title = true; if ($request->isFormPost()) { $v_title = $request->getStr('title'); $v_content = $request->getStr('content'); $v_wiki = $request->getStr('answerWiki'); $v_projects = $request->getArr('projects'); $v_view = $request->getStr('viewPolicy'); $v_space = $request->getStr('spacePHID'); $v_status = $request->getStr('status'); $len = phutil_utf8_strlen($v_title); if ($len < 1) { $errors[] = pht('Title must not be empty.'); $e_title = pht('Required'); } else { if ($len > 255) { $errors[] = pht('Title is too long.'); $e_title = pht('Too Long'); } } if (!$errors) { $template = id(new PonderQuestionTransaction()); $xactions = array(); $xactions[] = id(clone $template)->setTransactionType(PonderQuestionTransaction::TYPE_TITLE)->setNewValue($v_title); $xactions[] = id(clone $template)->setTransactionType(PonderQuestionTransaction::TYPE_CONTENT)->setNewValue($v_content); $xactions[] = id(clone $template)->setTransactionType(PonderQuestionTransaction::TYPE_ANSWERWIKI)->setNewValue($v_wiki); if (!$is_new) { $xactions[] = id(clone $template)->setTransactionType(PonderQuestionTransaction::TYPE_STATUS)->setNewValue($v_status); } $xactions[] = id(clone $template)->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)->setNewValue($v_view); $xactions[] = id(clone $template)->setTransactionType(PhabricatorTransactions::TYPE_SPACE)->setNewValue($v_space); $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PonderQuestionTransaction())->setTransactionType(PhabricatorTransactions::TYPE_EDGE)->setMetadataValue('edge:type', $proj_edge_type)->setNewValue(array('=' => array_fuse($v_projects))); $editor = id(new PonderQuestionEditor())->setActor($viewer)->setContentSourceFromRequest($request)->setContinueOnNoEffect(true); $editor->applyTransactions($question, $xactions); return id(new AphrontRedirectResponse())->setURI('/Q' . $question->getID()); } } $policies = id(new PhabricatorPolicyQuery())->setViewer($viewer)->setObject($question)->execute(); $form = id(new AphrontFormView())->setUser($viewer)->appendChild(id(new AphrontFormTextControl())->setLabel(pht('Question'))->setName('title')->setValue($v_title)->setError($e_title))->appendChild(id(new PhabricatorRemarkupControl())->setUser($viewer)->setName('content')->setID('content')->setValue($v_content)->setLabel(pht('Question Details'))->setUser($viewer))->appendChild(id(new PhabricatorRemarkupControl())->setUser($viewer)->setName('answerWiki')->setID('answerWiki')->setValue($v_wiki)->setLabel(pht('Answer Summary'))->setUser($viewer))->appendControl(id(new AphrontFormPolicyControl())->setName('viewPolicy')->setPolicyObject($question)->setSpacePHID($v_space)->setPolicies($policies)->setValue($v_view)->setCapability(PhabricatorPolicyCapability::CAN_VIEW)); if (!$is_new) { $form->appendChild(id(new AphrontFormSelectControl())->setLabel(pht('Status'))->setName('status')->setValue($v_status)->setOptions(PonderQuestionStatus::getQuestionStatusMap())); } $form->appendControl(id(new AphrontFormTokenizerControl())->setLabel(pht('Tags'))->setName('projects')->setValue($v_projects)->setDatasource(new PhabricatorProjectDatasource())); $form->appendChild(id(new AphrontFormSubmitControl())->addCancelButton($this->getApplicationURI())->setValue(pht('Submit'))); $preview = id(new PHUIRemarkupPreviewPanel())->setHeader(pht('Question Preview'))->setControlID('content')->setPreviewURI($this->getApplicationURI('preview/')); $answer_preview = id(new PHUIRemarkupPreviewPanel())->setHeader(pht('Answer Summary Preview'))->setControlID('answerWiki')->setPreviewURI($this->getApplicationURI('preview/')); $crumbs = $this->buildApplicationCrumbs(); $id = $question->getID(); if ($id) { $crumbs->addTextCrumb("Q{$id}", "/Q{$id}"); $crumbs->addTextCrumb(pht('Edit')); $title = pht('Edit Question'); $header = id(new PHUIHeaderView())->setHeader($title)->setHeaderIcon('fa-pencil'); } else { $crumbs->addTextCrumb(pht('Ask Question')); $title = pht('Ask New Question'); $header = id(new PHUIHeaderView())->setHeader($title)->setHeaderIcon('fa-plus-square'); } $crumbs->setBorder(true); $box = id(new PHUIObjectBoxView())->setHeaderText(pht('Question'))->setFormErrors($errors)->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)->setForm($form); $view = id(new PHUITwoColumnView())->setHeader($header)->setFooter(array($box, $preview, $answer_preview)); return $this->newPage()->setTitle($title)->setCrumbs($crumbs)->appendChild($view); }
protected function validateTransaction(PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); switch ($type) { case PhameBlogTransaction::TYPE_NAME: $missing = $this->validateIsEmptyTextField($object->getName(), $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError($type, pht('Required'), pht('Name is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); if (phutil_utf8_strlen($new) > 64) { $errors[] = new PhabricatorApplicationTransactionValidationError($type, pht('Invalid'), pht('The selected blog title is too long. The maximum length ' . 'of a blog title is 64 characters.'), $xaction); } } break; case PhameBlogTransaction::TYPE_SUBTITLE: foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); if (phutil_utf8_strlen($new) > 64) { $errors[] = new PhabricatorApplicationTransactionValidationError($type, pht('Invalid'), pht('The selected blog subtitle is too long. The maximum length ' . 'of a blog subtitle is 64 characters.'), $xaction); } } break; case PhameBlogTransaction::TYPE_PARENTDOMAIN: if (!$xactions) { continue; } $parent_domain = last($xactions)->getNewValue(); if (empty($parent_domain)) { continue; } try { PhabricatorEnv::requireValidRemoteURIForLink($parent_domain); } catch (Exception $ex) { $error = new PhabricatorApplicationTransactionValidationError($type, pht('Invalid URI'), pht('Parent Domain must be set to a valid Remote URI.'), nonempty(last($xactions), null)); $errors[] = $error; } break; case PhameBlogTransaction::TYPE_FULLDOMAIN: if (!$xactions) { continue; } $custom_domain = last($xactions)->getNewValue(); if (empty($custom_domain)) { continue; } list($error_label, $error_text) = $object->validateCustomDomain($custom_domain); if ($error_label) { $error = new PhabricatorApplicationTransactionValidationError($type, $error_label, $error_text, nonempty(last($xactions), null)); $errors[] = $error; } if ($object->getViewPolicy() != PhabricatorPolicies::POLICY_PUBLIC) { $error_text = pht('For custom domains to work, the blog must have a view policy of ' . 'public.'); $error = new PhabricatorApplicationTransactionValidationError(PhabricatorTransactions::TYPE_VIEW_POLICY, pht('Invalid Policy'), $error_text, nonempty(last($xactions), null)); $errors[] = $error; } $domain = new PhutilURI($custom_domain); $domain = $domain->getDomain(); $duplicate_blog = id(new PhameBlogQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withDomain($domain)->executeOne(); if ($duplicate_blog && $duplicate_blog->getID() != $object->getID()) { $error = new PhabricatorApplicationTransactionValidationError($type, pht('Not Unique'), pht('Domain must be unique; another blog already has this domain.'), nonempty(last($xactions), null)); $errors[] = $error; } break; } return $errors; }
public function correctSpelling($input, array $options) { $matrix = $this->getEditDistanceMatrix(); if (!$matrix) { throw new PhutilInvalidStateException('setEditDistanceMatrix'); } $max_distance = $this->getMaximumDistance(); if (!$max_distance) { throw new PhutilInvalidStateException('setMaximumDistance'); } $input = $this->normalizeString($input); foreach ($options as $key => $option) { $options[$key] = $this->normalizeString($option); } $distances = array(); $inputv = phutil_utf8v($input); foreach ($options as $option) { $optionv = phutil_utf8v($option); $matrix->setSequences($optionv, $inputv); $distances[$option] = $matrix->getEditDistance(); } asort($distances); $best = min($max_distance, head($distances)); foreach ($distances as $option => $distance) { if ($distance > $best) { unset($distances[$option]); } } // Before filtering, check if we have multiple equidistant matches and // return them if we do. This prevents us from, e.g., matching "alnd" with // both "land" and "amend", then dropping "land" for being too short, and // incorrectly completing to "amend". if (count($distances) > 1) { return array_keys($distances); } foreach ($distances as $option => $distance) { if (phutil_utf8_strlen($option) < $distance) { unset($distances[$option]); } } return array_keys($distances); }
/** * Shorten a string to provide a summary, respecting UTF-8 characters. This * function attempts to truncate strings at word boundaries. * * NOTE: This function makes a best effort to apply some reasonable rules but * will not work well for the full range of unicode languages. For instance, * no effort is made to deal with combining characters. * * @param string UTF-8 string to shorten. * @param int Maximum length of the result. * @param string If the string is shortened, add this at the end. Defaults to * horizontal ellipsis. * @return string A string with no more than the specified character length. * * @group utf8 */ function phutil_utf8_shorten($string, $length, $terminal = "…") { $terminal_len = phutil_utf8_strlen($terminal); if ($terminal_len >= $length) { // If you provide a terminal we still enforce that the result (including // the terminal) is no longer than $length, but we can't do that if the // terminal is too long. throw new Exception("String terminal length must be less than string length!"); } $string_v = phutil_utf8v($string); $string_len = count($string_v); if ($string_len <= $length) { // If the string is already shorter than the requested length, simply return // it unmodified. return $string; } // NOTE: This is not complete, and there are many other word boundary // characters and reasonable places to break words in the UTF-8 character // space. For now, this gives us reasonable behavior for latin langauges. We // don't necessarily have access to PCRE+Unicode so there isn't a great way // for us to look up character attributes. // If we encounter these, prefer to break on them instead of cutting the // string off in the middle of a word. static $break_characters = array(' ' => true, "\n" => true, ';' => true, ':' => true, '[' => true, '(' => true, ',' => true, '-' => true); // If we encounter these, shorten to this character exactly without appending // the terminal. static $stop_characters = array('.' => true, '!' => true, '?' => true); // Search backward in the string, looking for reasonable places to break it. $word_boundary = null; $stop_boundary = null; // If we do a word break with a terminal, we have to look beyond at least the // number of characters in the terminal. $terminal_area = $length - $terminal_len; for ($ii = $length; $ii >= 0; $ii--) { $c = $string_v[$ii]; if (isset($break_characters[$c]) && $ii <= $terminal_area) { $word_boundary = $ii; } else { if (isset($stop_characters[$c]) && $ii < $length) { $stop_boundary = $ii + 1; break; } else { if ($word_boundary !== null) { break; } } } } if ($stop_boundary !== null) { // We found a character like ".". Cut the string there, without appending // the terminal. $string_part = array_slice($string_v, 0, $stop_boundary); return implode('', $string_part); } // If we didn't find any boundary characters or we found ONLY boundary // characters, just break at the maximum character length. if ($word_boundary === null || $word_boundary === 0) { $word_boundary = $length - $terminal_len; } $string_part = array_slice($string_v, 0, $word_boundary); $string_part = implode('', $string_part); return $string_part . $terminal; }