pax_global_header 0000666 0000000 0000000 00000000064 14207747141 0014521 g ustar 00root root 0000000 0000000 52 comment=8ea06e8a78ecd61726352d8665c2c97104854dab
blueprint-compiler-main/ 0000775 0000000 0000000 00000000000 14207747141 0015623 5 ustar 00root root 0000000 0000000 blueprint-compiler-main/.coveragerc 0000664 0000000 0000000 00000000200 14207747141 0017734 0 ustar 00root root 0000000 0000000 [report]
exclude_lines =
pragma: no cover
raise AssertionError
raise NotImplementedError
raise CompilerBugError
blueprint-compiler-main/.gitignore 0000664 0000000 0000000 00000000244 14207747141 0017613 0 ustar 00root root 0000000 0000000 __pycache__
/build
/dist
*.egg-info
blueprint-compiler.pc
/.coverage
/htmlcov
coverage.xml
.mypy_cache
/subprojects/gtk-blueprint-tool
/blueprint-regression-tests
blueprint-compiler-main/.gitlab-ci.yml 0000664 0000000 0000000 00000001363 14207747141 0020262 0 ustar 00root root 0000000 0000000 stages:
- build
- pages
build:
image: registry.gitlab.gnome.org/jwestman/blueprint-compiler
stage: build
script:
- mypy blueprintcompiler
- coverage run -m unittest
- coverage html
- coverage xml
- meson _build -Ddocs=true
- ninja -C _build
- ninja -C _build test
- ninja -C _build install
- git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git
- cd blueprint-regression-tests
- ./test.sh
- cd ..
artifacts:
paths:
- _build
- htmlcov
reports:
cobertura: coverage.xml
pages:
stage: pages
dependencies:
- build
script:
- mv _build/docs/en public
- mv htmlcov public/coverage
artifacts:
paths:
- public
only:
- main
blueprint-compiler-main/COPYING 0000664 0000000 0000000 00000016744 14207747141 0016672 0 ustar 00root root 0000000 0000000 GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
blueprint-compiler-main/README.md 0000664 0000000 0000000 00000005047 14207747141 0017110 0 ustar 00root root 0000000 0000000 # Blueprint
A markup language for GTK user interface files.
## Motivation
GtkBuilder XML format is quite verbose, and many app developers don't like
using WYSIWYG editors for creating UIs. Blueprint files are intended to be a
concise, easy-to-read format that makes it easier to create and edit GTK UIs.
Internally, it compiles to GtkBuilder XML as part of an app's build system. It
adds no new features, just makes the features that exist more accessible.
Another goal is to have excellent developer tooling--including a language
server--so that less knowledge of the format is required. Hopefully this will
increase adoption of cool advanced features like GtkExpression.
## Example
Here is what [the libshumate demo's UI definition](https://gitlab.gnome.org/GNOME/libshumate/-/blob/main/demos/shumate-demo-window.ui)
looks like ported to this new format:
```
using Gtk 4.0;
using Shumate 1.0;
template ShumateDemoWindow : Gtk.ApplicationWindow {
can-focus: yes;
title: _("Shumate Demo");
default-width: 800;
default-height: 600;
[titlebar]
Gtk.HeaderBar {
Gtk.DropDown layers_dropdown {
notify::selected => on_layers_dropdown_notify_selected() swapped;
}
}
Gtk.Overlay overlay {
vexpand: true;
Shumate.Map map {}
[overlay]
Shumate.Scale scale {
halign: start;
valign: end;
}
[overlay]
Gtk.Box {
orientation: vertical;
halign: end;
valign: end;
Shumate.Compass compass {
halign: end;
map: map;
}
Shumate.License license {
halign: end;
}
}
}
}
```
## Editor plugins
### Vim
- [Syntax highlighting by thetek42](https://github.com/thetek42/vim-blueprint-syntax)
- [Syntax highlighting by gabmus](https://gitlab.com/gabmus/vim-blueprint)
## License
Copyright (C) 2021 James Westman
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see .
## Donate
You can support my work on GitHub Sponsors!
blueprint-compiler-main/blueprint-compiler.pc.in 0000664 0000000 0000000 00000000154 14207747141 0022370 0 ustar 00root root 0000000 0000000 Name: blueprint-compiler
Description: Markup compiler for GTK user interface definitions
Version: @VERSION@
blueprint-compiler-main/blueprint-compiler.py 0000775 0000000 0000000 00000001560 14207747141 0022016 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
# blueprint-compiler.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from blueprintcompiler import main
if __name__ == "__main__":
main.main()
blueprint-compiler-main/blueprintcompiler/ 0000775 0000000 0000000 00000000000 14207747141 0021362 5 ustar 00root root 0000000 0000000 blueprint-compiler-main/blueprintcompiler/__init__.py 0000664 0000000 0000000 00000000000 14207747141 0023461 0 ustar 00root root 0000000 0000000 blueprint-compiler-main/blueprintcompiler/ast_utils.py 0000664 0000000 0000000 00000013635 14207747141 0023753 0 ustar 00root root 0000000 0000000 # ast_utils.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from collections import ChainMap, defaultdict
from .errors import *
from .lsp_utils import SemanticToken
from .utils import lazy_prop
from .xml_emitter import XmlEmitter
class Children:
""" Allows accessing children by type using array syntax. """
def __init__(self, children):
self._children = children
def __iter__(self):
return iter(self._children)
def __getitem__(self, key):
return [child for child in self._children if isinstance(child, key)]
class AstNode:
""" Base class for nodes in the abstract syntax tree. """
completers: T.List = []
def __init__(self, group, children, tokens, incomplete=False):
self.group = group
self.children = Children(children)
self.tokens = ChainMap(tokens, defaultdict(lambda: None))
self.incomplete = incomplete
self.parent = None
for child in self.children:
child.parent = self
def __init_subclass__(cls):
cls.completers = []
cls.validators = [getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator")]
@property
def root(self):
if self.parent is None:
return self
else:
return self.parent.root
def parent_by_type(self, type):
if self.parent is None:
return None
elif isinstance(self.parent, type):
return self.parent
else:
return self.parent.parent_by_type(type)
@lazy_prop
def errors(self):
return list(self._get_errors())
def _get_errors(self):
for validator in self.validators:
try:
validator(self)
except CompileError as e:
yield e
for child in self.children:
yield from child._get_errors()
def _attrs_by_type(self, attr_type):
for name in dir(type(self)):
item = getattr(type(self), name)
if isinstance(item, attr_type):
yield name, item
def generate(self) -> str:
""" Generates an XML string from the node. """
xml = XmlEmitter()
self.emit_xml(xml)
return xml.result
def emit_xml(self, xml: XmlEmitter):
""" Emits the XML representation of this AST node to the XmlEmitter. """
raise NotImplementedError()
def get_docs(self, idx: int) -> T.Optional[str]:
for name, attr in self._attrs_by_type(Docs):
if attr.token_name:
token = self.group.tokens.get(attr.token_name)
if token and token.start <= idx < token.end:
return getattr(self, name)
else:
return getattr(self, name)
for child in self.children:
if child.group.start <= idx < child.group.end:
docs = child.get_docs(idx)
if docs is not None:
return docs
return None
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
for child in self.children:
yield from child.get_semantic_tokens()
def iterate_children_recursive(self) -> T.Iterator["AstNode"]:
yield self
for child in self.children:
yield from child.iterate_children_recursive()
def validate(token_name=None, end_token_name=None, skip_incomplete=False):
""" Decorator for functions that validate an AST node. Exceptions raised
during validation are marked with range information from the tokens. """
def decorator(func):
def inner(self):
if skip_incomplete and self.incomplete:
return
try:
func(self)
except CompileError as e:
# If the node is only partially complete, then an error must
# have already been reported at the parsing stage
if self.incomplete:
return
# This mess of code sets the error's start and end positions
# from the tokens passed to the decorator, if they have not
# already been set
if e.start is None:
if token := self.group.tokens.get(token_name):
e.start = token.start
else:
e.start = self.group.start
if e.end is None:
if token := self.group.tokens.get(end_token_name):
e.end = token.end
elif token := self.group.tokens.get(token_name):
e.end = token.end
else:
e.end = self.group.end
# Re-raise the exception
raise e
inner._validator = True
return inner
return decorator
class Docs:
def __init__(self, func, token_name=None):
self.func = func
self.token_name = token_name
def __get__(self, instance, owner):
if instance is None:
return self
return self.func(instance)
def docs(*args, **kwargs):
""" Decorator for functions that return documentation for tokens. """
def decorator(func):
return Docs(func, *args, **kwargs)
return decorator
blueprint-compiler-main/blueprintcompiler/completions.py 0000664 0000000 0000000 00000012050 14207747141 0024266 0 ustar 00root root 0000000 0000000 # completions.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from . import gir, language
from .ast_utils import AstNode
from .completions_utils import *
from .lsp_utils import Completion, CompletionItemKind
from .parser import SKIP_TOKENS
from .tokenizer import TokenType, Token
Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]]
def _complete(ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int) -> T.Iterator[Completion]:
for child in ast_node.children:
if child.group.start <= idx and (idx < child.group.end or (idx == child.group.end and child.incomplete)):
yield from _complete(child, tokens, idx, token_idx)
return
prev_tokens: T.List[Token] = []
# collect the 5 previous non-skipped tokens
while len(prev_tokens) < 5 and token_idx >= 0:
token = tokens[token_idx]
if token.type not in SKIP_TOKENS:
prev_tokens.insert(0, token)
token_idx -= 1
for completer in ast_node.completers:
yield from completer(prev_tokens, ast_node)
def complete(ast_node: AstNode, tokens: T.List[Token], idx: int) -> T.Iterator[Completion]:
token_idx = 0
# find the current token
for i, token in enumerate(tokens):
if token.start < idx <= token.end:
token_idx = i
# if the current token is an identifier or whitespace, move to the token before it
while tokens[token_idx].type in [TokenType.IDENT, TokenType.WHITESPACE]:
idx = tokens[token_idx].start
token_idx -= 1
yield from _complete(ast_node, tokens, idx, token_idx)
@completer([language.GtkDirective])
def using_gtk(ast_node, match_variables):
yield Completion("using Gtk 4.0;", CompletionItemKind.Keyword)
@completer(
applies_in=[language.UI, language.ObjectContent, language.Template],
matches=new_statement_patterns
)
def namespace(ast_node, match_variables):
yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.")
for ns in ast_node.root.children[language.Import]:
if ns.gir_namespace is not None:
yield Completion(ns.gir_namespace.name, CompletionItemKind.Module, text=ns.gir_namespace.name + ".")
@completer(
applies_in=[language.UI, language.ObjectContent, language.Template],
matches=[
[(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)],
[(TokenType.IDENT, None), (TokenType.OP, ".")],
]
)
def object_completer(ast_node, match_variables):
ns = ast_node.root.gir.namespaces.get(match_variables[0])
if ns is not None:
for c in ns.classes.values():
yield Completion(c.name, CompletionItemKind.Class, docs=c.doc)
@completer(
applies_in=[language.ObjectContent],
matches=new_statement_patterns,
)
def property_completer(ast_node, match_variables):
if ast_node.gir_class:
for prop in ast_node.gir_class.properties:
yield Completion(prop, CompletionItemKind.Property, snippet=f"{prop}: $0;")
@completer(
applies_in=[language.Property, language.BaseTypedAttribute],
matches=[
[(TokenType.IDENT, None), (TokenType.OP, ":")]
],
)
def prop_value_completer(ast_node, match_variables):
if isinstance(ast_node.value_type, gir.Enumeration):
for name, member in ast_node.value_type.members.items():
yield Completion(name, CompletionItemKind.EnumMember, docs=member.doc)
elif isinstance(ast_node.value_type, gir.BoolType):
yield Completion("true", CompletionItemKind.Constant)
yield Completion("false", CompletionItemKind.Constant)
@completer(
applies_in=[language.ObjectContent],
matches=new_statement_patterns,
)
def signal_completer(ast_node, match_variables):
if ast_node.gir_class:
for signal in ast_node.gir_class.signals:
if not isinstance(ast_node.parent, language.Object):
name = "on"
else:
name = "on_" + (ast_node.parent.tokens["id"] or ast_node.parent.tokens["class_name"].lower())
yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;")
@completer(
applies_in=[language.UI],
matches=new_statement_patterns
)
def template_completer(ast_node, match_variables):
yield Completion(
"template", CompletionItemKind.Snippet,
snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}"
)
blueprint-compiler-main/blueprintcompiler/completions_utils.py 0000664 0000000 0000000 00000005266 14207747141 0025521 0 ustar 00root root 0000000 0000000 # completions_utils.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from .tokenizer import Token, TokenType
from .lsp_utils import Completion
new_statement_patterns = [
[(TokenType.PUNCTUATION, "{")],
[(TokenType.PUNCTUATION, "}")],
[(TokenType.PUNCTUATION, ";")],
]
def applies_to(*ast_types):
""" Decorator describing which AST nodes the completer should apply in. """
def decorator(func):
for c in ast_types:
c.completers.append(func)
return func
return decorator
def completer(applies_in: T.List, matches: T.List=[], applies_in_subclass=None):
def decorator(func):
def inner(prev_tokens: T.List[Token], ast_node):
# For completers that apply in ObjectContent nodes, we can further
# check that the object is the right class
if applies_in_subclass is not None:
type = ast_node.root.gir.get_type(applies_in_subclass[1], applies_in_subclass[0])
if ast_node.gir_class and not ast_node.gir_class.assignable_to(type):
return
any_match = len(matches) == 0
match_variables: T.List[str] = []
for pattern in matches:
match_variables = []
if len(pattern) <= len(prev_tokens):
for i in range(0, len(pattern)):
type, value = pattern[i]
token = prev_tokens[i - len(pattern)]
if token.type != type or (value is not None and str(token) != value):
break
if value is None:
match_variables.append(str(token))
else:
any_match = True
break
if not any_match:
return
yield from func(ast_node, match_variables)
for c in applies_in:
c.completers.append(inner)
return inner
return decorator
blueprint-compiler-main/blueprintcompiler/decompiler.py 0000664 0000000 0000000 00000022033 14207747141 0024057 0 ustar 00root root 0000000 0000000 # decompiler.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import re
from enum import Enum
import typing as T
from dataclasses import dataclass
from .xml_reader import Element, parse
from .gir import *
from .utils import Colors
__all__ = ["decompile"]
_DECOMPILERS: T.Dict = {}
_CLOSING = {
"{": "}",
"[": "]",
}
_NAMESPACES = [
("GLib", "2.0"),
("GObject", "2.0"),
("Gio", "2.0"),
("Adw", "1.0"),
]
class LineType(Enum):
NONE = 1
STMT = 2
BLOCK_START = 3
BLOCK_END = 4
class DecompileCtx:
def __init__(self):
self._result = ""
self.gir = GirContext()
self._indent = 0
self._blocks_need_end = []
self._last_line_type = LineType.NONE
self.gir.add_namespace(get_namespace("Gtk", "4.0"))
@property
def result(self):
imports = "\n".join([
f"using {ns} {namespace.version};"
for ns, namespace in self.gir.namespaces.items()
])
return imports + "\n" + self._result
def type_by_cname(self, cname):
if type := self.gir.get_type_by_cname(cname):
return type
for ns, version in _NAMESPACES:
try:
namespace = get_namespace(ns, version)
if type := namespace.get_type_by_cname(cname):
self.gir.add_namespace(namespace)
return type
except:
pass
def start_block(self):
self._blocks_need_end.append(None)
def end_block(self):
if close := self._blocks_need_end.pop():
self.print(close)
def end_block_with(self, text):
self._blocks_need_end[-1] = text
def print(self, line, newline=True):
if line == "}" or line == "]":
self._indent -= 1
# Add blank lines between different types of lines, for neatness
if newline:
if line == "}" or line == "]":
line_type = LineType.BLOCK_END
elif line.endswith("{") or line.endswith("]"):
line_type = LineType.BLOCK_START
elif line.endswith(";"):
line_type = LineType.STMT
else:
line_type = LineType.NONE
if line_type != self._last_line_type and self._last_line_type != LineType.BLOCK_START and line_type != LineType.BLOCK_END:
self._result += "\n"
self._last_line_type = line_type
self._result += (" " * self._indent) + line
if newline:
self._result += "\n"
if line.endswith("{") or line.endswith("["):
if len(self._blocks_need_end):
self._blocks_need_end[-1] = _CLOSING[line[-1]]
self._indent += 1
def print_attribute(self, name, value, type):
if type is None:
self.print(f"{name}: \"{escape_quote(value)}\";")
elif type.assignable_to(FloatType()):
self.print(f"{name}: {value};")
elif type.assignable_to(BoolType()):
val = truthy(value)
self.print(f"{name}: {'true' if val else 'false'};")
elif (
type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Texture"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger"))
):
self.print(f"{name}: \"{escape_quote(value)}\";")
elif type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("GObject.Object")):
self.print(f"{name}: {value};")
elif isinstance(type, Enumeration):
for member in type.members.values():
if member.nick == value or member.c_ident == value:
self.print(f"{name}: {member.name};")
break
else:
self.print(f"{name}: {value.replace('-', '_')};")
elif isinstance(type, Bitfield):
flags = re.sub(r"\s*\|\s*", " | ", value).replace("-", "_")
self.print(f"{name}: {flags};")
else:
self.print(f"{name}: \"{escape_quote(value)}\";")
def _decompile_element(ctx: DecompileCtx, gir, xml):
try:
decompiler = _DECOMPILERS.get(xml.tag)
if decompiler is None:
raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>")
args = {canon(name): value for name, value in xml.attrs.items()}
if decompiler._cdata:
if len(xml.children):
args["cdata"] = None
else:
args["cdata"] = xml.cdata
ctx.start_block()
gir = decompiler(ctx, gir, **args)
for child_type in xml.children.values():
for child in child_type:
_decompile_element(ctx, gir, child)
ctx.end_block()
except UnsupportedError as e:
raise e
except TypeError as e:
raise UnsupportedError(tag=xml.tag)
def decompile(data):
ctx = DecompileCtx()
xml = parse(data)
_decompile_element(ctx, None, xml)
return ctx.result
def canon(string: str) -> str:
if string == "class":
return "klass"
else:
return string.replace("-", "_").lower()
def truthy(string: str) -> bool:
return string.lower() in ["yes", "true", "t", "y", "1"]
def full_name(gir):
return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name
def lookup_by_cname(gir, cname: str):
if isinstance(gir, GirContext):
return gir.get_type_by_cname(cname)
else:
return gir.get_containing(Repository).get_type_by_cname(cname)
def decompiler(tag, cdata=False):
def decorator(func):
func._cdata = cdata
_DECOMPILERS[tag] = func
return func
return decorator
def escape_quote(string: str) -> str:
return (string
.replace("\\", "\\\\")
.replace("\'", "\\'")
.replace("\"", "\\\"")
.replace("\n", "\\n"))
@decompiler("interface")
def decompile_interface(ctx, gir):
return gir
@decompiler("requires")
def decompile_requires(ctx, gir, lib=None, version=None):
return gir
@decompiler("property", cdata=True)
def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=None, bind_flags=None, translatable="false", comments=None, context=None):
name = name.replace("_", "-")
if comments is not None:
ctx.print(f"/* Translators: {comments} */")
if cdata is None:
ctx.print(f"{name}: ", False)
ctx.end_block_with(";")
elif bind_source:
flags = ""
bind_flags = bind_flags or []
if "sync-create" not in bind_flags:
flags += " no-sync-create"
if "invert-boolean" in bind_flags:
flags += " inverted"
if "bidirectional" in bind_flags:
flags += " bidirectional"
ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};")
elif truthy(translatable):
if context is not None:
ctx.print(f"{name}: C_(\"{escape_quote(context)}\", \"{escape_quote(cdata)}\");")
else:
ctx.print(f"{name}: _(\"{escape_quote(cdata)}\");")
elif gir is None or gir.properties.get(name) is None:
ctx.print(f"{name}: \"{escape_quote(cdata)}\";")
else:
ctx.print_attribute(name, cdata, gir.properties.get(name).type)
return gir
@decompiler("attribute", cdata=True)
def decompile_attribute(ctx, gir, name, cdata, translatable="false", comments=None, context=None):
decompile_property(ctx, gir, name, cdata, translatable=translatable, comments=comments, context=context)
@decompiler("attributes")
def decompile_attributes(ctx, gir):
ctx.print("attributes {")
@dataclass
class UnsupportedError(Exception):
message: str = "unsupported feature"
tag: T.Optional[str] = None
def print(self, filename: str):
print(f"\n{Colors.RED}{Colors.BOLD}error: {self.message}{Colors.CLEAR}")
print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}")
if self.tag:
print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}")
print(f"""{Colors.FAINT}The gtk-blueprint-tool compiler might support this feature, but the
porting tool does not. You probably need to port this file manually.{Colors.CLEAR}\n""")
blueprint-compiler-main/blueprintcompiler/errors.py 0000664 0000000 0000000 00000011071 14207747141 0023250 0 ustar 00root root 0000000 0000000 # errors.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from dataclasses import dataclass
import typing as T
import sys, traceback
from . import utils
from .utils import Colors
class PrintableError(Exception):
""" Parent class for errors that can be pretty-printed for the user, e.g.
compilation warnings and errors. """
def pretty_print(self, filename, code):
raise NotImplementedError()
class CompileError(PrintableError):
""" A PrintableError with a start/end position and optional hints """
category = "error"
color = Colors.RED
def __init__(self, message, start=None, end=None, did_you_mean=None, hints=None, actions=None):
super().__init__(message)
self.message = message
self.start = start
self.end = end
self.hints = hints or []
self.actions = actions or []
if did_you_mean is not None:
self._did_you_mean(*did_you_mean)
def hint(self, hint: str):
self.hints.append(hint)
return self
def _did_you_mean(self, word: str, options: T.List[str]):
if word.replace("_", "-") in options:
self.hint(f"use '-', not '_': `{word.replace('_', '-')}`")
return
recommend = utils.did_you_mean(word, options)
if recommend is not None:
if word.casefold() == recommend.casefold():
self.hint(f"Did you mean `{recommend}` (note the capitalization)?")
else:
self.hint(f"Did you mean `{recommend}`?")
self.actions.append(CodeAction(f"Change to `{recommend}`", recommend))
else:
self.hint("Did you check your spelling?")
self.hint("Are your dependencies up to date?")
def pretty_print(self, filename, code, stream=sys.stdout):
line_num, col_num = utils.idx_to_pos(self.start + 1, code)
line = code.splitlines(True)[line_num]
# Display 1-based line numbers
line_num += 1
stream.write(f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR}
at {filename} line {line_num} column {col_num}:
{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""")
for hint in self.hints:
stream.write(f"{Colors.FAINT}hint: {hint}{Colors.CLEAR}\n")
stream.write("\n")
class CompileWarning(CompileError):
category = "warning"
color = Colors.YELLOW
class UnexpectedTokenError(CompileError):
def __init__(self, start, end):
super().__init__("Unexpected tokens", start, end)
@dataclass
class CodeAction:
title: str
replace_with: str
class MultipleErrors(PrintableError):
""" If multiple errors occur during compilation, they can be collected into
a list and re-thrown using the MultipleErrors exception. It will
pretty-print all of the errors and a count of how many errors there are. """
def __init__(self, errors: T.List[CompileError]):
super().__init__()
self.errors = errors
def pretty_print(self, filename, code) -> None:
for error in self.errors:
error.pretty_print(filename, code)
if len(self.errors) != 1:
print(f"{len(self.errors)} errors")
class CompilerBugError(Exception):
""" Emitted on assertion errors """
def assert_true(truth: bool, message:str=None):
if not truth:
raise CompilerBugError(message)
def report_bug(): # pragma: no cover
""" Report an error and ask people to report it. """
print(traceback.format_exc())
print(f"Arguments: {sys.argv}\n")
print(f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG *****
The blueprint-compiler program has crashed. Please report the above stacktrace,
along with the input file(s) if possible, on GitLab:
{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue
{Colors.CLEAR}""")
blueprint-compiler-main/blueprintcompiler/gir.py 0000664 0000000 0000000 00000035753 14207747141 0022532 0 ustar 00root root 0000000 0000000 # gir.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
import os, sys
from .errors import CompileError, CompilerBugError
from .utils import lazy_prop
from . import xml_reader
extra_search_paths: T.List[str] = []
_namespace_cache = {}
_search_paths = []
xdg_data_home = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
_search_paths.append(os.path.join(xdg_data_home, "gir-1.0"))
xdg_data_dirs = os.environ.get("XDG_DATA_DIRS", "/usr/share:/usr/local/share").split(":")
_search_paths += [os.path.join(dir, "gir-1.0") for dir in xdg_data_dirs]
def get_namespace(namespace, version):
filename = f"{namespace}-{version}.gir"
if filename not in _namespace_cache:
for search_path in _search_paths:
path = os.path.join(search_path, filename)
if os.path.exists(path) and os.path.isfile(path):
xml = xml_reader.parse(path, xml_reader.PARSE_GIR)
repository = Repository(xml)
_namespace_cache[filename] = repository.namespaces.get(namespace)
break
if filename not in _namespace_cache:
raise CompileError(f"Namespace {namespace}-{version} could not be found")
return _namespace_cache[filename]
class GirType:
@property
def doc(self):
return None
def assignable_to(self, other) -> bool:
raise NotImplementedError()
@property
def full_name(self) -> str:
raise NotImplementedError()
class BasicType(GirType):
name: str = "unknown type"
@property
def full_name(self) -> str:
return self.name
class BoolType(BasicType):
name = "bool"
def assignable_to(self, other) -> bool:
return isinstance(other, BoolType)
class IntType(BasicType):
name = "int"
def assignable_to(self, other) -> bool:
return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType)
class UIntType(BasicType):
name = "uint"
def assignable_to(self, other) -> bool:
return isinstance(other, IntType) or isinstance(other, UIntType) or isinstance(other, FloatType)
class FloatType(BasicType):
name = "float"
def assignable_to(self, other) -> bool:
return isinstance(other, FloatType)
class StringType(BasicType):
name = "string"
def assignable_to(self, other) -> bool:
return isinstance(other, StringType)
_BASIC_TYPES = {
"gboolean": BoolType,
"int": IntType,
"gint": IntType,
"gint64": IntType,
"guint": UIntType,
"guint64": UIntType,
"gfloat": FloatType,
"gdouble": FloatType,
"float": FloatType,
"double": FloatType,
"utf8": StringType,
}
class GirNode:
def __init__(self, container, xml):
self.container = container
self.xml = xml
def get_containing(self, container_type):
if self.container is None:
return None
elif isinstance(self.container, container_type):
return self.container
else:
return self.container.get_containing(container_type)
@lazy_prop
def glib_type_name(self):
return self.xml["glib:type-name"]
@lazy_prop
def full_name(self):
if self.container is None:
return self.name
else:
return f"{self.container.name}.{self.name}"
@lazy_prop
def name(self) -> str:
return self.xml["name"]
@lazy_prop
def cname(self) -> str:
return self.xml["c:type"]
@lazy_prop
def available_in(self) -> str:
return self.xml.get("version")
@lazy_prop
def doc(self) -> T.Optional[str]:
sections = []
if self.signature:
sections.append("```\n" + self.signature + "\n```")
el = self.xml.get_elements("doc")
if len(el) == 1:
sections.append(el[0].cdata.strip())
return "\n\n---\n\n".join(sections)
@property
def signature(self) -> T.Optional[str]:
return None
@property
def type_name(self):
return self.xml.get_elements('type')[0]['name']
@property
def type(self):
return self.get_containing(Namespace).lookup_type(self.type_name)
class Property(GirNode):
def __init__(self, klass, xml: xml_reader.Element):
super().__init__(klass, xml)
@property
def signature(self):
return f"{self.type_name} {self.container.name}.{self.name}"
class Parameter(GirNode):
def __init__(self, container: GirNode, xml: xml_reader.Element):
super().__init__(container, xml)
class Signal(GirNode):
def __init__(self, klass, xml: xml_reader.Element):
super().__init__(klass, xml)
if parameters := xml.get_elements('parameters'):
self.params = [Parameter(self, child) for child in parameters[0].get_elements('parameter')]
else:
self.params = []
@property
def signature(self):
args = ", ".join([f"{p.type_name} {p.name}" for p in self.params])
return f"signal {self.container.name}.{self.name} ({args})"
class Interface(GirNode, GirType):
def __init__(self, ns, xml: xml_reader.Element):
super().__init__(ns, xml)
self.properties = {child["name"]: Property(self, child) for child in xml.get_elements("property")}
self.signals = {child["name"]: Signal(self, child) for child in xml.get_elements("glib:signal")}
self.prerequisites = [child["name"] for child in xml.get_elements("prerequisite")]
def assignable_to(self, other) -> bool:
if self == other:
return True
for pre in self.prerequisites:
if self.get_containing(Namespace).lookup_type(pre).assignable_to(other):
return True
return False
class Class(GirNode, GirType):
def __init__(self, ns, xml: xml_reader.Element):
super().__init__(ns, xml)
self._parent = xml["parent"]
self.implements = [impl["name"] for impl in xml.get_elements("implements")]
self.own_properties = {child["name"]: Property(self, child) for child in xml.get_elements("property")}
self.own_signals = {child["name"]: Signal(self, child) for child in xml.get_elements("glib:signal")}
@property
def signature(self):
result = f"class {self.container.name}.{self.name}"
if self.parent is not None:
result += f" : {self.parent.container.name}.{self.parent.name}"
if len(self.implements):
result += " implements " + ", ".join(self.implements)
return result
@lazy_prop
def properties(self):
return { p.name: p for p in self._enum_properties() }
@lazy_prop
def signals(self):
return { s.name: s for s in self._enum_signals() }
@lazy_prop
def parent(self):
if self._parent is None:
return None
return self.get_containing(Namespace).lookup_type(self._parent)
def assignable_to(self, other) -> bool:
if self == other:
return True
elif self.parent and self.parent.assignable_to(other):
return True
else:
for iface in self.implements:
if self.get_containing(Namespace).lookup_type(iface).assignable_to(other):
return True
return False
def _enum_properties(self):
yield from self.own_properties.values()
if self.parent is not None:
yield from self.parent.properties.values()
for impl in self.implements:
yield from self.get_containing(Namespace).lookup_type(impl).properties.values()
def _enum_signals(self):
yield from self.own_signals.values()
if self.parent is not None:
yield from self.parent.signals.values()
for impl in self.implements:
yield from self.get_containing(Namespace).lookup_type(impl).signals.values()
class EnumMember(GirNode):
def __init__(self, ns, xml: xml_reader.Element):
super().__init__(ns, xml)
self._value = xml["value"]
@property
def value(self):
return self._value
@property
def nick(self):
return self.xml["glib:nick"]
@property
def c_ident(self):
return self.xml["c:identifier"]
@property
def signature(self):
return f"enum member {self.full_name} = {self.value}"
class Enumeration(GirNode, GirType):
def __init__(self, ns, xml: xml_reader.Element):
super().__init__(ns, xml)
self.members = { child["name"]: EnumMember(self, child) for child in xml.get_elements("member") }
@property
def signature(self):
return f"enum {self.full_name}"
def assignable_to(self, type):
return type == self
class BitfieldMember(GirNode):
def __init__(self, ns, xml: xml_reader.Element):
super().__init__(ns, xml)
self._value = xml["value"]
@property
def value(self):
return self._value
@property
def signature(self):
return f"bitfield member {self.full_name} = {bin(self.value)}"
class Bitfield(GirNode, GirType):
def __init__(self, ns, xml: xml_reader.Element):
super().__init__(ns, xml)
self.members = { child["name"]: EnumMember(self, child) for child in xml.get_elements("member") }
@property
def signature(self):
return f"bitfield {self.full_name}"
def assignable_to(self, type):
return type == self
class Namespace(GirNode):
def __init__(self, repo, xml: xml_reader.Element):
super().__init__(repo, xml)
self.classes = { child["name"]: Class(self, child) for child in xml.get_elements("class") }
self.interfaces = { child["name"]: Interface(self, child) for child in xml.get_elements("interface") }
self.enumerations = { child["name"]: Enumeration(self, child) for child in xml.get_elements("enumeration") }
self.bitfields = { child["name"]: Bitfield(self, child) for child in xml.get_elements("bitfield") }
self.version = xml["version"]
@property
def signature(self):
return f"namespace {self.name} {self.version}"
def get_type(self, name):
""" Gets a type (class, interface, enum, etc.) from this namespace. """
return (
self.classes.get(name)
or self.interfaces.get(name)
or self.enumerations.get(name)
or self.bitfields.get(name)
)
def get_type_by_cname(self, cname: str):
""" Gets a type from this namespace by its C name. """
for item in [*self.classes.values(), *self.interfaces.values(), *self.enumerations.values()]:
if item.cname == cname:
return item
def lookup_type(self, type_name: str):
""" Looks up a type in the scope of this namespace (including in the
namespace's dependencies). """
if type_name in _BASIC_TYPES:
return _BASIC_TYPES[type_name]()
elif "." in type_name:
ns, name = type_name.split(".", 1)
return self.get_containing(Repository).get_type(name, ns)
else:
return self.get_type(type_name)
class Repository(GirNode):
def __init__(self, xml: xml_reader.Element):
super().__init__(None, xml)
self.namespaces = { child["name"]: Namespace(self, child) for child in xml.get_elements("namespace") }
try:
self.includes = { include["name"]: get_namespace(include["name"], include["version"]) for include in xml.get_elements("include") }
except:
raise CompilerBugError(f"Failed to load dependencies.")
def get_type(self, name: str, ns: str) -> T.Optional[GirNode]:
if namespace := self.namespaces.get(ns):
return namespace.get_type(name)
else:
return self.lookup_namespace(ns).get_type(name)
def get_type_by_cname(self, name: str) -> T.Optional[GirNode]:
for ns in self.namespaces.values():
if type := ns.get_type_by_cname(name):
return type
return None
def lookup_namespace(self, ns: str):
""" Finds a namespace among this namespace's dependencies. """
if namespace := self.namespaces.get(ns):
return namespace
else:
for include in self.includes.values():
if namespace := include.get_containing(Repository).lookup_namespace(ns):
return namespace
class GirContext:
def __init__(self):
self.namespaces = {}
def add_namespace(self, namespace: Namespace):
other = self.namespaces.get(namespace.name)
if other is not None and other.version != namespace.version:
raise CompileError(f"Namespace {namespace.name}-{namespace.version} can't be imported because version {other.version} was imported earlier")
self.namespaces[namespace.name] = namespace
def get_type_by_cname(self, name: str) -> T.Optional[GirNode]:
for ns in self.namespaces.values():
if type := ns.get_type_by_cname(name):
return type
return None
def get_type(self, name: str, ns: str) -> T.Optional[GirNode]:
ns = ns or "Gtk"
if ns not in self.namespaces:
return None
return self.namespaces[ns].get_type(name)
def get_class(self, name: str, ns: str) -> T.Optional[Class]:
type = self.get_type(name, ns)
if isinstance(type, Class):
return type
else:
return None
def validate_ns(self, ns: str):
""" Raises an exception if there is a problem looking up the given
namespace. """
ns = ns or "Gtk"
if ns not in self.namespaces:
raise CompileError(
f"Namespace {ns} was not imported",
did_you_mean=(ns, self.namespaces.keys()),
)
def validate_class(self, name: str, ns: str):
""" Raises an exception if there is a problem looking up the given
class (it doesn't exist, it isn't a class, etc.) """
ns = ns or "Gtk"
self.validate_ns(ns)
type = self.get_type(name, ns)
if type is None:
raise CompileError(
f"Namespace {ns} does not contain a class called {name}",
did_you_mean=(name, self.namespaces[ns].classes.keys()),
)
elif not isinstance(type, Class):
raise CompileError(
f"{ns}.{name} is not a class",
did_you_mean=(name, self.namespaces[ns].classes.keys()),
)
blueprint-compiler-main/blueprintcompiler/interactive_port.py 0000664 0000000 0000000 00000022626 14207747141 0025325 0 ustar 00root root 0000000 0000000 # interactive_port.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
import difflib
import os
from . import decompiler, tokenizer, parser
from .errors import MultipleErrors, PrintableError
from .utils import Colors
# A tool to interactively port projects to blueprints.
class CouldNotPort:
def __init__(self, message):
self.message = message
def change_suffix(f):
return f.removesuffix(".ui") + ".blp"
def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
if os.path.exists(out_file):
return CouldNotPort("already exists")
try:
decompiled = decompiler.decompile(in_file)
try:
# make sure the output compiles
tokens = tokenizer.tokenize(decompiled)
ast, errors, warnings = parser.parse(tokens)
for warning in warnings:
warning.pretty_print(out_file, decompiled)
if errors:
raise errors
if len(ast.errors):
raise MultipleErrors(ast.errors)
ast.generate()
except PrintableError as e:
e.pretty_print(out_file, decompiled)
print(f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}")
print(f"in {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE}")
print(
f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the
porting tool. If you think it's a bug (which is likely), please file an issue on GitLab:
{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""")
return CouldNotPort("does not compile")
return decompiled
except decompiler.UnsupportedError as e:
e.print(in_file)
return CouldNotPort("could not convert")
def listdir_recursive(subdir):
files = os.listdir(subdir)
for file in files:
if file in ["_build", "build"]:
continue
full = os.path.join(subdir, file)
if full == "./subprojects":
# skip the subprojects directory
return
if os.path.isfile(full):
yield full
elif os.path.isdir(full):
yield from listdir_recursive(full)
def yesno(prompt):
while True:
response = input(f"{Colors.BOLD}{prompt} [y/n] {Colors.CLEAR}")
if response.lower() in ["yes", "y"]:
return True
elif response.lower() in ["no", "n"]:
return False
def enter():
input(f"{Colors.BOLD}Press Enter when you have done that: {Colors.CLEAR}")
def step1():
print(f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}")
if os.path.exists("subprojects/blueprint-compiler.wrap"):
print("subprojects/blueprint-compiler.wrap already exists, skipping\n")
return
if yesno("Create subprojects/blueprint-compiler.wrap?"):
try:
os.mkdir("subprojects")
except:
pass
with open("subprojects/blueprint-compiler.wrap", "w") as wrap:
wrap.write("""[wrap-git]
directory = blueprint-compiler
url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git
revision = main
depth = 1
[provide]
program_names = blueprint-compiler""")
print()
def step2():
print(f"{Colors.BOLD}STEP 2: Set up .gitignore{Colors.CLEAR}")
if os.path.exists(".gitignore"):
with open(".gitignore", "r+") as gitignore:
ignored = [line.strip() for line in gitignore.readlines()]
if "/subprojects/blueprint-compiler" not in ignored:
if yesno("Add '/subprojects/blueprint-compiler' to .gitignore?"):
gitignore.write("\n/subprojects/blueprint-compiler\n")
else:
print("'/subprojects/blueprint-compiler' already in .gitignore, skipping")
else:
if yesno("Create .gitignore with '/subprojects/blueprint-compiler'?"):
with open(".gitignore", "w") as gitignore:
gitignore.write("/subprojects/blueprint-compiler\n")
print()
def step3():
print(f"{Colors.BOLD}STEP 3: Convert UI files{Colors.CLEAR}")
files = [
(file, change_suffix(file), decompile_file(file, change_suffix(file)))
for file in listdir_recursive(".")
if file.endswith(".ui")
]
success = 0
for in_file, out_file, result in files:
if isinstance(result, CouldNotPort):
if result.message == "already exists":
print(Colors.FAINT, end="")
print(f"{Colors.RED}will not port {Colors.UNDERLINE}{in_file}{Colors.NO_UNDERLINE} -> {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE} [{result.message}]{Colors.CLEAR}")
else:
print(f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}")
success += 1
print()
if len(files) == 0:
print(f"{Colors.RED}No UI files found.{Colors.CLEAR}")
elif success == len(files):
print(f"{Colors.GREEN}All files were converted.{Colors.CLEAR}")
elif success > 0:
print(f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}")
else:
print(f"{Colors.RED}None of the files could be converted.{Colors.CLEAR}")
if success > 0 and yesno("Save these changes?"):
for in_file, out_file, result in files:
if not isinstance(result, CouldNotPort):
with open(out_file, "x") as file:
file.write(result)
print()
results = [
(in_file, out_file)
for in_file, out_file, result in files
if not isinstance(result, CouldNotPort) or result.message == "already exists"
]
if len(results):
return zip(*results)
else:
return ([], [])
def step4(ported):
print(f"{Colors.BOLD}STEP 4: Set up meson.build{Colors.CLEAR}")
print(f"{Colors.BOLD}{Colors.YELLOW}NOTE: Depending on your build system setup, you may need to make some adjustments to this step.{Colors.CLEAR}")
meson_files = [file for file in listdir_recursive(".") if os.path.basename(file) == "meson.build"]
for meson_file in meson_files:
with open(meson_file, "r") as f:
if "gnome.compile_resources" in f.read():
parent = os.path.dirname(meson_file)
file_list = "\n ".join([
f"'{os.path.relpath(file, parent)}',"
for file in ported
if file.startswith(parent)
])
if len(file_list):
print(f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}")
print(f"""
blueprints = custom_target('blueprints',
input: files(
{file_list}
),
output: '.',
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
)
""")
enter()
print(f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()'
arguments in {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}
dependencies: blueprints,
""")
enter()
print()
def step5(in_files):
print(f"{Colors.BOLD}STEP 5: Update POTFILES.in{Colors.CLEAR}")
if not os.path.exists("po/POTFILES.in"):
print(f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n")
return
with open("po/POTFILES.in", "r") as potfiles:
old_lines = potfiles.readlines()
lines = old_lines.copy()
for in_file in in_files:
for i, line in enumerate(lines):
if line.strip() == in_file.removeprefix("./"):
lines[i] = change_suffix(line.strip()) + "\n"
new_data = "".join(lines)
print(f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}")
print(
"".join([
(Colors.GREEN if line.startswith('+') else Colors.RED + Colors.FAINT if line.startswith('-') else '') + line + Colors.CLEAR
for line in difflib.unified_diff(old_lines, lines)
])
)
if yesno("Is this ok?"):
with open("po/POTFILES.in", "w") as potfiles:
potfiles.writelines(lines)
print()
def step6(in_files):
print(f"{Colors.BOLD}STEP 6: Clean up{Colors.CLEAR}")
if yesno("Delete old XML files?"):
for file in in_files:
try:
os.remove(file)
except:
pass
def run(opts):
step1()
step2()
in_files, out_files = step3()
step4(out_files)
step5(in_files)
step6(in_files)
print(f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}")
blueprint-compiler-main/blueprintcompiler/language/ 0000775 0000000 0000000 00000000000 14207747141 0023145 5 ustar 00root root 0000000 0000000 blueprint-compiler-main/blueprintcompiler/language/__init__.py 0000664 0000000 0000000 00000002172 14207747141 0025260 0 ustar 00root root 0000000 0000000 """ Contains all the syntax beyond basic objects, properties, signal, and
templates. """
from .attributes import BaseAttribute, BaseTypedAttribute
from .gobject_object import Object, ObjectContent
from .gobject_property import Property
from .gobject_signal import Signal
from .gtk_a11y import A11y
from .gtk_combo_box_text import Items
from .gtk_file_filter import mime_types, patterns, suffixes
from .gtk_layout import Layout
from .gtk_menu import menu
from .gtk_size_group import Widgets
from .gtk_string_list import Strings
from .gtk_styles import Styles
from .gtkbuilder_child import Child
from .gtkbuilder_template import Template
from .imports import GtkDirective, Import
from .ui import UI
from .values import IdentValue, TranslatedStringValue, FlagsValue, LiteralValue
from .common import *
OBJECT_HOOKS.children = [
menu,
Object,
]
OBJECT_CONTENT_HOOKS.children = [
Signal,
Property,
A11y,
Styles,
Layout,
mime_types,
patterns,
suffixes,
Widgets,
Items,
Strings,
Child,
]
VALUE_HOOKS.children = [
TranslatedStringValue,
FlagsValue,
IdentValue,
LiteralValue,
]
blueprint-compiler-main/blueprintcompiler/language/attributes.py 0000664 0000000 0000000 00000002720 14207747141 0025706 0 ustar 00root root 0000000 0000000 # attributes.py
#
# Copyright 2022 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .values import Value, TranslatedStringValue
from .common import *
class BaseAttribute(AstNode):
""" A helper class for attribute syntax of the form `name: literal_value;`"""
tag_name: str = ""
attr_name: str = "name"
def emit_xml(self, xml: XmlEmitter):
value = self.children[Value][0]
attrs = { self.attr_name: self.tokens["name"] }
if isinstance(value, TranslatedStringValue):
attrs = { **attrs, **value.attrs }
xml.start_tag(self.tag_name, **attrs)
value.emit_xml(xml)
xml.end_tag()
class BaseTypedAttribute(BaseAttribute):
""" A BaseAttribute whose parent has a value_type property that can assist
in validation. """
blueprint-compiler-main/blueprintcompiler/language/common.py 0000664 0000000 0000000 00000002466 14207747141 0025017 0 ustar 00root root 0000000 0000000 # common.py
#
# Copyright 2022 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .. import gir
from ..ast_utils import AstNode, validate, docs
from ..errors import CompileError, MultipleErrors
from ..completions_utils import *
from .. import decompiler as decompile
from ..decompiler import DecompileCtx, decompiler
from ..gir import StringType, BoolType, IntType, FloatType, GirType
from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType
from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter
OBJECT_HOOKS = AnyOf()
OBJECT_CONTENT_HOOKS = AnyOf()
VALUE_HOOKS = AnyOf()
blueprint-compiler-main/blueprintcompiler/language/gobject_object.py 0000664 0000000 0000000 00000010462 14207747141 0026465 0 ustar 00root root 0000000 0000000 # gobject_object.py
#
# Copyright 2022 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from functools import cached_property
from .common import *
from .response_id import ResponseId
class ObjectContent(AstNode):
grammar = ["{", Until(OBJECT_CONTENT_HOOKS, "}")]
@property
def gir_class(self):
return self.parent.gir_class
# @validate()
# def only_one_style_class(self):
# if len(self.children[Style]) > 1:
# raise CompileError(
# f"Only one style directive allowed per object, but this object contains {len(self.children[Style])}",
# start=self.children[Style][1].group.start,
# )
def emit_xml(self, xml: XmlEmitter):
for x in self.children:
x.emit_xml(xml)
class Object(AstNode):
grammar: T.Any = [
class_name,
Optional(UseIdent("id")),
ObjectContent,
]
@validate("namespace")
def gir_ns_exists(self):
if not self.tokens["ignore_gir"]:
self.root.gir.validate_ns(self.tokens["namespace"])
@validate("class_name")
def gir_class_exists(self):
if self.tokens["class_name"] and not self.tokens["ignore_gir"] and self.gir_ns is not None:
self.root.gir.validate_class(self.tokens["class_name"], self.tokens["namespace"])
@property
def gir_ns(self):
if not self.tokens["ignore_gir"]:
return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk")
@property
def gir_class(self):
if self.tokens["class_name"] and not self.tokens["ignore_gir"]:
return self.root.gir.get_class(self.tokens["class_name"], self.tokens["namespace"])
@docs("namespace")
def namespace_docs(self):
if ns := self.root.gir.namespaces.get(self.tokens["namespace"]):
return ns.doc
@docs("class_name")
def class_docs(self):
if self.gir_class:
return self.gir_class.doc
@cached_property
def action_widgets(self) -> T.List[ResponseId]:
"""Get list of widget's action widgets.
Empty if object doesn't have action widgets.
"""
from .gtkbuilder_child import Child
return [
child.response_id
for child in self.children[ObjectContent][0].children[Child]
if child.response_id
]
def emit_xml(self, xml: XmlEmitter):
from .gtkbuilder_child import Child
xml.start_tag("object", **{
"class": self.gir_class.glib_type_name if self.gir_class else self.tokens["class_name"],
"id": self.tokens["id"],
})
for child in self.children:
child.emit_xml(xml)
# List action widgets
action_widgets = self.action_widgets
if action_widgets:
xml.start_tag("action-widgets")
for action_widget in action_widgets:
action_widget.emit_action_widget(xml)
xml.end_tag()
xml.end_tag()
def validate_parent_type(node, ns: str, name: str, err_msg: str):
parent = node.root.gir.get_type(name, ns)
container_type = node.parent_by_type(Object).gir_class
if container_type and not container_type.assignable_to(parent):
raise CompileError(f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}")
@decompiler("object")
def decompile_object(ctx, gir, klass, id=None):
gir_class = ctx.type_by_cname(klass)
klass_name = decompile.full_name(gir_class) if gir_class is not None else "." + klass
if id is None:
ctx.print(f"{klass_name} {{")
else:
ctx.print(f"{klass_name} {id} {{")
return gir_class
blueprint-compiler-main/blueprintcompiler/language/gobject_property.py 0000664 0000000 0000000 00000011034 14207747141 0027077 0 ustar 00root root 0000000 0000000 # gobject_property.py
#
# Copyright 2022 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import Object
from .gtkbuilder_template import Template
from .values import Value, TranslatedStringValue
from .common import *
class Property(AstNode):
grammar = AnyOf(
Statement(
UseIdent("name"),
":",
"bind",
UseIdent("bind_source").expected("the ID of a source object to bind from"),
".",
UseIdent("bind_property").expected("a property name to bind from"),
ZeroOrMore(AnyOf(
["no-sync-create", UseLiteral("no_sync_create", True)],
["inverted", UseLiteral("inverted", True)],
["bidirectional", UseLiteral("bidirectional", True)],
Match("sync-create").warn("sync-create is deprecated in favor of no-sync-create"),
)),
),
Statement(
UseIdent("name"),
":",
AnyOf(
OBJECT_HOOKS,
VALUE_HOOKS,
).expected("a value"),
),
)
@property
def gir_class(self):
return self.parent.parent.gir_class
@property
def gir_property(self):
if self.gir_class is not None:
return self.gir_class.properties.get(self.tokens["name"])
@property
def value_type(self):
if self.gir_property is not None:
return self.gir_property.type
@validate("name")
def property_exists(self):
if self.gir_class is None:
# Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself
return
if isinstance(self.parent.parent, Template):
# If the property is part of a template, it might be defined by
# the application and thus not in gir
return
if self.gir_property is None:
raise CompileError(
f"Class {self.gir_class.full_name} does not contain a property called {self.tokens['name']}",
did_you_mean=(self.tokens["name"], self.gir_class.properties.keys())
)
@validate()
def obj_property_type(self):
if len(self.children[Object]) == 0:
return
object = self.children[Object][0]
type = self.value_type
if object and type and object.gir_class and not object.gir_class.assignable_to(type):
raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {type.full_name}"
)
@docs("name")
def property_docs(self):
if self.gir_property is not None:
return self.gir_property.doc
def emit_xml(self, xml: XmlEmitter):
values = self.children[Value]
value = values[0] if len(values) == 1 else None
bind_flags = []
if self.tokens["bind_source"] and not self.tokens["no_sync_create"]:
bind_flags.append("sync-create")
if self.tokens["inverted"]:
bind_flags.append("invert-boolean")
if self.tokens["bidirectional"]:
bind_flags.append("bidirectional")
bind_flags_str = "|".join(bind_flags) or None
props = {
"name": self.tokens["name"],
"bind-source": self.tokens["bind_source"],
"bind-property": self.tokens["bind_property"],
"bind-flags": bind_flags_str,
}
if isinstance(value, TranslatedStringValue):
props = { **props, **value.attrs }
if len(self.children[Object]) == 1:
xml.start_tag("property", **props)
self.children[Object][0].emit_xml(xml)
xml.end_tag()
elif value is None:
xml.put_self_closing("property", **props)
else:
xml.start_tag("property", **props)
value.emit_xml(xml)
xml.end_tag()
blueprint-compiler-main/blueprintcompiler/language/gobject_signal.py 0000664 0000000 0000000 00000007003 14207747141 0026471 0 ustar 00root root 0000000 0000000 # gobject_signal.py
#
# Copyright 2022 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gtkbuilder_template import Template
from .common import *
class Signal(AstNode):
grammar = Statement(
UseIdent("name"),
Optional([
"::",
UseIdent("detail_name").expected("a signal detail name"),
]),
"=>",
UseIdent("handler").expected("the name of a function to handle the signal"),
Match("(").expected("argument list"),
Optional(UseIdent("object")).expected("object identifier"),
Match(")").expected(),
ZeroOrMore(AnyOf(
[Keyword("swapped"), UseLiteral("swapped", True)],
[Keyword("after"), UseLiteral("after", True)],
)),
)
@property
def gir_signal(self):
if self.gir_class is not None:
return self.gir_class.signals.get(self.tokens["name"])
@property
def gir_class(self):
return self.parent.parent.gir_class
@validate("name")
def signal_exists(self):
if self.gir_class is None:
# Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself
return
if isinstance(self.parent.parent, Template):
# If the signal is part of a template, it might be defined by
# the application and thus not in gir
return
if self.gir_signal is None:
raise CompileError(
f"Class {self.gir_class.full_name} does not contain a signal called {self.tokens['name']}",
did_you_mean=(self.tokens["name"], self.gir_class.signals.keys())
)
@validate("object")
def object_exists(self):
object_id = self.tokens["object"]
if object_id is None:
return
if self.root.objects_by_id.get(object_id) is None:
raise CompileError(
f"Could not find object with ID '{object_id}'"
)
@docs("name")
def signal_docs(self):
if self.gir_signal is not None:
return self.gir_signal.doc
def emit_xml(self, xml: XmlEmitter):
name = self.tokens["name"]
if self.tokens["detail_name"]:
name += "::" + self.tokens["detail_name"]
xml.put_self_closing(
"signal",
name=name,
handler=self.tokens["handler"],
swapped="true" if self.tokens["swapped"] else None,
object=self.tokens["object"]
)
@decompiler("signal")
def decompile_signal(ctx, gir, name, handler, swapped="false", object=None):
object_name = object or ""
name = name.replace("_", "-")
if decompile.truthy(swapped):
ctx.print(f"{name} => {handler}({object_name}) swapped;")
else:
ctx.print(f"{name} => {handler}({object_name});")
return gir
blueprint-compiler-main/blueprintcompiler/language/gtk_a11y.py 0000664 0000000 0000000 00000013645 14207747141 0025150 0 ustar 00root root 0000000 0000000 # gtk_a11y.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import ObjectContent, validate_parent_type
from .attributes import BaseTypedAttribute
from .values import Value
from .common import *
def get_property_types(gir):
# from
return {
"autocomplete": gir.get_type("AccessibleAutocomplete", "Gtk"),
"description": StringType(),
"has_popup": BoolType(),
"key_shortcuts": StringType(),
"label": StringType(),
"level": IntType(),
"modal": BoolType(),
"multi_line": BoolType(),
"multi_selectable": BoolType(),
"orientation": gir.get_type("Orientation", "Gtk"),
"placeholder": StringType(),
"read_only": BoolType(),
"required": BoolType(),
"role_description": StringType(),
"sort": gir.get_type("AccessibleSort", "Gtk"),
"value_max": FloatType(),
"value_min": FloatType(),
"value_now": FloatType(),
"value_text": StringType(),
}
def get_relation_types(gir):
# from
widget = gir.get_type("Widget", "Gtk")
return {
"active_descendant": widget,
"col_count": IntType(),
"col_index": IntType(),
"col_index_text": StringType(),
"col_span": IntType(),
"controls": widget,
"described_by": widget,
"details": widget,
"error_message": widget,
"flow_to": widget,
"labelled_by": widget,
"owns": widget,
"pos_in_set": IntType(),
"row_count": IntType(),
"row_index": IntType(),
"row_index_text": StringType(),
"row_span": IntType(),
"set_size": IntType(),
}
def get_state_types(gir):
# from
return {
"busy": BoolType(),
"checked": gir.get_type("AccessibleTristate", "Gtk"),
"disabled": BoolType(),
"expanded": BoolType(),
"hidden": BoolType(),
"invalid": gir.get_type("AccessibleInvalidState", "Gtk"),
"pressed": gir.get_type("AccessibleTristate", "Gtk"),
"selected": BoolType(),
}
def get_types(gir):
return {
**get_property_types(gir),
**get_relation_types(gir),
**get_state_types(gir),
}
def _get_docs(gir, name):
return (
gir.get_type("AccessibleProperty", "Gtk").members.get(name)
or gir.get_type("AccessibleRelation", "Gtk").members.get(name)
or gir.get_type("AccessibleState", "Gtk").members.get(name)
).doc
class A11yProperty(BaseTypedAttribute):
grammar = Statement(
UseIdent("name"),
":",
VALUE_HOOKS.expected("a value"),
)
@property
def tag_name(self):
name = self.tokens["name"]
gir = self.root.gir
if name in get_property_types(gir):
return "property"
elif name in get_relation_types(gir):
return "relation"
elif name in get_state_types(gir):
return "state"
else:
raise CompilerBugError()
@property
def value_type(self) -> GirType:
return get_types(self.root.gir).get(self.tokens["name"])
@validate("name")
def is_valid_property(self):
types = get_types(self.root.gir)
if self.tokens["name"] not in types:
raise CompileError(
f"'{self.tokens['name']}' is not an accessibility property, relation, or state",
did_you_mean=(self.tokens["name"], types.keys()),
)
@docs("name")
def prop_docs(self):
if self.tokens["name"] in get_types(self.root.gir):
return _get_docs(self.root.gir, self.tokens["name"])
class A11y(AstNode):
grammar = [
Keyword("accessibility"),
"{",
Until(A11yProperty, "}"),
]
@validate("accessibility")
def container_is_widget(self):
validate_parent_type(self, "Gtk", "Widget", "accessibility properties")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("accessibility")
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@completer(
applies_in=[ObjectContent],
matches=new_statement_patterns,
)
def a11y_completer(ast_node, match_variables):
yield Completion(
"accessibility", CompletionItemKind.Snippet,
snippet="accessibility {\n $0\n}"
)
@completer(
applies_in=[A11y],
matches=new_statement_patterns,
)
def a11y_name_completer(ast_node, match_variables):
for name, type in get_types(ast_node.root.gir).items():
yield Completion(name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type))
@decompiler("relation", cdata=True)
def decompile_relation(ctx, gir, name, cdata):
ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name))
@decompiler("state", cdata=True)
def decompile_state(ctx, gir, name, cdata, translatable="false"):
if decompile.truthy(translatable):
ctx.print(f"{name}: _(\"{_escape_quote(cdata)}\");")
else:
ctx.print_attribute(name, cdata, get_types(ctx.gir).get(name))
@decompiler("accessibility")
def decompile_accessibility(ctx, gir):
ctx.print("accessibility {")
blueprint-compiler-main/blueprintcompiler/language/gtk_combo_box_text.py 0000664 0000000 0000000 00000003567 14207747141 0027412 0 ustar 00root root 0000000 0000000 # gtk_combo_box_text.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .attributes import BaseTypedAttribute
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class Item(BaseTypedAttribute):
tag_name = "item"
attr_name = "id"
@property
def value_type(self):
return StringType()
item = Group(
Item,
[
Optional([
UseIdent("name"),
":",
]),
VALUE_HOOKS,
]
)
class Items(AstNode):
grammar = [
Keyword("items"),
"[",
Delimited(item, ","),
"]",
]
@validate("items")
def container_is_combo_box_text(self):
validate_parent_type(self, "Gtk", "ComboBoxText", "combo box items")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("items")
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "ComboBoxText"),
matches=new_statement_patterns,
)
def items_completer(ast_node, match_variables):
yield Completion(
"items", CompletionItemKind.Snippet,
snippet="items [$0]"
)
blueprint-compiler-main/blueprintcompiler/language/gtk_file_filter.py 0000664 0000000 0000000 00000005771 14207747141 0026662 0 ustar 00root root 0000000 0000000 # gtk_file_filter.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class Filters(AstNode):
@validate()
def container_is_file_filter(self):
validate_parent_type(self, "Gtk", "FileFilter", "file filter properties")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag(self.tokens["tag_name"])
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
class FilterString(AstNode):
def emit_xml(self, xml):
xml.start_tag(self.tokens["tag_name"])
xml.put_text(self.tokens["name"])
xml.end_tag()
def create_node(tag_name: str, singular: str):
return Group(
Filters,
[
Keyword(tag_name),
UseLiteral("tag_name", tag_name),
"[",
Delimited(
Group(
FilterString,
[
UseQuoted("name"),
UseLiteral("tag_name", singular),
]
),
",",
),
"]",
]
)
mime_types = create_node("mime-types", "mime-type")
patterns = create_node("patterns", "pattern")
suffixes = create_node("suffixes", "suffix")
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "FileFilter"),
matches=new_statement_patterns,
)
def file_filter_completer(ast_node, match_variables):
yield Completion("mime-types", CompletionItemKind.Snippet, snippet="mime-types [\"$0\"]")
yield Completion("patterns", CompletionItemKind.Snippet, snippet="patterns [\"$0\"]")
yield Completion("suffixes", CompletionItemKind.Snippet, snippet="suffixes [\"$0\"]")
@decompiler("mime-types")
def decompile_mime_types(ctx, gir):
ctx.print("mime-types [")
@decompiler("mime-type", cdata=True)
def decompile_mime_type(ctx, gir, cdata):
ctx.print(f'"{cdata}",')
@decompiler("patterns")
def decompile_patterns(ctx, gir):
ctx.print("patterns [")
@decompiler("pattern", cdata=True)
def decompile_pattern(ctx, gir, cdata):
ctx.print(f'"{cdata}",')
@decompiler("suffixes")
def decompile_suffixes(ctx, gir):
ctx.print("suffixes [")
@decompiler("suffix", cdata=True)
def decompile_suffix(ctx, gir, cdata):
ctx.print(f'"{cdata}",')
blueprint-compiler-main/blueprintcompiler/language/gtk_layout.py 0000664 0000000 0000000 00000003735 14207747141 0025711 0 ustar 00root root 0000000 0000000 # gtk_layout.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .attributes import BaseAttribute
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class LayoutProperty(BaseAttribute):
tag_name = "property"
@property
def value_type(self):
# there isn't really a way to validate these
return None
layout_prop = Group(
LayoutProperty,
Statement(
UseIdent("name"),
":",
VALUE_HOOKS.expected("a value"),
)
)
class Layout(AstNode):
grammar = Sequence(
Keyword("layout"),
"{",
Until(layout_prop, "}"),
)
@validate("layout")
def container_is_widget(self):
validate_parent_type(self, "Gtk", "Widget", "layout properties")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("layout")
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Widget"),
matches=new_statement_patterns,
)
def layout_completer(ast_node, match_variables):
yield Completion(
"layout", CompletionItemKind.Snippet,
snippet="layout {\n $0\n}"
)
@decompiler("layout")
def decompile_layout(ctx, gir):
ctx.print("layout {")
blueprint-compiler-main/blueprintcompiler/language/gtk_menu.py 0000664 0000000 0000000 00000011424 14207747141 0025332 0 ustar 00root root 0000000 0000000 # gtk_menus.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .attributes import BaseAttribute
from .gobject_object import Object, ObjectContent
from .ui import UI
from .common import *
class Menu(Object):
def emit_xml(self, xml: XmlEmitter):
xml.start_tag(self.tokens["tag"], id=self.tokens["id"])
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@property
def gir_class(self):
return self.root.gir.namespaces["Gtk"].lookup_type("Gio.MenuModel")
class MenuAttribute(BaseAttribute):
tag_name = "attribute"
@property
def value_type(self):
return None
menu_contents = Sequence()
menu_section = Group(
Menu,
[
"section",
UseLiteral("tag", "section"),
Optional(UseIdent("id")),
menu_contents
]
)
menu_submenu = Group(
Menu,
[
"submenu",
UseLiteral("tag", "submenu"),
Optional(UseIdent("id")),
menu_contents
]
)
menu_attribute = Group(
MenuAttribute,
[
UseIdent("name"),
":",
VALUE_HOOKS.expected("a value"),
Match(";").expected(),
]
)
menu_item = Group(
Menu,
[
"item",
UseLiteral("tag", "item"),
Optional(UseIdent("id")),
Match("{").expected(),
Until(menu_attribute, "}"),
]
)
menu_item_shorthand = Group(
Menu,
[
"item",
UseLiteral("tag", "item"),
"(",
Group(
MenuAttribute,
[UseLiteral("name", "label"), VALUE_HOOKS],
),
Optional([
",",
Optional([
Group(
MenuAttribute,
[UseLiteral("name", "action"), VALUE_HOOKS],
),
Optional([
",",
Group(
MenuAttribute,
[UseLiteral("name", "icon"), VALUE_HOOKS],
),
])
])
]),
Match(")").expected(),
]
)
menu_contents.children = [
Match("{"),
Until(AnyOf(
menu_section,
menu_submenu,
menu_item_shorthand,
menu_item,
menu_attribute,
), "}"),
]
menu = Group(
Menu,
[
"menu",
UseLiteral("tag", "menu"),
Optional(UseIdent("id")),
menu_contents
],
)
@completer(
applies_in=[UI],
matches=new_statement_patterns,
)
def menu_completer(ast_node, match_variables):
yield Completion(
"menu", CompletionItemKind.Snippet,
snippet="menu {\n $0\n}"
)
@completer(
applies_in=[Menu],
matches=new_statement_patterns,
)
def menu_content_completer(ast_node, match_variables):
yield Completion(
"submenu", CompletionItemKind.Snippet,
snippet="submenu {\n $0\n}"
)
yield Completion(
"section", CompletionItemKind.Snippet,
snippet="section {\n $0\n}"
)
yield Completion(
"item", CompletionItemKind.Snippet,
snippet="item {\n $0\n}"
)
yield Completion(
"item (shorthand)", CompletionItemKind.Snippet,
snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")'
)
yield Completion(
"label", CompletionItemKind.Snippet,
snippet='label: $0;'
)
yield Completion(
"action", CompletionItemKind.Snippet,
snippet='action: "$0";'
)
yield Completion(
"icon", CompletionItemKind.Snippet,
snippet='icon: "$0";'
)
@decompiler("menu")
def decompile_menu(ctx, gir, id=None):
if id:
ctx.print(f"menu {id} {{")
else:
ctx.print("menu {")
@decompiler("submenu")
def decompile_submenu(ctx, gir, id=None):
if id:
ctx.print(f"submenu {id} {{")
else:
ctx.print("submenu {")
@decompiler("item")
def decompile_item(ctx, gir, id=None):
if id:
ctx.print(f"item {id} {{")
else:
ctx.print("item {")
@decompiler("section")
def decompile_section(ctx, gir, id=None):
if id:
ctx.print(f"section {id} {{")
else:
ctx.print("section {")
blueprint-compiler-main/blueprintcompiler/language/gtk_size_group.py 0000664 0000000 0000000 00000004441 14207747141 0026555 0 ustar 00root root 0000000 0000000 # gtk_size_group.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class Widget(AstNode):
grammar = UseIdent("name")
@validate("name")
def obj_widget(self):
object = self.root.objects_by_id.get(self.tokens["name"])
type = self.root.gir.get_type("Widget", "Gtk")
if object is None:
raise CompileError(
f"Could not find object with ID {self.tokens['name']}",
did_you_mean=(self.tokens['name'], self.root.objects_by_id.keys()),
)
elif object.gir_class and not object.gir_class.assignable_to(type):
raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {type.full_name}"
)
def emit_xml(self, xml: XmlEmitter):
xml.put_self_closing("widget", name=self.tokens["name"])
class Widgets(AstNode):
grammar = [
Keyword("widgets"),
"[",
Delimited(Widget, ","),
"]",
]
@validate("widgets")
def container_is_size_group(self):
validate_parent_type(self, "Gtk", "SizeGroup", "size group properties")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("widgets")
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "SizeGroup"),
matches=new_statement_patterns,
)
def size_group_completer(ast_node, match_variables):
yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]")
blueprint-compiler-main/blueprintcompiler/language/gtk_string_list.py 0000664 0000000 0000000 00000004000 14207747141 0026717 0 ustar 00root root 0000000 0000000 # gtk_combo_box_text.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .attributes import BaseTypedAttribute
from .gobject_object import ObjectContent, validate_parent_type
from .values import Value, TranslatedStringValue
from .common import *
class Item(AstNode):
grammar = VALUE_HOOKS
@property
def value_type(self):
return StringType()
def emit_xml(self, xml: XmlEmitter):
value = self.children[Value][0]
attrs = value.attrs if isinstance(value, TranslatedStringValue) else {}
xml.start_tag("item", **attrs)
value.emit_xml(xml)
xml.end_tag()
class Strings(AstNode):
grammar = [
Keyword("strings"),
"[",
Delimited(Item, ","),
"]",
]
@validate("items")
def container_is_string_list(self):
validate_parent_type(self, "Gtk", "StringList", "StringList items")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("items")
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "StringList"),
matches=new_statement_patterns,
)
def strings_completer(ast_node, match_variables):
yield Completion(
"strings", CompletionItemKind.Snippet,
snippet="strings [$0]"
)
blueprint-compiler-main/blueprintcompiler/language/gtk_styles.py 0000664 0000000 0000000 00000003522 14207747141 0025711 0 ustar 00root root 0000000 0000000 # gtk_styles.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class StyleClass(AstNode):
grammar = UseQuoted("name")
def emit_xml(self, xml):
xml.put_self_closing("class", name=self.tokens["name"])
class Styles(AstNode):
grammar = [
Keyword("styles"),
"[",
Delimited(StyleClass, ","),
"]",
]
@validate("styles")
def container_is_widget(self):
validate_parent_type(self, "Gtk", "Widget", "style classes")
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("style")
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Widget"),
matches=new_statement_patterns,
)
def style_completer(ast_node, match_variables):
yield Completion("styles", CompletionItemKind.Keyword, snippet="styles [\"$0\"]")
@decompiler("style")
def decompile_style(ctx, gir):
ctx.print(f"styles [")
@decompiler("class")
def decompile_style_class(ctx, gir, name):
ctx.print(f'"{name}",')
blueprint-compiler-main/blueprintcompiler/language/gtkbuilder_child.py 0000664 0000000 0000000 00000004241 14207747141 0027017 0 ustar 00root root 0000000 0000000 # gtkbuilder_child.py
#
# Copyright 2022 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from functools import cached_property
from .gobject_object import Object
from .response_id import ResponseId
from .common import *
class Child(AstNode):
grammar = [
Optional([
"[",
Optional(["internal-child", UseLiteral("internal_child", True)]),
UseIdent("child_type").expected("a child type"),
Optional(ResponseId),
"]",
]),
Object,
]
@cached_property
def response_id(self) -> T.Optional[ResponseId]:
"""Get action widget's response ID.
If child is not action widget, returns `None`.
"""
response_ids = self.children[ResponseId]
if response_ids:
return response_ids[0]
else:
return None
def emit_xml(self, xml: XmlEmitter):
child_type = internal_child = None
if self.tokens["internal_child"]:
internal_child = self.tokens["child_type"]
else:
child_type = self.tokens["child_type"]
xml.start_tag("child", type=child_type, internal_child=internal_child)
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@decompiler("child")
def decompile_child(ctx, gir, type=None, internal_child=None):
if type is not None:
ctx.print(f"[{type}]")
elif internal_child is not None:
ctx.print(f"[internal-child {internal_child}]")
return gir
blueprint-compiler-main/blueprintcompiler/language/gtkbuilder_template.py 0000664 0000000 0000000 00000003474 14207747141 0027556 0 ustar 00root root 0000000 0000000 # gtkbuilder_template.py
#
# Copyright 2022 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .gobject_object import Object, ObjectContent
from .common import *
class Template(Object):
grammar = [
"template",
UseIdent("name").expected("template class name"),
Optional([
Match(":"),
class_name.expected("parent class"),
]),
ObjectContent,
]
def emit_xml(self, xml: XmlEmitter):
if self.gir_class:
parent = self.gir_class.glib_type_name
elif self.tokens["class_name"]:
parent = self.tokens["class_name"]
else:
parent = None
xml.start_tag("template", **{"class": self.tokens["name"]}, parent=parent)
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@decompiler("template")
def decompile_template(ctx: DecompileCtx, gir, klass, parent="Widget"):
gir_class = ctx.type_by_cname(parent)
if gir_class is None:
ctx.print(f"template {klass} : .{parent} {{")
else:
ctx.print(f"template {klass} : {decompile.full_name(gir_class)} {{")
return gir_class
blueprint-compiler-main/blueprintcompiler/language/imports.py 0000664 0000000 0000000 00000004510 14207747141 0025214 0 ustar 00root root 0000000 0000000 # imports.py
#
# Copyright 2022 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .. import gir
from .common import *
class GtkDirective(AstNode):
grammar = Statement(
Match("using").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"),
Match("Gtk").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"),
UseNumberText("version").expected("a version number for GTK"),
)
@validate("version")
def gtk_version(self):
if self.tokens["version"] not in ["4.0"]:
err = CompileError("Only GTK 4 is supported")
if self.tokens["version"].startswith("4"):
err.hint("Expected the GIR version, not an exact version number. Use `using Gtk 4.0;`.")
else:
err.hint("Expected `using Gtk 4.0;`")
raise err
@property
def gir_namespace(self):
return gir.get_namespace("Gtk", self.tokens["version"])
def emit_xml(self, xml: XmlEmitter):
xml.put_self_closing("requires", lib="gtk", version=self.tokens["version"])
class Import(AstNode):
grammar = Statement(
"using",
UseIdent("namespace").expected("a GIR namespace"),
UseNumberText("version").expected("a version number"),
)
@validate("namespace", "version")
def namespace_exists(self):
gir.get_namespace(self.tokens["namespace"], self.tokens["version"])
@property
def gir_namespace(self):
try:
return gir.get_namespace(self.tokens["namespace"], self.tokens["version"])
except CompileError:
return None
def emit_xml(self, xml):
pass
blueprint-compiler-main/blueprintcompiler/language/response_id.py 0000664 0000000 0000000 00000011241 14207747141 0026030 0 ustar 00root root 0000000 0000000 # response_id.py
#
# Copyright 2022 Gleb Smirnov
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from .common import *
class ResponseId(AstNode):
"""Response ID of action widget."""
ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [
("Gtk", "Dialog"),
("Gtk", "InfoBar")
]
grammar = [
UseIdent("response"),
"=",
AnyOf(
UseIdent("response_id"),
UseNumber("response_id")
),
Optional([
Keyword("default"), UseLiteral("is_default", True)
])
]
@validate()
def child_type_is_action(self) -> None:
"""Check that child type is "action"."""
child_type = self.parent.tokens["child_type"]
if child_type != "action":
raise CompileError(f"Only action widget can have response ID")
@validate()
def parent_has_action_widgets(self) -> None:
"""Chech that parent widget has allowed type."""
from .gobject_object import Object
container_type = self.parent_by_type(Object).gir_class
gir = self.root.gir
for namespace, name in ResponseId.ALLOWED_PARENTS:
parent_type = gir.get_type(name, namespace)
if container_type.assignable_to(parent_type):
break
else:
raise CompileError(
f"{container_type.full_name} doesn't have action widgets"
)
@validate()
def widget_have_id(self) -> None:
"""Check that action widget have ID."""
from .gobject_object import Object
_object = self.parent.children[Object][0]
if _object.tokens["id"] is None:
raise CompileError(f"Action widget must have ID")
@validate("response_id")
def correct_response_type(self) -> None:
"""Validate response type.
Response type might be GtkResponseType member
or positive number.
"""
gir = self.root.gir
response = self.tokens["response_id"]
if isinstance(response, int):
if response < 0:
raise CompileError(
"Numeric response type can't be negative")
elif isinstance(response, float):
raise CompileError(
"Response type must be GtkResponseType member or integer,"
" not float"
)
else:
responses = gir.get_type("ResponseType", "Gtk").members.keys()
if response not in responses:
raise CompileError(
f"Response type \"{response}\" doesn't exist")
@validate("default")
def no_multiple_default(self) -> None:
"""Only one action widget in dialog can be default."""
from .gtkbuilder_child import Child
from .gobject_object import Object
if not self.tokens["is_default"]:
return
action_widgets = self.parent_by_type(Object).action_widgets
for widget in action_widgets:
if widget == self:
break
if widget.tokens["is_default"]:
raise CompileError("Default response is already set")
@property
def widget_id(self) -> str:
"""Get action widget ID."""
from .gobject_object import Object
_object: Object = self.parent.children[Object][0]
return _object.tokens["id"]
def emit_xml(self, xml: XmlEmitter) -> None:
"""Emit nothing.
Response ID don't have to emit any XML in place,
but have to emit action-widget tag in separate
place (see `ResponseId.emit_action_widget`)
"""
def emit_action_widget(self, xml: XmlEmitter) -> None:
"""Emit action-widget XML.
Must be called while tag is open.
For more details see `GtkDialog` and `GtkInfoBar` docs.
"""
xml.start_tag(
"action-widget",
response=self.tokens["response_id"],
default=self.tokens["is_default"]
)
xml.put_text(self.widget_id)
xml.end_tag()
blueprint-compiler-main/blueprintcompiler/language/ui.py 0000664 0000000 0000000 00000006246 14207747141 0024144 0 ustar 00root root 0000000 0000000 # ui.py
#
# Copyright 2022 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .. import gir
from .imports import GtkDirective, Import
from .gtkbuilder_template import Template
from .common import *
class UI(AstNode):
""" The AST node for the entire file """
grammar = [
GtkDirective,
ZeroOrMore(Import),
Until(AnyOf(
Template,
OBJECT_HOOKS,
), Eof()),
]
@property
def gir(self):
gir_ctx = gir.GirContext()
self._gir_errors = []
try:
gir_ctx.add_namespace(self.children[GtkDirective][0].gir_namespace)
except CompileError as e:
e.start = self.children[GtkDirective][0].group.start
e.end = self.children[GtkDirective][0].group.end
self._gir_errors.append(e)
for i in self.children[Import]:
try:
if i.gir_namespace is not None:
gir_ctx.add_namespace(i.gir_namespace)
except CompileError as e:
e.start = i.group.tokens["namespace"].start
e.end = i.group.tokens["version"].end
self._gir_errors.append(e)
return gir_ctx
@property
def objects_by_id(self):
return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None }
@validate()
def gir_errors(self):
# make sure gir is loaded
self.gir
if len(self._gir_errors):
raise MultipleErrors(self._gir_errors)
@validate()
def at_most_one_template(self):
if len(self.children[Template]) > 1:
for template in self.children[Template][1:]:
raise CompileError(
f"Only one template may be defined per file, but this file contains {len(self.children[Template])}",
template.group.tokens["name"].start, template.group.tokens["name"].end,
)
@validate()
def unique_ids(self):
passed = {}
for obj in self.iterate_children_recursive():
if obj.tokens["id"] is None:
continue
if obj.tokens["id"] in passed:
token = obj.group.tokens["id"]
raise CompileError(f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end)
passed[obj.tokens["id"]] = obj
def emit_xml(self, xml: XmlEmitter):
xml.start_tag("interface")
for x in self.children:
x.emit_xml(xml)
xml.end_tag()
blueprint-compiler-main/blueprintcompiler/language/values.py 0000664 0000000 0000000 00000014341 14207747141 0025021 0 ustar 00root root 0000000 0000000 # values.py
#
# Copyright 2022 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .common import *
class Value(AstNode):
pass
class TranslatedStringValue(Value):
grammar = AnyOf(
[
"_",
"(",
UseQuoted("value").expected("a quoted string"),
Match(")").expected(),
],
[
"C_",
"(",
UseQuoted("context").expected("a quoted string"),
",",
UseQuoted("value").expected("a quoted string"),
Optional(","),
Match(")").expected(),
],
)
@property
def attrs(self):
attrs = { "translatable": "true" }
if "context" in self.tokens:
attrs["context"] = self.tokens["context"]
return attrs
def emit_xml(self, xml: XmlEmitter):
xml.put_text(self.tokens["value"])
class LiteralValue(Value):
grammar = AnyOf(
UseNumber("value"),
UseQuoted("value"),
)
def emit_xml(self, xml: XmlEmitter):
xml.put_text(self.tokens["value"])
@validate()
def validate_for_type(self):
type = self.parent.value_type
if isinstance(type, gir.IntType):
try:
int(self.tokens["value"])
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to integer")
elif isinstance(type, gir.UIntType):
try:
int(self.tokens["value"])
if int(self.tokens["value"]) < 0:
raise Exception()
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to unsigned integer")
elif isinstance(type, gir.FloatType):
try:
float(self.tokens["value"])
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to float")
elif isinstance(type, gir.StringType):
pass
elif isinstance(type, gir.Class) or isinstance(type, gir.Interface):
parseable_types = [
"Gdk.Paintable",
"Gdk.Texture",
"Gdk.Pixbuf",
"GLib.File",
"Gtk.ShortcutTrigger",
"Gtk.ShortcutAction",
]
if type.full_name not in parseable_types:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}")
elif type is not None:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}")
class Flag(AstNode):
grammar = UseIdent("value")
@validate()
def validate_for_type(self):
type = self.parent.parent.value_type
if isinstance(type, gir.Bitfield) and self.tokens["value"] not in type.members:
raise CompileError(
f"{self.tokens['value']} is not a member of {type.full_name}",
did_you_mean=(self.tokens['value'], type.members.keys()),
)
class FlagsValue(Value):
grammar = [Flag, "|", Delimited(Flag, "|")]
@validate()
def parent_is_bitfield(self):
type = self.parent.value_type
if not isinstance(type, gir.Bitfield):
raise CompileError(f"{type.full_name} is not a bitfield type")
def emit_xml(self, xml: XmlEmitter):
xml.put_text("|".join([flag.tokens["value"] for flag in self.children[Flag]]))
class IdentValue(Value):
grammar = UseIdent("value")
def emit_xml(self, xml: XmlEmitter):
if isinstance(self.parent.value_type, gir.Enumeration):
xml.put_text(self.parent.value_type.members[self.tokens["value"]].nick)
else:
xml.put_text(self.tokens["value"])
@validate()
def validate_for_type(self):
type = self.parent.value_type
if isinstance(type, gir.Enumeration) or isinstance(type, gir.Bitfield):
if self.tokens["value"] not in type.members:
raise CompileError(
f"{self.tokens['value']} is not a member of {type.full_name}",
did_you_mean=(self.tokens['value'], type.members.keys()),
)
elif isinstance(type, gir.BoolType):
if self.tokens["value"] not in ["true", "false"]:
raise CompileError(
f"Expected 'true' or 'false' for boolean value",
did_you_mean=(self.tokens['value'], ["true", "false"]),
)
elif type is not None:
object = self.root.objects_by_id.get(self.tokens["value"])
if object is None:
raise CompileError(
f"Could not find object with ID {self.tokens['value']}",
did_you_mean=(self.tokens['value'], self.root.objects_by_id.keys()),
)
elif object.gir_class and not object.gir_class.assignable_to(type):
raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {type.full_name}"
)
@docs()
def docs(self):
type = self.parent.value_type
if isinstance(type, gir.Enumeration):
if member := type.members.get(self.tokens["value"]):
return member.doc
else:
return type.doc
elif isinstance(type, gir.GirNode):
return type.doc
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
if isinstance(self.parent.value_type, gir.Enumeration):
token = self.group.tokens["value"]
yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember)
blueprint-compiler-main/blueprintcompiler/lsp.py 0000664 0000000 0000000 00000022715 14207747141 0022541 0 ustar 00root root 0000000 0000000 # lsp.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
import json, sys, traceback
from .completions import complete
from .errors import PrintableError, CompileError, MultipleErrors
from .lsp_utils import *
from . import tokenizer, parser, utils, xml_reader
def command(json_method):
def decorator(func):
func._json_method = json_method
return func
return decorator
class OpenFile:
def __init__(self, uri, text, version):
self.uri = uri
self.text = text
self.version = version
self.ast = None
self.tokens = None
self._update()
def apply_changes(self, changes):
for change in changes:
start = utils.pos_to_idx(change["range"]["start"]["line"], change["range"]["start"]["character"], self.text)
end = utils.pos_to_idx(change["range"]["end"]["line"], change["range"]["end"]["character"], self.text)
self.text = self.text[:start] + change["text"] + self.text[end:]
self._update()
def _update(self):
self.diagnostics = []
try:
self.tokens = tokenizer.tokenize(self.text)
self.ast, errors, warnings = parser.parse(self.tokens)
self.diagnostics += warnings
if errors is not None:
self.diagnostics += errors.errors
self.diagnostics += self.ast.errors
except MultipleErrors as e:
self.diagnostics += e.errors
except CompileError as e:
self.diagnostics.append(e)
def calc_semantic_tokens(self) -> T.List[int]:
tokens = list(self.ast.get_semantic_tokens())
token_lists = [
[
*utils.idx_to_pos(token.start, self.text), # line and column
token.end - token.start, # length
token.type,
0, # token modifiers
] for token in tokens]
# convert line, column numbers to deltas
for i, token_list in enumerate(token_lists[1:]):
token_list[0] -= token_lists[i][0]
if token_list[0] == 0:
token_list[1] -= token_lists[i][1]
# flatten the list
return [x for y in token_lists for x in y]
class LanguageServer:
commands: T.Dict[str, T.Callable] = {}
def __init__(self, logfile=None):
self.client_capabilities = {}
self._open_files: {str: OpenFile} = {}
self.logfile = logfile
def run(self):
# Read tags from gir files. During normal compilation these are
# ignored.
xml_reader.PARSE_GIR.add("doc")
try:
while True:
line = ""
content_len = -1
while content_len == -1 or (line != "\n" and line != "\r\n"):
line = sys.stdin.buffer.readline().decode()
if line == "":
return
if line.startswith("Content-Length:"):
content_len = int(line.split("Content-Length:")[1].strip())
line = sys.stdin.buffer.read(content_len).decode()
self._log("input: " + line)
data = json.loads(line)
method = data.get("method")
id = data.get("id")
params = data.get("params")
if method in self.commands:
self.commands[method](self, id, params)
except Exception as e:
self._log(traceback.format_exc())
def _send(self, data):
data["jsonrpc"] = "2.0"
line = json.dumps(data, separators=(",", ":")) + "\r\n"
self._log("output: " + line)
sys.stdout.write(f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}")
sys.stdout.flush()
def _log(self, msg):
if self.logfile is not None:
self.logfile.write(str(msg))
self.logfile.write("\n")
self.logfile.flush()
def _send_response(self, id, result):
self._send({
"id": id,
"result": result,
})
def _send_notification(self, method, params):
self._send({
"method": method,
"params": params,
})
@command("initialize")
def initialize(self, id, params):
self.client_capabilities = params.get("capabilities")
self._send_response(id, {
"capabilities": {
"textDocumentSync": {
"openClose": True,
"change": TextDocumentSyncKind.Incremental,
},
"semanticTokensProvider": {
"legend": {
"tokenTypes": ["enumMember"],
},
"full": True,
},
"completionProvider": {},
"codeActionProvider": {},
"hoverProvider": True,
}
})
@command("textDocument/didOpen")
def didOpen(self, id, params):
doc = params.get("textDocument")
uri = doc.get("uri")
version = doc.get("version")
text = doc.get("text")
open_file = OpenFile(uri, text, version)
self._open_files[uri] = open_file
self._send_file_updates(open_file)
@command("textDocument/didChange")
def didChange(self, id, params):
if params is not None:
open_file = self._open_files[params["textDocument"]["uri"]]
open_file.apply_changes(params["contentChanges"])
self._send_file_updates(open_file)
@command("textDocument/didClose")
def didClose(self, id, params):
del self._open_files[params["textDocument"]["uri"]]
@command("textDocument/hover")
def hover(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
docs = open_file.ast and open_file.ast.get_docs(utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text))
if docs:
self._send_response(id, {
"contents": {
"kind": "markdown",
"value": docs,
}
})
else:
self._send_response(id, None)
@command("textDocument/completion")
def completion(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
if open_file.ast is None:
self._send_response(id, [])
return
idx = utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text)
completions = complete(open_file.ast, open_file.tokens, idx)
self._send_response(id, [completion.to_json(True) for completion in completions])
@command("textDocument/semanticTokens/full")
def semantic_tokens(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
self._send_response(id, {
"data": open_file.calc_semantic_tokens(),
})
@command("textDocument/codeAction")
def code_actions(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
range_start = utils.pos_to_idx(params["range"]["start"]["line"], params["range"]["start"]["character"], open_file.text)
range_end = utils.pos_to_idx(params["range"]["end"]["line"], params["range"]["end"]["character"], open_file.text)
actions = [
{
"title": action.title,
"kind": "quickfix",
"diagnostics": [self._create_diagnostic(open_file.text, diagnostic)],
"edit": {
"changes": {
open_file.uri: [{
"range": utils.idxs_to_range(diagnostic.start, diagnostic.end, open_file.text),
"newText": action.replace_with
}]
}
}
}
for diagnostic in open_file.diagnostics
if not (diagnostic.end < range_start or diagnostic.start > range_end)
for action in diagnostic.actions
]
self._send_response(id, actions)
def _send_file_updates(self, open_file: OpenFile):
self._send_notification("textDocument/publishDiagnostics", {
"uri": open_file.uri,
"diagnostics": [self._create_diagnostic(open_file.text, err) for err in open_file.diagnostics],
})
def _create_diagnostic(self, text, err):
return {
"range": utils.idxs_to_range(err.start, err.end, text),
"message": err.message,
"severity": 1,
}
for name in dir(LanguageServer):
item = getattr(LanguageServer, name)
if callable(item) and hasattr(item, "_json_method"):
LanguageServer.commands[item._json_method] = item
blueprint-compiler-main/blueprintcompiler/lsp_utils.py 0000664 0000000 0000000 00000005335 14207747141 0023760 0 ustar 00root root 0000000 0000000 # lsp_enums.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from dataclasses import dataclass
import enum
import typing as T
from .errors import *
from .utils import *
class TextDocumentSyncKind(enum.IntEnum):
None_ = 0
Full = 1
Incremental = 2
class CompletionItemTag(enum.IntEnum):
Deprecated = 1
class InsertTextFormat(enum.IntEnum):
PlainText = 1
Snippet = 2
class CompletionItemKind(enum.IntEnum):
Text = 1
Method = 2
Function = 3
Constructor = 4
Field = 5
Variable = 6
Class = 7
Interface = 8
Module = 9
Property = 10
Unit = 11
Value = 12
Enum = 13
Keyword = 14
Snippet = 15
Color = 16
File = 17
Reference = 18
Folder = 19
EnumMember = 20
Constant = 21
Struct = 22
Event = 23
Operator = 24
TypeParameter = 25
@dataclass
class Completion:
label: str
kind: CompletionItemKind
signature: T.Optional[str] = None
deprecated: bool = False
docs: T.Optional[str] = None
text: T.Optional[str] = None
snippet: T.Optional[str] = None
def to_json(self, snippets: bool):
insert_text = self.text or self.label
insert_text_format = InsertTextFormat.PlainText
if snippets and self.snippet:
insert_text = self.snippet
insert_text_format = InsertTextFormat.Snippet
result = {
"label": self.label,
"kind": self.kind,
"tags": [CompletionItemTag.Deprecated] if self.deprecated else None,
"detail": self.signature,
"documentation": {
"kind": "markdown",
"value": self.docs,
} if self.docs else None,
"deprecated": self.deprecated,
"insertText": insert_text,
"insertTextFormat": insert_text_format,
}
return { k: v for k, v in result.items() if v is not None }
class SemanticTokenType(enum.IntEnum):
EnumMember = 0
@dataclass
class SemanticToken:
start: int
end: int
type: SemanticTokenType
blueprint-compiler-main/blueprintcompiler/main.py 0000664 0000000 0000000 00000011702 14207747141 0022661 0 ustar 00root root 0000000 0000000 # main.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
import argparse, json, os, sys
from .errors import PrintableError, report_bug, MultipleErrors
from .lsp import LanguageServer
from . import parser, tokenizer, decompiler, interactive_port
from .utils import Colors
from .xml_emitter import XmlEmitter
VERSION = "0.1.0"
class BlueprintApp:
def main(self):
self.parser = argparse.ArgumentParser()
self.subparsers = self.parser.add_subparsers(metavar="command")
self.parser.set_defaults(func=self.cmd_help)
compile = self.add_subcommand("compile", "Compile blueprint files", self.cmd_compile)
compile.add_argument("--output", dest="output", default="-")
compile.add_argument("input", metavar="filename", default=sys.stdin, type=argparse.FileType('r'))
batch_compile = self.add_subcommand("batch-compile", "Compile many blueprint files at once", self.cmd_batch_compile)
batch_compile.add_argument("output_dir", metavar="output-dir")
batch_compile.add_argument("input_dir", metavar="input-dir")
batch_compile.add_argument("inputs", nargs="+", metavar="filenames", default=sys.stdin, type=argparse.FileType('r'))
port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port)
lsp = self.add_subcommand("lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp)
lsp.add_argument("--logfile", dest="logfile", default=None, type=argparse.FileType('a'))
self.add_subcommand("help", "Show this message", self.cmd_help)
try:
opts = self.parser.parse_args()
opts.func(opts)
except SystemExit as e:
raise e
except KeyboardInterrupt:
print(f"\n\n{Colors.RED}{Colors.BOLD}Interrupted.{Colors.CLEAR}")
except EOFError:
print(f"\n\n{Colors.RED}{Colors.BOLD}Interrupted.{Colors.CLEAR}")
except:
report_bug()
def add_subcommand(self, name, help, func):
parser = self.subparsers.add_parser(name, help=help)
parser.set_defaults(func=func)
return parser
def cmd_help(self, opts):
self.parser.print_help()
def cmd_compile(self, opts):
data = opts.input.read()
try:
xml, warnings = self._compile(data)
for warning in warnings:
warning.pretty_print(opts.input.name, data, stream=sys.stderr)
if opts.output == "-":
print(xml)
else:
with open(opts.output, "w") as file:
file.write(xml)
except PrintableError as e:
e.pretty_print(opts.input.name, data)
sys.exit(1)
def cmd_batch_compile(self, opts):
for file in opts.inputs:
data = file.read()
try:
if not os.path.commonpath([file.name, opts.input_dir]):
print(f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}")
sys.exit(1)
xml, warnings = self._compile(data)
for warning in warnings:
warning.pretty_print(file.name, data, stream=sys.stderr)
path = os.path.join(
opts.output_dir,
os.path.relpath(
os.path.splitext(file.name)[0] + ".ui",
opts.input_dir
)
)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as file:
file.write(xml)
except PrintableError as e:
e.pretty_print(file.name, data)
sys.exit(1)
def cmd_lsp(self, opts):
langserv = LanguageServer(opts.logfile)
langserv.run()
def cmd_port(self, opts):
interactive_port.run(opts)
def _compile(self, data: str) -> T.Tuple[str, T.List[PrintableError]]:
tokens = tokenizer.tokenize(data)
ast, errors, warnings = parser.parse(tokens)
if errors:
raise errors
if len(ast.errors):
raise MultipleErrors(ast.errors)
return ast.generate(), warnings
def main():
BlueprintApp().main()
blueprint-compiler-main/blueprintcompiler/parse_tree.py 0000664 0000000 0000000 00000043414 14207747141 0024073 0 ustar 00root root 0000000 0000000 # parse_tree.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
""" Utilities for parsing an AST from a token stream. """
import typing as T
from collections import defaultdict
from enum import Enum
from .errors import assert_true, CompilerBugError, CompileError, CompileWarning, UnexpectedTokenError
from .tokenizer import Token, TokenType
SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE]
class ParseResult(Enum):
""" Represents the result of parsing. The extra EMPTY result is necessary
to avoid freezing the parser: imagine a ZeroOrMore node containing a node
that can match empty. It will repeatedly match empty and never advance
the parser. So, ZeroOrMore stops when a failed *or empty* match is
made. """
SUCCESS = 0
FAILURE = 1
EMPTY = 2
def matched(self):
return self == ParseResult.SUCCESS
def succeeded(self):
return self != ParseResult.FAILURE
def failed(self):
return self == ParseResult.FAILURE
class ParseGroup:
""" A matching group. Match groups have an AST type, children grouped by
type, and key=value pairs. At the end of parsing, the match groups will
be converted to AST nodes by passing the children and key=value pairs to
the AST node constructor. """
def __init__(self, ast_type, start: int):
self.ast_type = ast_type
self.children: T.List[ParseGroup] = []
self.keys: T.Dict[str, T.Any] = {}
self.tokens: T.Dict[str, Token] = {}
self.start = start
self.end = None
self.incomplete = False
def add_child(self, child):
self.children.append(child)
def set_val(self, key, val, token):
assert_true(key not in self.keys)
self.keys[key] = val
self.tokens[key] = token
def to_ast(self):
""" Creates an AST node from the match group. """
children = [child.to_ast() for child in self.children]
try:
return self.ast_type(self, children, self.keys, incomplete=self.incomplete)
except TypeError as e:
raise CompilerBugError(f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace.")
def __str__(self):
result = str(self.ast_type.__name__)
result += "".join([f"\n{key}: {val}" for key, val in self.keys.items()]) + "\n"
result += "\n".join([str(child) for children in self.children.values() for child in children])
return result.replace("\n", "\n ")
class ParseContext:
""" Contains the state of the parser. """
def __init__(self, tokens, index=0):
self.tokens = list(tokens)
self.index = index
self.start = index
self.group = None
self.group_keys = {}
self.group_children = []
self.last_group = None
self.group_incomplete = False
self.errors = []
self.warnings = []
def create_child(self):
""" Creates a new ParseContext at this context's position. The new
context will be used to parse one node. If parsing is successful, the
new context will be applied to "self". If parsing fails, the new
context will be discarded. """
ctx = ParseContext(self.tokens, self.index)
ctx.errors = self.errors
ctx.warnings = self.warnings
return ctx
def apply_child(self, other):
""" Applies a child context to this context. """
if other.group is not None:
# If the other context had a match group, collect all the matched
# values into it and then add it to our own match group.
for key, (val, token) in other.group_keys.items():
other.group.set_val(key, val, token)
for child in other.group_children:
other.group.add_child(child)
other.group.end = other.tokens[other.index - 1].end
other.group.incomplete = other.group_incomplete
self.group_children.append(other.group)
else:
# If the other context had no match group of its own, collect all
# its matched values
self.group_keys = {**self.group_keys, **other.group_keys}
self.group_children += other.group_children
self.group_incomplete |= other.group_incomplete
self.index = other.index
# Propagate the last parsed group down the stack so it can be easily
# retrieved at the end of the process
if other.group:
self.last_group = other.group
elif other.last_group:
self.last_group = other.last_group
def start_group(self, ast_type):
""" Sets this context to have its own match group. """
assert_true(self.group is None)
self.group = ParseGroup(ast_type, self.tokens[self.index].start)
def set_group_val(self, key, value, token):
""" Sets a matched key=value pair on the current match group. """
assert_true(key not in self.group_keys)
self.group_keys[key] = (value, token)
def set_group_incomplete(self):
""" Marks the current match group as incomplete (it could not be fully
parsed, but the parser recovered). """
self.group_incomplete = True
def skip(self):
""" Skips whitespace and comments. """
while self.index < len(self.tokens) and self.tokens[self.index].type in SKIP_TOKENS:
self.index += 1
def next_token(self) -> Token:
""" Advances the token iterator and returns the next token. """
self.skip()
token = self.tokens[self.index]
self.index += 1
return token
def peek_token(self) -> Token:
""" Returns the next token without advancing the iterator. """
self.skip()
token = self.tokens[self.index]
return token
def skip_unexpected_token(self):
""" Skips a token and logs an "unexpected token" error. """
self.skip()
start = self.tokens[self.index].start
self.next_token()
self.skip()
end = self.tokens[self.index - 1].end
if (len(self.errors)
and isinstance((err := self.errors[-1]), UnexpectedTokenError)
and err.end == start):
err.end = end
else:
self.errors.append(UnexpectedTokenError(start, end))
def is_eof(self) -> Token:
return self.index >= len(self.tokens) or self.peek_token().type == TokenType.EOF
class ParseNode:
""" Base class for the nodes in the parser tree. """
def parse(self, ctx: ParseContext) -> ParseResult:
""" Attempts to match the ParseNode at the context's current location. """
start_idx = ctx.index
inner_ctx = ctx.create_child()
if self._parse(inner_ctx):
ctx.apply_child(inner_ctx)
if ctx.index == start_idx:
return ParseResult.EMPTY
else:
return ParseResult.SUCCESS
else:
return ParseResult.FAILURE
def _parse(self, ctx: ParseContext) -> bool:
raise NotImplementedError()
def err(self, message):
""" Causes this ParseNode to raise an exception if it fails to parse.
This prevents the parser from backtracking, so you should understand
what it does and how the parser works before using it. """
return Err(self, message)
def expected(self, expect):
""" Convenience method for err(). """
return self.err("Expected " + expect)
def warn(self, message):
""" Causes this ParseNode to emit a warning if it parses successfully. """
return Warning(self, message)
class Err(ParseNode):
""" ParseNode that emits a compile error if it fails to parse. """
def __init__(self, child, message):
self.child = to_parse_node(child)
self.message = message
def _parse(self, ctx):
if self.child.parse(ctx).failed():
start_idx = ctx.start
while ctx.tokens[start_idx].type in SKIP_TOKENS:
start_idx += 1
start_token = ctx.tokens[start_idx]
end_token = ctx.tokens[ctx.index]
raise CompileError(self.message, start_token.start, end_token.end)
return True
class Warning(ParseNode):
""" ParseNode that emits a compile warning if it parses successfully. """
def __init__(self, child, message):
self.child = to_parse_node(child)
self.message = message
def _parse(self, ctx):
ctx.skip()
start_idx = ctx.index
if self.child.parse(ctx).succeeded():
start_token = ctx.tokens[start_idx]
end_token = ctx.tokens[ctx.index]
ctx.warnings.append(CompileWarning(self.message, start_token.start, end_token.end))
return True
class Fail(ParseNode):
""" ParseNode that emits a compile error if it parses successfully. """
def __init__(self, child, message):
self.child = to_parse_node(child)
self.message = message
def _parse(self, ctx):
if self.child.parse(ctx).succeeded():
start_idx = ctx.start
while ctx.tokens[start_idx].type in SKIP_TOKENS:
start_idx += 1
start_token = ctx.tokens[start_idx]
end_token = ctx.tokens[ctx.index]
raise CompileError(self.message, start_token.start, end_token.end)
return True
class Group(ParseNode):
""" ParseNode that creates a match group. """
def __init__(self, ast_type, child):
self.ast_type = ast_type
self.child = to_parse_node(child)
def _parse(self, ctx: ParseContext) -> bool:
ctx.skip()
ctx.start_group(self.ast_type)
return self.child.parse(ctx).succeeded()
class Sequence(ParseNode):
""" ParseNode that attempts to match all of its children in sequence. """
def __init__(self, *children):
self.children = [to_parse_node(child) for child in children]
def _parse(self, ctx) -> bool:
for child in self.children:
if child.parse(ctx).failed():
return False
return True
class Statement(ParseNode):
""" ParseNode that attempts to match all of its children in sequence. If any
child raises an error, the error will be logged but parsing will continue. """
def __init__(self, *children):
self.children = [to_parse_node(child) for child in children]
def _parse(self, ctx) -> bool:
for child in self.children:
try:
if child.parse(ctx).failed():
return False
except CompileError as e:
ctx.errors.append(e)
ctx.set_group_incomplete()
return True
token = ctx.peek_token()
if str(token) != ";":
ctx.errors.append(CompileError("Expected `;`", token.start, token.end))
else:
ctx.next_token()
return True
class AnyOf(ParseNode):
""" ParseNode that attempts to match exactly one of its children. Child
nodes are attempted in order. """
def __init__(self, *children):
self.children = children
@property
def children(self):
return self._children
@children.setter
def children(self, children):
self._children = [to_parse_node(child) for child in children]
def _parse(self, ctx):
for child in self.children:
if child.parse(ctx).succeeded():
return True
return False
class Until(ParseNode):
""" ParseNode that repeats its child until a delimiting token is found. If
the child does not match, one token is skipped and the match is attempted
again. """
def __init__(self, child, delimiter):
self.child = to_parse_node(child)
self.delimiter = to_parse_node(delimiter)
def _parse(self, ctx):
while not self.delimiter.parse(ctx).succeeded():
try:
if not self.child.parse(ctx).matched():
ctx.skip_unexpected_token()
except CompileError as e:
ctx.errors.append(e)
ctx.next_token()
if ctx.is_eof():
return True
return True
class ZeroOrMore(ParseNode):
""" ParseNode that matches its child any number of times (including zero
times). It cannot fail to parse. If its child raises an exception, one token
will be skipped and parsing will continue. """
def __init__(self, child):
self.child = to_parse_node(child)
def _parse(self, ctx):
while True:
try:
if not self.child.parse(ctx).matched():
return True
except CompileError as e:
ctx.errors.append(e)
ctx.next_token()
class Delimited(ParseNode):
""" ParseNode that matches its first child any number of times (including zero
times) with its second child in between and optionally at the end. """
def __init__(self, child, delimiter):
self.child = to_parse_node(child)
self.delimiter = to_parse_node(delimiter)
def _parse(self, ctx):
while self.child.parse(ctx).matched() and self.delimiter.parse(ctx).matched():
pass
return True
class Optional(ParseNode):
""" ParseNode that matches its child zero or one times. It cannot fail to
parse. """
def __init__(self, child):
self.child = to_parse_node(child)
def _parse(self, ctx):
self.child.parse(ctx)
return True
class Eof(ParseNode):
""" ParseNode that matches an EOF token. """
def _parse(self, ctx: ParseContext) -> bool:
token = ctx.next_token()
return token.type == TokenType.EOF
class Match(ParseNode):
""" ParseNode that matches the given literal token. """
def __init__(self, op):
self.op = op
def _parse(self, ctx: ParseContext) -> bool:
token = ctx.next_token()
return str(token) == self.op
def expected(self, expect: str = None):
""" Convenience method for err(). """
if expect is None:
return self.err(f"Expected '{self.op}'")
else:
return self.err("Expected " + expect)
class UseIdent(ParseNode):
""" ParseNode that matches any identifier and sets it in a key=value pair on
the containing match group. """
def __init__(self, key):
self.key = key
def _parse(self, ctx: ParseContext):
token = ctx.next_token()
if token.type != TokenType.IDENT:
return False
ctx.set_group_val(self.key, str(token), token)
return True
class UseNumber(ParseNode):
""" ParseNode that matches a number and sets it in a key=value pair on
the containing match group. """
def __init__(self, key):
self.key = key
def _parse(self, ctx: ParseContext):
token = ctx.next_token()
if token.type != TokenType.NUMBER:
return False
number = token.get_number()
if number % 1.0 == 0:
number = int(number)
ctx.set_group_val(self.key, number, token)
return True
class UseNumberText(ParseNode):
""" ParseNode that matches a number, but sets its *original text* it in a
key=value pair on the containing match group. """
def __init__(self, key):
self.key = key
def _parse(self, ctx: ParseContext):
token = ctx.next_token()
if token.type != TokenType.NUMBER:
return False
ctx.set_group_val(self.key, str(token), token)
return True
class UseQuoted(ParseNode):
""" ParseNode that matches a quoted string and sets it in a key=value pair
on the containing match group. """
def __init__(self, key):
self.key = key
def _parse(self, ctx: ParseContext):
token = ctx.next_token()
if token.type != TokenType.QUOTED:
return False
string = (str(token)[1:-1]
.replace("\\n", "\n")
.replace("\\\"", "\"")
.replace("\\\\", "\\")
.replace("\\'", "\'"))
ctx.set_group_val(self.key, string, token)
return True
class UseLiteral(ParseNode):
""" ParseNode that doesn't match anything, but rather sets a static key=value
pair on the containing group. Useful for, e.g., property and signal flags:
`Sequence(Keyword("swapped"), UseLiteral("swapped", True))` """
def __init__(self, key, literal):
self.key = key
self.literal = literal
def _parse(self, ctx: ParseContext):
ctx.set_group_val(self.key, self.literal, None)
return True
class Keyword(ParseNode):
""" Matches the given identifier and sets it as a named token, with the name
being the identifier itself. """
def __init__(self, kw):
self.kw = kw
self.set_token = True
def _parse(self, ctx: ParseContext):
token = ctx.next_token()
ctx.set_group_val(self.kw, True, token)
return str(token) == self.kw
def to_parse_node(value) -> ParseNode:
if isinstance(value, str):
return Match(value)
elif isinstance(value, list):
return Sequence(*value)
elif isinstance(value, type) and hasattr(value, "grammar"):
return Group(value, getattr(value, "grammar"))
elif isinstance(value, ParseNode):
return value
else:
raise CompilerBugError()
blueprint-compiler-main/blueprintcompiler/parser.py 0000664 0000000 0000000 00000002575 14207747141 0023241 0 ustar 00root root 0000000 0000000 # parser.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .errors import MultipleErrors, PrintableError
from .parse_tree import *
from .parser_utils import *
from .tokenizer import TokenType
from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI
def parse(tokens) -> T.Tuple[UI, T.Optional[MultipleErrors], T.List[PrintableError]]:
""" Parses a list of tokens into an abstract syntax tree. """
ctx = ParseContext(tokens)
AnyOf(UI).parse(ctx)
ast_node = ctx.last_group.to_ast() if ctx.last_group else None
errors = MultipleErrors(ctx.errors) if len(ctx.errors) else None
warnings = ctx.warnings
return (ast_node, errors, warnings)
blueprint-compiler-main/blueprintcompiler/parser_utils.py 0000664 0000000 0000000 00000002015 14207747141 0024446 0 ustar 00root root 0000000 0000000 # parser_utils.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .parse_tree import *
class_name = AnyOf(
[
UseIdent("namespace"),
".",
UseIdent("class_name"),
],
[
".",
UseIdent("class_name"),
UseLiteral("ignore_gir", True),
],
UseIdent("class_name"),
)
blueprint-compiler-main/blueprintcompiler/tokenizer.py 0000664 0000000 0000000 00000005320 14207747141 0023746 0 ustar 00root root 0000000 0000000 # tokenizer.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
import re
from enum import Enum
from .errors import CompileError
class TokenType(Enum):
EOF = 0
IDENT = 1
QUOTED = 2
NUMBER = 3
OP = 4
WHITESPACE = 5
COMMENT = 6
PUNCTUATION = 7
_tokens = [
(TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"),
(TokenType.QUOTED, r'"(\\"|[^"\n])*"'),
(TokenType.QUOTED, r"'(\\'|[^'\n])*'"),
(TokenType.NUMBER, r"[-+]?[\d_]+(\.[\d_]+)?"),
(TokenType.NUMBER, r"0x[A-Fa-f0-9]+"),
(TokenType.WHITESPACE, r"\s+"),
(TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"),
(TokenType.COMMENT, r"\/\/[^\n]*"),
(TokenType.OP, r"[:=\.=\|<>\+\-/\*]+"),
(TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"),
]
_TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens]
class Token:
def __init__(self, type, start, end, string):
self.type = type
self.start = start
self.end = end
self.string = string
def __str__(self):
return self.string[self.start:self.end]
def get_number(self):
if self.type != TokenType.NUMBER:
return None
string = str(self)
if string.startswith("0x"):
return int(string, 16)
else:
return float(string)
def _tokenize(ui_ml: str):
i = 0
while i < len(ui_ml):
matched = False
for (type, regex) in _TOKENS:
match = regex.match(ui_ml, i)
if match is not None:
yield Token(type, match.start(), match.end(), ui_ml)
i = match.end()
matched = True
break
if not matched:
raise CompileError("Could not determine what kind of syntax is meant here", i, i)
yield Token(TokenType.EOF, i, i, ui_ml)
def tokenize(data: str) -> T.List[Token]:
return list(_tokenize(data))
blueprint-compiler-main/blueprintcompiler/utils.py 0000664 0000000 0000000 00000005723 14207747141 0023103 0 ustar 00root root 0000000 0000000 # utils.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
class Colors:
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[33m'
FAINT = '\033[2m'
BOLD = '\033[1m'
BLUE = '\033[34m'
UNDERLINE = '\033[4m'
NO_UNDERLINE = '\033[24m'
CLEAR = '\033[0m'
def lazy_prop(func):
key = "_lazy_prop_" + func.__name__
@property
def real_func(self):
if key not in self.__dict__:
self.__dict__[key] = func(self)
return self.__dict__[key]
return real_func
def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]:
if len(options) == 0:
return None
def levenshtein(a, b):
# see https://en.wikipedia.org/wiki/Levenshtein_distance
m = len(a)
n = len(b)
distances = [[0 for j in range(n)] for i in range(m)]
for i in range(m):
distances[i][0] = i
for j in range(n):
distances[0][j] = j
for j in range(1, n):
for i in range(1, m):
cost = 0
if a[i] != b[j]:
if a[i].casefold() == b[j].casefold():
cost = 1
else:
cost = 2
distances[i][j] = min(distances[i-1][j] + 2, distances[i][j-1] + 2, distances[i-1][j-1] + cost)
return distances[m-1][n-1]
distances = [(option, levenshtein(word, option)) for option in options]
closest = min(distances, key=lambda item:item[1])
if closest[1] <= 5:
return closest[0]
return None
def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]:
if idx == 0:
return (0, 0)
sp = text[:idx].splitlines(keepends=True)
line_num = len(sp)
col_num = len(sp[-1])
return (line_num - 1, col_num)
def pos_to_idx(line: int, col: int, text: str) -> int:
lines = text.splitlines(keepends=True)
return sum([len(line) for line in lines[:line]]) + col
def idxs_to_range(start: int, end: int, text: str):
start_l, start_c = idx_to_pos(start, text)
end_l, end_c = idx_to_pos(end, text)
return {
"start": {
"line": start_l,
"character": start_c,
},
"end": {
"line": end_l,
"character": end_c,
},
}
blueprint-compiler-main/blueprintcompiler/xml_emitter.py 0000664 0000000 0000000 00000004105 14207747141 0024265 0 ustar 00root root 0000000 0000000 # xml_emitter.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from xml.sax import saxutils
class XmlEmitter:
def __init__(self, indent=2):
self.indent = indent
self.result = ''
self._tag_stack = []
self._needs_newline = False
def start_tag(self, tag, **attrs):
self._indent()
self.result += f"<{tag}"
for key, val in attrs.items():
if val is not None:
self.result += f' {key.replace("_", "-")}="{saxutils.escape(str(val))}"'
self.result += ">"
self._tag_stack.append(tag)
self._needs_newline = False
def put_self_closing(self, tag, **attrs):
self._indent()
self.result += f"<{tag}"
for key, val in attrs.items():
if val is not None:
self.result += f' {key}="{saxutils.escape(str(val))}"'
self.result += "/>"
self._needs_newline = True
def end_tag(self):
tag = self._tag_stack.pop()
if self._needs_newline:
self._indent()
self.result += f"{tag}>"
self._needs_newline = True
def put_text(self, text):
self.result += saxutils.escape(str(text))
self._needs_newline = False
def _indent(self):
if self.indent is not None:
self.result += "\n" + " " * (self.indent * len(self._tag_stack))
blueprint-compiler-main/blueprintcompiler/xml_reader.py 0000664 0000000 0000000 00000005402 14207747141 0024057 0 ustar 00root root 0000000 0000000 # xml_reader.py
#
# Copyright 2021 James Westman
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see .
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from collections import defaultdict
import typing as T
from xml import sax
from .utils import lazy_prop
# To speed up parsing, we ignore all tags except these
PARSE_GIR = set([
"repository", "namespace", "class", "interface", "property", "glib:signal",
"include", "implements", "type", "parameter", "parameters", "enumeration",
"member", "bitfield",
])
class Element:
def __init__(self, tag, attrs: T.Dict[str, str]):
self.tag = tag
self.attrs = attrs
self.children: T.Dict[str, T.List["Element"]] = defaultdict(list)
self.cdata_chunks: T.List[str] = []
@lazy_prop
def cdata(self):
return ''.join(self.cdata_chunks)
def get_elements(self, name) -> T.List["Element"]:
return self.children.get(name, [])
def __getitem__(self, key):
return self.attrs.get(key)
class Handler(sax.handler.ContentHandler):
def __init__(self, parse_type):
self.root = None
self.stack = []
self.skipping = 0
self._interesting_elements = parse_type
def startElement(self, name, attrs):
if self._interesting_elements is not None and name not in self._interesting_elements:
self.skipping += 1
if self.skipping > 0:
return
element = Element(name, attrs.copy())
if len(self.stack):
last = self.stack[-1]
last.children[name].append(element)
else:
self.root = element
self.stack.append(element)
def endElement(self, name):
if self.skipping == 0:
self.stack.pop()
if self._interesting_elements is not None and name not in self._interesting_elements:
self.skipping -= 1
def characters(self, content):
if not self.skipping:
self.stack[-1].cdata_chunks.append(content)
def parse(filename, parse_type=None):
parser = sax.make_parser()
handler = Handler(parse_type)
parser.setContentHandler(handler)
parser.parse(filename)
return handler.root
blueprint-compiler-main/build-aux/ 0000775 0000000 0000000 00000000000 14207747141 0017515 5 ustar 00root root 0000000 0000000 blueprint-compiler-main/build-aux/Dockerfile 0000664 0000000 0000000 00000000263 14207747141 0021510 0 ustar 00root root 0000000 0000000 FROM fedora:latest
RUN dnf install -y meson python3-pip gtk4-devel gobject-introspection-devel libadwaita-devel
RUN pip3 install furo mypy sphinx coverage
RUN dnf install -y git
blueprint-compiler-main/docs/ 0000775 0000000 0000000 00000000000 14207747141 0016553 5 ustar 00root root 0000000 0000000 blueprint-compiler-main/docs/conf.py 0000664 0000000 0000000 00000003500 14207747141 0020050 0 ustar 00root root 0000000 0000000 # Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'Blueprint'
copyright = '2021, James Westman'
author = 'James Westman'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'furo'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
blueprint-compiler-main/docs/examples.rst 0000664 0000000 0000000 00000015266 14207747141 0021135 0 ustar 00root root 0000000 0000000 ========
Examples
========
Namespaces and libraries
------------------------
GTK declaration
~~~~~~~~~~~~~~~
.. code-block::
// Required in every blueprint file. Defines the major version
// of GTK the file is designed for.
using Gtk 4.0;
Importing libraries
~~~~~~~~~~~~~~~~~~~
.. code-block::
// Import Adwaita 1. The name given here is the GIR namespace name, which
// might not match the library name or C prefix.
using Adw 1;
Objects
-------
Defining objects with properties
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block::
Gtk.Box {
orientation: vertical;
Gtk.Label {
label: "Hello, world!";
}
}
Referencing an object in code
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block::
// Your code can reference the object by `my_window`
Gtk.Window my_window {
title: "My window";
}
Using classes defined by your app
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use a leading ``.`` to tell the compiler that the class is defined in your
app, not in the GIR, so it should skip validation.
.. code-block::
.MyAppCustomWidget my_widget {
my-custom-property: 3.14;
}
Templates
---------
Defining a template
~~~~~~~~~~~~~~~~~~~
Many language bindings have a way to create subclasses that are defined both
in code and in the blueprint file. Check your language's documentation on
how to use this feature.
In this example, we create a class called ``MyAppWindow`` that inherits from
``Gtk.ApplicationWindow``.
.. code-block::
template MyAppWindow : Gtk.ApplicationWindow {
my-custom-property: 3.14;
}
Properties
----------
Translations
~~~~~~~~~~~~
Use ``_("...")`` to mark strings as translatable. You can put a comment for
translators on the line above if needed.
.. code-block::
Gtk.Label label {
/* Translators: This is the main text of the welcome screen */
label: _("Hello, world!");
}
Use ``C_("context", "...")`` to add a *message context* to a string to
disambiguate it, in case the same string appears in different places. Remember,
two strings might be the same in one language but different in another depending
on context.
.. code-block::
Gtk.Label label {
/* Translators: This is a section in the preferences window */
label: C_("preferences window", "Hello, world!");
}
Referencing objects by ID
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block::
Gtk.Range range1 {
adjustment: my_adjustment;
}
Gtk.Range range2 {
adjustment: my_adjustment;
}
Gtk.Adjustment my_adjustment {
}
Defining object properties inline
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block::
Gtk.Range {
adjustment: Gtk.Adjustment my_adjustment {
value: 10;
};
}
Gtk.Range range1 {
// You can even still reference the object by ID
adjustment: my_adjustment;
}
.. note::
Note the semicolon after the closing brace of the ``Gtk.Adjustment``. It is
required.
Bindings
~~~~~~~~
Use the ``bind`` keyword to bind a property to another object's property in
the same file.
.. code-block::
Gtk.ProgressBar bar1 {
}
Gtk.ProgressBar bar2 {
value: bind bar1.value;
}
Binding Flags
~~~~~~~~~~~~~
Use the ``no-sync-create`` keyword to only update the target value when the
source value changes, not when the binding is first created.
.. code-block::
Gtk.ProgressBar bar1 {
value: 10;
}
Gtk.ProgressBar bar2 {
value: bind bar1.value no-sync-create;
}
Use the ``bidirectional`` keyword to bind properties in both directions.
.. code-block::
// Text of entry1 is bound to text
// of entry2 and vice versa
Gtk.Entry entry1 {
text: bind entry2.text bidirectional;
}
Gtk.Entry entry2 {
}
Use the ``inverted`` keyword to invert to bind a boolean property
to inverted value of another one.
.. code-block::
// When switch1 is on, switch2 will be off
Gtk.Switch switch1 {
active: bind switch2.active inverted bidirectional;
}
// When switch2 is on, switch1 will be off
Gtk.Switch switch2 {
}
Signals
-------
Basic Usage
~~~~~~~~~~~
.. code-block::
Gtk.Button {
// on_button_clicked is defined in your application
clicked => on_button_clicked();
}
Flags
~~~~~
.. code-block::
Gtk.Button {
clicked => on_button_clicked() swapped;
}
Object
~~~~~~
By default the widget is passed to callback as first argument. However,
you can specify another object to use as first argument of callback.
.. code-block::
Gtk.Entry {
activate => grab_focus(another_entry);
}
Gtk.Entry another_entry {
}
CSS Styles
----------
Basic Usage
~~~~~~~~~~~
.. code-block::
Gtk.Label {
styles ["dim-label", "title"]
}
Menus
-----
Basic Usage
~~~~~~~~~~~
.. code-block::
menu my_menu {
section {
label: _("File");
item {
label: _("Open");
action: "win.open";
}
item {
label: _("Save");
action: "win.save";
}
submenu {
label: _("Save As");
item {
label: _("PDF");
action: "win.save_as_pdf";
}
}
}
}
Item Shorthand
~~~~~~~~~~~~~~
For menu items with only a label, action, and/or icon, you can define all three
on one line. The action and icon are optional.
.. code-block::
menu {
item (_("Copy"), "app.copy", "copy-symbolic")
}
Layout Properties
-----------------
Basic Usage
~~~~~~~~~~~
.. code-block::
Gtk.Grid {
Gtk.Label {
layout {
row: 0;
column: 1;
}
}
}
Accessibility Properties
------------------------
Basic Usage
~~~~~~~~~~~
.. code-block::
Gtk.Widget {
accessibility {
orientation: vertical;
labelled_by: my_label;
checked: true;
}
}
Gtk.Label my_label {}
Widget-Specific Items
---------------------
Gtk.ComboBoxText
~~~~~~~~~~~~~~~~
.. code-block::
Gtk.ComboBoxText {
items [
item1: "Item 1",
item2: _("Items can be translated"),
"The item ID is not required",
]
}
Gtk.FileFilter
~~~~~~~~~~~~~~
.. code-block::
Gtk.FileFilter {
mime-types ["image/jpeg", "video/webm"]
patterns ["*.txt"]
suffixes ["png"]
}
Gtk.SizeGroup
~~~~~~~~~~~~~
.. code-block::
Gtk.SizeGroup {
mode: both;
widgets [label1, label2]
}
Gtk.Label label1 {}
Gtk.Label label2 {}
Gtk.StringList
~~~~~~~~~~~~~~
.. code-block::
Gtk.StringList {
strings ["Hello, world!", _("Translated string")]
}
Gtk.Dialog and Gtk.InfoBar
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block::
Gtk.Dialog {
[action response=ok]
Gtk.Button ok_response {}
[action response=cancel]
Gtk.Button cancel_response {}
[action response=9]
Gtk.Button app_defined_response {}
}
blueprint-compiler-main/docs/index.rst 0000664 0000000 0000000 00000003156 14207747141 0020421 0 ustar 00root root 0000000 0000000 Overview
========
Blueprint is a markup language and compiler for GTK 4 user interfaces.
.. toctree::
:maxdepth: 1
:caption: Contents:
setup
examples
.. code-block::
using Gtk 4.0;
template MyAppWindow : ApplicationWindow {
default-width: 600;
default-height: 300;
title: _("Hello, Blueprint!");
[titlebar]
HeaderBar {}
Label {
label: bind MyAppWindow.main_text;
}
}
Blueprint helps you build user interfaces in GTK quickly and declaratively.
It has modern IDE features like code completion and hover documentation, and
the compiler points out mistakes early on so you can focus on making your app
look amazing.
Features
--------
- **Easy setup.** A porting tool is available to help port your projects from
XML. The compiler's only dependency is Python, and it can be included as
a meson subproject. :doc:`See the Setup page for more information. `
- **Concise syntax.** No more clumsy XML! Blueprint is designed from the ground
up to match GTK's widget model, including templates, child types, signal
handlers, and menus.
- **Easy to learn.** The syntax should be very familiar to most people. Scroll
through the :doc:`examples page ` for a quick overview of the whole
language.
- **Modern tooling.** IDE integration for `GNOME Builder `_
is in progress, and a VS Code extension is also planned.
Links
-----
- `Source code `_
- `Vim syntax highlighting plugin `_
blueprint-compiler-main/docs/meson.build 0000664 0000000 0000000 00000000452 14207747141 0020716 0 ustar 00root root 0000000 0000000 if get_option('docs')
sphinx = find_program(['sphinx-build-3', 'sphinx-build'], required: true)
custom_target('docs',
command: [sphinx, '-b', 'html', '-c', meson.current_source_dir(), meson.current_source_dir(), '@OUTPUT@'],
output: 'en',
build_by_default: true
)
endif
blueprint-compiler-main/docs/setup.rst 0000664 0000000 0000000 00000003325 14207747141 0020450 0 ustar 00root root 0000000 0000000 =====
Setup
=====
Setting up Blueprint on a new or existing project
-------------------------------------------------
Using the porting tool
~~~~~~~~~~~~~~~~~~~~~~
Clone `blueprint-compiler `_
from source. You can install it using ``meson _build`` and ``ninja -C _build install``,
or you can leave it uninstalled.
In your project's directory, run ``blueprint-compiler port`` (or `` port``)
to start the porting process. It will walk you through the steps outlined below.
It should work for most projects, but if something goes wrong you may need to
follow the manual steps instead.
Manually
~~~~~~~~
blueprint-compiler works as a meson subproject.
#. Save the following file as ``subprojects/blueprint-compiler.wrap``:
.. code-block:: cfg
[wrap-git]
directory = blueprint-compiler
url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git
revision = main
depth = 1
[provide]
program_names = blueprint-compiler
#. Add this to your ``.gitignore``:
.. code-block::
/subprojects/blueprint-compiler
#. Rewrite your .ui XML files in blueprint format.
#. Add this to the ``meson.build`` file where you build your GResources:
.. code-block:: meson.build
blueprints = custom_target('blueprints',
input: files(
# LIST YOUR BLUEPRINT FILES HERE
),
output: '.',
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
)
#. In the same ``meson.build`` file, add this argument to your ``gnome.compile_resources`` command:
.. code-block:: meson.build
dependencies: blueprints,
blueprint-compiler-main/meson.build 0000664 0000000 0000000 00000001374 14207747141 0017772 0 ustar 00root root 0000000 0000000 project('blueprint-compiler',
version: '0.1.0',
)
subdir('docs')
prefix = get_option('prefix')
libdir = join_paths(prefix, get_option('libdir'))
py = import('python').find_installation('python3')
configure_file(
input: 'blueprint-compiler.pc.in',
output: 'blueprint-compiler.pc',
configuration: { 'VERSION': meson.project_version() },
install: not meson.is_subproject(),
install_dir: join_paths(libdir, 'pkgconfig'),
)
install_data(
'blueprint-compiler.py',
install_dir: get_option('bindir'),
rename: 'blueprint-compiler',
)
meson.override_find_program('blueprint-compiler', find_program('blueprint-compiler.py'))
if not meson.is_subproject()
install_subdir('blueprintcompiler', install_dir: py.get_install_dir())
endif
subdir('tests')
blueprint-compiler-main/meson_options.txt 0000664 0000000 0000000 00000000056 14207747141 0021261 0 ustar 00root root 0000000 0000000 option('docs', type: 'boolean', value: false)
blueprint-compiler-main/tests/ 0000775 0000000 0000000 00000000000 14207747141 0016765 5 ustar 00root root 0000000 0000000 blueprint-compiler-main/tests/__init__.py 0000664 0000000 0000000 00000000000 14207747141 0021064 0 ustar 00root root 0000000 0000000 blueprint-compiler-main/tests/meson.build 0000664 0000000 0000000 00000000112 14207747141 0021121 0 ustar 00root root 0000000 0000000 test('tests', py, args: ['-m', 'unittest'], workdir: meson.source_root())
blueprint-compiler-main/tests/sample_errors/ 0000775 0000000 0000000 00000000000 14207747141 0021642 5 ustar 00root root 0000000 0000000 blueprint-compiler-main/tests/sample_errors/a11y_in_non_widget.blp 0000664 0000000 0000000 00000000111 14207747141 0026010 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
using GObject 2.0;
GObject.Object {
accessibility {}
}
blueprint-compiler-main/tests/sample_errors/a11y_in_non_widget.err 0000664 0000000 0000000 00000000127 14207747141 0026032 0 ustar 00root root 0000000 0000000 5,3,13,GObject.Object is not a Gtk.Widget, so it doesn't have accessibility properties
blueprint-compiler-main/tests/sample_errors/a11y_prop_dne.blp 0000664 0000000 0000000 00000000122 14207747141 0024775 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Widget {
accessibility {
not_a_prop: "Hello, world!";
}
}
blueprint-compiler-main/tests/sample_errors/a11y_prop_dne.err 0000664 0000000 0000000 00000000111 14207747141 0025006 0 ustar 00root root 0000000 0000000 5,5,10,'not_a_prop' is not an accessibility property, relation, or state
blueprint-compiler-main/tests/sample_errors/a11y_prop_obj_dne.blp 0000664 0000000 0000000 00000000121 14207747141 0025626 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Widget {
accessibility {
labelled_by: not_an_object;
}
}
blueprint-compiler-main/tests/sample_errors/a11y_prop_obj_dne.err 0000664 0000000 0000000 00000000064 14207747141 0025647 0 ustar 00root root 0000000 0000000 5,18,13,Could not find object with ID not_an_object
blueprint-compiler-main/tests/sample_errors/a11y_prop_type.blp 0000664 0000000 0000000 00000000105 14207747141 0025211 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Widget {
accessibility {
orientation: 1;
}
}
blueprint-compiler-main/tests/sample_errors/a11y_prop_type.err 0000664 0000000 0000000 00000000053 14207747141 0025226 0 ustar 00root root 0000000 0000000 5,18,1,Cannot convert 1 to Gtk.Orientation
blueprint-compiler-main/tests/sample_errors/action_widget_float_response.blp 0000664 0000000 0000000 00000000140 14207747141 0030257 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Dialog {
[action response=17.9]
Button float_response_button {
}
}
blueprint-compiler-main/tests/sample_errors/action_widget_float_response.err 0000664 0000000 0000000 00000000112 14207747141 0030271 0 ustar 00root root 0000000 0000000 4,22,4,Response type must be GtkResponseType member or integer, not float
blueprint-compiler-main/tests/sample_errors/action_widget_have_no_id.blp 0000664 0000000 0000000 00000000147 14207747141 0027336 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Dialog {
[action response=cancel]
Button {
label: _("Cancel");
}
}
blueprint-compiler-main/tests/sample_errors/action_widget_have_no_id.err 0000664 0000000 0000000 00000000043 14207747141 0027344 0 ustar 00root root 0000000 0000000 4,13,15,Action widget must have ID
blueprint-compiler-main/tests/sample_errors/action_widget_in_invalid_container.blp 0000664 0000000 0000000 00000000117 14207747141 0031416 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Box {
[action response=ok]
Button ok_button {
}
}
blueprint-compiler-main/tests/sample_errors/action_widget_in_invalid_container.err 0000664 0000000 0000000 00000000054 14207747141 0031431 0 ustar 00root root 0000000 0000000 4,13,11,Gtk.Box doesn't have action widgets
blueprint-compiler-main/tests/sample_errors/action_widget_multiple_default.blp 0000664 0000000 0000000 00000000234 14207747141 0030577 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Dialog {
[action response=yes default]
Button yes_button {
}
[action response=no default]
Button no_button {
}
}
blueprint-compiler-main/tests/sample_errors/action_widget_multiple_default.err 0000664 0000000 0000000 00000000047 14207747141 0030614 0 ustar 00root root 0000000 0000000 9,25,7,Default response is already set
blueprint-compiler-main/tests/sample_errors/action_widget_negative_response.blp 0000664 0000000 0000000 00000000144 14207747141 0030760 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Dialog {
[action response = -179]
Button numeric_response_button {
}
}
blueprint-compiler-main/tests/sample_errors/action_widget_negative_response.err 0000664 0000000 0000000 00000000057 14207747141 0030776 0 ustar 00root root 0000000 0000000 4,24,4,Numeric response type can't be negative
blueprint-compiler-main/tests/sample_errors/action_widget_not_action.blp 0000664 0000000 0000000 00000000125 14207747141 0027374 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Dialog {
[some_type response=ok]
Button ok_button {
}
}
blueprint-compiler-main/tests/sample_errors/action_widget_not_action.err 0000664 0000000 0000000 00000000060 14207747141 0027405 0 ustar 00root root 0000000 0000000 4,16,11,Only action widget can have response ID
blueprint-compiler-main/tests/sample_errors/action_widget_response_dne.blp 0000664 0000000 0000000 00000000144 14207747141 0027724 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Dialog {
[action response=hello-world]
Button hello_world_button {
}
}
blueprint-compiler-main/tests/sample_errors/action_widget_response_dne.err 0000664 0000000 0000000 00000000062 14207747141 0027736 0 ustar 00root root 0000000 0000000 4,22,11,Response type "hello-world" doesn't exist
blueprint-compiler-main/tests/sample_errors/assign_inline_menu.blp 0000664 0000000 0000000 00000000055 14207747141 0026207 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Button {
label: menu {};
}
blueprint-compiler-main/tests/sample_errors/assign_inline_menu.err 0000664 0000000 0000000 00000000055 14207747141 0026222 0 ustar 00root root 0000000 0000000 4,3,15,Cannot assign Gio.MenuModel to string
blueprint-compiler-main/tests/sample_errors/bitfield_member_dne.blp 0000664 0000000 0000000 00000000130 14207747141 0026272 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
EventControllerScroll {
flags: vertical | not_a_value;
name: a|b;
}
blueprint-compiler-main/tests/sample_errors/bitfield_member_dne.err 0000664 0000000 0000000 00000000152 14207747141 0026311 0 ustar 00root root 0000000 0000000 4,21,11,not_a_value is not a member of Gtk.EventControllerScrollFlags
5,9,3,string is not a bitfield type
blueprint-compiler-main/tests/sample_errors/class_assign.blp 0000664 0000000 0000000 00000000063 14207747141 0025011 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Box box {}
Label {
label: box;
}
blueprint-compiler-main/tests/sample_errors/class_assign.err 0000664 0000000 0000000 00000000047 14207747141 0025026 0 ustar 00root root 0000000 0000000 5,10,3,Cannot assign Gtk.Box to string
blueprint-compiler-main/tests/sample_errors/class_dne.blp 0000664 0000000 0000000 00000000075 14207747141 0024276 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
template TestTemplate : Gtk.NotARealClass {}
blueprint-compiler-main/tests/sample_errors/class_dne.err 0000664 0000000 0000000 00000000104 14207747141 0024302 0 ustar 00root root 0000000 0000000 3,29,13,Namespace Gtk does not contain a class called NotARealClass
blueprint-compiler-main/tests/sample_errors/consecutive_unexpected_tokens.blp 0000664 0000000 0000000 00000000127 14207747141 0030477 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Button {
visible: false;
not actually blueprint code;
Label {}
}
blueprint-compiler-main/tests/sample_errors/consecutive_unexpected_tokens.err 0000664 0000000 0000000 00000000031 14207747141 0030504 0 ustar 00root root 0000000 0000000 5,3,31,Unexpected tokens
blueprint-compiler-main/tests/sample_errors/does_not_implement.blp 0000664 0000000 0000000 00000000075 14207747141 0026227 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Label label {}
DropDown {
model: label;
}
blueprint-compiler-main/tests/sample_errors/does_not_implement.err 0000664 0000000 0000000 00000000060 14207747141 0026234 0 ustar 00root root 0000000 0000000 6,10,5,Cannot assign Gtk.Label to Gio.ListModel
blueprint-compiler-main/tests/sample_errors/duplicate_obj_id.blp 0000664 0000000 0000000 00000000066 14207747141 0025623 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Gtk.Label label {}
Gtk.Label label {}
blueprint-compiler-main/tests/sample_errors/duplicate_obj_id.err 0000664 0000000 0000000 00000000043 14207747141 0025631 0 ustar 00root root 0000000 0000000 4,11,5,Duplicate object ID 'label'
blueprint-compiler-main/tests/sample_errors/enum_member_dne.blp 0000664 0000000 0000000 00000000061 14207747141 0025457 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Box {
orientation: diagonal;
}
blueprint-compiler-main/tests/sample_errors/enum_member_dne.err 0000664 0000000 0000000 00000000063 14207747141 0025474 0 ustar 00root root 0000000 0000000 4,16,8,diagonal is not a member of Gtk.Orientation
blueprint-compiler-main/tests/sample_errors/filters_in_non_file_filter.blp 0000664 0000000 0000000 00000000107 14207747141 0027713 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Widget {
mime-types []
patterns []
suffixes []
}
blueprint-compiler-main/tests/sample_errors/filters_in_non_file_filter.err 0000664 0000000 0000000 00000000377 14207747141 0027737 0 ustar 00root root 0000000 0000000 4,3,13,Gtk.Widget is not a Gtk.FileFilter, so it doesn't have file filter properties
5,3,11,Gtk.Widget is not a Gtk.FileFilter, so it doesn't have file filter properties
6,3,11,Gtk.Widget is not a Gtk.FileFilter, so it doesn't have file filter properties
blueprint-compiler-main/tests/sample_errors/invalid_bool.blp 0000664 0000000 0000000 00000000054 14207747141 0025001 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Label {
visible: maybe;
}
blueprint-compiler-main/tests/sample_errors/invalid_bool.err 0000664 0000000 0000000 00000000064 14207747141 0025015 0 ustar 00root root 0000000 0000000 4,12,5,Expected 'true' or 'false' for boolean value
blueprint-compiler-main/tests/sample_errors/layout_in_non_widget.blp 0000664 0000000 0000000 00000000102 14207747141 0026552 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
using GObject 2.0;
GObject.Object {
layout {}
}
blueprint-compiler-main/tests/sample_errors/layout_in_non_widget.err 0000664 0000000 0000000 00000000117 14207747141 0026573 0 ustar 00root root 0000000 0000000 5,3,6,GObject.Object is not a Gtk.Widget, so it doesn't have layout properties
blueprint-compiler-main/tests/sample_errors/not_a_class.blp 0000664 0000000 0000000 00000000072 14207747141 0024625 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
template TestTemplate : Gtk.Orientable {}
blueprint-compiler-main/tests/sample_errors/not_a_class.err 0000664 0000000 0000000 00000000046 14207747141 0024641 0 ustar 00root root 0000000 0000000 3,29,10,Gtk.Orientable is not a class
blueprint-compiler-main/tests/sample_errors/ns_not_imported.blp 0000664 0000000 0000000 00000000101 14207747141 0025534 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
template TestTemplate : Adw.ApplicationWindow {}
blueprint-compiler-main/tests/sample_errors/ns_not_imported.err 0000664 0000000 0000000 00000000046 14207747141 0025557 0 ustar 00root root 0000000 0000000 3,25,3,Namespace Adw was not imported
blueprint-compiler-main/tests/sample_errors/obj_class_dne.blp 0000664 0000000 0000000 00000000042 14207747141 0025122 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
NotARealWidget {}
blueprint-compiler-main/tests/sample_errors/obj_in_string_list.blp 0000664 0000000 0000000 00000000105 14207747141 0026216 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
StringList {
strings [
id,
]
}
Widget id {}
blueprint-compiler-main/tests/sample_errors/obj_in_string_list.err 0000664 0000000 0000000 00000000051 14207747141 0026231 0 ustar 00root root 0000000 0000000 5,5,2,Cannot assign Gtk.Widget to string
blueprint-compiler-main/tests/sample_errors/obj_prop_type.blp 0000664 0000000 0000000 00000000062 14207747141 0025212 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Scale {
adjustment: Label {};
}
blueprint-compiler-main/tests/sample_errors/obj_prop_type.err 0000664 0000000 0000000 00000000061 14207747141 0025224 0 ustar 00root root 0000000 0000000 4,3,21,Cannot assign Gtk.Label to Gtk.Adjustment
blueprint-compiler-main/tests/sample_errors/object_dne.blp 0000664 0000000 0000000 00000000055 14207747141 0024435 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Label {
label: my_label;
}
blueprint-compiler-main/tests/sample_errors/object_dne.err 0000664 0000000 0000000 00000000056 14207747141 0024451 0 ustar 00root root 0000000 0000000 4,10,8,Could not find object with ID my_label
blueprint-compiler-main/tests/sample_errors/property_dne.blp 0000664 0000000 0000000 00000000102 14207747141 0025044 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Label {
not-a-real-property: "Hello, world!";
}
blueprint-compiler-main/tests/sample_errors/property_dne.err 0000664 0000000 0000000 00000000116 14207747141 0025064 0 ustar 00root root 0000000 0000000 4,3,19,Class Gtk.Label does not contain a property called not-a-real-property
blueprint-compiler-main/tests/sample_errors/signal_dne.blp 0000664 0000000 0000000 00000000124 14207747141 0024441 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Button {
eaten-by-velociraptors => on_eaten_by_velociraptors();
}
blueprint-compiler-main/tests/sample_errors/signal_dne.err 0000664 0000000 0000000 00000000120 14207747141 0024450 0 ustar 00root root 0000000 0000000 4,3,22,Class Gtk.Button does not contain a signal called eaten-by-velociraptors
blueprint-compiler-main/tests/sample_errors/signal_object_dne.blp 0000664 0000000 0000000 00000000075 14207747141 0025774 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Button {
clicked => function(dinosaur);
} blueprint-compiler-main/tests/sample_errors/signal_object_dne.err 0000664 0000000 0000000 00000000057 14207747141 0026007 0 ustar 00root root 0000000 0000000 4,25,8,Could not find object with ID 'dinosaur' blueprint-compiler-main/tests/sample_errors/size_group_non_widget.blp 0000664 0000000 0000000 00000000162 14207747141 0026743 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
using GObject 2.0;
SizeGroup {
mode: horizontal;
widgets [object]
}
GObject.Object object {}
blueprint-compiler-main/tests/sample_errors/size_group_non_widget.err 0000664 0000000 0000000 00000000062 14207747141 0026755 0 ustar 00root root 0000000 0000000 6,12,6,Cannot assign GObject.Object to Gtk.Widget
blueprint-compiler-main/tests/sample_errors/size_group_obj_dne.blp 0000664 0000000 0000000 00000000130 14207747141 0026201 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
using GObject 2.0;
SizeGroup {
mode: horizontal;
widgets [object]
}
blueprint-compiler-main/tests/sample_errors/size_group_obj_dne.err 0000664 0000000 0000000 00000000054 14207747141 0026221 0 ustar 00root root 0000000 0000000 6,12,6,Could not find object with ID object
blueprint-compiler-main/tests/sample_errors/styles_in_non_widget.blp 0000664 0000000 0000000 00000000102 14207747141 0026560 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
using GObject 2.0;
GObject.Object {
styles []
}
blueprint-compiler-main/tests/sample_errors/styles_in_non_widget.err 0000664 0000000 0000000 00000000113 14207747141 0026575 0 ustar 00root root 0000000 0000000 5,3,6,GObject.Object is not a Gtk.Widget, so it doesn't have style classes
blueprint-compiler-main/tests/sample_errors/two_templates.blp 0000664 0000000 0000000 00000000127 14207747141 0025230 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
template ClassName : Gtk.Button {}
template ClassName2 : Gtk.Button {}
blueprint-compiler-main/tests/sample_errors/two_templates.err 0000664 0000000 0000000 00000000114 14207747141 0025237 0 ustar 00root root 0000000 0000000 4,10,10,Only one template may be defined per file, but this file contains 2
blueprint-compiler-main/tests/sample_errors/uint.blp 0000664 0000000 0000000 00000000062 14207747141 0023316 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
FlowBox {
column-spacing: -2;
}
blueprint-compiler-main/tests/sample_errors/uint.err 0000664 0000000 0000000 00000000055 14207747141 0023333 0 ustar 00root root 0000000 0000000 4,19,2,Cannot convert -2 to unsigned integer
blueprint-compiler-main/tests/sample_errors/using_invalid_namespace.blp 0000664 0000000 0000000 00000000054 14207747141 0027207 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
using NotARealNamespace 2.0;
blueprint-compiler-main/tests/sample_errors/using_invalid_namespace.err 0000664 0000000 0000000 00000000072 14207747141 0027222 0 ustar 00root root 0000000 0000000 2,7,21,Namespace NotARealNamespace-2.0 could not be found
blueprint-compiler-main/tests/sample_errors/widgets_in_non_size_group.blp 0000664 0000000 0000000 00000000050 14207747141 0027610 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Widget {
widgets []
}
blueprint-compiler-main/tests/sample_errors/widgets_in_non_size_group.err 0000664 0000000 0000000 00000000122 14207747141 0027623 0 ustar 00root root 0000000 0000000 4,3,7,Gtk.Widget is not a Gtk.SizeGroup, so it doesn't have size group properties
blueprint-compiler-main/tests/samples/ 0000775 0000000 0000000 00000000000 14207747141 0020431 5 ustar 00root root 0000000 0000000 blueprint-compiler-main/tests/samples/accessibility.blp 0000664 0000000 0000000 00000000231 14207747141 0023753 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Gtk.Widget {
accessibility {
label: _("Hello, world!");
labelled_by: my_label;
checked: true;
}
}
Gtk.Label my_label {}
blueprint-compiler-main/tests/samples/accessibility.ui 0000664 0000000 0000000 00000000625 14207747141 0023622 0 ustar 00root root 0000000 0000000
blueprint-compiler-main/tests/samples/accessibility_dec.blp 0000664 0000000 0000000 00000000222 14207747141 0024566 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Widget {
accessibility {
label: _("Hello, world!");
labelled_by: my_label;
checked: true;
}
}
Label my_label {
}
blueprint-compiler-main/tests/samples/action_widgets.blp 0000664 0000000 0000000 00000000625 14207747141 0024136 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Dialog {
[action response=cancel]
Button cancel_button {
label: _("Cancel");
}
[action reponse=9]
Button custom_response_button {
label: _("Reinstall Windows");
}
[action response=ok default]
Button ok_button {
label: _("Ok");
}
}
InfoBar {
[action response=ok]
Button ok_info_button {
label: _("Ok");
}
}
blueprint-compiler-main/tests/samples/action_widgets.ui 0000664 0000000 0000000 00000002364 14207747141 0024000 0 ustar 00root root 0000000 0000000
Okok_info_button
blueprint-compiler-main/tests/samples/binding.blp 0000664 0000000 0000000 00000000256 14207747141 0022545 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Box {
visible: bind box2.visible inverted;
orientation: bind box2.orientation;
spacing: bind box2.spacing no-sync-create;
}
Box box2 {
spacing: 6;
}
blueprint-compiler-main/tests/samples/binding.ui 0000664 0000000 0000000 00000001015 14207747141 0022377 0 ustar 00root root 0000000 0000000
6
blueprint-compiler-main/tests/samples/child_type.blp 0000664 0000000 0000000 00000000162 14207747141 0023253 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Window {
[titlebar]
HeaderBar {
}
}
Dialog {
[internal-child context_area]
Box {
}
}
blueprint-compiler-main/tests/samples/child_type.ui 0000664 0000000 0000000 00000000554 14207747141 0023120 0 ustar 00root root 0000000 0000000
blueprint-compiler-main/tests/samples/combo_box_text.blp 0000664 0000000 0000000 00000000152 14207747141 0024141 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
ComboBoxText {
items [
"Hello, world!",
_("Hello!"),
item_id: "item",
]
}
blueprint-compiler-main/tests/samples/combo_box_text.ui 0000664 0000000 0000000 00000000442 14207747141 0024003 0 ustar 00root root 0000000 0000000
Hello, world!Hello!item
blueprint-compiler-main/tests/samples/comments.blp 0000664 0000000 0000000 00000000562 14207747141 0022760 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
using GObject 2.0;
GObject.Object {
/* multiline-style comment 1 */
}
Gtk.Label {
/* Translators: multiline-style comment 2 */
label: _("Test"); // single-line comment
/**/ visible: false; /**/
}
/* Note: The output XML does not need to contain translator comments. The
translation tooling reads blueprint files, not the generated files. */
blueprint-compiler-main/tests/samples/comments.ui 0000664 0000000 0000000 00000000437 14207747141 0022621 0 ustar 00root root 0000000 0000000
Testfalse
blueprint-compiler-main/tests/samples/enum.blp 0000664 0000000 0000000 00000000101 14207747141 0022064 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
ScrolledWindow {
window-placement: top_left;
}
blueprint-compiler-main/tests/samples/enum.ui 0000664 0000000 0000000 00000000321 14207747141 0021730 0 ustar 00root root 0000000 0000000
top-left
blueprint-compiler-main/tests/samples/file_filter.blp 0000664 0000000 0000000 00000000255 14207747141 0023416 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
FileFilter {
name: "File Filter Name";
mime-types [
"text/plain",
"image/ *",
]
patterns [
"*.txt",
]
suffixes [
"png",
]
}
blueprint-compiler-main/tests/samples/file_filter.ui 0000664 0000000 0000000 00000000662 14207747141 0023260 0 ustar 00root root 0000000 0000000
File Filter Nametext/plainimage/ **.txtpng
blueprint-compiler-main/tests/samples/flags.blp 0000664 0000000 0000000 00000000205 14207747141 0022221 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
using Gio 2.0;
Gio.Application {
flags: is_service | handles_open;
}
EventControllerScroll {
flags: vertical;
}
blueprint-compiler-main/tests/samples/flags.ui 0000664 0000000 0000000 00000000467 14207747141 0022073 0 ustar 00root root 0000000 0000000
is_service|handles_openvertical
blueprint-compiler-main/tests/samples/id_prop.blp 0000664 0000000 0000000 00000000101 14207747141 0022554 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Scale {
adjustment: adj;
}
Adjustment adj {
}
blueprint-compiler-main/tests/samples/id_prop.ui 0000664 0000000 0000000 00000000360 14207747141 0022423 0 ustar 00root root 0000000 0000000
adj
blueprint-compiler-main/tests/samples/inline_menu.blp 0000664 0000000 0000000 00000000103 14207747141 0023424 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
MenuButton {
menu-model: menu primary_menu {};
}
blueprint-compiler-main/tests/samples/inline_menu.ui 0000664 0000000 0000000 00000000352 14207747141 0023272 0 ustar 00root root 0000000 0000000
blueprint-compiler-main/tests/samples/layout.blp 0000664 0000000 0000000 00000000131 14207747141 0022440 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Grid {
Label {
layout {
column: 0;
row: 1;
}
}
}
blueprint-compiler-main/tests/samples/layout.ui 0000664 0000000 0000000 00000000524 14207747141 0022306 0 ustar 00root root 0000000 0000000
01
blueprint-compiler-main/tests/samples/layout_dec.blp 0000664 0000000 0000000 00000000135 14207747141 0023257 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
Grid {
Label {
layout {
column: "0";
row: "1";
}
}
}
blueprint-compiler-main/tests/samples/menu.blp 0000664 0000000 0000000 00000000543 14207747141 0022076 0 ustar 00root root 0000000 0000000 using Gtk 4.0;
menu {
label: _("menu label");
test-custom-attribute: 3.1415;
submenu {
section {
label: "test section";
}
item {
label: "test item";
}
item ("test item shorthand 1")
item ("test item shorthand 2", "app.test-action")
item ("test item shorthand 3", "app.test-action", "test-symbolic")
}
}
blueprint-compiler-main/tests/samples/menu.ui 0000664 0000000 0000000 00000001620 14207747141 0021733 0 ustar 00root root 0000000 0000000