dev: Use realLength from doclayout instead of strWidth and textWidth. (#895)

This gives us more accurate string length calculations. In particular,
it handles emoji and other scripts properly.
This commit is contained in:
Stephen Morgan 2021-11-12 12:49:26 +11:00 committed by Simon Michael
parent d1ae0c10d6
commit ff0132df28
14 changed files with 32 additions and 79 deletions

View File

@ -46,6 +46,7 @@ import qualified Data.Set as S
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Tree (Tree(..)) import Data.Tree (Tree(..))
import Text.DocLayout (realLength)
import Hledger.Data.Types import Hledger.Data.Types
import Hledger.Utils import Hledger.Utils
@ -186,7 +187,7 @@ elideAccountName width s
where where
elideparts :: Int -> [Text] -> [Text] -> [Text] elideparts :: Int -> [Text] -> [Text] -> [Text]
elideparts width done ss elideparts width done ss
| textWidth (accountNameFromComponents $ done++ss) <= width = done++ss | realLength (accountNameFromComponents $ done++ss) <= width = done++ss
| length ss > 1 = elideparts width (done++[textTakeWidth 2 $ head ss]) (tail ss) | length ss > 1 = elideparts width (done++[textTakeWidth 2 $ head ss]) (tail ss)
| otherwise = done++ss | otherwise = done++ss

View File

@ -171,7 +171,7 @@ import Test.Tasty.HUnit ((@?=), assertBool, testCase)
import Hledger.Data.Types import Hledger.Data.Types
import Hledger.Utils (colorB) import Hledger.Utils (colorB)
import Hledger.Utils.Text (textQuoteIfNeeded) import Hledger.Utils.Text (textQuoteIfNeeded)
import Text.WideString (WideBuilder(..), textWidth, wbToText, wbUnpack) import Text.WideString (WideBuilder(..), wbFromText, wbToText, wbUnpack)
-- A 'Commodity' is a symbol representing a currency or some other kind of -- A 'Commodity' is a symbol representing a currency or some other kind of
@ -469,14 +469,13 @@ showAmountB :: AmountDisplayOpts -> Amount -> WideBuilder
showAmountB _ Amount{acommodity="AUTO"} = mempty showAmountB _ Amount{acommodity="AUTO"} = mempty
showAmountB opts a@Amount{astyle=style} = showAmountB opts a@Amount{astyle=style} =
color $ case ascommodityside style of color $ case ascommodityside style of
L -> showC c' space <> quantity' <> price L -> showC (wbFromText c) space <> quantity' <> price
R -> quantity' <> showC space c' <> price R -> quantity' <> showC space (wbFromText c) <> price
where where
quantity = showamountquantity a quantity = showamountquantity a
(quantity',c) | amountLooksZero a && not (displayZeroCommodity opts) = (WideBuilder (TB.singleton '0') 1,"") (quantity',c) | amountLooksZero a && not (displayZeroCommodity opts) = (WideBuilder (TB.singleton '0') 1,"")
| otherwise = (quantity, quoteCommoditySymbolIfNeeded $ acommodity a) | otherwise = (quantity, quoteCommoditySymbolIfNeeded $ acommodity a)
space = if not (T.null c) && ascommodityspaced style then WideBuilder (TB.singleton ' ') 1 else mempty space = if not (T.null c) && ascommodityspaced style then WideBuilder (TB.singleton ' ') 1 else mempty
c' = WideBuilder (TB.fromText c) (textWidth c)
showC l r = if isJust (displayOrder opts) then mempty else l <> r showC l r = if isJust (displayOrder opts) then mempty else l <> r
price = if displayPrice opts then showAmountPrice a else mempty price = if displayPrice opts then showAmountPrice a else mempty
color = if displayColour opts && isNegativeAmount a then colorB Dull Red else id color = if displayColour opts && isNegativeAmount a then colorB Dull Red else id

View File

@ -89,6 +89,7 @@ import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB import qualified Data.Text.Lazy.Builder as TB
import Data.Time.Calendar (Day) import Data.Time.Calendar (Day)
import Safe (headDef, maximumDef) import Safe (headDef, maximumDef)
import Text.DocLayout (realLength)
import Text.Tabular.AsciiWide import Text.Tabular.AsciiWide
@ -255,7 +256,7 @@ postingAsLines elideamount onelineamounts acctwidth amtwidth p =
assertion = maybe mempty ((WideBuilder (TB.singleton ' ') 1 <>).showBalanceAssertion) $ pbalanceassertion p assertion = maybe mempty ((WideBuilder (TB.singleton ' ') 1 <>).showBalanceAssertion) $ pbalanceassertion p
-- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned -- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned
statusandaccount = lineIndent . fitText (Just $ 2 + acctwidth) Nothing False True $ pstatusandacct p statusandaccount = lineIndent . fitText (Just $ 2 + acctwidth) Nothing False True $ pstatusandacct p
thisacctwidth = textWidth $ pacctstr p thisacctwidth = realLength $ pacctstr p
pacctstr p' = showAccountName Nothing (ptype p') (paccount p') pacctstr p' = showAccountName Nothing (ptype p') (paccount p')
pstatusandacct p' = pstatusprefix p' <> pacctstr p' pstatusandacct p' = pstatusprefix p' <> pacctstr p'

View File

@ -42,7 +42,7 @@ import Text.Printf (printf)
import Hledger.Utils.Parse import Hledger.Utils.Parse
import Hledger.Utils.Regex (toRegex', regexReplace) import Hledger.Utils.Regex (toRegex', regexReplace)
import Text.WideString (charWidth, strWidth) import Text.DocLayout (charWidth, realLength)
-- | Take elements from the end of a list. -- | Take elements from the end of a list.
@ -174,6 +174,10 @@ takeWidth w (c:cs) | cw <= w = c:takeWidth (w-cw) cs
strWidthAnsi :: String -> Int strWidthAnsi :: String -> Int
strWidthAnsi = strWidth . stripAnsi strWidthAnsi = strWidth . stripAnsi
-- | Alias for 'realLength'.
strWidth :: String -> Int
strWidth = realLength
-- | Strip ANSI escape sequences from a string. -- | Strip ANSI escape sequences from a string.
-- --
-- >>> stripAnsi "\ESC[31m-1\ESC[m" -- >>> stripAnsi "\ESC[31m-1\ESC[m"

View File

@ -43,7 +43,6 @@ module Hledger.Utils.Text
wbToText, wbToText,
wbFromText, wbFromText,
wbUnpack, wbUnpack,
textWidth,
textTakeWidth, textTakeWidth,
-- * Reading -- * Reading
readDecimal, readDecimal,
@ -58,12 +57,13 @@ import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB import qualified Data.Text.Lazy.Builder as TB
import Text.DocLayout (charWidth, realLength)
import Test.Tasty (testGroup) import Test.Tasty (testGroup)
import Test.Tasty.HUnit ((@?=), testCase) import Test.Tasty.HUnit ((@?=), testCase)
import Text.Tabular.AsciiWide import Text.Tabular.AsciiWide
(Align(..), Header(..), Properties(..), TableOpts(..), renderRow, textCell) (Align(..), Header(..), Properties(..), TableOpts(..), renderRow, textCell)
import Text.WideString (WideBuilder(..), wbToText, wbFromText, wbUnpack, charWidth, textWidth) import Text.WideString (WideBuilder(..), wbToText, wbFromText, wbUnpack)
-- lowercase, uppercase :: String -> String -- lowercase, uppercase :: String -> String
@ -206,7 +206,7 @@ fitText mminwidth mmaxwidth ellipsify rightside = clip . pad
clip s = clip s =
case mmaxwidth of case mmaxwidth of
Just w Just w
| textWidth s > w -> | realLength s > w ->
if rightside if rightside
then textTakeWidth (w - T.length ellipsis) s <> ellipsis then textTakeWidth (w - T.length ellipsis) s <> ellipsis
else ellipsis <> T.reverse (textTakeWidth (w - T.length ellipsis) $ T.reverse s) else ellipsis <> T.reverse (textTakeWidth (w - T.length ellipsis) $ T.reverse s)
@ -224,7 +224,7 @@ fitText mminwidth mmaxwidth ellipsify rightside = clip . pad
else T.replicate (w - sw) " " <> s else T.replicate (w - sw) " " <> s
| otherwise -> s | otherwise -> s
Nothing -> s Nothing -> s
where sw = textWidth s where sw = realLength s
-- | Double-width-character-aware string truncation. Take as many -- | Double-width-character-aware string truncation. Take as many
-- characters as possible from a string without exceeding the -- characters as possible from a string without exceeding the

View File

@ -35,7 +35,7 @@ import qualified Data.Text.Lazy as TL
import Data.Text.Lazy.Builder (Builder, fromString, fromText, singleton, toLazyText) import Data.Text.Lazy.Builder (Builder, fromString, fromText, singleton, toLazyText)
import Safe (maximumMay) import Safe (maximumMay)
import Text.Tabular import Text.Tabular
import Text.WideString (WideBuilder(..), wbFromText, textWidth) import Text.WideString (WideBuilder(..), wbFromText)
-- | The options to use for rendering a table. -- | The options to use for rendering a table.
@ -63,7 +63,7 @@ emptyCell = Cell TopRight []
-- | Create a single-line cell from the given contents with its natural width. -- | Create a single-line cell from the given contents with its natural width.
textCell :: Align -> Text -> Cell textCell :: Align -> Text -> Cell
textCell a x = Cell a . map (\x -> WideBuilder (fromText x) (textWidth x)) $ if T.null x then [""] else T.lines x textCell a x = Cell a . map wbFromText $ if T.null x then [""] else T.lines x
-- | Create a multi-line cell from the given contents with its natural width. -- | Create a multi-line cell from the given contents with its natural width.
textsCell :: Align -> [Text] -> Cell textsCell :: Align -> [Text] -> Cell

View File

@ -1,10 +1,6 @@
-- | Calculate the width of String and Text, being aware of wide characters. -- | Calculate the width of String and Text, being aware of wide characters.
module Text.WideString ( module Text.WideString (
-- * wide-character-aware layout
strWidth,
textWidth,
charWidth,
-- * Text Builders which keep track of length -- * Text Builders which keep track of length
WideBuilder(..), WideBuilder(..),
wbUnpack, wbUnpack,
@ -16,6 +12,7 @@ import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB import qualified Data.Text.Lazy.Builder as TB
import Text.DocLayout (realLength)
-- | Helper for constructing Builders while keeping track of text width. -- | Helper for constructing Builders while keeping track of text width.
@ -36,68 +33,8 @@ wbToText = TL.toStrict . TB.toLazyText . wbBuilder
-- | Convert a strict Text to a WideBuilder. -- | Convert a strict Text to a WideBuilder.
wbFromText :: Text -> WideBuilder wbFromText :: Text -> WideBuilder
wbFromText t = WideBuilder (TB.fromText t) (textWidth t) wbFromText t = WideBuilder (TB.fromText t) (realLength t)
-- | Convert a WideBuilder to a String. -- | Convert a WideBuilder to a String.
wbUnpack :: WideBuilder -> String wbUnpack :: WideBuilder -> String
wbUnpack = TL.unpack . TB.toLazyText . wbBuilder wbUnpack = TL.unpack . TB.toLazyText . wbBuilder
-- | Calculate the render width of a string, considering
-- wide characters (counted as double width)
strWidth :: String -> Int
strWidth = foldr (\a b -> charWidth a + b) 0
-- | Calculate the render width of a string, considering
-- wide characters (counted as double width)
textWidth :: Text -> Int
textWidth = T.foldr (\a b -> charWidth a + b) 0
-- from Pandoc (copyright John MacFarlane, GPL)
-- see also http://unicode.org/reports/tr11/#Description
-- | Get the designated render width of a character: 0 for a combining
-- character, 1 for a regular character, 2 for a wide character.
-- (Wide characters are rendered as exactly double width in apps and
-- fonts that support it.) (From Pandoc.)
charWidth :: Char -> Int
charWidth c
| c < '\x0300' = 1
| c >= '\x0300' && c <= '\x036F' = 0 -- combining
| c >= '\x0370' && c <= '\x10FC' = 1
| c >= '\x1100' && c <= '\x115F' = 2
| c >= '\x1160' && c <= '\x11A2' = 1
| c >= '\x11A3' && c <= '\x11A7' = 2
| c >= '\x11A8' && c <= '\x11F9' = 1
| c >= '\x11FA' && c <= '\x11FF' = 2
| c >= '\x1200' && c <= '\x2328' = 1
| c >= '\x2329' && c <= '\x232A' = 2
| c >= '\x232B' && c <= '\x2E31' = 1
| c >= '\x2E80' && c <= '\x303E' = 2
| c == '\x303F' = 1
| c >= '\x3041' && c <= '\x3247' = 2
| c >= '\x3248' && c <= '\x324F' = 1 -- ambiguous
| c >= '\x3250' && c <= '\x4DBF' = 2
| c >= '\x4DC0' && c <= '\x4DFF' = 1
| c >= '\x4E00' && c <= '\xA4C6' = 2
| c >= '\xA4D0' && c <= '\xA95F' = 1
| c >= '\xA960' && c <= '\xA97C' = 2
| c >= '\xA980' && c <= '\xABF9' = 1
| c >= '\xAC00' && c <= '\xD7FB' = 2
| c >= '\xD800' && c <= '\xDFFF' = 1
| c >= '\xE000' && c <= '\xF8FF' = 1 -- ambiguous
| c >= '\xF900' && c <= '\xFAFF' = 2
| c >= '\xFB00' && c <= '\xFDFD' = 1
| c >= '\xFE00' && c <= '\xFE0F' = 1 -- ambiguous
| c >= '\xFE10' && c <= '\xFE19' = 2
| c >= '\xFE20' && c <= '\xFE26' = 1
| c >= '\xFE30' && c <= '\xFE6B' = 2
| c >= '\xFE70' && c <= '\xFEFF' = 1
| c >= '\xFF01' && c <= '\xFF60' = 2
| c >= '\xFF61' && c <= '\x16A38' = 1
| c >= '\x1B000' && c <= '\x1B001' = 2
| c >= '\x1D000' && c <= '\x1F1FF' = 1
| c >= '\x1F200' && c <= '\x1F251' = 2
| c >= '\x1F300' && c <= '\x1F773' = 1
| c >= '\x20000' && c <= '\x3FFFD' = 2
| otherwise = 1

View File

@ -109,6 +109,7 @@ library
, containers >=0.5.9 , containers >=0.5.9
, data-default >=0.5 , data-default >=0.5
, directory , directory
, doclayout ==0.3.*
, extra >=1.6.3 , extra >=1.6.3
, file-embed >=0.0.10 , file-embed >=0.0.10
, filepath , filepath
@ -158,6 +159,7 @@ test-suite doctest
, containers >=0.5.9 , containers >=0.5.9
, data-default >=0.5 , data-default >=0.5
, directory , directory
, doclayout ==0.3.*
, doctest >=0.18.1 , doctest >=0.18.1
, extra >=1.6.3 , extra >=1.6.3
, file-embed >=0.0.10 , file-embed >=0.0.10
@ -210,6 +212,7 @@ test-suite unittest
, containers >=0.5.9 , containers >=0.5.9
, data-default >=0.5 , data-default >=0.5
, directory , directory
, doclayout ==0.3.*
, extra >=1.6.3 , extra >=1.6.3
, file-embed >=0.0.10 , file-embed >=0.0.10
, filepath , filepath

View File

@ -47,6 +47,7 @@ dependencies:
- data-default >=0.5 - data-default >=0.5
- Decimal >=0.5.1 - Decimal >=0.5.1
- directory - directory
- doclayout >=0.3 && <0.4
- file-embed >=0.0.10 - file-embed >=0.0.10
- filepath - filepath
- hashtables >=1.2.3.1 - hashtables >=1.2.3.1

View File

@ -26,6 +26,7 @@ import Lens.Micro.Platform
import Safe import Safe
import System.Console.ANSI import System.Console.ANSI
import System.FilePath (takeFileName) import System.FilePath (takeFileName)
import Text.DocLayout (realLength)
import Hledger import Hledger
import Hledger.Cli hiding (progname,prognameandversion) import Hledger.Cli hiding (progname,prognameandversion)
@ -122,7 +123,7 @@ asDraw UIState{aopts=_uopts@UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec}}
- 2 -- XXX due to margin ? shouldn't be necessary (cf UIUtils) - 2 -- XXX due to margin ? shouldn't be necessary (cf UIUtils)
displayitems = s ^. asList . listElementsL displayitems = s ^. asList . listElementsL
acctwidths = V.map (\AccountsScreenItem{..} -> asItemIndentLevel + Hledger.Cli.textWidth asItemDisplayAccountName) displayitems acctwidths = V.map (\AccountsScreenItem{..} -> asItemIndentLevel + realLength asItemDisplayAccountName) displayitems
balwidths = V.map (maybe 0 (wbWidth . showMixedAmountB oneLine) . asItemMixedAmount) displayitems balwidths = V.map (maybe 0 (wbWidth . showMixedAmountB oneLine) . asItemMixedAmount) displayitems
preferredacctwidth = V.maximum acctwidths preferredacctwidth = V.maximum acctwidths
totalacctwidthseen = V.sum acctwidths totalacctwidthseen = V.sum acctwidths

View File

@ -73,6 +73,7 @@ executable hledger-ui
, containers >=0.5.9 , containers >=0.5.9
, data-default , data-default
, directory , directory
, doclayout ==0.3.*
, extra >=1.6.3 , extra >=1.6.3
, filepath , filepath
, fsnotify >=0.2.1.2 && <0.4 , fsnotify >=0.2.1.2 && <0.4

View File

@ -50,6 +50,7 @@ dependencies:
- containers >=0.5.9 - containers >=0.5.9
- data-default - data-default
- directory - directory
- doclayout >=0.3 && <0.4
- extra >=1.6.3 - extra >=1.6.3
- filepath - filepath
- fsnotify >=0.2.1.2 && <0.4 - fsnotify >=0.2.1.2 && <0.4

View File

@ -15,6 +15,8 @@ packages:
extra-deps: extra-deps:
# for Shake.hs (regex doesn't support base-compat-0.11): # for Shake.hs (regex doesn't support base-compat-0.11):
- regex-1.0.2.0@rev:1 - regex-1.0.2.0@rev:1
- doclayout-0.3.1.1
- emojis-0.1.2
# for testing base-compat 0.11 compatibility (mutually exclusive with the above): # for testing base-compat 0.11 compatibility (mutually exclusive with the above):
# - aeson-1.4.6.0 # - aeson-1.4.6.0
# - aeson-compat-0.3.9 # - aeson-compat-0.3.9

View File

@ -17,6 +17,8 @@ extra-deps:
- pretty-simple-4.0.0.0 - pretty-simple-4.0.0.0
- prettyprinter-1.7.0 - prettyprinter-1.7.0
- doctest-0.18.1 - doctest-0.18.1
- doclayout-0.3.1.1
- emojis-0.1.2
# for hledger: # for hledger:
# for hledger-ui: # for hledger-ui:
# for hledger-web: # for hledger-web: