feat: add missing KF6 framework recipes
This commit is contained in:
+490
@@ -0,0 +1,490 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2020 Alex Turbov <i.zaufi@gmail.com>
|
||||
# SPDX-FileContributor: Juraj Oravec <jurajoravec@mailo.com>
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
|
||||
'''
|
||||
CLI utility to convert old `kate`s schema/hlcolors files to the
|
||||
new JSON theme format.
|
||||
'''
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import enum
|
||||
import functools
|
||||
import itertools
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
from xml.etree import ElementTree
|
||||
from typing import \
|
||||
Dict \
|
||||
, Final \
|
||||
, Iterable \
|
||||
, Generator \
|
||||
, List \
|
||||
, Literal \
|
||||
, Pattern \
|
||||
, Set \
|
||||
, TextIO \
|
||||
, Tuple \
|
||||
, TypedDict \
|
||||
, TypeVar
|
||||
|
||||
import click
|
||||
import columnize # type: ignore
|
||||
|
||||
# BEGIN Type declarations
|
||||
T = TypeVar('T') # pylint: disable=invalid-name
|
||||
PropName = Literal[
|
||||
'background-color'
|
||||
, 'bold'
|
||||
, 'italic'
|
||||
, 'selected-text-color'
|
||||
, 'seleted-background-color'
|
||||
, 'strike-through'
|
||||
, 'text-color'
|
||||
, 'text-color'
|
||||
, 'underline'
|
||||
]
|
||||
StylePropsDict = TypedDict(
|
||||
'StylePropsDict'
|
||||
, {
|
||||
'background-color': str
|
||||
, 'bold': bool
|
||||
, 'italic': bool
|
||||
, 'selected-text-color': str
|
||||
, 'seleted-background-color': str
|
||||
, 'strike-through': bool
|
||||
, 'text-color': str
|
||||
, 'underline': bool
|
||||
}
|
||||
, total=False
|
||||
)
|
||||
CustomStyleDict = Dict[str, StylePropsDict]
|
||||
CustomStylesDict = Dict[str, CustomStyleDict]
|
||||
EditorColorsDict = Dict[str, str]
|
||||
TextStylesDict = Dict[str, StylePropsDict]
|
||||
MetadataDict = TypedDict(
|
||||
'MetadataDict'
|
||||
, {
|
||||
'name': str
|
||||
, 'revision': int
|
||||
}
|
||||
)
|
||||
ThemeDict = TypedDict(
|
||||
'ThemeDict'
|
||||
, {
|
||||
'custom-styles': CustomStylesDict
|
||||
, 'editor-colors': EditorColorsDict
|
||||
, 'metadata': MetadataDict
|
||||
, 'text-styles': TextStylesDict
|
||||
}
|
||||
, total=False
|
||||
)
|
||||
SyntaxesDict = Dict[str, Set[str]]
|
||||
# END Type declarations
|
||||
|
||||
|
||||
class QtColorItemOffset(enum.IntEnum):
|
||||
'''
|
||||
Enumeration class with offsets in the CSV record
|
||||
of the old style definition.
|
||||
'''
|
||||
_UNKNOWN = 0
|
||||
TEXT = enum.auto()
|
||||
SELECTED_TEXT = enum.auto()
|
||||
BOLD = enum.auto()
|
||||
ITALIC = enum.auto()
|
||||
STRIKE_THROUGH = enum.auto()
|
||||
UNDERLINE = enum.auto()
|
||||
BACKGROUND = enum.auto()
|
||||
SELECTED_BACKGROUND = enum.auto()
|
||||
_IGNORED_FONT_FAMILY = enum.auto()
|
||||
_TRAILING_DASHES = enum.auto()
|
||||
# Special item to validate the components count
|
||||
CUSTOM_COLOR_EXPECTED_SIZE = enum.auto()
|
||||
STANDARD_COLOR_EXPECTED_SIZE = 10
|
||||
|
||||
|
||||
_EXPECTED_OLD_COLOR_LEN: Final[int] = 8
|
||||
_OLD_COLOR_LEADING_STRIP_SIZE: Final[int] = 2
|
||||
_HIGHLIGHTING_PFX: Final[str] = 'Highlighting '
|
||||
_COLUMIZED_LIST_INDENT_PFX: Final[str] = ' '
|
||||
_EDITOR_COLORS: Final[Dict[str, str]] = {
|
||||
"Color Background": "BackgroundColor"
|
||||
, "Color Code Folding": "CodeFolding"
|
||||
, "Color Current Line Number": "CurrentLineNumber"
|
||||
, "Color Highlighted Bracket": "BracketMatching"
|
||||
, "Color Highlighted Line": "CurrentLine"
|
||||
, "Color Icon Bar": "IconBorder"
|
||||
, "Color Indentation Line": "IndentationLine"
|
||||
, "Color Line Number": "LineNumbers"
|
||||
, "Color MarkType 1": "MarkBookmark"
|
||||
, "Color MarkType 2": "MarkBreakpointActive"
|
||||
, "Color MarkType 3": "MarkBreakpointReached"
|
||||
, "Color MarkType 4": "MarkBreakpointDisabled"
|
||||
, "Color MarkType 5": "MarkExecution"
|
||||
, "Color MarkType 6": "MarkWarning"
|
||||
, "Color MarkType 7": "MarkError"
|
||||
, "Color Modified Lines": "ModifiedLines"
|
||||
, "Color Replace Highlight": "ReplaceHighlight"
|
||||
, "Color Saved Lines": "SavedLines"
|
||||
, "Color Search Highlight": "SearchHighlight"
|
||||
, "Color Selection": "TextSelection"
|
||||
, "Color Separator": "Separator"
|
||||
, "Color Spelling Mistake Line": "SpellChecking"
|
||||
, "Color Tab Marker": "TabMarker"
|
||||
, "Color Template Background": "TemplateBackground"
|
||||
, "Color Template Editable Placeholder": "TemplatePlaceholder"
|
||||
, "Color Template Focused Editable Placeholder": "TemplateFocusedPlaceholder"
|
||||
, "Color Template Not Editable Placeholder": "TemplateReadOnlyPlaceholder"
|
||||
, "Color Word Wrap Marker": "WordWrapMarker"
|
||||
}
|
||||
_TEXT_STYLES: Final[Dict[str, str]] = {
|
||||
"Alert": "Alert"
|
||||
, "Annotation": "Annotation"
|
||||
, "Attribute": "Attribute"
|
||||
, "Base-N Integer": "BaseN"
|
||||
, "Built-in": "BuiltIn"
|
||||
, "Character": "Char"
|
||||
, "Comment": "Comment"
|
||||
, "Comment Variable": "CommentVar"
|
||||
, "Constant": "Constant"
|
||||
, "Control Flow": "ControlFlow"
|
||||
, "Data Type": "DataType"
|
||||
, "Decimal/Value": "DecVal"
|
||||
, "Documentation": "Documentation"
|
||||
, "Error": "Error"
|
||||
, "Extension": "Extension"
|
||||
, "Floating Point": "Float"
|
||||
, "Function": "Function"
|
||||
, "Import": "Import"
|
||||
, "Information": "Information"
|
||||
, "Keyword": "Keyword"
|
||||
, "Normal": "Normal"
|
||||
, "Operator": "Operator"
|
||||
, "Others": "Others"
|
||||
, "Preprocessor": "Preprocessor"
|
||||
, "Region Marker": "RegionMarker"
|
||||
, "Special Character": "SpecialChar"
|
||||
, "Special String": "SpecialString"
|
||||
, "String": "String"
|
||||
, "Variable": "Variable"
|
||||
, "Verbatim String": "VerbatimString"
|
||||
, "Warning": "Warning"
|
||||
}
|
||||
_OFFSET2NAME: Final[Dict[QtColorItemOffset, PropName]] = {
|
||||
QtColorItemOffset.TEXT: 'text-color'
|
||||
, QtColorItemOffset.SELECTED_TEXT: 'selected-text-color'
|
||||
, QtColorItemOffset.BOLD: 'bold'
|
||||
, QtColorItemOffset.ITALIC: 'italic'
|
||||
, QtColorItemOffset.STRIKE_THROUGH: 'strike-through'
|
||||
, QtColorItemOffset.UNDERLINE: 'underline'
|
||||
, QtColorItemOffset.BACKGROUND: 'background-color'
|
||||
, QtColorItemOffset.SELECTED_BACKGROUND: 'seleted-background-color'
|
||||
}
|
||||
_META_SECTIONS: Final[List[str]] = ['KateSchema', 'KateHLColors']
|
||||
_SECTION_MATCH: Final[Pattern] = re.compile(r'\[(?P<header>[^]]+?)( - Schema .*)?\]')
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.help_option(
|
||||
'--help'
|
||||
, '-h'
|
||||
)
|
||||
@click.version_option()
|
||||
@click.option(
|
||||
'--skip-included'
|
||||
, '-d'
|
||||
, default=True
|
||||
, is_flag=True
|
||||
, help='Do not write custom colors included from another syntax files.'
|
||||
)
|
||||
@click.option(
|
||||
'-s'
|
||||
, '--syntax-dirs'
|
||||
, multiple=True
|
||||
, metavar='DIRECTORY...'
|
||||
, type=click.Path(exists=True, file_okay=False, dir_okay=True)
|
||||
, help='Specify the directory to search for syntax files. '
|
||||
'If given, extra validation going to happen. Multiple '
|
||||
'options allowed.'
|
||||
)
|
||||
@click.argument(
|
||||
'input-file'
|
||||
, type=click.File('r')
|
||||
, default='-'
|
||||
)
|
||||
def kateschema2theme(skip_included: bool, syntax_dirs: List[click.Path], input_file: TextIO) -> int:
|
||||
''' Kate colors/schema to theme converter. '''
|
||||
config = configparser.ConfigParser(
|
||||
delimiters=['=']
|
||||
, interpolation=None
|
||||
)
|
||||
setattr(config, 'optionxform', str)
|
||||
setattr(config, 'SECTCRE', _SECTION_MATCH)
|
||||
|
||||
try:
|
||||
config.read_file(input_file)
|
||||
except configparser.DuplicateOptionError as ex:
|
||||
eerror(f'{ex!s}')
|
||||
return 1
|
||||
|
||||
result: ThemeDict = {}
|
||||
sections: List[str] = config.sections()
|
||||
|
||||
if 'Editor Colors' in sections:
|
||||
result['editor-colors'] = functools.reduce(
|
||||
convert_editor_color
|
||||
, config.items('Editor Colors')
|
||||
, {}
|
||||
)
|
||||
|
||||
if 'Default Item Styles' in sections:
|
||||
result['text-styles'] = functools.reduce(
|
||||
collect_standard_colors
|
||||
, config.items('Default Item Styles')
|
||||
, {}
|
||||
)
|
||||
|
||||
custom_styles: CustomStylesDict = functools.reduce(
|
||||
collect_custom_colors
|
||||
, hl_colors(config, skip_included)
|
||||
, {}
|
||||
)
|
||||
|
||||
if bool(custom_styles):
|
||||
known_syntaxes: SyntaxesDict = get_syntaxes_available(syntax_dirs) \
|
||||
if bool(syntax_dirs) else {}
|
||||
if bool(known_syntaxes):
|
||||
custom_styles = verify_converted_styles(custom_styles, known_syntaxes)
|
||||
|
||||
result['custom-styles'] = custom_styles
|
||||
|
||||
meta_section_name = first_true(lambda name: name in sections, _META_SECTIONS)
|
||||
if meta_section_name is not None:
|
||||
result['metadata'] = {
|
||||
'name': config[meta_section_name]['schema']
|
||||
, 'revision': 1
|
||||
}
|
||||
|
||||
print(json.dumps(result, sort_keys=True, indent=4))
|
||||
return 0
|
||||
|
||||
|
||||
def convert_editor_color(state: Dict[str, str], color_line: Tuple[str, str]) -> Dict[str, str]:
|
||||
'''Convert standard editor color names from old to new using the mapping table.'''
|
||||
name, color_settings = color_line
|
||||
assert name in _EDITOR_COLORS
|
||||
state[_EDITOR_COLORS[name]] = decode_rgb_set(color_settings)
|
||||
return state
|
||||
|
||||
|
||||
def decode_rgb_set(color_settings: str) -> str:
|
||||
'''Transform the RGB record given as CSV string to web-hex format.'''
|
||||
return rgb2hex(*map(int, color_settings.split(',')))
|
||||
|
||||
|
||||
def rgb2hex(red: int, green: int, blue: int) -> str:
|
||||
'''Convert R,G,B integers to web-hex string'''
|
||||
return f'#{red:02x}{green:02x}{blue:02x}'
|
||||
|
||||
|
||||
def collect_standard_colors(state, item):
|
||||
'''Convert standard text styles from old to new names using the mapping table.'''
|
||||
name, value = item
|
||||
state[_TEXT_STYLES[name]] = parse_qcolor_value(value)
|
||||
return state
|
||||
|
||||
|
||||
def collect_custom_colors(state: CustomStylesDict, item: Tuple[str, str, str]) -> CustomStylesDict:
|
||||
'''A functor to convert one old style setting to the new format
|
||||
and update the given `state` (a dict).
|
||||
'''
|
||||
syntax, syntax_item, value = item
|
||||
|
||||
props = parse_qcolor_value(value)
|
||||
if bool(props):
|
||||
syntax_node: CustomStyleDict = state.get(syntax, {})
|
||||
syntax_node[syntax_item] = props
|
||||
state[syntax] = syntax_node
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def hl_colors(config: configparser.ConfigParser, skip_included: bool) \
|
||||
-> Generator[Tuple[str, str, str], None, None]:
|
||||
'''A generator function to iterate over custom styles in the old format.'''
|
||||
for section in config.sections():
|
||||
if not section.startswith(_HIGHLIGHTING_PFX):
|
||||
continue
|
||||
|
||||
for name, value in config.items(section):
|
||||
syntax, *parts = name.split(':')
|
||||
if not bool(parts):
|
||||
ewarn(f'Unexpected color name: `{name}` in section `{section}`')
|
||||
|
||||
elif not skip_included or section[len(_HIGHLIGHTING_PFX):] == syntax:
|
||||
yield syntax, ':'.join(parts), value
|
||||
|
||||
|
||||
def parse_qcolor_value(value: str) -> StylePropsDict:
|
||||
'''Convert old color settings (QColor stored as a CSV config item)
|
||||
into a dict of new styles.
|
||||
'''
|
||||
components = value.split(',')
|
||||
if len(components) == QtColorItemOffset.CUSTOM_COLOR_EXPECTED_SIZE:
|
||||
components.pop()
|
||||
assert len(components) == QtColorItemOffset.STANDARD_COLOR_EXPECTED_SIZE
|
||||
return transform_qcolor_to_dict(components)
|
||||
|
||||
|
||||
def transform_qcolor_to_dict(components: List[str]) -> StylePropsDict:
|
||||
'''Convert old color settings given as a list of items
|
||||
into a dict of new styles.
|
||||
'''
|
||||
init: StylePropsDict = {}
|
||||
return functools.reduce(convert_color_property, enumerate(components), init)
|
||||
|
||||
|
||||
def convert_color_property(state: StylePropsDict, prop: Tuple[int, str]) -> StylePropsDict:
|
||||
'''A reducer functor to convert one item of the former color record (CSV)
|
||||
into a new property name and a value.
|
||||
'''
|
||||
offset = QtColorItemOffset(prop[0])
|
||||
value = prop[1]
|
||||
assert offset < QtColorItemOffset.CUSTOM_COLOR_EXPECTED_SIZE
|
||||
|
||||
if bool(value) and offset in _OFFSET2NAME:
|
||||
custom_prop_name = _OFFSET2NAME[offset]
|
||||
if custom_prop_name.endswith('-color'):
|
||||
if len(value) == _EXPECTED_OLD_COLOR_LEN:
|
||||
state[custom_prop_name] = '#' + value[_OLD_COLOR_LEADING_STRIP_SIZE:]
|
||||
else:
|
||||
state[custom_prop_name] = bool(value == '1')
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def first_true(pred, iterable: Iterable[T], default=None) -> T:
|
||||
'''A helper function to return first item for which predicate is true.'''
|
||||
return next(filter(pred, iterable), default)
|
||||
|
||||
|
||||
def get_syntaxes_available(dirs: List[click.Path]) -> SyntaxesDict:
|
||||
'''Collect syntaxs available in the given path.
|
||||
|
||||
Returns a dict of syntax names to a list of syntax items in it.
|
||||
'''
|
||||
return functools.reduce(
|
||||
load_syntax_data
|
||||
, filter(
|
||||
lambda p: p.suffix == '.xml'
|
||||
, itertools.chain(
|
||||
*map(
|
||||
lambda p: pathlib.Path(str(p)).iterdir()
|
||||
, dirs
|
||||
)
|
||||
)
|
||||
)
|
||||
, {}
|
||||
)
|
||||
|
||||
|
||||
def load_syntax_data(state: SyntaxesDict, syntax_file: pathlib.Path) -> SyntaxesDict:
|
||||
'''A reducer functor to obtain syntax items.'''
|
||||
tree = ElementTree.parse(syntax_file)
|
||||
root = tree.getroot()
|
||||
|
||||
syntax_name = root.get('name')
|
||||
assert syntax_name is not None
|
||||
|
||||
if syntax_name in state:
|
||||
ewarn(
|
||||
f'Use `{syntax_name}` found '
|
||||
f'in `{click.format_filename(str(syntax_file))}`'
|
||||
)
|
||||
|
||||
state[syntax_name] = functools.reduce(
|
||||
collect_syntax_item_data
|
||||
, root.iterfind('highlighting/itemDatas/itemData')
|
||||
, set()
|
||||
)
|
||||
return state
|
||||
|
||||
|
||||
def verify_converted_styles(custom_styles: CustomStylesDict, known_syntaxes: SyntaxesDict) \
|
||||
-> CustomStylesDict:
|
||||
'''Validate the given `custom_styles` according to actual syntax items
|
||||
described in the known syntax files.
|
||||
|
||||
Returns a dict of syntaxes without unused syntax items.
|
||||
'''
|
||||
for syntax, styles in custom_styles.items():
|
||||
if syntax not in known_syntaxes:
|
||||
ewarn(f'The `{syntax}` is not known. Ignoring validation.')
|
||||
continue
|
||||
|
||||
found_custom_items = set(styles.keys())
|
||||
|
||||
if unused_items := [*found_custom_items.difference(known_syntaxes[syntax])]:
|
||||
ewarn(
|
||||
f'The following styles are not used by `{syntax}` syntax anymore:'
|
||||
+ '\n'
|
||||
+ format_columns(unused_items)
|
||||
)
|
||||
custom_styles[syntax] = functools.reduce(
|
||||
remove_unused_syntax_item
|
||||
, unused_items
|
||||
, styles
|
||||
)
|
||||
|
||||
if undefined_items := [*known_syntaxes[syntax].difference(found_custom_items)]:
|
||||
ewarn(
|
||||
f'The following styles are not defined in the converted `{syntax}` syntax:'
|
||||
+ '\n'
|
||||
+ format_columns(undefined_items)
|
||||
)
|
||||
|
||||
return custom_styles
|
||||
|
||||
|
||||
def remove_unused_syntax_item(state: CustomStyleDict, item: str) -> CustomStyleDict:
|
||||
'''Remove the given `item` from the `state`.'''
|
||||
assert item in state
|
||||
del state[item]
|
||||
return state
|
||||
|
||||
|
||||
def format_columns(iterable: Iterable[str]) -> str:
|
||||
'''A helper functor to output the list in columns.'''
|
||||
term_width = click.get_terminal_size()[0] - len(_COLUMIZED_LIST_INDENT_PFX)
|
||||
return textwrap.indent(
|
||||
columnize.columnize(iterable, displaywidth=term_width, colsep=' │ ')
|
||||
, prefix=_COLUMIZED_LIST_INDENT_PFX
|
||||
)
|
||||
|
||||
|
||||
def collect_syntax_item_data(items: Set[str], node: ElementTree.Element) -> Set[str]:
|
||||
'''A reducer functor to append a syntax item name to the given set.'''
|
||||
name = node.get('name')
|
||||
assert name is not None
|
||||
items.add(name)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def eerror(msg: str):
|
||||
'''A helper function to display an error message.'''
|
||||
click.echo(' ' + click.style('*', fg='red', bold=True) + f' {msg}', err=True)
|
||||
|
||||
|
||||
def ewarn(msg: str):
|
||||
'''A helper function to display a warning message.'''
|
||||
click.echo(' ' + click.style('*', fg='yellow') + f' {msg}', err=True)
|
||||
Reference in New Issue
Block a user