diff --git a/src/applications/project/remarkup/ProjectRemarkupRule.php b/src/applications/project/remarkup/ProjectRemarkupRule.php index 5bdebfa13e..fa0552593f 100644 --- a/src/applications/project/remarkup/ProjectRemarkupRule.php +++ b/src/applications/project/remarkup/ProjectRemarkupRule.php @@ -1,62 +1,76 @@ getEngine()->isTextMode()) { return '#'.$id; } return $handle->renderTag(); } protected function getObjectIDPattern() { // NOTE: This explicitly does not match strings which contain only // digits, because digit strings like "#123" are used to reference tasks at // Facebook and are somewhat conventional in general. // The latter half of this rule matches monograms with internal periods, // like `#domain.com`, but does not match monograms with terminal periods, // because they're probably just puncutation. // Broadly, this will not match every possible project monogram, and we // accept some false negatives -- like `#1` or `#dot.` -- in order to avoid // a bunch of false positives on general use of the `#` character. // In other contexts, the PhabricatorProjectProjectPHIDType pattern is // controlling and these names should parse correctly. - return '[^\s.\d!,:;{}#\(\)]+(?:[^\s!,:;{}#\(\)]*[^\s.!,:;{}#\(\)]+)*'; + // These characters may never appear anywhere in a hashtag. + $never = '\s?!,:;{}#\\(\\)"\''; + + // These characters may not appear at the beginning. + $never_first = '.\d'; + + // These characters may not appear at the end. + $never_last = '.'; + + return + '[^'.$never_first.$never.']+'. + '(?:'. + '[^'.$never.']*'. + '[^'.$never_last.$never.']+'. + ')*'; } protected function loadObjects(array $ids) { $viewer = $this->getEngine()->getConfig('viewer'); // Put the "#" back on the front of these IDs. $names = array(); foreach ($ids as $id) { $names[] = '#'.$id; } // Issue a query by object name. $query = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withNames($names); $query->execute(); $projects = $query->getNamedResults(); // Slice the "#" off again. $result = array(); foreach ($projects as $name => $project) { $result[substr($name, 1)] = $project; } return $result; } } diff --git a/src/applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php b/src/applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php index 4df2aad3d9..437b025491 100644 --- a/src/applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php +++ b/src/applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php @@ -1,93 +1,123 @@ array( 'embed' => array(), 'ref' => array( array( 'offset' => 8, 'id' => 'ducks', ), ), ), 'We should make a post on #blog.example.com tomorrow.' => array( 'embed' => array(), 'ref' => array( array( 'offset' => 26, 'id' => 'blog.example.com', ), ), ), 'We should make a post on #blog.example.com.' => array( 'embed' => array(), 'ref' => array( array( 'offset' => 26, 'id' => 'blog.example.com', ), ), ), '#123' => array( 'embed' => array(), 'ref' => array(), ), '#security#123' => array( 'embed' => array(), 'ref' => array( array( 'offset' => 1, 'id' => 'security', 'tail' => '123', ), ), ), // Don't match a terminal parenthesis. This fixes these constructs in // natural language. 'There is some documentation (see #guides).' => array( 'embed' => array(), 'ref' => array( array( 'offset' => 34, 'id' => 'guides', ), ), ), // Don't match internal parentheses either. This makes the terminal // parenthesis behavior less arbitrary (otherwise, we match open // parentheses but not closing parentheses, which is surprising). '#a(b)c' => array( 'embed' => array(), 'ref' => array( array( 'offset' => 1, 'id' => 'a', ), ), ), '#s3' => array( 'embed' => array(), 'ref' => array( array( 'offset' => 1, 'id' => 's3', ), ), ), + 'Is this #urgent?' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 9, + 'id' => 'urgent', + ), + ), + ), + + 'This is "#urgent".' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 10, + 'id' => 'urgent', + ), + ), + ), + + 'This is \'#urgent\'.' => array( + 'embed' => array(), + 'ref' => array( + array( + 'offset' => 10, + 'id' => 'urgent', + ), + ), + ), + ); foreach ($cases as $input => $expect) { $rule = new ProjectRemarkupRule(); $matches = $rule->extractReferences($input); $this->assertEqual($expect, $matches, $input); } } }