def Linkify(_mr, autolink_regex_match, _component_ref_artifacts): """Examine a textual reference and replace it with a hyperlink or not. This is a callback for use with the autolink feature. The function parameters are standard for this type of callback. Args: _mr: unused information parsed from the HTTP request. autolink_regex_match: regex match for the textual reference. _component_ref_artifacts: unused result of call to GetReferencedIssues. Returns: A list of TextRuns with tag=a for all matched ftp, http, https and mailto links converted into HTML hyperlinks. """ hyperlink = autolink_regex_match.group(0) trailing = '' for begin, end in _LINK_TRAILING_CHARS: if hyperlink.endswith(end): if not begin or hyperlink[:-len(end)].find(begin) == -1: trailing = end + trailing hyperlink = hyperlink[:-len(end)] tag_match = _CLOSING_TAG_RE.search(hyperlink) if tag_match: trailing = hyperlink[tag_match.start(0):] + trailing hyperlink = hyperlink[:tag_match.start(0)] href = hyperlink if not href.lower().startswith(('http', 'ftp', 'mailto')): # We use http because redirects for https are not all set up. href = 'http://' + href if (not validate.IsValidURL(href) and not (href.startswith('mailto') and validate.IsValidEmail(href[7:]))): return [template_helpers.TextRun(autolink_regex_match.group(0))] result = [template_helpers.TextRun(hyperlink, tag='a', href=href)] if trailing: result.append(template_helpers.TextRun(trailing)) return result
def _ApplySubstFunctionToRuns(self, text_runs, regex, subst_fun, mr, component_ref_artifacts): """Apply autolink regex and substitution function to each text run. Args: text_runs: list of TextRun objects with parts of the original comment. regex: Regular expression for detecting textual references to artifacts. subst_fun: function to return autolink markup, or original text. mr: common info parsed from the user HTTP request. component_ref_artifacts: already-looked-up destination artifacts to use when computing substitution text. Returns: A new list with more and smaller runs, some of which may have tag and link attributes set. """ result_runs = [] for run in text_runs: content = run.content if run.tag: # This chunk has already been substituted, don't allow nested # autolinking to mess up our output. result_runs.append(run) else: pos = 0 for match in regex.finditer(content): if match.start() > pos: result_runs.append( template_helpers.TextRun( content[pos:match.start()])) replacement_runs = subst_fun(mr, match, component_ref_artifacts) result_runs.extend(replacement_runs) pos = match.end() if run.content[ pos:]: # Keep any text that came after the last match result_runs.append( template_helpers.TextRun(run.content[pos:])) # TODO(jrobbins): ideally we would merge consecutive plain text runs # so that regexes can match across those run boundaries. return result_runs
def testMarkupAutolinks_TooBig(self): """If the issue has too much text, we just do regex-based autolinking.""" all_ref_artifacts = self.aa.GetAllReferencedArtifacts( None, self.comments, max_total_length=10) result = self.aa.MarkupAutolinks( None, [template_helpers.TextRun(self.comment1)], all_ref_artifacts) self.assertEqual(5, len(result)) self.assertEqual('Feel free to contact me at ', result[0].content) # The test autolink handlers in this file do not link email addresses. self.assertEqual('a AT other.com', result[1].content) self.assertIsNone(result[1].href)
def testMarkupAutolinks(self): all_ref_artifacts = self.aa.GetAllReferencedArtifacts( None, self.comments) result = self.aa.MarkupAutolinks( None, [template_helpers.TextRun(self.comment1)], all_ref_artifacts) self.assertEqual('Feel free to contact me at ', result[0].content) self.assertEqual('a AT other.com', result[1].content) self.assertEqual(', or ', result[2].content) self.assertEqual('*****@*****.**', result[3].content) self.assertEqual('mailto:[email protected]', result[3].href) self.assertEqual(', or [email protected].', result[4].content) result = self.aa.MarkupAutolinks( None, [template_helpers.TextRun(self.comment2)], all_ref_artifacts) self.assertEqual('no matches in this comment', result[0].content) result = self.aa.MarkupAutolinks( None, [template_helpers.TextRun(self.comment3)], all_ref_artifacts) self.assertEqual('just matches with no ref: ', result[0].content) self.assertEqual('a AT other.com', result[1].content) self.assertEqual(', [email protected]', result[2].content)
def _Linkify(autolink_regex_match, shorthand=False): """Examine a textual reference and replace it with a hyperlink or not. This is a callback for use with the autolink feature. Args: autolink_regex_match: regex match for the textual reference. shorthand: Set to True to allow shorthand links without "http". Returns: A list of TextRuns with tag=a for all matched ftp, http, https and mailto links converted into HTML hyperlinks. """ hyperlink = autolink_regex_match.group(0) trailing = '' for begin, end in _LINK_TRAILING_CHARS: if hyperlink.endswith(end): if not begin or hyperlink[:-len(end)].find(begin) == -1: trailing = end + trailing hyperlink = hyperlink[:-len(end)] tag_match = _CLOSING_TAG_RE.search(hyperlink) if tag_match: trailing = hyperlink[tag_match.start(0):] + trailing hyperlink = hyperlink[:tag_match.start(0)] href = hyperlink if shorthand and not href.lower().startswith('http'): # We use http because redirects for https are not all set up. href = 'http://' + href if (not validate.IsValidURL(href) and not validate.IsValidEmail(href)): return [template_helpers.TextRun(autolink_regex_match.group(0))] result = [template_helpers.TextRun(hyperlink, tag='a', href=href)] if trailing: result.append(template_helpers.TextRun(trailing)) return result
def _ReplaceIssueRef(autolink_regex_match, component_ref_artifacts, single_issue_regex, default_project_name): """Examine a textual reference and replace it with an autolink or not. Args: autolink_regex_match: regex match for the textual reference. component_ref_artifacts: result of earlier call to GetReferencedIssues. single_issue_regex: regular expression to parse individual issue references out of a multi-issue-reference phrase. E.g., "issues 12 and 34". default_project_name: project name to use when not specified. Returns: A list of IssueRefRuns and TextRuns to replace the textual reference. If there is an issue to autolink to, we return an HTML hyperlink. Otherwise, we the run will have the original plain text. """ open_dict, closed_dict = {}, {} if component_ref_artifacts: open_dict, closed_dict = component_ref_artifacts original = autolink_regex_match.group(0) logging.info('called ReplaceIssueRef on %r', original) result_runs = [] pos = 0 for submatch in single_issue_regex.finditer(original): if submatch.start() >= pos: if original[pos:submatch.start()]: result_runs.append( template_helpers.TextRun(original[pos:submatch.start()])) replacement_run = _ReplaceSingleIssueRef(submatch, open_dict, closed_dict, default_project_name) result_runs.append(replacement_run) pos = submatch.end() if original[pos:]: result_runs.append(template_helpers.TextRun(original[pos:])) return result_runs
def _ReplaceSingleIssueRef(submatch, open_dict, closed_dict, default_project_name): """Replace one issue reference with a link, or the original text.""" content = submatch.group(0) project_name = submatch.group('project_name') if project_name: project_name = project_name.lstrip().rstrip(':#') else: # We need project_name for the URL, even if it is not in the text. project_name = default_project_name local_id = int(submatch.group('local_id')) issue_key = _IssueProjectKey(project_name, local_id) if issue_key in open_dict: return IssueRefRun(open_dict[issue_key], False, project_name, content) elif issue_key in closed_dict: return IssueRefRun(closed_dict[issue_key], True, project_name, content) else: # Don't link to non-existent issues. return template_helpers.TextRun(content)
def __init__(self, pb, mr, prefetched_issues, users_by_id, prefetched_projects, prefetched_configs, autolink=None, all_ref_artifacts=None, ending=None, highlight=None): """Constructs an ActivityView out of an Activity protocol buffer. Args: pb: an IssueComment or Activity protocol buffer. mr: HTTP request info, used by the artifact autolink. prefetched_issues: dictionary of the issues for the comments being shown. users_by_id: dict {user_id: UserView} for all relevant users. prefetched_projects: dict {project_id: project} including all the projects that we might need. prefetched_configs: dict {project_id: config} for those projects. autolink: Autolink instance. all_ref_artifacts: list of all artifacts in the activity stream. ending: ending type for activity titles, 'in_project' or 'by_user' highlight: what to highlight in the middle column on user updates pages i.e. 'project', 'user', or None """ template_helpers.PBProxy.__init__(self, pb) activity_type = 'ProjectIssueUpdate' # TODO(jrobbins): more types self.comment = None self.issue = None self.field_changed = None self.multiple_fields_changed = ezt.boolean(False) self.project = None self.user = None self.timestamp = time.time( ) # Bogus value makes bad ones highly visible. if isinstance(pb, tracker_pb2.IssueComment): self.timestamp = pb.timestamp issue = prefetched_issues[pb.issue_id] if self.timestamp == issue.opened_timestamp: issue_change_id = None # This comment is the description. else: issue_change_id = pb.timestamp # instead of seq num. self.comment = tracker_views.IssueCommentView( mr.project_name, pb, users_by_id, autolink, all_ref_artifacts, mr, issue) # TODO(jrobbins): pass effective_ids of the commenter so that he/she # can be identified as a project member or not. config = prefetched_configs[issue.project_id] self.issue = tracker_views.IssueView(issue, users_by_id, config) self.user = self.comment.creator project = prefetched_projects[issue.project_id] self.project_name = project.project_name self.project = project_views.ProjectView(project) else: logging.warn('unknown activity object %r', pb) nested_page_data = { 'activity_type': activity_type, 'issue_change_id': issue_change_id, 'comment': self.comment, 'issue': self.issue, 'project': self.project, 'user': self.user, 'timestamp': self.timestamp, 'ending_type': ending, } self.escaped_title = self._TITLE_TEMPLATE.GetResponse( nested_page_data).strip() self.escaped_body = self._BODY_TEMPLATE.GetResponse( nested_page_data).strip() if autolink is not None and all_ref_artifacts is not None: # TODO(jrobbins): actually parse the comment text. Actually render runs. runs = autolink.MarkupAutolinks( mr, [template_helpers.TextRun(self.escaped_body)], all_ref_artifacts) self.escaped_body = ''.join(run.content for run in runs) self.date_bucket, self.date_relative = timestr.GetHumanScaleDate( self.timestamp) time_tuple = time.localtime(self.timestamp) self.date_tooltip = time.asctime(time_tuple) # We always highlight the user for starring activities if activity_type.startswith('UserStar'): self.highlight = 'user' else: self.highlight = highlight
def _ChunkToRun(chunk): """Convert a substring of the user's comment to a TextRun object.""" if chunk.startswith('<b>') and chunk.endswith('</b>'): return template_helpers.TextRun(chunk[3:-4], tag='b') else: return template_helpers.TextRun(chunk)
def MakeHyperLink(_mr, match, _comp_ref_artifacts): domain = match.group(0) return [ template_helpers.TextRun(tag='a', href=domain, content=domain) ]