feat: add missing KF6 framework recipes
This commit is contained in:
+793
@@ -0,0 +1,793 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Jonathan Poelen <jonathan.poelen@gmail.com>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
def parse_scores(s: str) -> list[int]:
|
||||
return [int(x) for x in s.split(',')]
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
description='''Contrast checker for themes
|
||||
|
||||
Allows you to view all the colors and backgrounds applied to a theme and rate the contrast based on the APCA (Accessible Perceptual Contrast Algorithm) used in WCAG 3. A very low score is a sign of poor contrast, and can make reading difficult or impossible.
|
||||
|
||||
However, color perception depends on the individual, hardware or software configurations (night/blue light filter), lighting or simply surrounding colors. For example, low contrast may remain legible in the editor when not surrounded by bright color.
|
||||
|
||||
There are 3 options for modifying the contract result:
|
||||
|
||||
-a / --add-luminance and -p / --add-percent-luminance to directly modify the output value. For example, -p -14 -a 15 increases the constrast a little when it's low and very little when it's high.
|
||||
|
||||
-C / --color-space Okl selects a color space with different properties, mainly on the color red.
|
||||
''')
|
||||
|
||||
parser.add_argument('-f', '--bg', metavar='BACKGROUND', action='append',
|
||||
help='show only the specified background color styles')
|
||||
parser.add_argument('-l', '--language', metavar='LANGUAGE', action='append',
|
||||
help='show only the specified language')
|
||||
|
||||
parser.add_argument('-c', '--no-custom-styles', action='store_true',
|
||||
help='do not display custom languages')
|
||||
parser.add_argument('-s', '--no-standard-styles', action='store_true',
|
||||
help='do not display standard colors')
|
||||
parser.add_argument('-b', '--no-borders', action='store_true',
|
||||
help='do not display border colors')
|
||||
parser.add_argument('-H', '--no-legend', action='store_true',
|
||||
help='do not display legend')
|
||||
|
||||
parser.add_argument('-M', '--min-luminance', metavar='LUMINANCE', type=float, default=0,
|
||||
help='only displays colors with a higher luminance')
|
||||
parser.add_argument('-L', '--max-luminance', metavar='LUMINANCE', type=float, default=110.0,
|
||||
help='only displays colors with a lower luminance')
|
||||
|
||||
parser.add_argument('-a', '--add-luminance', metavar='LUMINANCE', type=float, default=0,
|
||||
help='add fixed value for luminance')
|
||||
parser.add_argument('-p', '--add-percent-luminance', metavar='LUMINANCE', type=float, default=0,
|
||||
help='add percent luminance. Apply before --add-luminance')
|
||||
parser.add_argument('-S', '--scores', metavar='SCORES', type=parse_scores,
|
||||
help='modify ratings values. Expects a comma-separated list of numbers. The order of the list is the same as in the legend.')
|
||||
|
||||
# sRGB is W3 in APCA
|
||||
parser.add_argument('-C', '--color-space', default='sRGB',
|
||||
choices=['sRGB', 'DisplayP3', 'AdobeRGB', 'Rec2020', 'Okl'],
|
||||
help='select a color space ; Okl is a color space that increases the contrast of red with black or blue background and decreases it with white or green background')
|
||||
|
||||
parser.add_argument('-d', '--compute-diff', action='store_true',
|
||||
help='compute luminance between 2 colors or more ; the first color represents the background, the others the foreground')
|
||||
|
||||
parser.add_argument('-F', '--output-format', default='ansi', choices=['ansi', 'html'])
|
||||
parser.add_argument('-T', '--html-title', help='title of html page when --output-format=html')
|
||||
|
||||
parser.add_argument('themes_or_colors', metavar='THEME_OR_COLOR', nargs='+',
|
||||
help='a .theme file or a color (#rgb, #rrggbb, #argb, #aarrggbb) when -d / --compute-diff is used')
|
||||
|
||||
args = parser.parse_intermixed_args()
|
||||
|
||||
|
||||
RGBColor = tuple[int, int, int]
|
||||
|
||||
def parse_rgb_color(color: str, bg: RGBColor) -> RGBColor:
|
||||
n = len(color)
|
||||
|
||||
if not color.startswith('#') or n not in (7, 9, 4, 5):
|
||||
raise Exception(f'Invalid argb format: {color}')
|
||||
|
||||
try:
|
||||
argb = int(color[1:], 16)
|
||||
except ValueError:
|
||||
raise Exception(f'Invalid argb format: {color}')
|
||||
|
||||
# format: #rrggbb or #aarrggbb
|
||||
if n == 7 or n == 9:
|
||||
result = (
|
||||
(argb >> 16) & 0xff,
|
||||
(argb >> 8) & 0xff,
|
||||
(argb ) & 0xff,
|
||||
)
|
||||
if n == 7:
|
||||
return result
|
||||
alpha = argb >> 24
|
||||
|
||||
# format: #rgb or #argb
|
||||
else:
|
||||
(r, g, b) = (
|
||||
(argb >> 8) & 0xf,
|
||||
(argb >> 4) & 0xf,
|
||||
(argb ) & 0xf,
|
||||
)
|
||||
result = ((r << 4 | r), (g << 4 | g), (b << 4 | b))
|
||||
if n == 4:
|
||||
return result
|
||||
alpha = argb >> 12
|
||||
alpha |= alpha << 4
|
||||
|
||||
# alpha blend
|
||||
return (
|
||||
(alpha * result[0] + (255 - alpha) * bg[0]) // 255,
|
||||
(alpha * result[1] + (255 - alpha) * bg[1]) // 255,
|
||||
(alpha * result[2] + (255 - alpha) * bg[2]) // 255,
|
||||
)
|
||||
|
||||
|
||||
# based on https://drafts.csswg.org/css-color/#color-conversion-code (CSS 4)
|
||||
# 17. Sample code for Color Conversions
|
||||
|
||||
def lin_sRGB(c: int) -> float:
|
||||
# [0, 255] to [0, 1]
|
||||
v = c / 255.0
|
||||
if v <= 0.04045:
|
||||
return v / 12.92
|
||||
return ((v + 0.055) / 1.055) ** 2.4
|
||||
|
||||
sRGB_to_Y_mat = (
|
||||
0.21263900587151036,
|
||||
0.71516867876775592,
|
||||
0.072192315360733714,
|
||||
)
|
||||
|
||||
DisplayP3_to_Y_mat = (
|
||||
0.22897456406974884,
|
||||
0.69173852183650619,
|
||||
0.079286914093744998,
|
||||
)
|
||||
|
||||
# not in CSS
|
||||
Okl_to_Y_mat = (
|
||||
# These values are the formula which calculates `l` in the XYZ to Okalab transformation.
|
||||
# (https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab)
|
||||
# Specifically taking these values doesn't really make sense, but compared to sRGB
|
||||
# - the luminance between red and green will be very greatly decreased
|
||||
# - the luminance between red and white will be decreased
|
||||
# - the luminance between red and blue / black color will be greatly increased
|
||||
# - the luminance between blue and green will be decreased
|
||||
0.4122214708,
|
||||
0.5363325363,
|
||||
0.0514459929,
|
||||
)
|
||||
|
||||
def lin_AdobeRGB(c: int) -> float:
|
||||
# [0, 255] to [0, 1]
|
||||
return (c / 255.0) ** 2.19921875
|
||||
|
||||
AdobeRGB_to_Y_mat = (
|
||||
0.29734497525053616,
|
||||
0.62736356625546597,
|
||||
0.07529145849399789,
|
||||
)
|
||||
|
||||
def lin_Rec2020(c: int) -> float:
|
||||
# [0, 255] to [0, 1]
|
||||
v = c / 255.0
|
||||
|
||||
if v < 0.08124285829863151:
|
||||
return v / 4.5
|
||||
|
||||
return ((v + 0.09929682680944) / 1.09929682680944) ** (1 / 0.45)
|
||||
|
||||
Rec2020_to_Y_mat = (
|
||||
0.26270021201126703,
|
||||
0.67799807151887104,
|
||||
0.059301716469861945,
|
||||
)
|
||||
|
||||
def make_to_Y(lin, mat):
|
||||
def to_Y(rgb: RGBColor) -> float:
|
||||
return (
|
||||
mat[0] * lin(rgb[0]) +
|
||||
mat[1] * lin(rgb[1]) +
|
||||
mat[2] * lin(rgb[2])
|
||||
)
|
||||
return to_Y
|
||||
|
||||
|
||||
if args.color_space == 'sRGB':
|
||||
rgb_to_Y = make_to_Y(lin_sRGB, sRGB_to_Y_mat)
|
||||
elif args.color_space == 'Okl':
|
||||
rgb_to_Y = make_to_Y(lin_sRGB, Okl_to_Y_mat)
|
||||
elif args.color_space == 'DisplayP3':
|
||||
rgb_to_Y = make_to_Y(lin_sRGB, DisplayP3_to_Y_mat)
|
||||
elif args.color_space == 'AdobeRGB':
|
||||
rgb_to_Y = make_to_Y(lin_AdobeRGB, AdobeRGB_to_Y_mat)
|
||||
else: # Rec2020
|
||||
rgb_to_Y = make_to_Y(lin_Rec2020, Rec2020_to_Y_mat)
|
||||
|
||||
|
||||
# https://github.com/Myndex/apca-w3
|
||||
|
||||
# G-4g constants for use with 2.4 exponent
|
||||
normBG = 0.56
|
||||
normTXT = 0.57
|
||||
revTXT = 0.62
|
||||
revBG = 0.65
|
||||
|
||||
# G-4g Clamps and Scalers
|
||||
blkThrs = 0.022
|
||||
blkClmp = 1.414
|
||||
scaleBoW = 1.14
|
||||
scaleWoB = 1.14
|
||||
loBoWoffset = 0.027
|
||||
loWoBoffset = 0.027
|
||||
deltaYmin = 0.0005
|
||||
loClip = 0.0 # originally 0.1, but this limits the contrast to 7.5
|
||||
|
||||
def APCA_contrast(txtY: float, bgY: float) -> float:
|
||||
## BLACK SOFT CLAMP
|
||||
|
||||
# Soft clamps Y for either color if it is near black.
|
||||
|
||||
if txtY <= blkThrs:
|
||||
txtY += (blkThrs - txtY) ** blkClmp
|
||||
|
||||
if bgY <= blkThrs:
|
||||
bgY += (blkThrs - bgY) ** blkClmp
|
||||
|
||||
# Return 0 Early for extremely low ∆Y
|
||||
if abs(bgY - txtY) < deltaYmin:
|
||||
return 0.0
|
||||
|
||||
## APCA/SAPC CONTRAST - LOW CLIP (W3 LICENSE)
|
||||
|
||||
# For normal polarity, black text on white (BoW)
|
||||
# Calculate the SAPC contrast value and scale
|
||||
if bgY > txtY:
|
||||
SAPC = (bgY ** normBG - txtY ** normTXT) * scaleBoW
|
||||
|
||||
# Low Contrast smooth rollout to prevent polarity reversal
|
||||
# and also a low-clip for very low contrasts
|
||||
outputContrast = 0.0 if SAPC < loClip else SAPC - loBoWoffset
|
||||
|
||||
# For reverse polarity, light text on dark (WoB)
|
||||
# WoB should always return negative value.
|
||||
else:
|
||||
SAPC = (bgY ** revBG - txtY ** revTXT) * scaleWoB
|
||||
|
||||
outputContrast = 0.0 if SAPC > -loClip else SAPC + loWoBoffset
|
||||
|
||||
return outputContrast * 100.0
|
||||
|
||||
|
||||
class ColorInfo:
|
||||
text: str = '#000'
|
||||
color: RGBColor = (0, 0, 0)
|
||||
Y: float = 0
|
||||
|
||||
def __init__(self, rgb: str, bg: RGBColor):
|
||||
self.text = rgb
|
||||
self.color = parse_rgb_color(rgb, bg)
|
||||
self.Y = rgb_to_Y(self.color)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.text
|
||||
|
||||
|
||||
NORMAL_LUMINANCE = (90, 75, 60, 45, 35)
|
||||
BOLD_LUMINANCE = (80, 65, 50, 38, 30)
|
||||
SPELL_LUMINANCE = (70, 55, 40, 30, 25)
|
||||
DECORATION_LUMINANCE = (60, 45, 30, 15, 10)
|
||||
|
||||
if args.scores:
|
||||
scores_update_len = min(20, len(args.scores))
|
||||
scores = [*NORMAL_LUMINANCE, *BOLD_LUMINANCE, *SPELL_LUMINANCE, *DECORATION_LUMINANCE]
|
||||
scores[:scores_update_len] = args.scores[:scores_update_len]
|
||||
NORMAL_LUMINANCE = scores[0:5]
|
||||
BOLD_LUMINANCE = scores[5:10]
|
||||
SPELL_LUMINANCE = scores[10:15]
|
||||
DECORATION_LUMINANCE = scores[15:20]
|
||||
|
||||
BOLD_TEXT = (BOLD_LUMINANCE, True, 'Text. ▐')
|
||||
NORMAL_TEXT = (NORMAL_LUMINANCE, False, 'Text. ▐')
|
||||
DECORATION_TEXT = (DECORATION_LUMINANCE, False, 'Text. ▐')
|
||||
HEADER = (
|
||||
'\x1b[35m'
|
||||
' Background |'
|
||||
' Foreground |'
|
||||
f' {(args.add_luminance or args.add_percent_luminance) and " Luminance " or " Lum "} |'
|
||||
' Score\x1b[m'
|
||||
)
|
||||
|
||||
RANK1 = '\x1b[32m'
|
||||
RANK2 = '\x1b[32m'
|
||||
RANK3 = '\x1b[32m'
|
||||
RANK4 = '\x1b[33m'
|
||||
RANK5 = '\x1b[31m'
|
||||
RANK6 = '\x1b[31;1m'
|
||||
|
||||
def ffloat(x: float) -> str:
|
||||
s = f'{x:>5.1f}\x1b[m'
|
||||
return s.replace('.', '\x1b[37m.')
|
||||
|
||||
def flum(luminance: float,
|
||||
add_luminance: float,
|
||||
add_percent_luminance: float,
|
||||
bg: ColorInfo,
|
||||
fg: ColorInfo,
|
||||
luminance_values: tuple[int, int, int, int, int],
|
||||
is_bold: bool,
|
||||
sample_text: str
|
||||
) -> str:
|
||||
"""
|
||||
Compute and formate a score
|
||||
"""
|
||||
luminance = abs(luminance)
|
||||
adjusted_luminance = luminance
|
||||
|
||||
adjusted_text = ''
|
||||
if add_luminance or add_percent_luminance:
|
||||
adjusted_luminance += adjusted_luminance * add_percent_luminance + add_luminance
|
||||
adjusted_luminance = max(0, min(108, adjusted_luminance))
|
||||
adjusted_text = f' \x1b[35m->\x1b[m {ffloat(adjusted_luminance)}'
|
||||
|
||||
(AAAA, AAA, AA, BAD, VERY_BAD) = luminance_values
|
||||
if adjusted_luminance >= AAAA:
|
||||
score = f'{RANK1}AAAA'
|
||||
elif adjusted_luminance >= AAA:
|
||||
score = f'{RANK2}AAA '
|
||||
elif adjusted_luminance >= AA:
|
||||
score = f'{RANK3}AA '
|
||||
elif adjusted_luminance >= BAD:
|
||||
score = f'{RANK4}A '
|
||||
elif adjusted_luminance >= VERY_BAD:
|
||||
score = f'{RANK5}FAIL'
|
||||
else:
|
||||
score = f'{RANK6}FAIL'
|
||||
|
||||
(r1, g1, b1) = fg.color
|
||||
(r2, g2, b2) = bg.color
|
||||
bold = ';1' if is_bold else ''
|
||||
return f'{ffloat(luminance)}{adjusted_text} | {score}\x1b[m ' \
|
||||
f'\x1b[38;2;{r1};{g1};{b1};48;2;{r2};{g2};{b2}{bold}m {sample_text} \x1b[m'
|
||||
|
||||
def color2ansi(rgb: RGBColor) -> str:
|
||||
return f'{rgb[0]};{rgb[1]};{rgb[2]}'
|
||||
|
||||
spaces = ' '
|
||||
def fcol_impl(name: str, rgb: str, n: int) -> str:
|
||||
w = spaces[0: n - (len(name) + len(rgb) + 3)]
|
||||
return f'{name} \x1b[37m({rgb})\x1b[m{w}'
|
||||
|
||||
def fcol1(name: str, rgb: str) -> str:
|
||||
return fcol_impl(name, rgb, 38)
|
||||
|
||||
def fcol2(name: str, rgb: str) -> str:
|
||||
return fcol_impl(name, rgb, 43)
|
||||
|
||||
def create_tab_from_text_styles(min_luminance: float,
|
||||
max_luminance: float,
|
||||
add_luminance: float,
|
||||
add_percent_luminance: float,
|
||||
col1: str,
|
||||
kstyle: str,
|
||||
bg: ColorInfo,
|
||||
text_styles: dict[str, dict[str, str | bool]]
|
||||
) -> str:
|
||||
lines = []
|
||||
for name, defs in sorted(text_styles.items()):
|
||||
if style := defs.get(kstyle):
|
||||
fg = ColorInfo(style, bg.color)
|
||||
lum = APCA_contrast(fg.Y, bg.Y)
|
||||
if min_luminance <= abs(lum) <= max_luminance:
|
||||
bold = defs.get('bold', False)
|
||||
result = flum(lum, add_luminance, add_percent_luminance,
|
||||
bg, fg, *(BOLD_TEXT if bold else NORMAL_TEXT))
|
||||
lines.append(f' {col1} | {fcol2(name, fg.text)} | {result}')
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
output = []
|
||||
|
||||
def run_borders(
|
||||
min_luminance: float,
|
||||
max_luminance: float,
|
||||
add_luminance: float,
|
||||
add_percent_luminance: float,
|
||||
editor_colors: dict[str, str],
|
||||
bg_editor: RGBColor
|
||||
) -> None:
|
||||
output.append('\n\x1b[34mIcon Border\x1b[m:\n')
|
||||
|
||||
bg_icon = ColorInfo(editor_colors['IconBorder'], (0, 0, 0))
|
||||
bg_current_line = ColorInfo(editor_colors['CurrentLine'], bg_editor.color)
|
||||
bg_current_line_border = ColorInfo(editor_colors['CurrentLine'], bg_icon.color)
|
||||
|
||||
fg_line = ColorInfo(editor_colors['LineNumbers'], bg_icon.color)
|
||||
fg_current = ColorInfo(editor_colors['CurrentLineNumber'], bg_icon.color)
|
||||
fg_separator = ColorInfo(editor_colors['Separator'], bg_icon.color)
|
||||
fg_modified = ColorInfo(editor_colors['ModifiedLines'], bg_icon.color)
|
||||
fg_saved = ColorInfo(editor_colors['SavedLines'], bg_icon.color)
|
||||
|
||||
xbg = color2ansi(bg_icon.color)
|
||||
xborder = f'\x1b[38;2;{color2ansi(fg_line.color)};48;2;{xbg}m'
|
||||
xsaved = f'\x1b[48;2;{color2ansi(fg_saved.color)};38;2;{xbg}m▋{xborder}'
|
||||
xmodified = f'\x1b[48;2;{color2ansi(fg_modified.color)};38;2;{xbg}m▋{xborder}'
|
||||
xeditor = f'\x1b[38;2;{color2ansi(fg_separator.color)}m▕\x1b[m'\
|
||||
f'\x1b[48;2;{color2ansi(bg_editor.color)}m \x1b[m'
|
||||
|
||||
cbg = color2ansi(bg_current_line_border.color)
|
||||
cborder = f'\x1b[38;2;{color2ansi(fg_current.color)};48;2;{cbg}m'
|
||||
csaved = f'\x1b[48;2;{color2ansi(fg_saved.color)};38;2;{cbg}m▋{cborder}'
|
||||
cmodified = f'\x1b[48;2;{color2ansi(fg_modified.color)};38;2;{cbg}m▋{cborder}'
|
||||
ceditor = f'\x1b[38;2;{color2ansi(fg_separator.color)}m▕\x1b[m'\
|
||||
f'\x1b[48;2;{color2ansi(bg_current_line.color)}m \x1b[m'
|
||||
|
||||
# imitates the border of Kate editor
|
||||
output.append(f' LineNumbers: {xborder} 42 {xeditor} bg: IconBorder | BackgroundColor\n'
|
||||
f' CurrentLineNumber: {cborder} 43 {ceditor} bg: CurrentLine | CurrentLine\n'
|
||||
f' LineNumbers: {xborder} 44{xmodified}{xeditor} (ModifiedLines)\n'
|
||||
f' CurrentLineNumber: {cborder} 45{cmodified}{ceditor}\n'
|
||||
f' LineNumbers: {xborder} 46{xsaved}{xeditor} (SavedLines)\n'
|
||||
f' CurrentLineNumber: {cborder} 47{csaved}{ceditor}\n'
|
||||
f' ⧹ Separator\n\n{HEADER}\n')
|
||||
|
||||
color_line_number = ('LineNumbers', fg_line, NORMAL_TEXT)
|
||||
colors = (
|
||||
('CurrentLineNumber', fg_current, NORMAL_TEXT),
|
||||
('ModifiedLines', fg_modified, DECORATION_TEXT),
|
||||
('SavedLines', fg_saved, DECORATION_TEXT),
|
||||
)
|
||||
|
||||
for name, bg, colors in (
|
||||
('IconBorder', bg_icon, (color_line_number, *colors)),
|
||||
('CurrentLine', bg_current_line_border, colors),
|
||||
):
|
||||
col = fcol1(name, bg.text)
|
||||
lines = []
|
||||
for k, fg, text_data in colors:
|
||||
lum = APCA_contrast(fg.Y, bg.Y)
|
||||
if min_luminance <= abs(lum) <= max_luminance:
|
||||
result = flum(lum, add_luminance, add_percent_luminance,
|
||||
bg, fg, *text_data)
|
||||
lines.append(f' {col} | {fcol2(k, fg.text)} | {result}')
|
||||
|
||||
if lines:
|
||||
output.append('\n'.join(lines))
|
||||
output.append('\n\n')
|
||||
|
||||
# table for Separator color
|
||||
for name, bg in (
|
||||
('IconBorder', bg_icon),
|
||||
('CurrentLine', bg_current_line_border),
|
||||
('BackgroundColor', bg_editor),
|
||||
):
|
||||
lum = APCA_contrast(fg_separator.Y, bg.Y)
|
||||
if min_luminance <= abs(lum) <= max_luminance:
|
||||
col = fcol1(name, bg.text)
|
||||
result = flum(lum, add_luminance, add_percent_luminance,
|
||||
bg, fg_separator, DECORATION_LUMINANCE, False, NORMAL_TEXT[2])
|
||||
output.append(f' {col} | {fcol2("Separator", fg_separator.text)} | {result}\n')
|
||||
|
||||
|
||||
def run(d: dict[str, str | dict[str, bool | str | dict[str, bool | str]]],
|
||||
min_luminance: float,
|
||||
max_luminance: float,
|
||||
add_luminance: float,
|
||||
add_percent_luminance: float,
|
||||
show_borders: bool,
|
||||
show_custom_styles: bool,
|
||||
show_standard_styles: bool,
|
||||
accepted_backgrounds: set[str] | None,
|
||||
accepted_languages: set[str] | None
|
||||
) -> None:
|
||||
editor_colors = d['editor-colors']
|
||||
|
||||
bg_editor = ColorInfo(editor_colors['BackgroundColor'], (0, 0, 0))
|
||||
|
||||
output.append(f'\x1b[34;1mTheme\x1b[m: {d["metadata"]["name"]}\n')
|
||||
|
||||
if show_borders:
|
||||
run_borders(min_luminance, max_luminance,
|
||||
add_luminance, add_percent_luminance,
|
||||
editor_colors, bg_editor)
|
||||
|
||||
#
|
||||
# Editor
|
||||
#
|
||||
|
||||
output.append('\n\x1b[34mText Area\x1b[m:\n')
|
||||
|
||||
editor_bg_colors = {
|
||||
k: (ColorInfo(editor_colors[k], bg_editor.color), 'text-color')
|
||||
for k in (
|
||||
'TemplateReadOnlyPlaceholder',
|
||||
'TemplatePlaceholder',
|
||||
'TemplateFocusedPlaceholder',
|
||||
'TemplateBackground',
|
||||
'MarkBookmark',
|
||||
'CodeFolding',
|
||||
'ReplaceHighlight',
|
||||
'SearchHighlight',
|
||||
'BracketMatching',
|
||||
)
|
||||
if not accepted_backgrounds or k in accepted_backgrounds
|
||||
}
|
||||
if not accepted_backgrounds or 'TextSelection' in accepted_backgrounds:
|
||||
editor_bg_colors['TextSelection'] = (
|
||||
ColorInfo(editor_colors['TextSelection'], bg_editor.color),
|
||||
'selected-text-color'
|
||||
)
|
||||
if not accepted_backgrounds or 'BackgroundColor' in accepted_backgrounds:
|
||||
editor_bg_colors['BackgroundColor'] = (bg_editor, 'text-color')
|
||||
|
||||
text_styles = d['text-styles']
|
||||
custom_styles = d.get('custom-styles', {}) if show_custom_styles else {}
|
||||
|
||||
for name, (bg, kstyle) in editor_bg_colors.items():
|
||||
col = fcol1(name, bg.text)
|
||||
|
||||
if show_standard_styles:
|
||||
tab = create_tab_from_text_styles(
|
||||
min_luminance, max_luminance,
|
||||
add_luminance, add_percent_luminance,
|
||||
col, kstyle, bg, text_styles
|
||||
)
|
||||
|
||||
#
|
||||
# Spell decoration
|
||||
#
|
||||
name = 'SpellChecking'
|
||||
fg = ColorInfo(editor_colors[name], bg_editor.color)
|
||||
lum = APCA_contrast(fg.Y, bg.Y)
|
||||
if min_luminance <= abs(lum) <= max_luminance:
|
||||
result = flum(lum, add_luminance, add_percent_luminance,
|
||||
bg, fg, SPELL_LUMINANCE, False, '~~~~~~~')
|
||||
spell_line = f' {col} | {fcol2(name, fg.text)} | {result}'
|
||||
tab = f'{tab}\n{spell_line}' if tab else spell_line
|
||||
|
||||
if tab:
|
||||
output.append(f'\n{HEADER}\n{tab}\n')
|
||||
|
||||
# table by language for custom styles
|
||||
for language, defs in sorted(custom_styles.items()):
|
||||
if accepted_languages and language not in accepted_languages:
|
||||
continue
|
||||
if tab := create_tab_from_text_styles(
|
||||
min_luminance, max_luminance,
|
||||
add_luminance, add_percent_luminance,
|
||||
col, kstyle, bg, defs
|
||||
):
|
||||
output.append(f'\n\x1b[36mLanguage: "{language}"\x1b[m:\n{tab}\n')
|
||||
|
||||
# ignored:
|
||||
# - WordWrapMarker
|
||||
# - TabMarker
|
||||
# - IndentationLine
|
||||
# - MarkBreakpointActive
|
||||
# - MarkBreakpointReached
|
||||
# - MarkBreakpointDisabled
|
||||
# - MarkExecution
|
||||
# - MarkWarning
|
||||
# - MarkError
|
||||
|
||||
def result_legend(AAAA: float, AAA: float, AA: float, BAD: float, VERY_BAD: float) -> str:
|
||||
return (
|
||||
f'{RANK1}AAAA\x1b[m (>={AAAA}) ; '
|
||||
f'{RANK2}AAA\x1b[m (>={AAA}) ; '
|
||||
f'{RANK3}AA\x1b[m (>={AA}) ; '
|
||||
f'{RANK4}A\x1b[m (>={BAD}) ; '
|
||||
f'{RANK5}FAIL\x1b[m (>={VERY_BAD}) ; '
|
||||
f'{RANK6}FAIL\x1b[m (<{VERY_BAD})'
|
||||
)
|
||||
|
||||
|
||||
if not args.no_legend:
|
||||
output.append(f'''Luminance legend:
|
||||
- Range for light theme: [0; 106]
|
||||
- Range for dark theme: [0; 108]
|
||||
- Result for normal text: {result_legend(*NORMAL_LUMINANCE)}
|
||||
- Result for bold text: {result_legend(*BOLD_LUMINANCE)}
|
||||
- Result for spelling error: {result_legend(*SPELL_LUMINANCE)}
|
||||
- Result for decoration: {result_legend(*DECORATION_LUMINANCE)}
|
||||
|
||||
Luminance adjustement: {args.add_percent_luminance:+}% {args.add_luminance:+} (see -p and -a)
|
||||
|
||||
Color space: {args.color_space}
|
||||
|
||||
''')
|
||||
|
||||
add_luminance = args.add_luminance
|
||||
add_percent_luminance = args.add_percent_luminance / 100
|
||||
|
||||
if args.compute_diff:
|
||||
bg = ColorInfo(args.themes_or_colors[0], (0,0,0))
|
||||
output.append('Background | Foreground\n')
|
||||
|
||||
# compares the background with all foreground colors in normal and bold
|
||||
for color in args.themes_or_colors[1:]:
|
||||
fg = ColorInfo(color, bg.color)
|
||||
lum = APCA_contrast(fg.Y, bg.Y)
|
||||
col = f'{bg.text:^10} | {fg.text:^10} | '
|
||||
output.append(col)
|
||||
output.append(flum(lum, add_luminance, add_percent_luminance, bg, fg, *NORMAL_TEXT))
|
||||
output.append('\n')
|
||||
output.append(col)
|
||||
output.append(flum(lum, add_luminance, add_percent_luminance, bg, fg, *BOLD_TEXT))
|
||||
output.append('\n')
|
||||
else:
|
||||
add_new_line = False
|
||||
for theme in args.themes_or_colors:
|
||||
if add_new_line:
|
||||
output.append('\n\n')
|
||||
add_new_line = True
|
||||
|
||||
# read json theme file
|
||||
try:
|
||||
if theme == '-':
|
||||
data = json.load(sys.stdin)
|
||||
else:
|
||||
with open(theme) as f:
|
||||
data = json.load(f)
|
||||
except OSError as e:
|
||||
print(f'\x1b[31m{e}\x1b[m', file=sys.stderr)
|
||||
continue
|
||||
|
||||
run(data,
|
||||
args.min_luminance,
|
||||
args.max_luminance,
|
||||
add_luminance,
|
||||
add_percent_luminance,
|
||||
not args.no_borders,
|
||||
not args.no_custom_styles,
|
||||
not args.no_standard_styles,
|
||||
args.bg and set(args.bg),
|
||||
args.language and set(args.language),
|
||||
)
|
||||
|
||||
is_html = args.output_format == 'html' and not args.compute_diff
|
||||
|
||||
output = ''.join(output)
|
||||
|
||||
if is_html:
|
||||
import re
|
||||
|
||||
ansi_to_html = {
|
||||
'1': 'bold',
|
||||
'31': 'red',
|
||||
'32': 'green',
|
||||
'33': 'orange',
|
||||
'34': 'blue',
|
||||
'35': 'purple',
|
||||
'36': 'cyan',
|
||||
'37': 'gray',
|
||||
}
|
||||
|
||||
extract_color = re.compile(r'([34])8;2;(\d+);(\d+);(\d+)')
|
||||
extract_effect = re.compile(r'\d+')
|
||||
|
||||
depth = 0
|
||||
def replace_styles(m) -> str:
|
||||
global depth
|
||||
|
||||
effects = m[1]
|
||||
if not effects:
|
||||
ret = '</span>' * depth
|
||||
depth = 0
|
||||
return ret
|
||||
|
||||
depth += 1
|
||||
colors = []
|
||||
def rgb(m) -> str:
|
||||
prop = 'color' if m[1] == '3' else 'background'
|
||||
colors.append(f'{prop}:rgb({m[2]},{m[3]},{m[4]})')
|
||||
return ''
|
||||
effects = extract_color.sub(rgb, effects)
|
||||
if colors:
|
||||
styles = ';'.join(colors)
|
||||
styles = f' style="{styles}"'
|
||||
else:
|
||||
styles = ''
|
||||
|
||||
classes = ' '.join(map(lambda s: ansi_to_html[s], extract_effect.findall(effects)))
|
||||
if classes:
|
||||
classes = f' class="{classes}"'
|
||||
|
||||
return f'<span{styles}{classes}>'
|
||||
|
||||
output = re.sub(r'\x1b\[([^m]*)m', replace_styles, output)
|
||||
|
||||
try:
|
||||
if is_html:
|
||||
bg = data['editor-colors']['BackgroundColor'];
|
||||
rgb = parse_rgb_color(bg, (0,0,0))
|
||||
if (rgb[0] < 127) + (rgb[0] < 127) + (rgb[0] < 127) >= 2:
|
||||
tmode1 = '#light:target'
|
||||
tmode2 = ''
|
||||
mode1 = 'dark'
|
||||
mode2 = 'light'
|
||||
else:
|
||||
tmode1 = ''
|
||||
tmode2 = '#dark:target'
|
||||
mode1 = 'light'
|
||||
mode2 = 'dark'
|
||||
title = args.html_title or data["metadata"]["name"]
|
||||
sys.stdout.write(f'''<!DOCTYPE html>
|
||||
<html><head><title>{title}</title><style>
|
||||
html, body, #mode {{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}}
|
||||
|
||||
body {{
|
||||
padding: .5em;
|
||||
}}
|
||||
|
||||
pre {{
|
||||
font-family: "JetBrains Mono", "Liberation Mono", Firacode, "DejaVu Sans Mono", Inconsolata, monospace;
|
||||
}}
|
||||
|
||||
.bold {{ font-weight: bold }}
|
||||
|
||||
/* light theme */
|
||||
|
||||
body{tmode1} {{
|
||||
background: #ddd;
|
||||
color: #000;
|
||||
}}
|
||||
|
||||
{tmode1} .red {{ color: #A02222 }}
|
||||
{tmode1} .green {{ color: #229022 }}
|
||||
{tmode1} .orange {{ color: #909022 }}
|
||||
{tmode1} .blue {{ color: #2222A0 }}
|
||||
{tmode1} .purple {{ color: #A022A0 }}
|
||||
{tmode1} .cyan {{ color: #22A0A0 }}
|
||||
{tmode1} .gray {{ color: Gray }}
|
||||
|
||||
/* dark theme */
|
||||
|
||||
body{tmode2} {{
|
||||
background: #222;
|
||||
color: #eee;
|
||||
}}
|
||||
{tmode2} .red {{ color: #D95555 }}
|
||||
{tmode2} .green {{ color: #55D055 }}
|
||||
{tmode2} .orange {{ color: #D0D055 }}
|
||||
{tmode2} .blue {{ color: #68A0E8 }}
|
||||
{tmode2} .purple {{ color: #D077D0 }}
|
||||
{tmode2} .cyan {{ color: Turquoise }}
|
||||
{tmode2} .gray {{ color: Gray }}
|
||||
|
||||
div {{
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 0;
|
||||
}}
|
||||
|
||||
#mode a {{
|
||||
padding: .5rem 1rem;
|
||||
}}
|
||||
|
||||
#light-mode {{
|
||||
color: #2222A0;
|
||||
background: #ddd;
|
||||
}}
|
||||
#light-mode:hover, #light-mode:focus {{ background: #ccc; }}
|
||||
|
||||
#dark-mode {{
|
||||
color: #68a0E8;
|
||||
background: #222;
|
||||
}}
|
||||
#dark-mode:hover, #dark-mode:focus {{ background: #333; }}
|
||||
|
||||
#{mode1}-mode {{ display: none }}
|
||||
#{mode2}-mode {{ display: inline-block }}
|
||||
#{mode2}:target #{mode2}-mode {{ display: none }}
|
||||
#{mode2}:target #{mode1}-mode {{ display: inline-block }}
|
||||
|
||||
</style></head><body id="{mode2}">
|
||||
<p id="mode"><a id="{mode2}-mode" href="#{mode2}">Switch to {mode2} mode</a><a id="{mode1}-mode" href="#{mode1}">Switch to {mode1} mode</a></p>
|
||||
<pre>''')
|
||||
sys.stdout.write(output)
|
||||
sys.stdout.write('</pre></body></html>')
|
||||
else:
|
||||
sys.stdout.write(output)
|
||||
|
||||
# flush output here to force SIGPIPE to be triggered
|
||||
sys.stdout.flush()
|
||||
# open in `less` then closing can cause this error
|
||||
except BrokenPipeError:
|
||||
# Python flushes standard streams on exit; redirect remaining output
|
||||
# to devnull to avoid another BrokenPipeError at shutdown
|
||||
import os
|
||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||
os.dup2(devnull, sys.stdout.fileno())
|
||||
Reference in New Issue
Block a user