Skip to content

Commit 3cabecc

Browse files
authored
Merge pull request #65 from willmcgugan/hyperlinks
Add Hyperlinks
2 parents cd4d444 + f4184ad commit 3cabecc

19 files changed

+311
-101
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.0] - 2020-05-10
9+
10+
### Added
11+
12+
- Added hyperlinks to Style and markup
13+
- Added justify and code theme switches to markdown command
14+
815
## [1.0.3] - 2020-05-08
916

1017
### Added

docs/source/markup.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ There is a shorthand for closing a style. If you omit the style name from the cl
2525
print("[bold red]Bold and red[/] not bold or red")
2626

2727

28+
Links
29+
~~~~~
30+
31+
Console markup can output hyperlinks with the following syntax: ``[link=URL]text[/link]``. Here's an example::
32+
33+
print("Visit my [link=https://www.willmcgugan.com]blog[/link]!")
34+
35+
If your terminal software supports hyperlinks, you will be able to click the word "blog" which will typically open a browser. If your terminal doesn't support hyperlinks, you will see the text but it won't be clickable.
36+
37+
2838
Escaping
2939
~~~~~~~~
3040

@@ -34,6 +44,7 @@ Occasionally you may want to print something that Rich would interpret as markup
3444
>>> print("foo[[bar]]")
3545
foo[bar]
3646

47+
The function :func:`~rich.markup.escape` will handle escape of text for you.
3748

3849
Rendering Markup
3950
----------------

docs/source/reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Reference
1111
reference/highlighter.rst
1212
reference/logging.rst
1313
reference/markdown.rst
14+
reference/markup.rst
1415
reference/measure.rst
1516
reference/padding.rst
1617
reference/panel.rst

docs/source/reference/markup.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
rich.markup
2+
===========
3+
4+
.. automodule:: rich.markup
5+
:members:

docs/source/style.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ Styles may be negated by prefixing the attribute with the word "not". This can b
5555

5656
This will print "foo" and "baz" in bold, but "bar" will be in normal text.
5757

58+
Styles may also have a ``"link"`` attribute, which will turn any styled text in to a *hyperlink* (if supported by your terminal software).
59+
60+
To add a link to a style, the definition should contain the word ``"link"`` followed by a URL. The following example will make a clickable link::
61+
62+
console.print("Google", style="link https://google.com")
63+
64+
.. note::
65+
If you are familiar with HTML you may find applying links in this way a little odd, but the terminal considers a link to be another attribute just like bold, italic etc.
66+
67+
5868

5969
Style Class
6070
-----------

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "rich"
33
homepage = "https://github.com/willmcgugan/rich"
44
documentation = "https://rich.readthedocs.io/en/latest/"
5-
version = "1.0.3"
5+
version = "1.1.0"
66
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
77
authors = ["Will McGugan <willmcgugan@gmail.com>"]
88
license = "MIT"

rich/console.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,8 @@ def get_style(
597597
return name
598598

599599
try:
600-
return self._styles.get(name) or Style.parse(name)
600+
style = self._styles.get(name)
601+
return style.copy() if style is not None else Style.parse(name)
601602
except errors.StyleSyntaxError as error:
602603
if default is not None:
603604
return self.get_style(default)

rich/containers.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
from itertools import zip_longest
12
from typing import Iterator, Iterable, List, overload, TypeVar, TYPE_CHECKING, Union
23
from typing_extensions import Literal
34

45
from .segment import Segment
5-
6+
from .style import Style
67

78
if TYPE_CHECKING:
89
from .console import (
@@ -97,6 +98,7 @@ def extend(self, lines: Iterable["Text"]) -> None:
9798

9899
def justify(
99100
self,
101+
console: "Console",
100102
width: int,
101103
align: Literal["none", "left", "center", "right", "full"] = "left",
102104
) -> None:
@@ -135,9 +137,17 @@ def justify(
135137
index = (index + 1) % len(spaces)
136138
tokens: List[Text] = []
137139
index = 0
138-
for index, word in enumerate(words):
140+
for index, (word, next_word) in enumerate(
141+
zip_longest(words, words[1:])
142+
):
139143
tokens.append(word)
140144
if index < len(spaces):
141-
tokens.append(Text(" " * spaces[index]))
145+
if next_word is None:
146+
space_style = Style()
147+
else:
148+
style = word.get_style_at_offset(console, -1)
149+
next_style = next_word.get_style_at_offset(console, 0)
150+
space_style = style if style == next_style else line.style
151+
tokens.append(Text(" " * spaces[index], style=space_style))
142152
index += 1
143153
self[line_index] = Text("").join(tokens)

rich/default_styles.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@
115115
"markdown.h5": Style(underline=True),
116116
"markdown.h6": Style(italic=True),
117117
"markdown.h7": Style(italic=True, dim=True),
118-
"markdown.link": Style(bold=True),
119-
"markdown.link_url": Style(underline=True),
118+
"markdown.link": Style(color="bright_blue"),
119+
"markdown.link_url": Style(color="blue"),
120120
}
121121

122122

rich/markdown.py

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult
153153
)
154154
else:
155155
# Styled text for h2 and beyond
156+
if self.level:
157+
yield Text("\n")
156158
yield text
157159

158160

@@ -173,8 +175,10 @@ def __init__(self, lexer_name: str, theme: str) -> None:
173175

174176
def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
175177
code = str(self.text).rstrip()
176-
syntax = Syntax(code, self.lexer_name, theme=self.theme)
177-
yield Panel(syntax, style="dim")
178+
syntax = Panel(
179+
Syntax(code, self.lexer_name, theme=self.theme), style="dim", box=box.SQUARE
180+
)
181+
yield syntax
178182

179183

180184
class BlockQuote(TextElement):
@@ -289,7 +293,7 @@ def render_number(
289293
yield new_line
290294

291295

292-
class ImageItem(MarkdownElement):
296+
class ImageItem(TextElement):
293297
"""Renders a placeholder for an image."""
294298

295299
new_line = False
@@ -305,17 +309,26 @@ def create(cls, markdown: "Markdown", node: Any) -> "MarkdownElement":
305309
Returns:
306310
MarkdownElement: A new markdown element
307311
"""
308-
return cls(node.title, node.destination)
312+
return cls(node.destination, markdown.hyperlinks)
309313

310-
def __init__(self, title: Optional[str], destination: str) -> None:
311-
self.title = title
314+
def __init__(self, destination: str, hyperlinks: bool) -> None:
312315
self.destination = destination
316+
self.hyperlinks = hyperlinks
317+
self.link: Optional[str] = None
313318
super().__init__()
314319

320+
def on_enter(self, context: "MarkdownContext") -> None:
321+
self.link = context.current_style.link
322+
self.text = Text(justify="left")
323+
super().on_enter(context)
324+
315325
def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
316-
yield Text.assemble(
317-
(self.title or "🌆", "italic"), " ", f"({self.destination}) ", end=""
318-
)
326+
link_style = Style(link=self.link or self.destination or None)
327+
title = self.text or Text(self.destination.strip("/").rsplit("/", 1)[-1])
328+
329+
if self.hyperlinks:
330+
title.stylize_all(link_style)
331+
yield Text.assemble("🌆 ", title, " ", end="")
319332

320333

321334
class MarkdownContext:
@@ -356,6 +369,7 @@ class Markdown:
356369
code_theme (str, optional): Pygments theme for code blocks. Defaults to "monokai".
357370
justify (JustifyValues, optional): Justify value for paragraphs. Defaults to None.
358371
style (Union[str, Style], optional): Optional style to apply to markdown.
372+
hyperlinks (bool, optional): Enable hyperlinks. Defaults to ``True``.
359373
"""
360374

361375
elements: ClassVar[Dict[str, Type[MarkdownElement]]] = {
@@ -368,21 +382,23 @@ class Markdown:
368382
"item": ListItem,
369383
"image": ImageItem,
370384
}
371-
inlines = {"emph", "strong", "code", "link", "strike"}
385+
inlines = {"emph", "strong", "code", "strike"}
372386

373387
def __init__(
374388
self,
375389
markup: str,
376390
code_theme: str = "monokai",
377391
justify: JustifyValues = None,
378392
style: Union[str, Style] = "none",
393+
hyperlinks: bool = True,
379394
) -> None:
380395
self.markup = markup
381396
parser = Parser()
382397
self.parsed = parser.parse(markup)
383398
self.code_theme = code_theme
384399
self.justify = justify
385400
self.style = style
401+
self.hyperlinks = hyperlinks
386402

387403
def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
388404
"""Render markdown to the console."""
@@ -392,7 +408,6 @@ def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult
392408
inlines = self.inlines
393409
new_line = False
394410
for current, entering in nodes:
395-
# print(current, current.literal)
396411
node_type = current.t
397412
if node_type in ("html_inline", "html_block", "text"):
398413
context.on_text(current.literal.replace("\n", " "))
@@ -402,6 +417,23 @@ def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult
402417
elif node_type == "softbreak":
403418
if entering:
404419
context.on_text(" ")
420+
elif node_type == "link":
421+
if entering:
422+
link_style = console.get_style("markdown.link")
423+
if self.hyperlinks:
424+
link_style.link = current.destination
425+
context.enter_style(link_style)
426+
else:
427+
context.leave_style()
428+
if not self.hyperlinks:
429+
context.on_text(" (")
430+
style = Style(underline=True) + console.get_style(
431+
"markdown.link_url"
432+
)
433+
context.enter_style(style)
434+
context.on_text(current.destination)
435+
context.leave_style()
436+
context.on_text(")")
405437
elif node_type in inlines:
406438
if current.is_container():
407439
if entering:
@@ -413,12 +445,6 @@ def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult
413445
if current.literal:
414446
context.on_text(current.literal)
415447
context.leave_style()
416-
if current.destination and not entering:
417-
context.on_text(" (")
418-
context.enter_style("markdown.link_url")
419-
context.on_text(current.destination)
420-
context.leave_style()
421-
context.on_text(") ")
422448
else:
423449
element_class = self.elements.get(node_type) or UnknownElement
424450
if current.is_container():
@@ -470,6 +496,20 @@ def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult
470496
action="store_true",
471497
help="force color for non-terminals",
472498
)
499+
parser.add_argument(
500+
"-t",
501+
"--code-theme",
502+
dest="code_theme",
503+
default="monokai",
504+
help="pygments code theme",
505+
)
506+
parser.add_argument(
507+
"-y",
508+
"--hyperlinks",
509+
dest="hyperlinks",
510+
action="store_true",
511+
help="enable hyperlinks",
512+
)
473513
parser.add_argument(
474514
"-w",
475515
"--width",
@@ -478,11 +518,23 @@ def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult
478518
default=None,
479519
help="width of output (default will auto-detect)",
480520
)
521+
parser.add_argument(
522+
"-j",
523+
"--justify",
524+
dest="justify",
525+
action="store_true",
526+
help="enable full text justify",
527+
)
481528
args = parser.parse_args()
482529

483530
from rich.console import Console
484531

485532
console = Console(force_terminal=args.force_color, width=args.width)
486533
with open(args.path, "rt") as markdown_file:
487-
markdown = Markdown(markdown_file.read())
534+
markdown = Markdown(
535+
markdown_file.read(),
536+
justify="full" if args.justify else "left",
537+
code_theme=args.code_theme,
538+
hyperlinks=args.hyperlinks,
539+
)
488540
console.print(markdown)

0 commit comments

Comments
 (0)