Skip to content

Commit ca04fa2

Browse files
committed
Introduce ANSI link support
The `Attr` constructor grows a field for a link target, which is just a `Maybe Text` for the time being. We deem nested links to be illegal, like HTML, and so the rendering state only has to keep track of one link at a time. When rendering to ANSI, we use OSC8 to include the link in the output. OSC8 links support an "id" parameter which tells terminals to treat discontiguous linked text spans as being "the same" link, for mouseover purposes. Our code could grow to support this.
1 parent 77ee0e8 commit ca04fa2

File tree

4 files changed

+63
-29
lines changed

4 files changed

+63
-29
lines changed

src/Text/DocLayout.hs

+40-16
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ module Text.DocLayout (
7070
, magenta
7171
, cyan
7272
, white
73+
, link
7374
, empty
7475
-- * Functions for concatenating documents
7576
, (<+>)
@@ -149,6 +150,7 @@ data Doc a = Text Int a -- ^ Text with specified width.
149150
| BlankLines Int -- ^ Ensure a number of blank lines.
150151
| Concat (Doc a) (Doc a) -- ^ Two documents concatenated.
151152
| Styled StyleReq (Doc a)
153+
| Linked Text (Doc a) -- ^ A hyperlink
152154
| Empty
153155
deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable,
154156
Data, Typeable, Generic)
@@ -261,6 +263,7 @@ data RenderState a = RenderState{
261263
, column :: Int
262264
, newlines :: Int -- ^ Number of preceding newlines
263265
, fontStack :: [Font]
266+
, linkTarget :: Maybe Text -- ^ Current link target
264267
}
265268

266269
peekFont :: RenderState a -> Font
@@ -274,9 +277,9 @@ newline = do
274277
let rawpref = prefix st'
275278
when (column st' == 0 && usePrefix st' && not (T.null rawpref)) $ do
276279
let pref = fromString $ T.unpack $ T.dropWhileEnd isSpace rawpref
277-
modify $ \st -> st{ output = Attr baseFont pref : output st
280+
modify $ \st -> st{ output = Attr Nothing baseFont pref : output st
278281
, column = column st + realLength pref }
279-
modify $ \st -> st { output = Attr baseFont "\n" : output st
282+
modify $ \st -> st { output = Attr Nothing baseFont "\n" : output st
280283
, column = 0
281284
, newlines = newlines st + 1
282285
}
@@ -287,9 +290,9 @@ outp off s = do -- offset >= 0 (0 might be combining char)
287290
let pref = if usePrefix st' then fromString $ T.unpack $ prefix st' else mempty
288291
let font = peekFont st'
289292
when (column st' == 0 && not (isNull pref && font == baseFont)) $
290-
modify $ \st -> st{ output = Attr baseFont pref : output st
293+
modify $ \st -> st{ output = Attr Nothing baseFont pref : output st
291294
, column = column st + realLength pref }
292-
modify $ \st -> st{ output = Attr font s : output st
295+
modify $ \st -> st{ output = Attr (linkTarget st) font s : output st
293296
, column = column st + off
294297
, newlines = 0 }
295298

@@ -300,22 +303,25 @@ render :: HasChars a => Maybe Int -> Doc a -> a
300303
render = renderPlain
301304

302305
renderANSI :: HasChars a => Maybe Int -> Doc a -> a
303-
renderANSI n d = snd $ go $ prerender n d where
304-
go (Attributed s) = foldl attrRender (baseFont, "") s
306+
renderANSI n d = (\(_,_,o) -> o) $ go $ prerender n d where
307+
go (Attributed s) = foldl attrRender (Nothing, baseFont, "") s
305308

306309
renderPlain :: HasChars a => Maybe Int -> Doc a -> a
307310
renderPlain n d = go $ prerender n d where
308311
go (Attributed s) = foldMap attrStrip s
309312

310313
attrStrip :: HasChars a => Attr a -> a
311-
attrStrip (Attr _ y) | isNull y = ""
312-
| otherwise = y
314+
attrStrip (Attr _ _ y) | isNull y = ""
315+
| otherwise = y
316+
317+
attrRender :: HasChars a => (Link, Font, a) -> Attr a -> (Link, Font, a)
318+
attrRender (l, f, acc) (Attr m g y)
319+
| isNull y = (l, f, acc)
320+
| otherwise = (m, g, acc <> newFont <> newLink <> y)
321+
where
322+
newLink = if l == m then mempty else renderOSC8 (fromString . T.unpack <$> m)
323+
newFont = if f == g then mempty else renderFont g
313324

314-
attrRender :: HasChars a => (Font, a) -> Attr a -> (Font, a)
315-
attrRender (f, acc) (Attr g y)
316-
| isNull y = (f, acc)
317-
| f == g = (f, acc <> y)
318-
| otherwise = (g, acc <> renderFont g <> y)
319325

320326
prerender :: HasChars a => Maybe Int -> Doc a -> Attributed a
321327
prerender linelen doc = fromList . reverse . output $
@@ -327,7 +333,8 @@ prerender linelen doc = fromList . reverse . output $
327333
, lineLength = linelen
328334
, column = 0
329335
, newlines = 2
330-
, fontStack = [] }
336+
, fontStack = []
337+
, linkTarget = Nothing }
331338

332339
renderDoc :: HasChars a => Doc a -> DocState a
333340
renderDoc = renderList . normalize . unfoldD
@@ -390,7 +397,7 @@ renderList (CookedText off s : xs) = do
390397
let pref = if usePrefix st' then fromString $ T.unpack $ prefix st' else mempty
391398
let elems (Attributed x) = reverse $ toList x
392399
when (column st' == 0 && not (isNull pref)) $
393-
modify $ \st -> st{ output = Attr baseFont pref : output st
400+
modify $ \st -> st{ output = Attr Nothing baseFont pref : output st
394401
, column = column st + realLength pref }
395402
modify $ \st -> st{ output = elems s ++ output st
396403
, column = column st + off
@@ -406,6 +413,18 @@ renderList (Styled style doc : xs) = do
406413
modify $ \s -> s{ fontStack = fontStack st, output = output s }
407414
renderList xs
408415

416+
-- Nested links are nonsensical, we only handle the outermost and
417+
-- silently ignore any attempts to have a link inside a link
418+
renderList (Linked target doc : xs) = do
419+
st <- get
420+
case linkTarget st of
421+
Nothing -> do
422+
modify $ \s -> s{linkTarget = Just target}
423+
renderDoc doc
424+
modify $ \s -> s{linkTarget = Nothing}
425+
_ -> renderDoc doc
426+
renderList xs
427+
409428
renderList (Prefixed pref d : xs) = do
410429
st <- get
411430
let oldPref = prefix st
@@ -474,7 +493,7 @@ renderList (b : xs) | isBlock b = do
474493
heightOf _ = 1
475494
let maxheight = maximum $ map heightOf (b:bs)
476495
let toBlockSpec (Block w ls) = (w, ls)
477-
toBlockSpec (VFill w t) = (w, map (singleton . (Attr font)) (take maxheight $ repeat t))
496+
toBlockSpec (VFill w t) = (w, map (singleton . (Attr (linkTarget st) font)) (take maxheight $ repeat t))
478497
toBlockSpec _ = (0, [])
479498
let (_, lns') = foldl (mergeBlocks maxheight) (toBlockSpec b)
480499
(map toBlockSpec bs)
@@ -519,6 +538,7 @@ startsBlank (BlankLines _) = True
519538
startsBlank (Concat Empty y) = startsBlank y
520539
startsBlank (Concat x _) = startsBlank x
521540
startsBlank (Styled _ x) = startsBlank x
541+
startsBlank (Linked _ x) = startsBlank x
522542
startsBlank Empty = True
523543

524544
isBlock :: Doc a -> Bool
@@ -636,6 +656,7 @@ getOffset breakWhen (!l, !c) x =
636656
CookedText n _ -> (l, c + n)
637657
Empty -> (l, c)
638658
Styled _ d -> getOffset breakWhen (l, c) d
659+
Linked _ d -> getOffset breakWhen (l, c) d
639660
CarriageReturn -> (max l c, 0)
640661
NewLine -> (max l c, 0)
641662
BlankLines _ -> (max l c, 0)
@@ -802,6 +823,9 @@ cyan = Cyan
802823
white :: Color
803824
white = White
804825

826+
link :: HasChars a => Text -> Doc a -> Doc a
827+
link = Linked
828+
805829
-- | Returns width of a character in a monospace font: 0 for a combining
806830
-- character, 1 for a regular character, 2 for an East Asian wide character.
807831
-- Ambiguous characters are treated as width 1.

src/Text/DocLayout/ANSIFont.hs

+9-2
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,25 @@ module Text.DocLayout.ANSIFont
1212
, Background(..)
1313
, (~>)
1414
, renderFont
15+
, renderOSC8
1516
) where
1617

1718
import Data.Data (Data)
1819
import Data.String
20+
import Data.Text (Text)
1921

2022
data Font = Font
2123
{ ftWeight :: Weight,
2224
ftShape :: Shape,
2325
ftUnderline :: Underline,
2426
ftForeground :: Foreground,
25-
ftBackground :: Background
27+
ftBackground :: Background,
28+
ftLink :: Maybe Text
2629
}
2730
deriving (Show, Eq, Read, Data, Ord)
2831

2932
baseFont :: Font
30-
baseFont = Font Normal Roman ULNone FGDefault BGDefault
33+
baseFont = Font Normal Roman ULNone FGDefault BGDefault Nothing
3134

3235
data Weight = Normal | Bold deriving (Show, Eq, Read, Data, Ord)
3336
data Shape = Roman | Italic deriving (Show, Eq, Read, Data, Ord)
@@ -84,3 +87,7 @@ renderFont f
8487
<> renderSGR (ftForeground f)
8588
<> renderSGR (ftBackground f)
8689
<> renderSGR (ftUnderline f)
90+
91+
renderOSC8 :: (Semigroup a, IsString a) => Maybe a -> a
92+
renderOSC8 Nothing = "\ESC]8;;\ESC\\"
93+
renderOSC8 (Just t) = "\ESC]8;;" <> t <> "\ESC\\"

src/Text/DocLayout/Attributed.hs

+9-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{-# LANGUAGE DeriveTraversable #-}
33
{-# LANGUAGE DeriveDataTypeable #-}
44
{-# LANGUAGE FlexibleInstances #-}
5-
module Text.DocLayout.Attributed (Attributed(..), Attr(..), fromList, singleton)
5+
module Text.DocLayout.Attributed (Attributed(..), Attr(..), Link, fromList, singleton)
66
where
77

88
import Data.String
@@ -11,16 +11,19 @@ import Data.Data (Data, Typeable)
1111
import GHC.Generics
1212
import Data.Sequence ((><))
1313
import qualified Data.Sequence as S
14+
import Data.Text (Text)
1415

15-
data Attr a = Attr Font a
16+
type Link = Maybe Text
17+
18+
data Attr a = Attr Link Font a
1619
deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable,
1720
Data, Typeable, Generic)
1821

1922
instance Semigroup a => Semigroup (Attr a) where
20-
(<>) (Attr f x) (Attr _ y) = Attr f $ x <> y -- This is arbitrary
23+
(<>) (Attr l f x) (Attr _ _ y) = Attr l f $ x <> y -- This is arbitrary
2124

2225
instance (IsString a, Monoid a) => Monoid (Attr a) where
23-
mempty = Attr baseFont (fromString "")
26+
mempty = Attr Nothing baseFont (fromString "")
2427

2528
newtype Attributed a = Attributed (S.Seq (Attr a))
2629
deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable,
@@ -33,10 +36,10 @@ singleton :: Attr a -> Attributed a
3336
singleton = Attributed . S.singleton
3437

3538
instance IsString a => IsString (Attr a) where
36-
fromString x = Attr baseFont (fromString x)
39+
fromString x = Attr Nothing baseFont (fromString x)
3740

3841
instance IsString a => IsString (Attributed a) where
39-
fromString x = Attributed $ S.singleton $ Attr baseFont (fromString x)
42+
fromString x = Attributed $ S.singleton $ Attr Nothing baseFont (fromString x)
4043

4144
instance Semigroup a => Semigroup (Attributed a) where
4245
(<>) (Attributed a) (Attributed b) = Attributed $ a >< b

src/Text/DocLayout/HasChars.hs

+5-5
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,18 @@ instance HasChars TL.Text where
5454
isNull = TL.null
5555

5656
instance HasChars a => HasChars (Attr a) where
57-
foldrChar f a (Attr _ x) = foldrChar f a x
58-
foldlChar f a (Attr _ x) = foldlChar f a x
59-
splitLines (Attr f x) = Attr f <$> splitLines x
57+
foldrChar f a (Attr _ _ x) = foldrChar f a x
58+
foldlChar f a (Attr _ _ x) = foldlChar f a x
59+
splitLines (Attr l f x) = Attr l f <$> splitLines x
6060

6161
instance (HasChars a) => HasChars (Attributed a) where
6262
foldrChar _ acc (Attributed S.Empty) = acc
63-
foldrChar f acc (Attributed (xs :|> (Attr _ x))) =
63+
foldrChar f acc (Attributed (xs :|> (Attr _ _ x))) =
6464
let l = foldrChar f acc x
6565
innerFold e a = foldrChar f a e
6666
in foldr innerFold l xs
6767
foldlChar _ acc (Attributed S.Empty) = acc
68-
foldlChar f acc (Attributed ((Attr _ x) :<| xs)) =
68+
foldlChar f acc (Attributed ((Attr _ _ x) :<| xs)) =
6969
let l = foldlChar f acc x
7070
innerFold e a = foldlChar f a e
7171
in foldr innerFold l xs

0 commit comments

Comments
 (0)