Compare commits

..

No commits in common. "main" and "v0.2.0" have entirely different histories.
main ... v0.2.0

362 changed files with 2589 additions and 11816 deletions

2
.gitignore vendored
View file

@ -12,5 +12,3 @@ coverage.xml
/corpus /corpus
/crashes /crashes
.vscode

View file

@ -3,23 +3,21 @@ stages:
- pages - pages
build: build:
image: registry.gitlab.gnome.org/gnome/blueprint-compiler image: registry.gitlab.gnome.org/jwestman/blueprint-compiler
stage: build stage: build
script: script:
- black --check --diff ./ tests - mypy blueprintcompiler
- isort --check --diff --profile black ./ tests - coverage run -m unittest
- mypy --python-version=3.9 blueprintcompiler/
- G_DEBUG=fatal-warnings xvfb-run coverage run -m unittest
- coverage report - coverage report
- coverage html - coverage html
- coverage xml - coverage xml
- meson _build -Ddocs=true --prefix=/usr - meson _build -Ddocs=true
- ninja -C _build - ninja -C _build
- ninja -C _build test
- ninja -C _build install - ninja -C _build install
- ninja -C _build docs/en
- git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git - git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git
- cd blueprint-regression-tests - cd blueprint-regression-tests
- git checkout 5f9e155c1333e84e6f683cdb26b02a5925fd8db3 - git checkout dba20ee77b0ba711893726208a0523073fc697e3
- ./test.sh - ./test.sh
- cd .. - cd ..
coverage: '/TOTAL.*\s([.\d]+)%/' coverage: '/TOTAL.*\s([.\d]+)%/'
@ -28,13 +26,12 @@ build:
- _build - _build
- htmlcov - htmlcov
reports: reports:
coverage_report: cobertura: coverage.xml
coverage_format: cobertura
path: coverage.xml
fuzz: fuzz:
image: registry.gitlab.gnome.org/gnome/blueprint-compiler image: registry.gitlab.gnome.org/jwestman/blueprint-compiler
stage: build stage: build
allow_failure: true
script: script:
- meson _build - meson _build
- ninja -C _build install - ninja -C _build install

View file

@ -1,28 +0,0 @@
First of all, thank you for contributing to Blueprint.
If you learn something useful, please add it to this file.
# Run the test suite
```sh
python -m unittest
```
# Formatting
Blueprint uses [Black](https://github.com/psf/black) for code formatting.
# Build the docs
```sh
pip install -U sphinx furo
meson -Ddocs=true build
# or
meson --reconfigure -Ddocs=true build
ninja -C build docs/en
python -m http.server 2310 --bind 127.0.0.1 --directory build/docs/en/
xdg-open http://127.0.0.1:2310/
```

View file

@ -1,18 +0,0 @@
## Releasing a new version
1. Look at the git log since the previous release. Note every significant change
in the NEWS file.
2. Update the version number, according to semver:
- At the top of meson.build
- In docs/flatpak.rst
3. Make a new commit with just these two changes. Use `Release v{version}` as the commit message. Tag the commit as `v{version}` and push the tag.
4. Create a "Post-release version bump" commit.
5. Go to the Releases page in GitLab and create a new release from the tag.
6. Announce the release through relevant channels (Mastodon, TWIG, etc.)
## Related projects
Blueprint is supported by the following syntax highlighters. If changes are made to the syntax, remember to update these projects as well.
- Pygments (https://github.com/pygments/pygments/blob/master/pygments/lexers/blueprint.py)
- GtkSourceView (https://gitlab.gnome.org/GNOME/gtksourceview/-/blob/master/data/language-specs/blueprint.lang)

244
NEWS.md
View file

@ -1,244 +0,0 @@
# v0.16.0
## Added
- Added more "go to reference" implementations in the language server
- Added semantic token support for flag members in the language server
- Added property documentation to the hover tooltip for notify signals
- The language server now shows relevant sections of the reference documentation when hovering over keywords and symbols
- Added `not-swapped` flag to signal handlers, which may be needed for signal handlers that specify an object
- Added expression literals, which allow you to specify a Gtk.Expression property (as opposed to the existing expression support, which is for property bindings)
## Changed
- The formatter adds trailing commas to lists (Alexey Yerin)
- The formatter removes trailing whitespace from comments (Alexey Yerin)
- Autocompleting a commonly translated property automatically adds the `_("")` syntax
- Marking a single-quoted string as translatable now generates a warning, since gettext does not recognize it when using the configuration recommended in the blueprint documentation
## Fixed
- Added support for libgirepository-2.0 so that blueprint doesn't crash due to import conflicts on newer versions of PyGObject (Jordan Petridis)
- Fixed a bug when decompiling/porting files with enum values
- Fixed several issues where tests would fail with versions of GTK that added new deprecations
- Addressed a problem with the language server protocol in some editors (Luoyayu)
- Fixed an issue where the compiler would crash instead of reporting compiler errors
- Fixed a crash in the language server that occurred when a detailed signal (e.g. `notify::*`) was not complete
- The language server now properly implements the shutdown command, fixing support for some editors and improving robustness when restarting (Alexey Yerin)
- Marking a string in an array as translatable now generates an error, since it doesn't work
-
## Documentation
- Added mention of `null` in the Literal Values section
- Add apps to Built with Blueprint section (Benedek Dévényi, Vladimir Vaskov)
- Corrected and updated many parts of the documentation
# v0.14.0
## Added
- Added a warning for unused imports.
- Added an option to not print the diff when formatting with the CLI. (Gregor Niehl)
- Added support for building Gtk.ColumnViewRow, Gtk.ColumnViewCell, and Gtk.ListHeader widgets with Gtk.BuilderListItemFactory.
- Added support for the `after` keyword for signals. This was previously documented but not implemented. (Gregor Niehl)
- Added support for string arrays. (Diego Augusto)
- Added hover documentation for properties in lookup expressions.
- The decompiler supports action widgets, translation domains, `typeof<>` syntax, and expressions. It also supports extension syntax for Adw.Breakpoint, Gtk.BuilderListItemFactory, Gtk.ComboBoxText, Gtk.SizeGroup, and Gtk.StringList.
- Added a `decompile` subcommand to the CLI, which decompiles an XML .ui file to blueprint.
- Accessibility relations that allow multiple values are supported using list syntax. (Julian Schmidhuber)
## Changed
- The decompiler sorts imports alphabetically.
- Translatable strings use `translatable="yes"` instead of `translatable="true"` for compatibility with xgettext. (Marco Köpcke)
- The first line of the documentation is shown in the completion list when using the language server. (Sonny Piers)
- Object autocomplete uses a snippet to add the braces and position the cursor inside them. (Sonny Piers)
- The carets in the CLI diagnostic output now span the whole error message up to the end of the first line, rather than just the first character.
- The decompiler emits double quotes, which are compatible with gettext.
## Fixed
- Fixed deprecation warnings in the language server.
- The decompiler no longer duplicates translator comments on properties.
- Subtemplates no longer output a redundant `@generated` comment.
- When extension syntax from a library that is not available is used, the compiler emits an error instead of crashing.
- The language server reports semantic token positions correctly. (Szepesi Tibor)
- The decompiler no longer emits the deprecated `bind-property` syntax. (Sonny Piers)
- Fixed the tests when used as a Meson subproject. (Benoit Pierre)
- Signal autocomplete generates correct syntax. (Sonny Piers)
- The decompiler supports templates that do not specify a parent class. (Sonny Piers)
- Adw.Breakpoint setters that set a property on the template no longer cause a crash.
- Fixed type checking with templates that do not have a parent class.
- Fixed online documentation links for interfaces.
- The wording of edit suggestions is fixed for insertions and deletions.
- When an input file uses tabs instead of spaces, the diagnostic output on the CLI aligns the caret correctly.
- The decompiler emits correct syntax when a property binding refers to the template object.
## Documentation
- Fixed typos in "Built with Blueprint" section. (Valéry Febvre, Dexter Reed)
# v0.12.0
## Added
- Add support for Adw.AlertDialog (Sonny Piers)
- Emit warnings for deprecated APIs - lsp and compiler
- lsp: Document symbols
- lsp: "Go to definition" (ctrl+click)
- lsp: Code action for "namespace not imported" diagnostics, that adds the missing import
- Add a formatter - cli and lsp (Gregor Niehl)
- Support for translation domain - see documentation
- cli: Print code actions in error messages
## Changed
- compiler: Add a header notice mentionning the file is generated (Urtsi Santsi)
- decompiler: Use single quotes for output
## Fixed
- Fixed multine strings support with the escape newline character
- lsp: Fixed the signal completion, which was missing the "$"
- lsp: Fixed property value completion (Ivan Kalinin)
- lsp: Added a missing semantic highlight (for the enum in Gtk.Scale marks)
- Handle big endian bitfields correctly (Jerry James)
- batch-compile: Fix mixing relative and absolute paths (Marco Köpcke )
## Documentation
- Fix grammar for bindings
- Add section on referencing templates
# v0.10.0
## Added
- The hover documentation now includes a link to the online documentation for the symbol, if available.
- Added hover documentation for the Adw.Breakpoint extensions, `condition` and `setters`.
## Changed
- Decompiling an empty file now produces an empty file rather than an error. (AkshayWarrier)
- More relevant documentation is shown when hovering over an identifier literal (such as an enum value or an object ID).
## Fixed
- Fixed an issue with the language server not conforming the spec. (seshotake)
- Fixed the signature section of the hover documentation for properties and signals.
- Fixed a bug where documentation was sometimes shown for a different symbol with the same name.
- Fixed a bug where documentation was not shown for accessibility properties that contain `-`.
- Number literals are now correctly parsed as floats if they contain a `.`, even if they are divisible by 1.
## Removed
- The `bind-property` keyword has been removed. Use `bind` instead. The old syntax is still accepted with a warning.
## Documentation
- Fixed the grammar for Extension, which was missing ExtAdwBreakpoint.
# v0.8.1
## Breaking Changes
- Duplicates in a number of places are now considered errors. For example, duplicate flags in several places, duplicate
strings in Gtk.FileFilters, etc.
## Fixed
- Fixed a number of bugs in the XML output when using `template` to refer to the template object.
## Documentation
- Fixed the example for ExtListItemFactory
# v0.8.0
## Breaking Changes
- A trailing `|` is no longer allowed in flags.
- The primitive type names `gboolean`, `gchararray`, `gint`, `gint64`, `guint`, `guint64`, `gfloat`, `gdouble`, `utf8`, and `gtype` are no longer permitted. Use the non-`g`-prefixed versions instead.
- Translated strings may no longer have trailing commas.
## Added
- Added cast expressions, which are sometimes needed to specify type information in expressions.
- Added support for closure expressions.
- Added the `--typelib-path` command line argument, which allows adding directories to the search path for typelib files.
- Added custom compile and decompile commands to the language server. (Sonny Piers)
- Added support for [Adw.MessageDialog](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.MessageDialog.html#adwmessagedialog-as-gtkbuildable) custom syntax.
- Added support for inline sub-templates for [Gtk.BuilderListItemFactory](https://docs.gtk.org/gtk4/class.BuilderListItemFactory.html). (Cameron Dehning)
- Added support for [Adw.Breakpoint](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Breakpoint.html) custom syntax.
- Added a warning when an object ID might be confusing.
- Added support for [Gtk.Scale](https://docs.gtk.org/gtk4/class.Scale.html#gtkscale-as-gtkbuildable) custom syntax.
## Changed
Some of these changes affect syntax, but the old syntax is still accepted with a purple "upgrade" warning, so they are not breaking changes yet. In editors that support code actions, such as Visual Studio Code, the blueprint language server can automatically fix these warnings.
- The XML output uses the integer value rather than GIR name for enum values.
- Compiler errors are now printed to stderr rather than stdout. (Sonny Piers)
- Introduced `$` to indicate types or callbacks that are provided in application code.
- Types that are provided by application code are now begin with a `$` rather than a leading `.`.
- The handler name in a signal is now prefixed with `$`.
- Closure expressions, which were added in this version, are also prefixed with `$`.
- When a namespace is not found, errors are supressed when the namespace is used.
- The compiler bug message now reports the version of blueprint-compiler.
- The `typeof` syntax now uses `<>` instead of `()` to match cast expressions.
- Menu sections and subsections can now have an ID.
- The interactive porting tool now ignores hidden folders. (Sonny Piers)
- Templates now use the typename syntax rather than an ID to specify the template's class. In most cases, this just means adding a `$` prefix to the ID, but for GtkListItem templates it should be shortened to ListItem (since the Gtk namespace is implied). The template object is now referenced with the `template` keyword rather than with the ID.
## Fixed
- Fixed a bug in the language server's acceptance of text change commands. (Sonny Piers)
- Fixed a bug in the display of diagnostics when the diagnostic is at the beginning of a line.
- Fixed a crash that occurred when dealing with array types.
- Fixed a bug that prevented Gio.File properties from being settable.
## Documentation
- Added a reference section to the documentation. This replaces the Examples page with a detailed description of each syntax feature, including a formal specification of the grammar.
# v0.6.0
## Breaking Changes
- Quoted and numeric literals are no longer interchangeable (e.g. `"800"` is no longer an accepted value for an
integer type).
- Boxed types are now type checked.
## Added
- There is now syntax for `GType` literals: the `typeof()` pseudo-function. For example, list stores have an `item-type`
property which is now specifiable like this: `item-type: typeof(.MyDataModel)`. See the documentation for more details.
## Changed
- The language server now logs to stderr.
## Fixed
- Fix the build on Windows, where backslashes in paths were not escaped. (William Roy)
- Remove the syntax for specifying menu objects inline, since it does not work.
- Fix a crash in the language server that was triggered in files with incomplete `using Gtk 4.0;` statements.
- Fixed compilation on big-endian systems.
- Fix an issue in the interactive port tool that would lead to missed files. (Frank Dana)
## Documentation
- Fix an issue for documentation contributors where changing the documentation files would not trigger a rebuild.
- Document the missing support for Gtk.Label `<attributes>`, which is intentional, and recommend alternatives. (Sonny
Piers)
- Add a prominent warning that Blueprint is still experimental
# v0.4.0
## Added
- Lookup expressions
- With the language server, hovering over a diagnostic message now shows any
associated hints.
## Changed
- The compiler now uses .typelib files rather than XML .gir files, which reduces
dependencies and should reduce compile times by about half a second.
## Fixed
- Fix the decompiler/porting tool not importing the Adw namespace when needed
- Fix a crash when trying to compile an empty file
- Fix parsing of number tokens
- Fix a bug where action widgets did not work in templates
- Fix a crash in the language server that occurred when a `using` statement had
no version
- If a compiler bug is reported, the process now exits with a non-zero code

View file

@ -65,31 +65,13 @@ template ShumateDemoWindow : Gtk.ApplicationWindow {
} }
``` ```
## Editors ## Editor plugins
[Workbench](https://github.com/sonnyp/Workbench) and [GNOME Builder](https://apps.gnome.org/app/org.gnome.Builder/) have builtin support for Blueprint. ### Vim
Vim
- [Syntax highlighting by thetek42](https://github.com/thetek42/vim-blueprint-syntax) - [Syntax highlighting by thetek42](https://github.com/thetek42/vim-blueprint-syntax)
- [Syntax highlighting by gabmus](https://gitlab.com/gabmus/vim-blueprint) - [Syntax highlighting by gabmus](https://gitlab.com/gabmus/vim-blueprint)
GNU Emacs
- [Major mode by DrBluefall](https://github.com/DrBluefall/blueprint-mode)
Visual Studio Code
- [Blueprint Language Plugin by bodil](https://github.com/bodil/vscode-blueprint)
## Donate
You can support my work on GitHub Sponsors! <https://github.com/sponsors/jameswestman>
## Getting in Touch
Matrix room: [#blueprint-language:matrix.org](https://matrix.to/#/#blueprint-language:matrix.org)
## License ## License
Copyright (C) 2021 James Westman <james@jwestman.net> Copyright (C) 2021 James Westman <james@jwestman.net>
@ -106,3 +88,11 @@ GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
## Donate
You can support my work on GitHub Sponsors! <https://github.com/sponsors/jameswestman>
## Getting in Touch
Matrix room: [#blueprint-language:matrix.org](https://matrix.to/#/#blueprint-language:matrix.org)

View file

@ -1,27 +0,0 @@
<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
xmlns:foaf="http://xmlns.com/foaf/0.1/"
xmlns:gnome="http://api.gnome.org/doap-extensions#"
xmlns="http://usefulinc.com/ns/doap#">
<name xml:lang="en">Blueprint</name>
<shortdesc xml:lang="en">A modern language for creating GTK interfaces</shortdesc>
<description xml:lang="en">Blueprint is a language and associated tooling for building user interfaces for GTK.</description>
<category rdf:resource="http://api.gnome.org/doap-extensions#apps" />
<programming-language>Python</programming-language>
<homepage
rdf:resource="https://gnome.gitlab.gnome.org/blueprint-compiler/" />
<download-page
rdf:resource="https://gitlab.gnome.org/GNOME/blueprint-compiler/-/releases" />
<bug-database
rdf:resource="https://gitlab.gnome.org/GNOME/blueprint-compiler/issues" />
<maintainer>
<foaf:Person>
<foaf:name>James Westman</foaf:name>
<foaf:mbox rdf:resource="mailto:james@jwestman.net" />
<gnome:userid>jwestman</gnome:userid>
</foaf:Person>
</maintainer>
</Project>

View file

@ -19,23 +19,20 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import os import os, sys
import sys
# These variables should be set by meson. If they aren't, we're running # Try to find the python module, assuming the current file is installed to (prefix)/bin
# uninstalled, and we might have to guess some values. dirname = os.path.join(os.path.dirname(os.path.dirname(__file__)), "share", "blueprint-compiler")
if os.path.isdir(os.path.join(dirname, "blueprintcompiler")):
sys.path.insert(0, dirname)
# Get the configured (or, if running from source, not configured) version number
version = "@VERSION@" version = "@VERSION@"
module_path = r"@MODULE_PATH@"
libdir = r"@LIBDIR@"
if version == "\u0040VERSION@": def literal(key):
version = "uninstalled" return "@" + key + "@"
libdir = None
else:
# If Meson set the configuration values, insert the module path it set
sys.path.insert(0, module_path)
from blueprintcompiler import main from blueprintcompiler import main
if __name__ == "__main__": if __name__ == "__main__":
main.main(version, libdir) main.main("uninstalled" if version == literal("VERSION") else version)

View file

@ -1,191 +0,0 @@
# annotations.py
#
# Copyright 2024 James Westman <james@jwestman.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
# Extra information about types in common libraries that's used for things like completions.
import typing as T
from dataclasses import dataclass
from . import gir
@dataclass
class Annotation:
translatable_properties: T.List[str]
def is_property_translated(property: gir.Property):
ns = property.get_containing(gir.Namespace)
ns_name = ns.name + "-" + ns.version
if annotation := _ANNOTATIONS.get(ns_name):
assert property.container is not None
return (
property.container.name + ":" + property.name
in annotation.translatable_properties
)
else:
return False
_ANNOTATIONS = {
"Gtk-4.0": Annotation(
translatable_properties=[
"AboutDialog:comments",
"AboutDialog:translator-credits",
"AboutDialog:website-label",
"AlertDialog:detail",
"AlertDialog:message",
"AppChooserButton:heading",
"AppChooserDialog:heading",
"AppChooserWidget:default-text",
"AssistantPage:title",
"Button:label",
"CellRendererText:markup",
"CellRendererText:placeholder-text",
"CellRendererText:text",
"CheckButton:label",
"ColorButton:title",
"ColorDialog:title",
"ColumnViewColumn:title",
"ColumnViewRow:accessible-description",
"ColumnViewRow:accessible-label",
"Entry:placeholder-text",
"Entry:primary-icon-tooltip-markup",
"Entry:primary-icon-tooltip-text",
"Entry:secondary-icon-tooltip-markup",
"Entry:secondary-icon-tooltip-text",
"EntryBuffer:text",
"Expander:label",
"FileChooserNative:accept-label",
"FileChooserNative:cancel-label",
"FileChooserWidget:subtitle",
"FileDialog:accept-label",
"FileDialog:title",
"FileDialog:initial-name",
"FileFilter:name",
"FontButton:title",
"FontDialog:title",
"Frame:label",
"Inscription:markup",
"Inscription:text",
"Label:label",
"ListItem:accessible-description",
"ListItem:accessible-label",
"LockButton:text-lock",
"LockButton:text-unlock",
"LockButton:tooltip-lock",
"LockButton:tooltip-not-authorized",
"LockButton:tooltip-unlock",
"MenuButton:label",
"MessageDialog:secondary-text",
"MessageDialog:text",
"NativeDialog:title",
"NotebookPage:menu-label",
"NotebookPage:tab-label",
"PasswordEntry:placeholder-text",
"Picture:alternative-text",
"PrintDialog:accept-label",
"PrintDialog:title",
"Printer:name",
"PrintJob:title",
"PrintOperation:custom-tab-label",
"PrintOperation:export-filename",
"PrintOperation:job-name",
"ProgressBar:text",
"SearchEntry:placeholder-text",
"ShortcutLabel:disabled-text",
"ShortcutsGroup:title",
"ShortcutsSection:title",
"ShortcutsShortcut:title",
"ShortcutsShortcut:subtitle",
"StackPage:title",
"Text:placeholder-text",
"TextBuffer:text",
"TreeViewColumn:title",
"Widget:tooltip-markup",
"Widget:tooltip-text",
"Window:title",
"Editable:text",
"FontChooser:preview-text",
]
),
"Adw-1": Annotation(
translatable_properties=[
"AboutDialog:comments",
"AboutDialog:translator-credits",
"AboutWindow:comments",
"AboutWindow:translator-credits",
"ActionRow:subtitle",
"ActionRow:title",
"AlertDialog:body",
"AlertDialog:heading",
"Avatar:text",
"Banner:button-label",
"Banner:title",
"ButtonContent:label",
"Dialog:title",
"ExpanderRow:subtitle",
"MessageDialog:body",
"MessageDialog:heading",
"NavigationPage:title",
"PreferencesGroup:description",
"PreferencesGroup:title",
"PreferencesPage:description",
"PreferencesPage:title",
"PreferencesRow:title",
"SplitButton:dropdown-tooltip",
"SplitButton:label",
"StatusPage:description",
"StatusPage:title",
"TabPage:indicator-tooltip",
"TabPage:keyword",
"TabPage:title",
"Toast:button-label",
"Toast:title",
"ViewStackPage:title",
"ViewSwitcherTitle:subtitle",
"ViewSwitcherTitle:title",
"WindowTitle:subtitle",
"WindowTitle:title",
]
),
"Shumate-1.0": Annotation(
translatable_properties=[
"License:extra-text",
"MapSource:license",
"MapSource:name",
]
),
"GtkSource-5": Annotation(
translatable_properties=[
"CompletionCell:markup",
"CompletionCell:text",
"CompletionSnippets:title",
"CompletionWords:title",
"GutterRendererText:markup",
"GutterRendererText:text",
"SearchSettings:search-text",
"Snippet:description",
"Snippet:name",
"SnippetChunk:tooltip-text",
"StyleScheme:description",
"StyleScheme:name",
]
),
}

View file

@ -17,80 +17,29 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from collections import ChainMap, defaultdict from collections import ChainMap, defaultdict
from functools import cached_property from functools import cached_property
import typing as T
from .errors import * from .errors import *
from .lsp_utils import DocumentSymbol, LocationLink, SemanticToken from .lsp_utils import SemanticToken
from .tokenizer import Range from .xml_emitter import XmlEmitter
TType = T.TypeVar("TType")
class Children: class Children:
"""Allows accessing children by type using array syntax.""" """ Allows accessing children by type using array syntax. """
def __init__(self, children): def __init__(self, children):
self._children = children self._children = children
def __iter__(self):
def __iter__(self) -> T.Iterator["AstNode"]:
return iter(self._children) return iter(self._children)
@T.overload
def __getitem__(self, key: T.Type[TType]) -> T.List[TType]: ...
@T.overload
def __getitem__(self, key: int) -> "AstNode": ...
def __getitem__(self, key): def __getitem__(self, key):
if isinstance(key, int): return [child for child in self._children if isinstance(child, key)]
if key >= len(self._children):
return None
else:
return self._children[key]
else:
return [child for child in self._children if isinstance(child, key)]
class Ranges:
def __init__(self, ranges: T.Dict[str, Range]):
self._ranges = ranges
def __getitem__(self, key: T.Union[str, tuple[str, str]]) -> T.Optional[Range]:
if isinstance(key, str):
return self._ranges.get(key)
elif isinstance(key, tuple):
start, end = key
return Range.join(self._ranges.get(start), self._ranges.get(end))
TCtx = T.TypeVar("TCtx")
TAttr = T.TypeVar("TAttr")
class Ctx:
"""Allows accessing values from higher in the syntax tree."""
def __init__(self, node: "AstNode") -> None:
self.node = node
def __getitem__(self, key: T.Type[TCtx]) -> T.Optional[TCtx]:
attrs = self.node._attrs_by_type(Context)
for name, attr in attrs:
if attr.type == key:
return getattr(self.node, name)
if self.node.parent is not None:
return self.node.parent.context[key]
else:
return None
class AstNode: class AstNode:
"""Base class for nodes in the abstract syntax tree.""" """ Base class for nodes in the abstract syntax tree. """
completers: T.List = [] completers: T.List = []
attrs_by_type: T.Dict[T.Type, T.List] = {}
def __init__(self, group, children, tokens, incomplete=False): def __init__(self, group, children, tokens, incomplete=False):
self.group = group self.group = group
@ -104,33 +53,19 @@ class AstNode:
def __init_subclass__(cls): def __init_subclass__(cls):
cls.completers = [] cls.completers = []
cls.validators = [ cls.validators = [getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator")]
getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator")
]
cls.attrs_by_type = {}
@cached_property
def context(self):
return Ctx(self)
@cached_property @property
def ranges(self):
return Ranges(self.group.ranges)
@cached_property
def root(self): def root(self):
if self.parent is None: if self.parent is None:
return self return self
else: else:
return self.parent.root return self.parent.root
@property def parent_by_type(self, type):
def range(self) -> Range:
return Range(self.group.start, self.group.end, self.group.text)
def parent_by_type(self, type: T.Type[TType]) -> TType:
if self.parent is None: if self.parent is None:
raise CompilerBugError() return None
elif isinstance(self.parent, type): elif isinstance(self.parent, type):
return self.parent return self.parent
else: else:
@ -138,19 +73,7 @@ class AstNode:
@cached_property @cached_property
def errors(self): def errors(self):
return list( return list(self._get_errors())
error
for error in self._get_errors()
if not isinstance(error, CompileWarning)
)
@cached_property
def warnings(self):
return list(
warning
for warning in self._get_errors()
if isinstance(warning, CompileWarning)
)
def _get_errors(self): def _get_errors(self):
for validator in self.validators: for validator in self.validators:
@ -160,23 +83,25 @@ class AstNode:
yield e yield e
if e.fatal: if e.fatal:
return return
except MultipleErrors as e:
for error in e.errors:
yield error
if error.fatal:
return
for child in self.children: for child in self.children:
yield from child._get_errors() yield from child._get_errors()
def _attrs_by_type(self, attr_type: T.Type[TAttr]) -> T.List[T.Tuple[str, TAttr]]: def _attrs_by_type(self, attr_type):
if attr_type not in self.attrs_by_type: for name in dir(type(self)):
self.attrs_by_type[attr_type] = [] item = getattr(type(self), name)
for name in dir(type(self)): if isinstance(item, attr_type):
item = getattr(type(self), name) yield name, item
if isinstance(item, attr_type):
self.attrs_by_type[attr_type].append((name, item)) def generate(self) -> str:
return self.attrs_by_type[attr_type] """ 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]: def get_docs(self, idx: int) -> T.Optional[str]:
for name, attr in self._attrs_by_type(Docs): for name, attr in self._attrs_by_type(Docs):
@ -184,46 +109,29 @@ class AstNode:
token = self.group.tokens.get(attr.token_name) token = self.group.tokens.get(attr.token_name)
if token and token.start <= idx < token.end: if token and token.start <= idx < token.end:
return getattr(self, name) return getattr(self, name)
else:
return getattr(self, name)
for child in self.children: for child in self.children:
if idx in child.range: if child.group.start <= idx < child.group.end:
if docs := child.get_docs(idx): docs = child.get_docs(idx)
if docs is not None:
return docs return docs
for name, attr in self._attrs_by_type(Docs):
if not attr.token_name:
return getattr(self, name)
return None return None
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
for child in self.children: for child in self.children:
yield from child.get_semantic_tokens() yield from child.get_semantic_tokens()
def get_reference(self, idx: int) -> T.Optional[LocationLink]:
def iterate_children_recursive(self) -> T.Iterator["AstNode"]:
yield self
for child in self.children: for child in self.children:
if idx in child.range: yield from child.iterate_children_recursive()
if ref := child.get_reference(idx):
return ref
return None
@property
def document_symbol(self) -> T.Optional[DocumentSymbol]:
return None
def get_document_symbols(self) -> T.List[DocumentSymbol]: def validate_unique_in_parent(self, error, check=None):
result = []
for child in self.children:
if s := child.document_symbol:
s.children = child.get_document_symbols()
result.append(s)
else:
result.extend(child.get_document_symbols())
return result
def validate_unique_in_parent(
self, error: str, check: T.Optional[T.Callable[["AstNode"], bool]] = None
):
for child in self.parent.children: for child in self.parent.children:
if child is self: if child is self:
break break
@ -232,38 +140,19 @@ class AstNode:
if check is None or check(child): if check is None or check(child):
raise CompileError( raise CompileError(
error, error,
references=[ references=[ErrorReference(child.group.start, child.group.end, "previous declaration was here")]
ErrorReference(
child.range,
"previous declaration was here",
)
],
) )
def validate( def validate(token_name=None, end_token_name=None, skip_incomplete=False):
token_name: T.Optional[str] = None, """ Decorator for functions that validate an AST node. Exceptions raised
end_token_name: T.Optional[str] = None, during validation are marked with range information from the tokens. """
skip_incomplete: bool = 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 decorator(func):
def inner(self: AstNode): def inner(self):
if skip_incomplete and self.incomplete: if skip_incomplete and self.incomplete:
return return
def fill_error(e: CompileError):
if e.range is None:
e.range = (
Range.join(
self.ranges[token_name],
self.ranges[end_token_name],
)
or self.range
)
try: try:
func(self) func(self)
except CompileError as e: except CompileError as e:
@ -272,18 +161,25 @@ def validate(
if self.incomplete: if self.incomplete:
return return
fill_error(e) # 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 # Re-raise the exception
raise e raise e
except MultipleErrors as e:
if self.incomplete:
return
for error in e.errors:
fill_error(error)
raise e
inner._validator = True inner._validator = True
return inner return inner
@ -303,34 +199,9 @@ class Docs:
def docs(*args, **kwargs): def docs(*args, **kwargs):
"""Decorator for functions that return documentation for tokens.""" """ Decorator for functions that return documentation for tokens. """
def decorator(func): def decorator(func):
return Docs(func, *args, **kwargs) return Docs(func, *args, **kwargs)
return decorator return decorator
class Context:
def __init__(self, type: T.Type[TCtx], func: T.Callable[[AstNode], TCtx]) -> None:
self.type = type
self.func = func
def __get__(self, instance, owner):
if instance is None:
return self
if ctx := getattr(instance, "_context_" + self.type.__name__, None):
return ctx
else:
ctx = self.func(instance)
setattr(instance, "_context_" + self.type.__name__, ctx)
return ctx
def context(type: T.Type[TCtx]):
"""Decorator for functions that return a context object, which is passed down to ."""
def decorator(func: T.Callable[[AstNode], TCtx]) -> Context:
return Context(type, func)
return decorator

View file

@ -19,25 +19,20 @@
import typing as T import typing as T
from . import annotations, gir, language from . import gir, language
from .ast_utils import AstNode from .ast_utils import AstNode
from .completions_utils import * from .completions_utils import *
from .language.types import ClassName
from .lsp_utils import Completion, CompletionItemKind from .lsp_utils import Completion, CompletionItemKind
from .parser import SKIP_TOKENS from .parser import SKIP_TOKENS
from .tokenizer import Token, TokenType from .tokenizer import TokenType, Token
Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]] Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]]
def _complete( def _complete(ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int) -> T.Iterator[Completion]:
lsp, ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int
) -> T.Iterator[Completion]:
for child in ast_node.children: for child in ast_node.children:
if child.group.start <= idx and ( if child.group.start <= idx and (idx < child.group.end or (idx == child.group.end and child.incomplete)):
idx < child.group.end or (idx == child.group.end and child.incomplete) yield from _complete(child, tokens, idx, token_idx)
):
yield from _complete(lsp, child, tokens, idx, token_idx)
return return
prev_tokens: T.List[Token] = [] prev_tokens: T.List[Token] = []
@ -50,12 +45,10 @@ def _complete(
token_idx -= 1 token_idx -= 1
for completer in ast_node.completers: for completer in ast_node.completers:
yield from completer(prev_tokens, ast_node, lsp) yield from completer(prev_tokens, ast_node)
def complete( def complete(ast_node: AstNode, tokens: T.List[Token], idx: int) -> T.Iterator[Completion]:
lsp, ast_node: AstNode, tokens: T.List[Token], idx: int
) -> T.Iterator[Completion]:
token_idx = 0 token_idx = 0
# find the current token # find the current token
for i, token in enumerate(tokens): for i, token in enumerate(tokens):
@ -67,29 +60,23 @@ def complete(
idx = tokens[token_idx].start idx = tokens[token_idx].start
token_idx -= 1 token_idx -= 1
yield from _complete(lsp, ast_node, tokens, idx, token_idx) yield from _complete(ast_node, tokens, idx, token_idx)
@completer([language.GtkDirective]) @completer([language.GtkDirective])
def using_gtk(lsp, ast_node, match_variables): def using_gtk(ast_node, match_variables):
yield Completion( yield Completion("using Gtk 4.0;", CompletionItemKind.Keyword)
"using Gtk 4.0", CompletionItemKind.Keyword, snippet="using Gtk 4.0;\n"
)
@completer( @completer(
applies_in=[language.UI, language.ObjectContent, language.Template], applies_in=[language.UI, language.ObjectContent, language.Template],
matches=new_statement_patterns, matches=new_statement_patterns
) )
def namespace(lsp, ast_node, match_variables): def namespace(ast_node, match_variables):
yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.")
for ns in ast_node.root.children[language.Import]: for ns in ast_node.root.children[language.Import]:
if ns.gir_namespace is not None: if ns.gir_namespace is not None:
yield Completion( yield Completion(ns.gir_namespace.name, CompletionItemKind.Module, text=ns.gir_namespace.name + ".")
ns.gir_namespace.name,
CompletionItemKind.Module,
text=ns.gir_namespace.name + ".",
)
@completer( @completer(
@ -97,156 +84,72 @@ def namespace(lsp, ast_node, match_variables):
matches=[ matches=[
[(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)], [(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)],
[(TokenType.IDENT, None), (TokenType.OP, ".")], [(TokenType.IDENT, None), (TokenType.OP, ".")],
], ]
) )
def object_completer(lsp, ast_node, match_variables): def object_completer(ast_node, match_variables):
ns = ast_node.root.gir.namespaces.get(match_variables[0]) ns = ast_node.root.gir.namespaces.get(match_variables[0])
if ns is not None: if ns is not None:
for c in ns.classes.values(): for c in ns.classes.values():
yield Completion( yield Completion(c.name, CompletionItemKind.Class, docs=c.doc)
c.name,
CompletionItemKind.Class,
snippet=f"{c.name} {{\n $0\n}}",
docs=c.doc,
detail=c.detail,
)
@completer( @completer(
applies_in=[language.UI, language.ObjectContent, language.Template], applies_in=[language.UI, language.ObjectContent, language.Template],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def gtk_object_completer(lsp, ast_node, match_variables): def gtk_object_completer(ast_node, match_variables):
ns = ast_node.root.gir.namespaces.get("Gtk") ns = ast_node.root.gir.namespaces.get("Gtk")
if ns is not None: if ns is not None:
for c in ns.classes.values(): for c in ns.classes.values():
yield Completion( yield Completion(c.name, CompletionItemKind.Class, docs=c.doc)
c.name,
CompletionItemKind.Class,
snippet=f"{c.name} {{\n $0\n}}",
docs=c.doc,
detail=c.detail,
)
@completer( @completer(
applies_in=[language.ObjectContent], applies_in=[language.ObjectContent],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def property_completer(lsp, ast_node, match_variables): def property_completer(ast_node, match_variables):
if ast_node.gir_class and hasattr(ast_node.gir_class, "properties"): if ast_node.gir_class:
for prop_name, prop in ast_node.gir_class.properties.items(): for prop in ast_node.gir_class.properties:
if ( yield Completion(prop, CompletionItemKind.Property, snippet=f"{prop}: $0;")
isinstance(prop.type, gir.BoolType)
and lsp.client_supports_completion_choice
):
yield Completion(
prop_name,
CompletionItemKind.Property,
sort_text=f"0 {prop_name}",
snippet=f"{prop_name}: ${{1|true,false|}};",
docs=prop.doc,
detail=prop.detail,
)
elif isinstance(prop.type, gir.StringType):
snippet = (
f'{prop_name}: _("$0");'
if annotations.is_property_translated(prop)
else f'{prop_name}: "$0";'
)
yield Completion(
prop_name,
CompletionItemKind.Property,
sort_text=f"0 {prop_name}",
snippet=snippet,
docs=prop.doc,
detail=prop.detail,
)
elif (
isinstance(prop.type, gir.Enumeration)
and len(prop.type.members) <= 10
and lsp.client_supports_completion_choice
):
choices = ",".join(prop.type.members.keys())
yield Completion(
prop_name,
CompletionItemKind.Property,
sort_text=f"0 {prop_name}",
snippet=f"{prop_name}: ${{1|{choices}|}};",
docs=prop.doc,
detail=prop.detail,
)
elif prop.type.full_name == "Gtk.Expression":
yield Completion(
prop_name,
CompletionItemKind.Property,
sort_text=f"0 {prop_name}",
snippet=f"{prop_name}: expr $0;",
docs=prop.doc,
detail=prop.detail,
)
else:
yield Completion(
prop_name,
CompletionItemKind.Property,
sort_text=f"0 {prop_name}",
snippet=f"{prop_name}: $0;",
docs=prop.doc,
detail=prop.detail,
)
@completer( @completer(
applies_in=[language.Property, language.A11yProperty], applies_in=[language.Property, language.BaseTypedAttribute],
matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], matches=[
[(TokenType.IDENT, None), (TokenType.OP, ":")]
],
) )
def prop_value_completer(lsp, ast_node, match_variables): def prop_value_completer(ast_node, match_variables):
if (vt := ast_node.value_type) is not None: if isinstance(ast_node.value_type, gir.Enumeration):
if isinstance(vt.value_type, gir.Enumeration): for name, member in ast_node.value_type.members.items():
for name, member in vt.value_type.members.items(): yield Completion(name, CompletionItemKind.EnumMember, docs=member.doc)
yield Completion(
name,
CompletionItemKind.EnumMember,
docs=member.doc,
detail=member.detail,
)
elif isinstance(vt.value_type, gir.BoolType): elif isinstance(ast_node.value_type, gir.BoolType):
yield Completion("true", CompletionItemKind.Constant) yield Completion("true", CompletionItemKind.Constant)
yield Completion("false", CompletionItemKind.Constant) yield Completion("false", CompletionItemKind.Constant)
@completer( @completer(
applies_in=[language.ObjectContent], applies_in=[language.ObjectContent],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def signal_completer(lsp, ast_node, match_variables): def signal_completer(ast_node, match_variables):
if ast_node.gir_class and hasattr(ast_node.gir_class, "signals"): if ast_node.gir_class:
for signal_name, signal in ast_node.gir_class.signals.items(): for signal in ast_node.gir_class.signals:
if not isinstance(ast_node.parent, language.Object): if not isinstance(ast_node.parent, language.Object):
name = "on" name = "on"
else: else:
name = "on_" + ( name = "on_" + (ast_node.parent.tokens["id"] or ast_node.parent.tokens["class_name"].lower())
ast_node.parent.children[ClassName][0].tokens["id"] yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;")
or ast_node.parent.children[ClassName][0]
.tokens["class_name"]
.lower()
)
yield Completion(
signal_name,
CompletionItemKind.Event,
sort_text=f"1 {signal_name}",
snippet=f"{signal_name} => \\$${{1:${name}_{signal_name.replace('-', '_')}}}()$0;",
docs=signal.doc,
detail=signal.detail,
)
@completer(applies_in=[language.UI], matches=new_statement_patterns) @completer(
def template_completer(lsp, ast_node, match_variables): applies_in=[language.UI],
matches=new_statement_patterns
)
def template_completer(ast_node, match_variables):
yield Completion( yield Completion(
"template", "template", CompletionItemKind.Snippet,
CompletionItemKind.Snippet, snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}"
snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}",
) )

View file

@ -20,27 +20,33 @@
import typing as T import typing as T
from .lsp_utils import Completion
from .tokenizer import Token, TokenType from .tokenizer import Token, TokenType
from .lsp_utils import Completion
new_statement_patterns = [ new_statement_patterns = [
[(TokenType.PUNCTUATION, "{")], [(TokenType.PUNCTUATION, "{")],
[(TokenType.PUNCTUATION, "}")], [(TokenType.PUNCTUATION, "}")],
[(TokenType.PUNCTUATION, "]")],
[(TokenType.PUNCTUATION, ";")], [(TokenType.PUNCTUATION, ";")],
] ]
def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None): def applies_to(*ast_types):
""" Decorator describing which AST nodes the completer should apply in. """
def decorator(func): def decorator(func):
def inner(prev_tokens: T.List[Token], ast_node, lsp): 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 # For completers that apply in ObjectContent nodes, we can further
# check that the object is the right class # check that the object is the right class
if applies_in_subclass is not None: if applies_in_subclass is not None:
type = ast_node.root.gir.get_type( type = ast_node.root.gir.get_type(applies_in_subclass[1], applies_in_subclass[0])
applies_in_subclass[1], applies_in_subclass[0] if ast_node.gir_class and not ast_node.gir_class.assignable_to(type):
)
if not ast_node.gir_class or not ast_node.gir_class.assignable_to(type):
return return
any_match = len(matches) == 0 any_match = len(matches) == 0
@ -53,9 +59,7 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None
for i in range(0, len(pattern)): for i in range(0, len(pattern)):
type, value = pattern[i] type, value = pattern[i]
token = prev_tokens[i - len(pattern)] token = prev_tokens[i - len(pattern)]
if token.type != type or ( if token.type != type or (value is not None and str(token) != value):
value is not None and str(token) != value
):
break break
if value is None: if value is None:
match_variables.append(str(token)) match_variables.append(str(token))
@ -66,7 +70,7 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None
if not any_match: if not any_match:
return return
yield from func(lsp, ast_node, match_variables) yield from func(ast_node, match_variables)
for c in applies_in: for c in applies_in:
c.completers.append(inner) c.completers.append(inner)

View file

@ -17,20 +17,20 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T import re
from collections import defaultdict
from dataclasses import dataclass
from enum import Enum from enum import Enum
import typing as T
from dataclasses import dataclass
from . import formatter from .xml_reader import Element, parse
from .gir import * from .gir import *
from .utils import Colors, escape_quote from .utils import Colors
from .xml_reader import Element, parse, parse_string
__all__ = ["decompile"] __all__ = ["decompile"]
_DECOMPILERS: dict[str, list] = defaultdict(list) _DECOMPILERS: T.Dict = {}
_CLOSING = { _CLOSING = {
"{": "}", "{": "}",
"[": "]", "[": "]",
@ -39,7 +39,7 @@ _NAMESPACES = [
("GLib", "2.0"), ("GLib", "2.0"),
("GObject", "2.0"), ("GObject", "2.0"),
("Gio", "2.0"), ("Gio", "2.0"),
("Adw", "1"), ("Adw", "1.0"),
] ]
@ -51,34 +51,26 @@ class LineType(Enum):
class DecompileCtx: class DecompileCtx:
def __init__(self, parent_gir: T.Optional[GirContext] = None) -> None: def __init__(self):
self.sub_decompiler = parent_gir is not None self._result = ""
self._result: str = "" self.gir = GirContext()
self.gir = parent_gir or GirContext() self._indent = 0
self._blocks_need_end: T.List[str] = [] self._blocks_need_end = []
self._last_line_type: LineType = LineType.NONE self._last_line_type = LineType.NONE
self._obj_type_stack: list[T.Optional[GirType]] = []
self._node_stack: list[Element] = []
self.gir.add_namespace(get_namespace("Gtk", "4.0")) self.gir.add_namespace(get_namespace("Gtk", "4.0"))
@property @property
def result(self) -> str: def result(self):
imports = "" imports = "\n".join([
f"using {ns} {namespace.version};"
for ns, namespace in self.gir.namespaces.items()
])
return imports + "\n" + self._result
if not self.sub_decompiler:
import_lines = sorted(
[
f"using {ns} {namespace.version};"
for ns, namespace in self.gir.namespaces.items()
if ns != "Gtk"
]
)
imports += "\n".join(["using Gtk 4.0;", *import_lines])
return formatter.format(imports + self._result) def type_by_cname(self, cname):
def type_by_cname(self, cname: str) -> T.Optional[GirType]:
if type := self.gir.get_type_by_cname(cname): if type := self.gir.get_type_by_cname(cname):
return type return type
@ -91,204 +83,115 @@ class DecompileCtx:
except: except:
pass pass
return None
def start_block(self) -> None: def start_block(self):
self._blocks_need_end.append("") self._blocks_need_end.append(None)
self._obj_type_stack.append(None)
def end_block(self) -> None: def end_block(self):
if close := self._blocks_need_end.pop(): if close := self._blocks_need_end.pop():
self.print(close) self.print(close)
self._obj_type_stack.pop()
@property def end_block_with(self, text):
def current_obj_type(self) -> T.Optional[GirType]:
return next((x for x in reversed(self._obj_type_stack) if x is not None), None)
def push_obj_type(self, type: T.Optional[GirType]) -> None:
self._obj_type_stack[-1] = type
@property
def current_node(self) -> T.Optional[Element]:
if len(self._node_stack) == 0:
return None
else:
return self._node_stack[-1]
@property
def parent_node(self) -> T.Optional[Element]:
if len(self._node_stack) < 2:
return None
else:
return self._node_stack[-2]
@property
def root_node(self) -> T.Optional[Element]:
if len(self._node_stack) == 0:
return None
else:
return self._node_stack[0]
@property
def template_class(self) -> T.Optional[str]:
assert self.root_node is not None
for child in self.root_node.children:
if child.tag == "template":
return child["class"]
return None
def find_object(self, id: str) -> T.Optional[Element]:
assert self.root_node is not None
for child in self.root_node.children:
if child.tag == "template" and child["class"] == id:
return child
def find_in_children(node: Element) -> T.Optional[Element]:
if node.tag in ["object", "menu"] and node["id"] == id:
return node
else:
for child in node.children:
if result := find_in_children(child):
return result
return None
return find_in_children(self.root_node)
def end_block_with(self, text: str) -> None:
self._blocks_need_end[-1] = text self._blocks_need_end[-1] = text
def print(self, line: str, newline: bool = True) -> None:
self._result += line 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 line.endswith("{") or line.endswith("["):
if len(self._blocks_need_end): if len(self._blocks_need_end):
self._blocks_need_end[-1] = _CLOSING[line[-1]] self._blocks_need_end[-1] = _CLOSING[line[-1]]
self._indent += 1
# Converts a value from an XML element to a blueprint string
# based on the given type. Returns a tuple of translator comments
# (if any) and the decompiled syntax.
def decompile_value(
self,
value: str,
type: T.Optional[GirType],
translatable: T.Optional[T.Tuple[str, str, str]] = None,
) -> T.Tuple[str, str]:
def get_enum_name(value):
for member in type.members.values():
if (
member.nick == value
or member.c_ident == value
or str(member.value) == value
):
return member.name
return value.replace("-", "_")
if translatable is not None and truthy(translatable[0]): def print_attribute(self, name, value, type):
return decompile_translatable(value, *translatable) if type is None:
elif type is None: self.print(f"{name}: \"{escape_quote(value)}\";")
return "", f"{escape_quote(value)}"
elif type.assignable_to(FloatType()): elif type.assignable_to(FloatType()):
return "", str(value) self.print(f"{name}: {value};")
elif type.assignable_to(BoolType()): elif type.assignable_to(BoolType()):
val = truthy(value) val = truthy(value)
return "", ("true" if val else "false") self.print(f"{name}: {'true' if val else 'false'};")
elif type.assignable_to(ArrayType(StringType())):
items = ", ".join([escape_quote(x) for x in value.split("\n")])
return "", f"[{items}]"
elif ( elif (
type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf")) 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.Texture"))
or type.assignable_to( or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable"))
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"))
or type.assignable_to(
self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction")
)
or type.assignable_to(
self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger")
)
): ):
return "", escape_quote(value) self.print(f"{name}: \"{escape_quote(value)}\";")
elif value == self.template_class: elif type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("GObject.Object")):
return "", "template" self.print(f"{name}: {value};")
elif type.assignable_to(
self.gir.namespaces["Gtk"].lookup_type("GObject.Object")
) or isinstance(type, Interface):
return "", ("null" if value == "" else value)
elif isinstance(type, Bitfield):
flags = [get_enum_name(flag) for flag in value.split("|")]
return "", " | ".join(flags)
elif isinstance(type, Enumeration): elif isinstance(type, Enumeration):
return "", get_enum_name(value) for member in type.members.values():
elif isinstance(type, TypeType): if member.nick == value or member.c_ident == value:
if t := self.type_by_cname(value): self.print(f"{name}: {member.name};")
return "", f"typeof<{full_name(t)}>" break
else: else:
return "", f"typeof<${value}>" self.print(f"{name}: {value.replace('-', '_')};")
elif isinstance(type, Bitfield):
flags = re.sub(r"\s*\|\s*", " | ", value).replace("-", "_")
self.print(f"{name}: {flags};")
else: else:
return "", escape_quote(value) self.print(f"{name}: \"{escape_quote(value)}\";")
def decompile_element( def _decompile_element(ctx: DecompileCtx, gir, xml):
ctx: DecompileCtx, gir: T.Optional[GirContext], xml: Element
) -> None:
try: try:
decompilers = [d for d in _DECOMPILERS[xml.tag] if d._filter(ctx)] decompiler = _DECOMPILERS.get(xml.tag)
if len(decompilers) == 0: if decompiler is None:
raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>") raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>")
decompiler = decompilers[0] 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
if decompiler._element:
args = [ctx, gir, xml]
kwargs: T.Dict[str, T.Optional[str]] = {}
else:
args = [ctx, gir]
kwargs = {canon(name): value for name, value in xml.attrs.items()}
if decompiler._cdata:
if len(xml.children):
kwargs["cdata"] = None
else:
kwargs["cdata"] = xml.cdata
ctx._node_stack.append(xml)
ctx.start_block() ctx.start_block()
gir = decompiler(ctx, gir, **args)
try: for child_type in xml.children.values():
gir = decompiler(*args, **kwargs) for child in child_type:
except TypeError as e: _decompile_element(ctx, gir, child)
raise UnsupportedError(tag=xml.tag)
if not decompiler._skip_children:
for child in xml.children:
decompile_element(ctx, gir, child)
ctx.end_block() ctx.end_block()
ctx._node_stack.pop()
except UnsupportedError as e: except UnsupportedError as e:
raise e raise e
except TypeError as e:
raise UnsupportedError(tag=xml.tag)
def decompile(data: str) -> str: def decompile(data):
ctx = DecompileCtx() ctx = DecompileCtx()
xml = parse(data) xml = parse(data)
decompile_element(ctx, None, xml) _decompile_element(ctx, None, xml)
return ctx.result return ctx.result
def decompile_string(data: str) -> str:
ctx = DecompileCtx()
xml = parse_string(data)
decompile_element(ctx, None, xml)
return ctx.result
def canon(string: str) -> str: def canon(string: str) -> str:
if string == "class": if string == "class":
@ -296,61 +199,37 @@ def canon(string: str) -> str:
else: else:
return string.replace("-", "_").lower() return string.replace("-", "_").lower()
def truthy(string: str) -> bool: def truthy(string: str) -> bool:
return string is not None and string.lower() in ["yes", "true", "t", "y", "1"] return string.lower() in ["yes", "true", "t", "y", "1"]
def full_name(gir):
def full_name(gir: GirType) -> str:
return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name
def lookup_by_cname(gir, cname: str):
def lookup_by_cname(gir, cname: str) -> T.Optional[GirType]:
if isinstance(gir, GirContext): if isinstance(gir, GirContext):
return gir.get_type_by_cname(cname) return gir.get_type_by_cname(cname)
else: else:
return gir.get_containing(Repository).get_type_by_cname(cname) return gir.get_containing(Repository).get_type_by_cname(cname)
def decompiler( def decompiler(tag, cdata=False):
tag,
cdata=False,
parent_type: T.Optional[str] = None,
parent_tag: T.Optional[str] = None,
skip_children=False,
element=False,
):
def decorator(func): def decorator(func):
func._cdata = cdata func._cdata = cdata
func._skip_children = skip_children _DECOMPILERS[tag] = func
func._element = element
def filter(ctx):
if parent_type is not None:
if (
ctx.current_obj_type is None
or ctx.current_obj_type.full_name != parent_type
):
return False
if parent_tag is not None:
if not any(x.tag == parent_tag for x in ctx._node_stack):
return False
return True
func._filter = filter
_DECOMPILERS[tag].append(func)
return func return func
return decorator return decorator
def escape_quote(string: str) -> str:
return (string
.replace("\\", "\\\\")
.replace("\'", "\\'")
.replace("\"", "\\\"")
.replace("\n", "\\n"))
@decompiler("interface") @decompiler("interface")
def decompile_interface(ctx, gir, domain=None): def decompile_interface(ctx, gir):
if domain is not None:
ctx.print(f"translation-domain {escape_quote(domain)};")
return gir return gir
@ -364,43 +243,14 @@ def decompile_placeholder(ctx, gir):
pass pass
def decompile_translatable(
string: str,
translatable: T.Optional[str],
context: T.Optional[str],
comments: T.Optional[str],
) -> T.Tuple[str, str]:
if translatable is not None and truthy(translatable):
if comments is None:
comments = ""
else:
comments = comments.replace("/*", " ").replace("*/", " ")
comments = f"/* Translators: {comments} */"
if context is not None:
return comments, f"C_({escape_quote(context)}, {escape_quote(string)})"
else:
return comments, f"_({escape_quote(string)})"
else:
return "", f"{escape_quote(string)}"
@decompiler("property", cdata=True) @decompiler("property", cdata=True)
def decompile_property( def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=None, bind_flags=None, translatable="false", comments=None, context=None):
ctx: DecompileCtx,
gir,
name,
cdata,
bind_source=None,
bind_property=None,
bind_flags=None,
translatable="false",
comments=None,
context=None,
):
name = name.replace("_", "-") name = name.replace("_", "-")
if comments is not None:
ctx.print(f"/* Translators: {comments} */")
if cdata is None: if cdata is None:
ctx.print(f"{name}: ") ctx.print(f"{name}: ", False)
ctx.end_block_with(";") ctx.end_block_with(";")
elif bind_source: elif bind_source:
flags = "" flags = ""
@ -411,50 +261,21 @@ def decompile_property(
flags += " inverted" flags += " inverted"
if "bidirectional" in bind_flags: if "bidirectional" in bind_flags:
flags += " bidirectional" flags += " bidirectional"
if bind_source == ctx.template_class:
bind_source = "template"
ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};") ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};")
elif truthy(translatable): elif truthy(translatable):
comments, translatable = decompile_translatable( if context is not None:
cdata, translatable, context, comments ctx.print(f"{name}: C_(\"{escape_quote(context)}\", \"{escape_quote(cdata)}\");")
) else:
if comments is not None: ctx.print(f"{name}: _(\"{escape_quote(cdata)}\");")
ctx.print(comments)
ctx.print(f"{name}: {translatable};")
elif gir is None or gir.properties.get(name) is None: elif gir is None or gir.properties.get(name) is None:
ctx.print(f"{name}: {escape_quote(cdata)};") ctx.print(f"{name}: \"{escape_quote(cdata)}\";")
elif (
gir.assignable_to(ctx.gir.get_class("BuilderListItemFactory", "Gtk"))
and name == "bytes"
):
sub_ctx = DecompileCtx(ctx.gir)
xml = parse_string(cdata)
decompile_element(sub_ctx, None, xml)
ctx.print(sub_ctx.result)
else: else:
_, string = ctx.decompile_value(cdata, gir.properties.get(name).type) ctx.print_attribute(name, cdata, gir.properties.get(name).type)
ctx.print(f"{name}: {string};")
return gir return gir
@decompiler("attribute", cdata=True) @decompiler("attribute", cdata=True)
def decompile_attribute( def decompile_attribute(ctx, gir, name, cdata, translatable="false", comments=None, context=None):
ctx, gir, name, cdata, translatable="false", comments=None, context=None decompile_property(ctx, gir, name, cdata, translatable=translatable, comments=comments, context=context)
):
decompile_property(
ctx,
gir,
name,
cdata,
translatable=translatable,
comments=comments,
context=context,
)
@decompiler("attributes") @decompiler("attributes")
def decompile_attributes(ctx, gir): def decompile_attributes(ctx, gir):
@ -471,7 +292,5 @@ class UnsupportedError(Exception):
print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}") print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}")
if self.tag: if self.tag:
print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}") print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}")
print( print(f"""{Colors.FAINT}The compiler might support this feature, but the porting tool does not. You
f"""{Colors.FAINT}The compiler might support this feature, but the porting tool does not. You probably need to port this file manually.{Colors.CLEAR}\n""")
probably need to port this file manually.{Colors.CLEAR}\n"""
)

View file

@ -17,50 +17,39 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import sys
import traceback
import typing as T
from dataclasses import dataclass from dataclasses import dataclass
import typing as T
import sys, traceback
from . import utils from . import utils
from .tokenizer import Range
from .utils import Colors from .utils import Colors
class PrintableError(Exception): class PrintableError(Exception):
"""Parent class for errors that can be pretty-printed for the user, e.g. """ Parent class for errors that can be pretty-printed for the user, e.g.
compilation warnings and errors.""" compilation warnings and errors. """
def pretty_print(self, filename, code, stream=sys.stdout): def pretty_print(self, filename, code):
raise NotImplementedError() raise NotImplementedError()
@dataclass @dataclass
class ErrorReference: class ErrorReference:
range: Range start: int
end: int
message: str message: str
class CompileError(PrintableError): class CompileError(PrintableError):
"""A PrintableError with a start/end position and optional hints""" """ A PrintableError with a start/end position and optional hints """
category = "error" category = "error"
color = Colors.RED color = Colors.RED
def __init__( def __init__(self, message, start=None, end=None, did_you_mean=None, hints=None, actions=None, fatal=False, references=None):
self,
message: str,
range: T.Optional[Range] = None,
did_you_mean: T.Optional[T.Tuple[str, T.List[str]]] = None,
hints: T.Optional[T.List[str]] = None,
actions: T.Optional[T.List["CodeAction"]] = None,
fatal: bool = False,
references: T.Optional[T.List[ErrorReference]] = None,
) -> None:
super().__init__(message) super().__init__(message)
self.message = message self.message = message
self.range = range self.start = start
self.end = end
self.hints = hints or [] self.hints = hints or []
self.actions = actions or [] self.actions = actions or []
self.references = references or [] self.references = references or []
@ -69,11 +58,12 @@ class CompileError(PrintableError):
if did_you_mean is not None: if did_you_mean is not None:
self._did_you_mean(*did_you_mean) self._did_you_mean(*did_you_mean)
def hint(self, hint: str) -> "CompileError": def hint(self, hint: str):
self.hints.append(hint) self.hints.append(hint)
return self return self
def _did_you_mean(self, word: str, options: T.List[str]) -> None:
def _did_you_mean(self, word: str, options: T.List[str]):
if word.replace("_", "-") in options: if word.replace("_", "-") in options:
self.hint(f"use '-', not '_': `{word.replace('_', '-')}`") self.hint(f"use '-', not '_': `{word.replace('_', '-')}`")
return return
@ -89,65 +79,28 @@ class CompileError(PrintableError):
self.hint("Did you check your spelling?") self.hint("Did you check your spelling?")
self.hint("Are your dependencies up to date?") self.hint("Are your dependencies up to date?")
def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None: def pretty_print(self, filename, code, stream=sys.stdout):
assert self.range is not None line_num, col_num = utils.idx_to_pos(self.start + 1, code)
line = code.splitlines(True)[line_num]
line_num, col_num = utils.idx_to_pos(self.range.start + 1, code)
end_line_num, end_col_num = utils.idx_to_pos(self.range.end + 1, code)
line = code.splitlines(True)[line_num] if code != "" else ""
# Display 1-based line numbers # Display 1-based line numbers
line_num += 1 line_num += 1
end_line_num += 1
n_spaces = col_num - 1 stream.write(f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR}
n_carets = (
(end_col_num - col_num)
if line_num == end_line_num
else (len(line) - n_spaces - 1)
)
n_spaces += line.count("\t", 0, col_num)
n_carets += line.count("\t", col_num, col_num + n_carets)
line = line.replace("\t", " ")
stream.write(
f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR}
at {filename} line {line_num} column {col_num}: at {filename} line {line_num} column {col_num}:
{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*n_spaces}{"^"*n_carets}{Colors.CLEAR}\n""" {Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""")
)
for hint in self.hints: for hint in self.hints:
stream.write(f"{Colors.FAINT}hint: {hint}{Colors.CLEAR}\n") stream.write(f"{Colors.FAINT}hint: {hint}{Colors.CLEAR}\n")
for i, action in enumerate(self.actions):
old = (
action.edit_range.text
if action.edit_range is not None
else self.range.text
)
if old == "":
stream.write(
f"suggestion: insert {Colors.GREEN}{action.replace_with}{Colors.CLEAR}\n"
)
elif action.replace_with == "":
stream.write(f"suggestion: remove {Colors.RED}{old}{Colors.CLEAR}\n")
else:
stream.write(
f"suggestion: replace {Colors.RED}{old}{Colors.CLEAR} with {Colors.GREEN}{action.replace_with}{Colors.CLEAR}\n"
)
for ref in self.references: for ref in self.references:
line_num, col_num = utils.idx_to_pos(ref.range.start + 1, code) line_num, col_num = utils.idx_to_pos(ref.start + 1, code)
line = code.splitlines(True)[line_num] line = code.splitlines(True)[line_num]
line_num += 1 line_num += 1
stream.write( stream.write(f"""{Colors.FAINT}note: {ref.message}:
f"""{Colors.FAINT}note: {ref.message}:
at {filename} line {line_num} column {col_num}: at {filename} line {line_num} column {col_num}:
{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""" {Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""")
)
stream.write("\n") stream.write("\n")
@ -157,70 +110,50 @@ class CompileWarning(CompileError):
color = Colors.YELLOW color = Colors.YELLOW
class DeprecatedWarning(CompileWarning):
pass
class UnusedWarning(CompileWarning):
pass
class UpgradeWarning(CompileWarning):
category = "upgrade"
color = Colors.PURPLE
class UnexpectedTokenError(CompileError): class UnexpectedTokenError(CompileError):
def __init__(self, range: Range) -> None: def __init__(self, start, end):
super().__init__("Unexpected tokens", range) super().__init__("Unexpected tokens", start, end)
@dataclass @dataclass
class CodeAction: class CodeAction:
title: str title: str
replace_with: str replace_with: str
edit_range: T.Optional[Range] = None
class MultipleErrors(PrintableError): class MultipleErrors(PrintableError):
"""If multiple errors occur during compilation, they can be collected into """ If multiple errors occur during compilation, they can be collected into
a list and re-thrown using the MultipleErrors exception. It will 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.""" pretty-print all of the errors and a count of how many errors there are. """
def __init__(self, errors: T.List[CompileError]) -> None: def __init__(self, errors: T.List[CompileError]):
super().__init__() super().__init__()
self.errors = errors self.errors = errors
def pretty_print(self, filename, code, stream=sys.stdout) -> None: def pretty_print(self, filename, code) -> None:
for error in self.errors: for error in self.errors:
error.pretty_print(filename, code, stream) error.pretty_print(filename, code)
if len(self.errors) != 1: if len(self.errors) != 1:
print(f"{len(self.errors)} errors") print(f"{len(self.errors)} errors")
class CompilerBugError(Exception): class CompilerBugError(Exception):
"""Emitted on assertion errors""" """ Emitted on assertion errors """
def assert_true(truth: bool, message: T.Optional[str] = None): def assert_true(truth: bool, message:str=None):
if not truth: if not truth:
raise CompilerBugError(message) raise CompilerBugError(message)
def report_bug(): # pragma: no cover def report_bug(): # pragma: no cover
"""Report an error and ask people to report it.""" """ Report an error and ask people to report it. """
from . import main
print(traceback.format_exc()) print(traceback.format_exc())
print(f"Arguments: {sys.argv}") print(f"Arguments: {sys.argv}\n")
print(f"Version: {main.VERSION}\n") print(f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG *****
print(
f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG *****
The blueprint-compiler program has crashed. Please report the above stacktrace, The blueprint-compiler program has crashed. Please report the above stacktrace,
along with the input file(s) if possible, on GitLab: along with the input file(s) if possible, on GitLab:
{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues/new?issue {Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue
{Colors.CLEAR}""" {Colors.CLEAR}""")
)
sys.exit(1)

View file

@ -1,232 +0,0 @@
# formatter.py
#
# Copyright 2023 Gregor Niehl <gregorniehl@web.de>
#
# 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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import re
from enum import Enum
from . import tokenizer
from .errors import CompilerBugError
from .tokenizer import TokenType
OPENING_TOKENS = ("{", "[")
CLOSING_TOKENS = ("}", "]")
NEWLINE_AFTER = tuple(";") + OPENING_TOKENS + CLOSING_TOKENS
NO_WHITESPACE_BEFORE = (",", ":", "::", ";", ")", ".", ">", "]", "=")
NO_WHITESPACE_AFTER = ("C_", "_", "(", ".", "$", "<", "::", "[", "=")
# NO_WHITESPACE_BEFORE takes precedence over WHITESPACE_AFTER
WHITESPACE_AFTER = (":", ",", ">", ")", "|", "=>")
WHITESPACE_BEFORE = ("{", "|")
class LineType(Enum):
STATEMENT = 0
BLOCK_OPEN = 1
BLOCK_CLOSE = 2
CHILD_TYPE = 3
COMMENT = 4
def format(data, tab_size=2, insert_space=True):
indent_levels = 0
tokens = tokenizer.tokenize(data)
end_str = ""
last_not_whitespace = tokens[0]
current_line = ""
prev_line_type = None
is_child_type = False
indent_item = " " * tab_size if insert_space else "\t"
watch_parentheses = False
parentheses_balance = 0
bracket_tracker = [None]
last_whitespace_contains_newline = False
def commit_current_line(
line_type=prev_line_type, redo_whitespace=False, newlines_before=1
):
nonlocal end_str, current_line, prev_line_type
indent_whitespace = indent_levels * indent_item
whitespace_to_add = "\n" + indent_whitespace
if redo_whitespace or newlines_before != 1:
end_str = end_str.strip() + "\n" * newlines_before
if newlines_before > 0:
end_str += indent_whitespace
end_str += current_line + whitespace_to_add
current_line = ""
prev_line_type = line_type
for item in tokens:
str_item = str(item)
if item.type == TokenType.WHITESPACE:
last_whitespace_contains_newline = "\n" in str_item
continue
whitespace_required = (
str_item in WHITESPACE_BEFORE
or str(last_not_whitespace) in WHITESPACE_AFTER
or (str_item == "(" and end_str.endswith(": bind"))
)
whitespace_blockers = (
str_item in NO_WHITESPACE_BEFORE
or str(last_not_whitespace) in NO_WHITESPACE_AFTER
or (str_item == "<" and str(last_not_whitespace) == "typeof")
)
this_or_last_is_ident = TokenType.IDENT in (item.type, last_not_whitespace.type)
current_line_is_empty = len(current_line) == 0
is_function = str_item == "(" and not re.match(
r"^([A-Za-z_\-])+(: bind)?$", current_line
)
any_blockers = whitespace_blockers or current_line_is_empty or is_function
if (whitespace_required or this_or_last_is_ident) and not any_blockers:
current_line += " "
current_line += str_item
if str_item in ("[", "("):
bracket_tracker.append(str_item)
elif str_item in ("]", ")"):
bracket_tracker.pop()
needs_newline_treatment = (
str_item in NEWLINE_AFTER or item.type == TokenType.COMMENT
)
if needs_newline_treatment:
if str_item in OPENING_TOKENS:
list_or_child_type = str_item == "["
if list_or_child_type:
is_child_type = current_line.startswith("[")
if is_child_type:
if str(last_not_whitespace) not in OPENING_TOKENS:
end_str = (
end_str.strip() + "\n\n" + (indent_item * indent_levels)
)
last_not_whitespace = item
continue
indent_levels += 1
keep_same_indent = prev_line_type not in (
LineType.CHILD_TYPE,
LineType.COMMENT,
LineType.BLOCK_OPEN,
)
if keep_same_indent:
end_str = (
end_str.strip() + "\n\n" + indent_item * (indent_levels - 1)
)
commit_current_line(LineType.BLOCK_OPEN)
elif str_item == "]" and is_child_type:
commit_current_line(LineType.CHILD_TYPE, False)
is_child_type = False
elif str_item in CLOSING_TOKENS:
if str_item == "]" and str(last_not_whitespace) != "[":
current_line = current_line[:-1]
if str(last_not_whitespace) != ",":
current_line += ","
commit_current_line()
current_line = "]"
elif str(last_not_whitespace) in OPENING_TOKENS:
end_str = end_str.strip()
commit_current_line(LineType.BLOCK_CLOSE, True, 0)
indent_levels -= 1
commit_current_line(LineType.BLOCK_CLOSE, True)
elif str_item == ";":
line_type = LineType.STATEMENT
newlines = 1
if len(current_line) == 1:
newlines = 0
line_type = LineType.BLOCK_CLOSE
elif prev_line_type == LineType.BLOCK_CLOSE:
newlines = 2
commit_current_line(line_type, newlines_before=newlines)
elif item.type == TokenType.COMMENT:
require_extra_newline = (
LineType.BLOCK_CLOSE,
LineType.STATEMENT,
LineType.COMMENT,
)
single_line_comment = str_item.startswith("//")
newlines = 1
if single_line_comment:
if not str_item.startswith("// "):
current_line = f"// {current_line[2:]}"
if not last_whitespace_contains_newline:
current_line = " " + current_line
newlines = 0
elif prev_line_type == LineType.BLOCK_CLOSE:
newlines = 2
elif prev_line_type in require_extra_newline:
newlines = 2
current_line = "\n".join(
[line.rstrip() for line in current_line.split("\n")]
)
commit_current_line(LineType.COMMENT, newlines_before=newlines)
else: # pragma: no cover
raise CompilerBugError()
elif str_item == "(" and (
re.match(r"^([A-Za-z_\-])+\s*\(", current_line) or watch_parentheses
):
watch_parentheses = True
parentheses_balance += 1
elif str_item == ")" and watch_parentheses:
parentheses_balance -= 1
all_parentheses_closed = parentheses_balance == 0
if all_parentheses_closed:
commit_current_line(
newlines_before=2 if prev_line_type == LineType.BLOCK_CLOSE else 1
)
watch_parentheses = False
tracker_is_empty = len(bracket_tracker) > 0
if tracker_is_empty:
last_in_tracker = bracket_tracker[-1]
is_list_comma = last_in_tracker == "[" and str_item == ","
if is_list_comma:
last_was_list_item = end_str.strip()[-1] not in ("[", ",")
if last_was_list_item:
end_str = end_str.strip()
commit_current_line()
last_not_whitespace = item
last_whitespace_contains_newline = False
return end_str.strip() + "\n"

File diff suppressed because it is too large Load diff

View file

@ -18,27 +18,25 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
import difflib import difflib
import os import os
import typing as T
from . import decompiler, parser, tokenizer from . import decompiler, tokenizer, parser
from .errors import CompilerBugError, MultipleErrors, PrintableError from .errors import MultipleErrors, PrintableError
from .outputs.xml import XmlOutput
from .utils import Colors from .utils import Colors
# A tool to interactively port projects to blueprints. # A tool to interactively port projects to blueprints.
class CouldNotPort: class CouldNotPort:
def __init__(self, message: str): def __init__(self, message):
self.message = message self.message = message
def change_suffix(f): def change_suffix(f):
return f.removesuffix(".ui") + ".blp" return f.removesuffix(".ui") + ".blp"
def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
if os.path.exists(out_file): if os.path.exists(out_file):
return CouldNotPort("already exists") return CouldNotPort("already exists")
@ -56,23 +54,19 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
if errors: if errors:
raise errors raise errors
if not ast: if len(ast.errors):
raise CompilerBugError() raise MultipleErrors(ast.errors)
output = XmlOutput() ast.generate()
output.emit(ast)
except PrintableError as e: except PrintableError as e:
e.pretty_print(out_file, decompiled) e.pretty_print(out_file, decompiled)
print( print(f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}")
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"in {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE}")
print( print(
f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the 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: 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/GNOME/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""" {Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""")
)
return CouldNotPort("does not compile") return CouldNotPort("does not compile")
@ -88,12 +82,10 @@ def listdir_recursive(subdir):
for file in files: for file in files:
if file in ["_build", "build"]: if file in ["_build", "build"]:
continue continue
if file.startswith("."):
continue
full = os.path.join(subdir, file) full = os.path.join(subdir, file)
if full == "./subprojects": if full == "./subprojects":
# skip the subprojects directory # skip the subprojects directory
continue return
if os.path.isfile(full): if os.path.isfile(full):
yield full yield full
elif os.path.isdir(full): elif os.path.isdir(full):
@ -114,9 +106,7 @@ def enter():
def step1(): def step1():
print( print(f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}")
f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}"
)
if os.path.exists("subprojects/blueprint-compiler.wrap"): if os.path.exists("subprojects/blueprint-compiler.wrap"):
print("subprojects/blueprint-compiler.wrap already exists, skipping\n") print("subprojects/blueprint-compiler.wrap already exists, skipping\n")
@ -129,20 +119,17 @@ def step1():
pass pass
from .main import VERSION from .main import VERSION
VERSION = "main" if VERSION == "uninstalled" else "v" + VERSION VERSION = "main" if VERSION == "uninstalled" else "v" + VERSION
with open("subprojects/blueprint-compiler.wrap", "w") as wrap: with open("subprojects/blueprint-compiler.wrap", "w") as wrap:
wrap.write( wrap.write(f"""[wrap-git]
f"""[wrap-git]
directory = blueprint-compiler directory = blueprint-compiler
url = https://gitlab.gnome.org/GNOME/blueprint-compiler.git url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git
revision = {VERSION} revision = {VERSION}
depth = 1 depth = 1
[provide] [provide]
program_names = blueprint-compiler""" program_names = blueprint-compiler""")
)
print() print()
@ -157,9 +144,7 @@ def step2():
if yesno("Add '/subprojects/blueprint-compiler' to .gitignore?"): if yesno("Add '/subprojects/blueprint-compiler' to .gitignore?"):
gitignore.write("\n/subprojects/blueprint-compiler\n") gitignore.write("\n/subprojects/blueprint-compiler\n")
else: else:
print( print("'/subprojects/blueprint-compiler' already in .gitignore, skipping")
"'/subprojects/blueprint-compiler' already in .gitignore, skipping"
)
else: else:
if yesno("Create .gitignore with '/subprojects/blueprint-compiler'?"): if yesno("Create .gitignore with '/subprojects/blueprint-compiler'?"):
with open(".gitignore", "w") as gitignore: with open(".gitignore", "w") as gitignore:
@ -182,13 +167,9 @@ def step3():
if isinstance(result, CouldNotPort): if isinstance(result, CouldNotPort):
if result.message == "already exists": if result.message == "already exists":
print(Colors.FAINT, end="") print(Colors.FAINT, end="")
print( 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}")
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: else:
print( print(f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}")
f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}"
)
success += 1 success += 1
print() print()
@ -197,9 +178,7 @@ def step3():
elif success == len(files): elif success == len(files):
print(f"{Colors.GREEN}All files were converted.{Colors.CLEAR}") print(f"{Colors.GREEN}All files were converted.{Colors.CLEAR}")
elif success > 0: elif success > 0:
print( print(f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}")
f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}"
)
else: else:
print(f"{Colors.RED}None of the files could be converted.{Colors.CLEAR}") print(f"{Colors.RED}None of the files could be converted.{Colors.CLEAR}")
@ -223,33 +202,22 @@ def step3():
def step4(ported): def step4(ported):
print(f"{Colors.BOLD}STEP 4: Set up meson.build{Colors.CLEAR}") print(f"{Colors.BOLD}STEP 4: Set up meson.build{Colors.CLEAR}")
print( 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}")
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 = [ meson_files = [file for file in listdir_recursive(".") if os.path.basename(file) == "meson.build"]
file
for file in listdir_recursive(".")
if os.path.basename(file) == "meson.build"
]
for meson_file in meson_files: for meson_file in meson_files:
with open(meson_file, "r") as f: with open(meson_file, "r") as f:
if "gnome.compile_resources" in f.read(): if "gnome.compile_resources" in f.read():
parent = os.path.dirname(meson_file) parent = os.path.dirname(meson_file)
file_list = "\n ".join( file_list = "\n ".join([
[ f"'{os.path.relpath(file, parent)}',"
f"'{os.path.relpath(file, parent)}'," for file in ported
for file in ported if file.startswith(parent)
if file.startswith(parent) ])
]
)
if len(file_list): if len(file_list):
print( print(f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}")
f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}" print(f"""
)
print(
f"""
blueprints = custom_target('blueprints', blueprints = custom_target('blueprints',
input: files( input: files(
{file_list} {file_list}
@ -257,17 +225,14 @@ blueprints = custom_target('blueprints',
output: '.', output: '.',
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
) )
""" """)
)
enter() enter()
print( print(f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()'
f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()'
arguments in {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR} arguments in {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}
dependencies: blueprints, dependencies: blueprints,
""" """)
)
enter() enter()
print() print()
@ -277,9 +242,7 @@ def step5(in_files):
print(f"{Colors.BOLD}STEP 5: Update POTFILES.in{Colors.CLEAR}") print(f"{Colors.BOLD}STEP 5: Update POTFILES.in{Colors.CLEAR}")
if not os.path.exists("po/POTFILES.in"): if not os.path.exists("po/POTFILES.in"):
print( print(f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n")
f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n"
)
return return
with open("po/POTFILES.in", "r") as potfiles: with open("po/POTFILES.in", "r") as potfiles:
@ -292,22 +255,12 @@ def step5(in_files):
new_data = "".join(lines) new_data = "".join(lines)
print(f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}")
print( print(
f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}" "".join([
) (Colors.GREEN if line.startswith('+') else Colors.RED + Colors.FAINT if line.startswith('-') else '') + line + Colors.CLEAR
print( for line in difflib.unified_diff(old_lines, lines)
"".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?"): if yesno("Is this ok?"):
@ -336,6 +289,5 @@ def run(opts):
step5(in_files) step5(in_files)
step6(in_files) step6(in_files)
print( print(f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}")
f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}"
)

View file

@ -1,78 +1,49 @@
from .adw_breakpoint import ( """ Contains all the syntax beyond basic objects, properties, signal, and
AdwBreakpointCondition, templates. """
AdwBreakpointSetter,
AdwBreakpointSetters, from .attributes import BaseAttribute, BaseTypedAttribute
)
from .adw_response_dialog import ExtAdwResponseDialog
from .binding import Binding
from .common import *
from .contexts import ScopeCtx, ValueTypeCtx
from .expression import (
CastExpr,
ClosureArg,
ClosureExpr,
ExprBase,
Expression,
LiteralExpr,
LookupOp,
)
from .gobject_object import Object, ObjectContent from .gobject_object import Object, ObjectContent
from .gobject_property import Property from .gobject_property import Property
from .gobject_signal import Signal from .gobject_signal import Signal
from .gtk_a11y import A11yProperty, ExtAccessibility from .gtk_a11y import A11y
from .gtk_combo_box_text import ExtComboBoxItems from .gtk_combo_box_text import Items
from .gtk_file_filter import ( from .gtk_file_filter import mime_types, patterns, suffixes
Filters, from .gtk_layout import Layout
ext_file_filter_mime_types, from .gtk_menu import menu
ext_file_filter_patterns, from .gtk_size_group import Widgets
ext_file_filter_suffixes, from .gtk_string_list import Strings
) from .gtk_styles import Styles
from .gtk_layout import ExtLayout from .gtkbuilder_child import Child
from .gtk_list_item_factory import ExtListItemFactory
from .gtk_menu import Menu, MenuAttribute, menu
from .gtk_scale import ExtScaleMarks
from .gtk_size_group import ExtSizeGroupWidgets
from .gtk_string_list import ExtStringListStrings
from .gtk_styles import ExtStyles
from .gtkbuilder_child import Child, ChildExtension, ChildInternal, ChildType
from .gtkbuilder_template import Template from .gtkbuilder_template import Template
from .imports import GtkDirective, Import from .imports import GtkDirective, Import
from .types import ClassName
from .ui import UI from .ui import UI
from .values import ( from .values import IdentValue, TranslatedStringValue, FlagsValue, LiteralValue
ArrayValue,
ExprValue, from .common import *
Flag,
Flags, OBJECT_HOOKS.children = [
IdentLiteral, menu,
Literal, Object,
NumberLiteral, ]
ObjectValue,
QuotedLiteral,
StringValue,
Translated,
TypeLiteral,
Value,
)
OBJECT_CONTENT_HOOKS.children = [ OBJECT_CONTENT_HOOKS.children = [
Signal, Signal,
Property, Property,
AdwBreakpointCondition, A11y,
AdwBreakpointSetters, Styles,
ExtAccessibility, Layout,
ExtAdwResponseDialog, mime_types,
ExtComboBoxItems, patterns,
ext_file_filter_mime_types, suffixes,
ext_file_filter_patterns, Widgets,
ext_file_filter_suffixes, Items,
ExtLayout, Strings,
ExtListItemFactory,
ExtScaleMarks,
ExtSizeGroupWidgets,
ExtStringListStrings,
ExtStyles,
Child, Child,
] ]
LITERAL.children = [Literal] VALUE_HOOKS.children = [
TranslatedStringValue,
FlagsValue,
IdentValue,
LiteralValue,
]

View file

@ -1,254 +0,0 @@
# adw_breakpoint.py
#
# Copyright 2023 James Westman <james@jwestman.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .common import *
from .contexts import ScopeCtx, ValueTypeCtx
from .gobject_object import Object, validate_parent_type
from .values import Value
class AdwBreakpointCondition(AstNode):
grammar = [
UseExact("kw", "condition"),
"(",
UseQuoted("condition"),
Match(")").expected(),
]
@property
def condition(self) -> str:
return self.tokens["condition"]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"condition",
SymbolKind.Property,
self.range,
self.group.tokens["kw"].range,
self.condition,
)
@docs("kw")
def keyword_docs(self):
klass = self.root.gir.get_type("Breakpoint", "Adw")
if klass is None:
return None
prop = klass.properties.get("condition")
assert isinstance(prop, gir.Property)
return prop.doc
@validate()
def unique(self):
self.validate_unique_in_parent("Duplicate condition statement")
class AdwBreakpointSetter(AstNode):
grammar = Statement(
UseIdent("object"),
Match(".").expected(),
UseIdent("property"),
Match(":").expected(),
Value,
)
@property
def object_id(self) -> str:
return self.tokens["object"]
@property
def object(self) -> T.Optional[Object]:
return self.context[ScopeCtx].objects.get(self.object_id)
@property
def property_name(self) -> T.Optional[str]:
return self.tokens["property"]
@property
def value(self) -> T.Optional[Value]:
return self.children[Value][0] if len(self.children[Value]) > 0 else None
@property
def gir_class(self) -> T.Optional[GirType]:
if self.object is not None:
return self.object.gir_class
else:
return None
@property
def gir_property(self) -> T.Optional[gir.Property]:
if (
self.gir_class is not None
and not isinstance(self.gir_class, ExternType)
and self.property_name is not None
):
assert isinstance(self.gir_class, gir.Class) or isinstance(
self.gir_class, gir.TemplateType
)
return self.gir_class.properties.get(self.property_name)
else:
return None
@property
def document_symbol(self) -> T.Optional[DocumentSymbol]:
if self.value is None:
return None
return DocumentSymbol(
f"{self.object_id}.{self.property_name}",
SymbolKind.Property,
self.range,
self.group.tokens["object"].range,
self.value.range.text,
)
def get_reference(self, idx: int) -> T.Optional[LocationLink]:
if idx in self.group.tokens["object"].range:
if self.object is not None:
return LocationLink(
self.group.tokens["object"].range,
self.object.range,
self.object.ranges["id"],
)
return None
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
if self.gir_property is not None:
type = self.gir_property.type
else:
type = None
return ValueTypeCtx(type, allow_null=True)
@docs("object")
def object_docs(self):
if self.object is not None:
return f"```\n{self.object.signature}\n```"
else:
return None
@docs("property")
def property_docs(self):
if self.gir_property is not None:
return self.gir_property.doc
else:
return None
@validate("object")
def object_exists(self):
if self.object is None:
raise CompileError(
f"Could not find object with ID {self.object_id}",
did_you_mean=(self.object_id, self.context[ScopeCtx].objects.keys()),
)
@validate("property")
def property_exists(self):
if self.gir_class is None or self.gir_class.incomplete:
# Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself
return
if self.gir_property is None and self.property_name is not None:
raise CompileError(
f"Class {self.gir_class.full_name} does not have a property called {self.property_name}",
did_you_mean=(self.property_name, self.gir_class.properties.keys()),
)
@validate()
def unique(self):
self.validate_unique_in_parent(
f"Duplicate setter for {self.object_id}.{self.property_name}",
lambda x: x.object_id == self.object_id
and x.property_name == self.property_name,
)
class AdwBreakpointSetters(AstNode):
grammar = [
Keyword("setters"),
Match("{").expected(),
Until(AdwBreakpointSetter, "}"),
]
@property
def setters(self) -> T.List[AdwBreakpointSetter]:
return self.children[AdwBreakpointSetter]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"setters",
SymbolKind.Struct,
self.range,
self.group.tokens["setters"].range,
)
@validate()
def container_is_breakpoint(self):
validate_parent_type(self, "Adw", "Breakpoint", "breakpoint setters")
@validate()
def unique(self):
self.validate_unique_in_parent("Duplicate setters block")
@docs("setters")
def ref_docs(self):
return get_docs_section("Syntax ExtAdwBreakpoint")
@decompiler("condition", cdata=True)
def decompile_condition(ctx: DecompileCtx, gir, cdata):
ctx.print(f"condition({escape_quote(cdata)})")
@decompiler("setter", element=True)
def decompile_setter(ctx: DecompileCtx, gir, element):
assert ctx.parent_node is not None
# only run for the first setter
for child in ctx.parent_node.children:
if child.tag == "setter":
if child != element:
# already decompiled
return
else:
break
ctx.print("setters {")
for child in ctx.parent_node.children:
if child.tag == "setter":
object_id = child["object"]
property_name = child["property"]
obj = ctx.find_object(object_id)
if obj is not None:
gir_class = ctx.type_by_cname(obj["class"])
else:
gir_class = None
if object_id == ctx.template_class:
object_id = "template"
comments, string = ctx.decompile_value(
child.cdata,
gir_class,
(child["translatable"], child["context"], child["comments"]),
)
ctx.print(f"{comments} {object_id}.{property_name}: {string};")

View file

@ -1,192 +0,0 @@
# adw_response_dialog.py
#
# Copyright 2023 James Westman <james@jwestman.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from ..decompiler import decompile_translatable, truthy
from .common import *
from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue
class ExtAdwResponseDialogFlag(AstNode):
grammar = AnyOf(
UseExact("flag", "destructive"),
UseExact("flag", "suggested"),
UseExact("flag", "disabled"),
)
@property
def flag(self) -> str:
return self.tokens["flag"]
@validate()
def unique(self):
self.validate_unique_in_parent(
f"Duplicate '{self.flag}' flag", check=lambda child: child.flag == self.flag
)
@validate()
def exclusive(self):
if self.flag in ["destructive", "suggested"]:
self.validate_unique_in_parent(
"'suggested' and 'destructive' are exclusive",
check=lambda child: child.flag in ["destructive", "suggested"],
)
class ExtAdwResponseDialogResponse(AstNode):
grammar = [
UseIdent("id"),
Match(":").expected(),
to_parse_node(StringValue).expected("a string or translatable string"),
ZeroOrMore(ExtAdwResponseDialogFlag),
]
@property
def id(self) -> str:
return self.tokens["id"]
@property
def flags(self) -> T.List[ExtAdwResponseDialogFlag]:
return self.children[ExtAdwResponseDialogFlag]
@property
def appearance(self) -> T.Optional[str]:
if any(flag.flag == "destructive" for flag in self.flags):
return "destructive"
elif any(flag.flag == "suggested" for flag in self.flags):
return "suggested"
else:
return None
@property
def enabled(self) -> bool:
return not any(flag.flag == "disabled" for flag in self.flags)
@property
def value(self) -> StringValue:
return self.children[0]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.id,
SymbolKind.Field,
self.range,
self.group.tokens["id"].range,
self.value.range.text,
)
@validate("id")
def unique_in_parent(self):
self.validate_unique_in_parent(
f"Duplicate response ID '{self.id}'",
check=lambda child: child.id == self.id,
)
class ExtAdwResponseDialog(AstNode):
grammar = [
Keyword("responses"),
Match("[").expected(),
Delimited(ExtAdwResponseDialogResponse, ","),
"]",
]
@property
def responses(self) -> T.List[ExtAdwResponseDialogResponse]:
return self.children
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"responses",
SymbolKind.Array,
self.range,
self.group.tokens["responses"].range,
)
@validate("responses")
def container_is_message_dialog_or_alert_dialog(self):
try:
validate_parent_type(self, "Adw", "MessageDialog", "responses")
except:
validate_parent_type(self, "Adw", "AlertDialog", "responses")
@validate("responses")
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate responses block")
@docs()
def ref_docs(self):
return get_docs_section("Syntax ExtAdwMessageDialog")
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Adw", "MessageDialog"),
matches=new_statement_patterns,
)
def complete_adw_message_dialog(lsp, ast_node, match_variables):
yield Completion(
"responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]"
)
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Adw", "AlertDialog"),
matches=new_statement_patterns,
)
def complete_adw_alert_dialog(lsp, ast_node, match_variables):
yield Completion(
"responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]"
)
@decompiler("responses")
def decompile_responses(ctx, gir):
ctx.print(f"responses [")
@decompiler("response", cdata=True)
def decompile_response(
ctx,
gir,
cdata,
id,
appearance=None,
enabled=None,
translatable=None,
context=None,
comments=None,
):
comments, translated = decompile_translatable(
cdata, translatable, context, comments
)
if comments is not None:
ctx.print(comments)
flags = ""
if appearance is not None:
flags += f" {appearance}"
if enabled is not None and not truthy(enabled):
flags += " disabled"
ctx.print(f"{id}: {translated}{flags},")

View file

@ -0,0 +1,49 @@
# attributes.py
#
# Copyright 2022 James Westman <james@jwestman.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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"
@property
def name(self):
return self.tokens["name"]
def emit_xml(self, xml: XmlEmitter):
value = self.children[Value][0]
attrs = { self.attr_name: self.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. """

View file

@ -1,123 +0,0 @@
# binding.py
#
# Copyright 2023 James Westman <james@jwestman.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from dataclasses import dataclass
from .common import *
from .expression import Expression, LiteralExpr, LookupOp
class BindingFlag(AstNode):
grammar = [
AnyOf(
UseExact("flag", "inverted"),
UseExact("flag", "bidirectional"),
UseExact("flag", "no-sync-create"),
UseExact("flag", "sync-create"),
)
]
@property
def flag(self) -> str:
return self.tokens["flag"]
@validate()
def sync_create(self):
if self.flag == "sync-create":
raise UpgradeWarning(
"'sync-create' is now the default. Use 'no-sync-create' if this is not wanted.",
actions=[CodeAction("remove 'sync-create'", "")],
)
@validate()
def unique(self):
self.validate_unique_in_parent(
f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag
)
@validate()
def flags_only_if_simple(self):
if self.parent.simple_binding is None:
raise CompileError(
"Only bindings with a single lookup can have flags",
)
@docs()
def ref_docs(self):
return get_docs_section("Syntax Binding")
class Binding(AstNode):
grammar = [
AnyOf(Keyword("bind"), UseExact("bind", "bind-property")),
Expression,
ZeroOrMore(BindingFlag),
]
@property
def expression(self) -> Expression:
return self.children[Expression][0]
@property
def flags(self) -> T.List[BindingFlag]:
return self.children[BindingFlag]
@property
def simple_binding(self) -> T.Optional["SimpleBinding"]:
if isinstance(self.expression.last, LookupOp):
if isinstance(self.expression.last.lhs, LiteralExpr):
from .values import IdentLiteral
if isinstance(self.expression.last.lhs.literal.value, IdentLiteral):
flags = [x.flag for x in self.flags]
return SimpleBinding(
self.expression.last.lhs.literal.value.ident,
self.expression.last.property_name,
no_sync_create="no-sync-create" in flags,
bidirectional="bidirectional" in flags,
inverted="inverted" in flags,
)
return None
@validate("bind")
def bind_property(self):
if self.tokens["bind"] == "bind-property":
raise UpgradeWarning(
"'bind-property' is no longer needed. Use 'bind' instead. (blueprint 0.8.2)",
actions=[CodeAction("use 'bind'", "bind")],
)
@docs("bind")
def ref_docs(self):
return get_docs_section("Syntax Binding")
@dataclass
class SimpleBinding:
source: str
property_name: str
no_sync_create: bool = False
bidirectional: bool = False
inverted: bool = False
@decompiler("binding")
def decompile_binding(ctx: DecompileCtx, gir: gir.GirContext, name: str):
ctx.end_block_with(";")
ctx.print(f"{name}: bind ")

View file

@ -18,46 +18,19 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .. import decompiler as decompile
from .. import gir from .. import gir
from ..ast_utils import AstNode, context, docs, validate from ..ast_utils import AstNode, validate, docs
from ..errors import CompileError, MultipleErrors
from ..completions_utils import * from ..completions_utils import *
from ..decompiler import ( from .. import decompiler as decompile
DecompileCtx, from ..decompiler import DecompileCtx, decompiler
decompile_translatable, from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration
decompiler, from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType
escape_quote,
truthy,
)
from ..errors import (
CodeAction,
CompileError,
CompileWarning,
DeprecatedWarning,
MultipleErrors,
UnusedWarning,
UpgradeWarning,
)
from ..gir import (
BoolType,
Enumeration,
ExternType,
FloatType,
GirType,
IntType,
StringType,
)
from ..lsp_utils import (
Completion,
CompletionItemKind,
DocumentSymbol,
LocationLink,
SemanticToken,
SemanticTokenType,
SymbolKind,
get_docs_section,
)
from ..parse_tree import * from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter
OBJECT_HOOKS = AnyOf()
OBJECT_CONTENT_HOOKS = AnyOf() OBJECT_CONTENT_HOOKS = AnyOf()
LITERAL = AnyOf() VALUE_HOOKS = AnyOf()

View file

@ -1,87 +0,0 @@
# contexts.py
#
# Copyright 2023 James Westman <james@jwestman.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from dataclasses import dataclass
from functools import cached_property
from .common import *
from .gobject_object import Object
from .gtkbuilder_template import Template
@dataclass
class ValueTypeCtx:
value_type: T.Optional[GirType]
allow_null: bool = False
must_infer_type: bool = False
@dataclass
class ScopeCtx:
node: AstNode
@cached_property
def template(self):
from .gtk_list_item_factory import ExtListItemFactory
from .ui import UI
if isinstance(self.node, UI):
return self.node.template
elif isinstance(self.node, ExtListItemFactory):
return self.node
@cached_property
def objects(self) -> T.Dict[str, Object]:
return {
obj.tokens["id"]: obj
for obj in self._iter_recursive(self.node)
if obj.tokens["id"] is not None
}
def validate_unique_ids(self) -> None:
from .gtk_list_item_factory import ExtListItemFactory
passed = {}
for obj in self._iter_recursive(self.node):
if obj.tokens["id"] is None:
continue
if obj.tokens["id"] in passed:
token = obj.group.tokens["id"]
if not isinstance(obj, Template) and not isinstance(
obj, ExtListItemFactory
):
raise CompileError(
f"Duplicate object ID '{obj.tokens['id']}'",
token.range,
)
passed[obj.tokens["id"]] = obj
def _iter_recursive(self, node: AstNode):
yield node
for child in node.children:
if child.context[ScopeCtx] is self:
yield from self._iter_recursive(child)
@dataclass
class ExprValueCtx:
"""Indicates that the context is an expression literal, where the
"item" keyword may be used."""

View file

@ -1,384 +0,0 @@
# expressions.py
#
# Copyright 2022 James Westman <james@jwestman.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from ..decompiler import decompile_element
from .common import *
from .contexts import ScopeCtx, ValueTypeCtx
from .types import TypeName
expr = Sequence()
class ExprBase(AstNode):
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
if rhs := self.rhs:
return rhs.context[ValueTypeCtx]
else:
return self.parent.context[ValueTypeCtx]
@property
def type(self) -> T.Optional[GirType]:
raise NotImplementedError()
@property
def rhs(self) -> T.Optional["ExprBase"]:
if isinstance(self.parent, Expression):
children = list(self.parent.children)
if children.index(self) + 1 < len(children):
return children[children.index(self) + 1]
else:
return self.parent.rhs
else:
return None
class Expression(ExprBase):
grammar = expr
@property
def last(self) -> ExprBase:
return self.children[-1]
@property
def type(self) -> T.Optional[GirType]:
return self.last.type
class InfixExpr(ExprBase):
@property
def lhs(self):
children = list(self.parent_by_type(Expression).children)
return children[children.index(self) - 1]
class LiteralExpr(ExprBase):
grammar = LITERAL
@property
def is_object(self) -> bool:
from .values import IdentLiteral
return isinstance(self.literal.value, IdentLiteral) and (
self.literal.value.ident in self.context[ScopeCtx].objects
or self.root.is_legacy_template(self.literal.value.ident)
)
@property
def is_this(self) -> bool:
from .values import IdentLiteral
return (
not self.is_object
and isinstance(self.literal.value, IdentLiteral)
and self.literal.value.ident == "item"
)
@property
def literal(self):
from .values import Literal
return self.children[Literal][0]
@property
def type(self) -> T.Optional[GirType]:
return self.literal.value.type
@validate()
def item_validations(self):
if self.is_this:
if not isinstance(self.rhs, CastExpr):
raise CompileError('"item" must be cast to its object type')
if not isinstance(self.rhs.rhs, LookupOp):
raise CompileError('"item" can only be used for looking up properties')
class LookupOp(InfixExpr):
grammar = [".", UseIdent("property")]
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(None, must_infer_type=True)
@property
def property_name(self) -> str:
return self.tokens["property"]
@property
def type(self) -> T.Optional[GirType]:
if isinstance(self.lhs.type, gir.Class) or isinstance(
self.lhs.type, gir.Interface
):
if property := self.lhs.type.properties.get(self.property_name):
return property.type
return None
@docs("property")
def property_docs(self):
if not (
isinstance(self.lhs.type, gir.Class)
or isinstance(self.lhs.type, gir.Interface)
):
return None
if property := self.lhs.type.properties.get(self.property_name):
return property.doc
@validate("property")
def property_exists(self):
if self.lhs.type is None:
# Literal values throw their own errors if the type isn't known
if isinstance(self.lhs, LiteralExpr):
return
raise CompileError(
f"Could not determine the type of the preceding expression",
hints=[
f"add a type cast so blueprint knows which type the property {self.property_name} belongs to"
],
)
if self.lhs.type.incomplete:
return
elif not isinstance(self.lhs.type, gir.Class) and not isinstance(
self.lhs.type, gir.Interface
):
raise CompileError(
f"Type {self.lhs.type.full_name} does not have properties"
)
elif self.lhs.type.properties.get(self.property_name) is None:
raise CompileError(
f"{self.lhs.type.full_name} does not have a property called {self.property_name}",
did_you_mean=(self.property_name, self.lhs.type.properties.keys()),
)
@validate("property")
def property_deprecated(self):
if self.lhs.type is None or not (
isinstance(self.lhs.type, gir.Class)
or isinstance(self.lhs.type, gir.Interface)
):
return
if property := self.lhs.type.properties.get(self.property_name):
if property.deprecated:
hints = []
if property.deprecated_doc:
hints.append(property.deprecated_doc)
raise DeprecatedWarning(
f"{property.signature} is deprecated",
hints=hints,
)
class CastExpr(InfixExpr):
grammar = [
Keyword("as"),
AnyOf(
["<", TypeName, Match(">").expected()],
[
UseExact("lparen", "("),
TypeName,
UseExact("rparen", ")").expected("')'"),
],
),
]
@context(ValueTypeCtx)
def value_type(self):
return ValueTypeCtx(self.type)
@property
def type(self) -> T.Optional[GirType]:
return self.children[TypeName][0].gir_type
@validate()
def cast_makes_sense(self):
if self.type is None or self.lhs.type is None:
return
if not self.type.assignable_to(self.lhs.type):
raise CompileError(
f"Invalid cast. No instance of {self.lhs.type.full_name} can be an instance of {self.type.full_name}."
)
@validate("lparen", "rparen")
def upgrade_to_angle_brackets(self):
if self.tokens["lparen"]:
raise UpgradeWarning(
"Use angle bracket syntax introduced in blueprint 0.8.0",
actions=[
CodeAction(
"Use <> instead of ()",
f"<{self.children[TypeName][0].as_string}>",
)
],
)
@docs("as")
def ref_docs(self):
return get_docs_section("Syntax CastExpression")
class ClosureArg(AstNode):
grammar = Expression
@property
def expr(self) -> Expression:
return self.children[Expression][0]
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(None)
class ClosureExpr(ExprBase):
grammar = [
Optional(["$", UseLiteral("extern", True)]),
UseIdent("name"),
"(",
Delimited(ClosureArg, ","),
")",
]
@property
def type(self) -> T.Optional[GirType]:
if isinstance(self.rhs, CastExpr):
return self.rhs.type
else:
return None
@property
def closure_name(self) -> str:
return self.tokens["name"]
@property
def args(self) -> T.List[ClosureArg]:
return self.children[ClosureArg]
@validate()
def cast_to_return_type(self):
if not isinstance(self.rhs, CastExpr):
raise CompileError(
"Closure expression must be cast to the closure's return type"
)
@validate()
def builtin_exists(self):
if not self.tokens["extern"]:
raise CompileError(f"{self.closure_name} is not a builtin function")
@docs("name")
def ref_docs(self):
return get_docs_section("Syntax ClosureExpression")
expr.children = [
AnyOf(ClosureExpr, LiteralExpr, ["(", Expression, ")"]),
ZeroOrMore(AnyOf(LookupOp, CastExpr)),
]
@decompiler("lookup", skip_children=True, cdata=True)
def decompile_lookup(
ctx: DecompileCtx,
gir: gir.GirContext,
cdata: str,
name: str,
type: T.Optional[str] = None,
):
if ctx.parent_node is not None and ctx.parent_node.tag == "property":
ctx.print("expr ")
if type is None:
type = ""
elif t := ctx.type_by_cname(type):
type = decompile.full_name(t)
else:
type = "$" + type
assert ctx.current_node is not None
constant = None
if len(ctx.current_node.children) == 0:
constant = cdata
elif (
len(ctx.current_node.children) == 1
and ctx.current_node.children[0].tag == "constant"
):
constant = ctx.current_node.children[0].cdata
if constant is not None:
if constant == ctx.template_class:
ctx.print("template." + name)
elif constant == "":
ctx.print(f"item as <{type}>.{name}")
else:
ctx.print(constant + "." + name)
return
else:
for child in ctx.current_node.children:
decompile.decompile_element(ctx, gir, child)
ctx.print(f" as <{type}>.{name}")
@decompiler("constant", cdata=True)
def decompile_constant(
ctx: DecompileCtx, gir: gir.GirContext, cdata: str, type: T.Optional[str] = None
):
if ctx.parent_node is not None and ctx.parent_node.tag == "property":
ctx.print("expr ")
if type is None:
if cdata == ctx.template_class:
ctx.print("template")
else:
ctx.print(cdata)
else:
_, string = ctx.decompile_value(cdata, ctx.type_by_cname(type))
ctx.print(string)
@decompiler("closure", skip_children=True)
def decompile_closure(ctx: DecompileCtx, gir: gir.GirContext, function: str, type: str):
if ctx.parent_node is not None and ctx.parent_node.tag == "property":
ctx.print("expr ")
if t := ctx.type_by_cname(type):
type = decompile.full_name(t)
else:
type = "$" + type
ctx.print(f"${function}(")
assert ctx.current_node is not None
for i, node in enumerate(ctx.current_node.children):
decompile_element(ctx, gir, node)
assert ctx.current_node is not None
if i < len(ctx.current_node.children) - 1:
ctx.print(", ")
ctx.end_block_with(f") as <{type}>")

View file

@ -21,25 +21,8 @@
import typing as T import typing as T
from functools import cached_property from functools import cached_property
from blueprintcompiler.errors import T
from blueprintcompiler.lsp_utils import DocumentSymbol
from .common import * from .common import *
from .response_id import ExtResponse from .response_id import ResponseId
from .types import ClassName, ConcreteClassName
RESERVED_IDS = {
"this",
"self",
"template",
"true",
"false",
"null",
"none",
"item",
"expr",
"typeof",
}
class ObjectContent(AstNode): class ObjectContent(AstNode):
@ -49,53 +32,59 @@ class ObjectContent(AstNode):
def gir_class(self): def gir_class(self):
return self.parent.gir_class return self.parent.gir_class
def emit_xml(self, xml: XmlEmitter):
for x in self.children:
x.emit_xml(xml)
class Object(AstNode): class Object(AstNode):
grammar: T.Any = [ grammar: T.Any = [
ConcreteClassName, class_name,
Optional(UseIdent("id")), Optional(UseIdent("id")),
ObjectContent, ObjectContent,
] ]
@property @validate("namespace")
def id(self) -> str: def gir_ns_exists(self):
return self.tokens["id"] 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"])
@validate("namespace", "class_name")
def not_abstract(self):
if self.gir_class is not None and self.gir_class.abstract:
raise CompileError(
f"{self.gir_class.full_name} can't be instantiated because it's abstract",
hints=[f"did you mean to use a subclass of {self.gir_class.full_name}?"]
)
@property @property
def class_name(self) -> ClassName: def gir_ns(self):
return self.children[ClassName][0] if not self.tokens["ignore_gir"]:
return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk")
@property @property
def content(self) -> ObjectContent: def gir_class(self):
return self.children[ObjectContent][0] if self.tokens["class_name"] and not self.tokens["ignore_gir"]:
return self.root.gir.get_class(self.tokens["class_name"], self.tokens["namespace"])
@property
def signature(self) -> str:
if self.id:
return f"{self.class_name.gir_type.full_name} {self.id}"
elif t := self.class_name.gir_type:
return f"{t.full_name}"
else:
return f"{self.class_name.as_string}"
@property @docs("namespace")
def document_symbol(self) -> T.Optional[DocumentSymbol]: def namespace_docs(self):
return DocumentSymbol( if ns := self.root.gir.namespaces.get(self.tokens["namespace"]):
self.class_name.as_string, return ns.doc
SymbolKind.Object,
self.range,
self.children[ClassName][0].range,
self.id,
)
@property
def gir_class(self) -> GirType: @docs("class_name")
if self.class_name is None: def class_docs(self):
raise CompilerBugError() if self.gir_class:
return self.class_name.gir_type return self.gir_class.doc
@cached_property @cached_property
def action_widgets(self) -> T.List[ExtResponse]: def action_widgets(self) -> T.List[ResponseId]:
"""Get list of widget's action widgets. """Get list of widget's action widgets.
Empty if object doesn't have action widgets. Empty if object doesn't have action widgets.
@ -104,36 +93,44 @@ class Object(AstNode):
return [ return [
child.response_id child.response_id
for child in self.content.children[Child] for child in self.children[ObjectContent][0].children[Child]
if child.response_id if child.response_id
] ]
@validate("id") def emit_xml(self, xml: XmlEmitter):
def object_id_not_reserved(self): from .gtkbuilder_child import Child
from .gtkbuilder_template import Template
if not isinstance(self, Template) and self.id in RESERVED_IDS: xml.start_tag("object", **{
raise CompileWarning(f"{self.id} may be a confusing object ID") "class": self.gir_class or 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): def validate_parent_type(node, ns: str, name: str, err_msg: str):
parent = node.root.gir.get_type(name, ns) parent = node.root.gir.get_type(name, ns)
container_type = node.parent_by_type(Object).gir_class container_type = node.parent_by_type(Object).gir_class
if container_type and not container_type.assignable_to(parent): if container_type and not container_type.assignable_to(parent):
raise CompileError( raise CompileError(f"{container_type.full_name} is not a {parent.full_name}, so it doesn't have {err_msg}")
f"{container_type.full_name} is not a {ns}.{name}, so it doesn't have {err_msg}"
)
@decompiler("object") @decompiler("object")
def decompile_object(ctx: DecompileCtx, gir, klass, id=None): def decompile_object(ctx, gir, klass, id=None):
gir_class = ctx.type_by_cname(klass) gir_class = ctx.type_by_cname(klass)
klass_name = ( klass_name = decompile.full_name(gir_class) if gir_class is not None else "." + klass
decompile.full_name(gir_class) if gir_class is not None else "$" + klass
)
if id is None: if id is None:
ctx.print(f"{klass_name} {{") ctx.print(f"{klass_name} {{")
else: else:
ctx.print(f"{klass_name} {id} {{") ctx.print(f"{klass_name} {id} {{")
ctx.push_obj_type(gir_class)
return gir_class return gir_class

View file

@ -18,83 +18,79 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .binding import Binding from .gobject_object import Object
from .gtkbuilder_template import Template
from .values import Value, TranslatedStringValue
from .common import * from .common import *
from .contexts import ValueTypeCtx
from .values import ArrayValue, ExprValue, ObjectValue, Value
class Property(AstNode): class Property(AstNode):
grammar = Statement( grammar = AnyOf(
UseIdent("name"), ":", AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue) Statement(
UseIdent("name"),
":",
Keyword("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 name(self) -> str:
return self.tokens["name"]
@property
def value(self) -> T.Union[Binding, ExprValue, ObjectValue, Value, ArrayValue]:
return self.children[0]
@property @property
def gir_class(self): def gir_class(self):
return self.parent.parent.gir_class return self.parent.parent.gir_class
@property @property
def gir_property(self) -> T.Optional[gir.Property]: def gir_property(self):
if self.gir_class is not None and not isinstance(self.gir_class, ExternType): if self.gir_class is not None:
return self.gir_class.properties.get(self.tokens["name"]) return self.gir_class.properties.get(self.tokens["name"])
else:
return None
@property @property
def document_symbol(self) -> DocumentSymbol: def value_type(self):
if isinstance(self.value, ObjectValue) or self.value is None:
detail = None
else:
detail = self.value.range.text
return DocumentSymbol(
self.name,
SymbolKind.Property,
self.range,
self.group.tokens["name"].range,
detail,
)
@validate()
def binding_valid(self):
if (
isinstance(self.value, Binding)
and self.gir_property is not None
and self.gir_property.construct_only
):
raise CompileError(
f"{self.gir_property.full_name} can't be bound because it is construct-only",
hints=["construct-only properties may only be set to a static value"],
)
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
if self.gir_property is not None: if self.gir_property is not None:
type = self.gir_property.type return self.gir_property.type
else:
type = None
return ValueTypeCtx(type)
@validate("name") @validate("name")
def property_exists(self): def property_exists(self):
if self.gir_class is None or self.gir_class.incomplete: if self.gir_class is None:
# Objects that we have no gir data on should not be validated # Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself # This happens for classes defined by the app itself
return 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: if self.gir_property is None:
raise CompileError( raise CompileError(
f"Class {self.gir_class.full_name} does not have a property called {self.tokens['name']}", 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()), did_you_mean=(self.tokens["name"], self.gir_class.properties.keys())
)
@validate("bind")
def property_bindable(self):
if self.tokens["bind"] and self.gir_property is not None and self.gir_property.construct_only:
raise CompileError(
f"{self.gir_property.full_name} can't be bound because it is construct-only",
hints=["construct-only properties may only be set to a static value"]
) )
@validate("name") @validate("name")
@ -102,25 +98,64 @@ class Property(AstNode):
if self.gir_property is not None and not self.gir_property.writable: if self.gir_property is not None and not self.gir_property.writable:
raise CompileError(f"{self.gir_property.full_name} is not writable") raise CompileError(f"{self.gir_property.full_name} is not writable")
@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}"
)
@validate("name") @validate("name")
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent( self.validate_unique_in_parent(
f"Duplicate property '{self.tokens['name']}'", f"Duplicate property '{self.tokens['name']}'",
check=lambda child: child.tokens["name"] == self.tokens["name"], check=lambda child: child.tokens["name"] == self.tokens["name"]
) )
@validate("name")
def deprecated(self) -> None:
if self.gir_property is not None and self.gir_property.deprecated:
hints = []
if self.gir_property.deprecated_doc:
hints.append(self.gir_property.deprecated_doc)
raise DeprecatedWarning(
f"{self.gir_property.signature} is deprecated",
hints=hints,
)
@docs("name") @docs("name")
def property_docs(self): def property_docs(self):
if self.gir_property is not None: if self.gir_property is not None:
return self.gir_property.doc 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()

View file

@ -17,233 +17,97 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from .common import *
from .contexts import ScopeCtx
from .gtkbuilder_template import Template from .gtkbuilder_template import Template
from .common import *
class SignalFlag(AstNode):
grammar = AnyOf(
UseExact("flag", "swapped"),
UseExact("flag", "not-swapped"),
UseExact("flag", "after"),
)
@property
def flag(self) -> str:
return self.tokens["flag"]
@validate()
def unique(self):
self.validate_unique_in_parent(
f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag
)
@validate()
def swapped_exclusive(self):
if self.flag in ["swapped", "not-swapped"]:
self.validate_unique_in_parent(
"'swapped' and 'not-swapped' flags cannot be used together",
lambda x: x.flag in ["swapped", "not-swapped"],
)
@validate()
def swapped_unnecessary(self):
if self.flag == "not-swapped" and self.parent.object_id is None:
raise CompileWarning(
"'not-swapped' is the default for handlers that do not specify an object",
actions=[CodeAction("Remove 'not-swapped' flag", "")],
)
elif self.flag == "swapped" and self.parent.object_id is not None:
raise CompileWarning(
"'swapped' is the default for handlers that specify an object",
actions=[CodeAction("Remove 'swapped' flag", "")],
)
@docs()
def ref_docs(self):
return get_docs_section("Syntax Signal")
class Signal(AstNode): class Signal(AstNode):
grammar = Statement( grammar = Statement(
UseIdent("name"), UseIdent("name"),
Optional( Optional([
[ "::",
"::", UseIdent("detail_name").expected("a signal detail name"),
UseIdent("detail_name").expected("a signal detail name"), ]),
] "=>",
),
Keyword("=>"),
Mark("detail_start"),
Optional(["$", UseLiteral("extern", True)]),
UseIdent("handler").expected("the name of a function to handle the signal"), UseIdent("handler").expected("the name of a function to handle the signal"),
Match("(").expected("argument list"), Match("(").expected("argument list"),
Optional(UseIdent("object")).expected("object identifier"), Optional(UseIdent("object")).expected("object identifier"),
Match(")").expected(), Match(")").expected(),
ZeroOrMore(SignalFlag), ZeroOrMore(AnyOf(
Mark("detail_end"), [Keyword("swapped"), UseLiteral("swapped", True)],
[Keyword("after"), UseLiteral("after", True)],
)),
) )
@property
def name(self) -> str:
return self.tokens["name"]
@property @property
def detail_name(self) -> T.Optional[str]: def gir_signal(self):
return self.tokens["detail_name"] if self.gir_class is not None:
@property
def full_name(self) -> str:
if self.detail_name is None:
return self.name
else:
return self.name + "::" + self.detail_name
@property
def handler(self) -> str:
return self.tokens["handler"]
@property
def object_id(self) -> T.Optional[str]:
return self.tokens["object"]
@property
def flags(self) -> T.List[SignalFlag]:
return self.children[SignalFlag]
# Returns True if the "swapped" flag is present, False if "not-swapped" is present, and None if neither are present.
# GtkBuilder's default if swapped is not specified is to not swap the arguments if no object is specified, and to
# swap them if an object is specified.
@property
def is_swapped(self) -> T.Optional[bool]:
for flag in self.flags:
if flag.flag == "swapped":
return True
elif flag.flag == "not-swapped":
return False
return None
@property
def is_after(self) -> bool:
return any(x.flag == "after" for x in self.flags)
@property
def gir_signal(self) -> T.Optional[gir.Signal]:
if self.gir_class is not None and not isinstance(self.gir_class, ExternType):
return self.gir_class.signals.get(self.tokens["name"]) return self.gir_class.signals.get(self.tokens["name"])
else:
return None
@property @property
def gir_class(self): def gir_class(self):
return self.parent.parent.gir_class return self.parent.parent.gir_class
@property
def document_symbol(self) -> DocumentSymbol:
detail = self.ranges["detail_start", "detail_end"]
return DocumentSymbol(
self.full_name,
SymbolKind.Event,
self.range,
self.group.tokens["name"].range,
detail.text if detail is not None else None,
)
def get_reference(self, idx: int) -> T.Optional[LocationLink]:
if self.object_id is not None and idx in self.group.tokens["object"].range:
obj = self.context[ScopeCtx].objects.get(self.object_id)
if obj is not None:
return LocationLink(
self.group.tokens["object"].range, obj.range, obj.ranges["id"]
)
return None
@validate("handler")
def old_extern(self):
if not self.tokens["extern"]:
if self.handler is not None:
raise UpgradeWarning(
"Use the '$' extern syntax introduced in blueprint 0.8.0",
actions=[CodeAction("Use '$' syntax", "$" + self.handler)],
)
@validate("name") @validate("name")
def signal_exists(self): def signal_exists(self):
if self.gir_class is None or self.gir_class.incomplete: if self.gir_class is None:
# Objects that we have no gir data on should not be validated # Objects that we have no gir data on should not be validated
# This happens for classes defined by the app itself # This happens for classes defined by the app itself
return 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: if self.gir_signal is None:
raise CompileError( raise CompileError(
f"Class {self.gir_class.full_name} does not contain a signal called {self.tokens['name']}", 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()), did_you_mean=(self.tokens["name"], self.gir_class.signals.keys())
) )
@validate("object") @validate("object")
def object_exists(self): def object_exists(self):
object_id = self.tokens["object"] object_id = self.tokens["object"]
if object_id is None: if object_id is None:
return return
if self.context[ScopeCtx].objects.get(object_id) is None: if self.root.objects_by_id.get(object_id) is None:
raise CompileError(f"Could not find object with ID '{object_id}'") raise CompileError(
f"Could not find object with ID '{object_id}'"
@validate("name")
def deprecated(self) -> None:
if self.gir_signal is not None and self.gir_signal.deprecated:
hints = []
if self.gir_signal.deprecated_doc:
hints.append(self.gir_signal.deprecated_doc)
raise DeprecatedWarning(
f"{self.gir_signal.signature} is deprecated",
hints=hints,
) )
@docs("name") @docs("name")
def signal_docs(self): def signal_docs(self):
if self.gir_signal is not None: if self.gir_signal is not None:
return self.gir_signal.doc return self.gir_signal.doc
@docs("detail_name")
def detail_docs(self):
if self.name == "notify":
if self.gir_class is not None and not isinstance(
self.gir_class, ExternType
):
prop = self.gir_class.properties.get(self.tokens["detail_name"])
if prop is not None:
return prop.doc
@docs("=>") def emit_xml(self, xml: XmlEmitter):
def ref_docs(self): name = self.tokens["name"]
return get_docs_section("Syntax Signal") 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") @decompiler("signal")
def decompile_signal( def decompile_signal(ctx, gir, name, handler, swapped="false", object=None):
ctx: DecompileCtx, gir, name, handler, swapped=None, after="false", object=None
):
object_name = object or "" object_name = object or ""
if object_name == ctx.template_class:
object_name = "template"
name = name.replace("_", "-") name = name.replace("_", "-")
line = f"{name} => ${handler}({object_name})"
if decompile.truthy(swapped): if decompile.truthy(swapped):
line += " swapped" ctx.print(f"{name} => {handler}({object_name}) swapped;")
elif swapped is not None: else:
line += " not-swapped" ctx.print(f"{name} => {handler}({object_name});")
if decompile.truthy(after):
line += " after"
line += ";"
ctx.print(line)
return gir return gir

View file

@ -17,12 +17,10 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import ObjectContent, validate_parent_type from .gobject_object import ObjectContent, validate_parent_type
from .attributes import BaseTypedAttribute
from .values import Value from .values import Value
from .common import *
def get_property_types(gir): def get_property_types(gir):
@ -88,7 +86,6 @@ def get_state_types(gir):
"selected": BoolType(), "selected": BoolType(),
} }
def get_types(gir): def get_types(gir):
return { return {
**get_property_types(gir), **get_property_types(gir),
@ -96,19 +93,7 @@ def get_types(gir):
**get_state_types(gir), **get_state_types(gir),
} }
allow_duplicates = [
"controls",
"described-by",
"details",
"flow-to",
"labelled-by",
"owns",
]
def _get_docs(gir, name): def _get_docs(gir, name):
name = name.replace("-", "_")
if gir_type := ( if gir_type := (
gir.get_type("AccessibleProperty", "Gtk").members.get(name) gir.get_type("AccessibleProperty", "Gtk").members.get(name)
or gir.get_type("AccessibleRelation", "Gtk").members.get(name) or gir.get_type("AccessibleRelation", "Gtk").members.get(name)
@ -117,11 +102,11 @@ def _get_docs(gir, name):
return gir_type.doc return gir_type.doc
class A11yProperty(AstNode): class A11yProperty(BaseTypedAttribute):
grammar = Statement( grammar = Statement(
UseIdent("name"), UseIdent("name"),
":", ":",
AnyOf(Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"]), VALUE_HOOKS.expected("a value"),
) )
@property @property
@ -142,22 +127,8 @@ class A11yProperty(AstNode):
return self.tokens["name"].replace("_", "-") return self.tokens["name"].replace("_", "-")
@property @property
def values(self) -> T.List[Value]: def value_type(self) -> GirType:
return list(self.children) return get_types(self.root.gir).get(self.tokens["name"])
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(get_types(self.root.gir).get(self.tokens["name"]))
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.name,
SymbolKind.Field,
self.range,
self.group.tokens["name"].range,
", ".join(v.range.text for v in self.values),
)
@validate("name") @validate("name")
def is_valid_property(self): def is_valid_property(self):
@ -175,46 +146,19 @@ class A11yProperty(AstNode):
check=lambda child: child.tokens["name"] == self.tokens["name"], check=lambda child: child.tokens["name"] == self.tokens["name"],
) )
@validate("name")
def list_only_allowed_for_subset(self):
if self.tokens["list_form"] and self.tokens["name"] not in allow_duplicates:
raise CompileError(
f"'{self.tokens['name']}' does not allow a list of values",
)
@validate("name")
def list_non_empty(self):
if len(self.values) == 0:
raise CompileError(
f"'{self.tokens['name']}' may not be empty",
)
@docs("name") @docs("name")
def prop_docs(self): def prop_docs(self):
if self.tokens["name"] in get_types(self.root.gir): if self.tokens["name"] in get_types(self.root.gir):
return _get_docs(self.root.gir, self.tokens["name"]) return _get_docs(self.root.gir, self.tokens["name"])
class ExtAccessibility(AstNode): class A11y(AstNode):
grammar = [ grammar = [
Keyword("accessibility"), Keyword("accessibility"),
"{", "{",
Until(A11yProperty, "}"), Until(A11yProperty, "}"),
] ]
@property
def properties(self) -> T.List[A11yProperty]:
return self.children[A11yProperty]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"accessibility",
SymbolKind.Struct,
self.range,
self.group.tokens["accessibility"].range,
)
@validate("accessibility") @validate("accessibility")
def container_is_widget(self): def container_is_widget(self):
validate_parent_type(self, "Gtk", "Widget", "accessibility properties") validate_parent_type(self, "Gtk", "Widget", "accessibility properties")
@ -223,65 +167,44 @@ class ExtAccessibility(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate accessibility block") self.validate_unique_in_parent("Duplicate accessibility block")
@docs("accessibility") def emit_xml(self, xml: XmlEmitter):
def ref_docs(self): xml.start_tag("accessibility")
return get_docs_section("Syntax ExtAccessibility") for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def a11y_completer(lsp, ast_node, match_variables): def a11y_completer(ast_node, match_variables):
yield Completion( yield Completion(
"accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}" "accessibility", CompletionItemKind.Snippet,
snippet="accessibility {\n $0\n}"
) )
@completer( @completer(
applies_in=[ExtAccessibility], applies_in=[A11y],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def a11y_name_completer(lsp, ast_node, match_variables): def a11y_name_completer(ast_node, match_variables):
for name, type in get_types(ast_node.root.gir).items(): for name, type in get_types(ast_node.root.gir).items():
yield Completion( yield Completion(name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type))
name,
CompletionItemKind.Property,
docs=_get_docs(ast_node.root.gir, type.name),
)
@decompiler("accessibility", skip_children=True, element=True) @decompiler("relation", cdata=True)
def decompile_accessibility(ctx: DecompileCtx, _gir, element): 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 {") ctx.print("accessibility {")
already_printed = set()
types = get_types(ctx.gir)
for child in element.children:
name = child["name"]
if name in allow_duplicates:
if name in already_printed:
continue
ctx.print(f"{name}: [")
for value in element.children:
if value["name"] == name:
comments, string = ctx.decompile_value(
value.cdata,
types.get(value["name"]),
(value["translatable"], value["context"], value["comments"]),
)
ctx.print(f"{comments} {string},")
ctx.print("];")
else:
comments, string = ctx.decompile_value(
child.cdata,
types.get(child["name"]),
(child["translatable"], child["context"], child["comments"]),
)
ctx.print(f"{comments} {name}: {string};")
already_printed.add(name)
ctx.print("}")
ctx.end_block_with("")

View file

@ -18,64 +18,40 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .common import * from .attributes import BaseTypedAttribute
from .gobject_object import ObjectContent, validate_parent_type from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue from .common import *
class Item(AstNode): class Item(BaseTypedAttribute):
grammar = [ tag_name = "item"
Optional([UseIdent("name"), ":"]), attr_name = "id"
StringValue,
@property
def value_type(self):
return StringType()
item = Group(
Item,
[
Optional([
UseIdent("name"),
":",
]),
VALUE_HOOKS,
] ]
)
@property
def name(self) -> T.Optional[str]:
return self.tokens["name"]
@property
def value(self) -> StringValue:
return self.children[StringValue][0]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.value.range.text,
SymbolKind.String,
self.range,
self.value.range,
self.name,
)
@validate("name")
def unique_in_parent(self):
if self.name is not None:
self.validate_unique_in_parent(
f"Duplicate item '{self.name}'", lambda x: x.name == self.name
)
@docs("name")
def ref_docs(self):
return get_docs_section("Syntax ExtComboBoxItems")
class ExtComboBoxItems(AstNode): class Items(AstNode):
grammar = [ grammar = [
Keyword("items"), Keyword("items"),
"[", "[",
Delimited(Item, ","), Delimited(item, ","),
"]", "]",
] ]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"items",
SymbolKind.Array,
self.range,
self.group.tokens["items"].range,
)
@validate("items") @validate("items")
def container_is_combo_box_text(self): def container_is_combo_box_text(self):
validate_parent_type(self, "Gtk", "ComboBoxText", "combo box items") validate_parent_type(self, "Gtk", "ComboBoxText", "combo box items")
@ -84,9 +60,11 @@ class ExtComboBoxItems(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate items block") self.validate_unique_in_parent("Duplicate items block")
@docs("items") def emit_xml(self, xml: XmlEmitter):
def ref_docs(self): xml.start_tag("items")
return get_docs_section("Syntax ExtComboBoxItems") for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@completer( @completer(
@ -94,31 +72,8 @@ class ExtComboBoxItems(AstNode):
applies_in_subclass=("Gtk", "ComboBoxText"), applies_in_subclass=("Gtk", "ComboBoxText"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def items_completer(lsp, ast_node, match_variables): def items_completer(ast_node, match_variables):
yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]") yield Completion(
"items", CompletionItemKind.Snippet,
snippet="items [$0]"
@decompiler("items", parent_type="Gtk.ComboBoxText")
def decompile_items(ctx: DecompileCtx, gir: gir.GirContext):
ctx.print("items [")
@decompiler("item", parent_type="Gtk.ComboBoxText", cdata=True)
def decompile_item(
ctx: DecompileCtx,
gir: gir.GirContext,
cdata: str,
id: T.Optional[str] = None,
translatable="false",
comments=None,
context=None,
):
comments, translatable = decompile_translatable(
cdata, translatable, context, comments
) )
if comments:
ctx.print(comments)
if id:
ctx.print(f"{id}: ")
ctx.print(translatable)
ctx.print(",")

View file

@ -18,63 +18,47 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .common import *
from .gobject_object import ObjectContent, validate_parent_type from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class Filters(AstNode): class Filters(AstNode):
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.tokens["tag_name"],
SymbolKind.Array,
self.range,
self.group.tokens["tag_name"].range,
)
@validate() @validate()
def container_is_file_filter(self): def container_is_file_filter(self):
validate_parent_type(self, "Gtk", "FileFilter", "file filter properties") validate_parent_type(self, "Gtk", "FileFilter", "file filter properties")
@validate("tag_name") @validate()
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent( # The token argument to validate() needs to be calculated based on
f"Duplicate {self.tokens['tag_name']} block", # the instance, hence wrapping it like this.
check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"], @validate(self.tokens["tag_name"])
) def wrapped_validator(self):
self.validate_unique_in_parent(
f"Duplicate {self.tokens['tag_name']} block",
check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"],
)
wrapped_validator(self)
@docs("tag_name") def emit_xml(self, xml: XmlEmitter):
def ref_docs(self): xml.start_tag(self.tokens["tag_name"])
return get_docs_section("Syntax ExtFileFilter") for child in self.children:
child.emit_xml(xml)
xml.end_tag()
class FilterString(AstNode): class FilterString(AstNode):
@property def emit_xml(self, xml):
def item(self) -> str: xml.start_tag(self.tokens["tag_name"])
return self.tokens["name"] xml.put_text(self.tokens["name"])
xml.end_tag()
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.item,
SymbolKind.String,
self.range,
self.group.tokens["name"].range,
)
@validate()
def unique_in_parent(self):
self.validate_unique_in_parent(
f"Duplicate {self.tokens['tag_name']} '{self.item}'",
check=lambda child: child.item == self.item,
)
def create_node(tag_name: str, singular: str): def create_node(tag_name: str, singular: str):
return Group( return Group(
Filters, Filters,
[ [
UseExact("tag_name", tag_name), Keyword(tag_name),
UseLiteral("tag_name", tag_name),
"[", "[",
Delimited( Delimited(
Group( Group(
@ -82,18 +66,18 @@ def create_node(tag_name: str, singular: str):
[ [
UseQuoted("name"), UseQuoted("name"),
UseLiteral("tag_name", singular), UseLiteral("tag_name", singular),
], ]
), ),
",", ",",
), ),
"]", "]",
], ]
) )
ext_file_filter_mime_types = create_node("mime-types", "mime-type") mime_types = create_node("mime-types", "mime-type")
ext_file_filter_patterns = create_node("patterns", "pattern") patterns = create_node("patterns", "pattern")
ext_file_filter_suffixes = create_node("suffixes", "suffix") suffixes = create_node("suffixes", "suffix")
@completer( @completer(
@ -101,39 +85,32 @@ ext_file_filter_suffixes = create_node("suffixes", "suffix")
applies_in_subclass=("Gtk", "FileFilter"), applies_in_subclass=("Gtk", "FileFilter"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def file_filter_completer(lsp, ast_node, match_variables): def file_filter_completer(ast_node, match_variables):
yield Completion( yield Completion("mime-types", CompletionItemKind.Snippet, snippet="mime-types [\"$0\"]")
"mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]' yield Completion("patterns", CompletionItemKind.Snippet, snippet="patterns [\"$0\"]")
) yield Completion("suffixes", CompletionItemKind.Snippet, snippet="suffixes [\"$0\"]")
yield Completion("patterns", CompletionItemKind.Snippet, snippet='patterns ["$0"]')
yield Completion("suffixes", CompletionItemKind.Snippet, snippet='suffixes ["$0"]')
@decompiler("mime-types") @decompiler("mime-types")
def decompile_mime_types(ctx, gir): def decompile_mime_types(ctx, gir):
ctx.print("mime-types [") ctx.print("mime-types [")
@decompiler("mime-type", cdata=True) @decompiler("mime-type", cdata=True)
def decompile_mime_type(ctx, gir, cdata): def decompile_mime_type(ctx, gir, cdata):
ctx.print(f"{escape_quote(cdata)},") ctx.print(f'"{cdata}",')
@decompiler("patterns") @decompiler("patterns")
def decompile_patterns(ctx, gir): def decompile_patterns(ctx, gir):
ctx.print("patterns [") ctx.print("patterns [")
@decompiler("pattern", cdata=True) @decompiler("pattern", cdata=True)
def decompile_pattern(ctx, gir, cdata): def decompile_pattern(ctx, gir, cdata):
ctx.print(f"{escape_quote(cdata)},") ctx.print(f'"{cdata}",')
@decompiler("suffixes") @decompiler("suffixes")
def decompile_suffixes(ctx, gir): def decompile_suffixes(ctx, gir):
ctx.print("suffixes [") ctx.print("suffixes [")
@decompiler("suffix", cdata=True) @decompiler("suffix", cdata=True)
def decompile_suffix(ctx, gir, cdata): def decompile_suffix(ctx, gir, cdata):
ctx.print(f"{escape_quote(cdata)},") ctx.print(f'"{cdata}",')

View file

@ -18,38 +18,18 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .common import * from .attributes import BaseAttribute
from .contexts import ValueTypeCtx
from .gobject_object import ObjectContent, validate_parent_type from .gobject_object import ObjectContent, validate_parent_type
from .values import Value from .common import *
class LayoutProperty(AstNode): class LayoutProperty(BaseAttribute):
grammar = Statement(UseIdent("name"), ":", Err(Value, "Expected a value"))
tag_name = "property" tag_name = "property"
@property @property
def name(self) -> str: def value_type(self):
return self.tokens["name"]
@property
def value(self) -> Value:
return self.children[Value][0]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.name,
SymbolKind.Field,
self.range,
self.group.tokens["name"].range,
self.value.range.text,
)
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
# there isn't really a way to validate these # there isn't really a way to validate these
return ValueTypeCtx(None) return None
@validate("name") @validate("name")
def unique_in_parent(self): def unique_in_parent(self):
@ -59,22 +39,23 @@ class LayoutProperty(AstNode):
) )
class ExtLayout(AstNode): layout_prop = Group(
LayoutProperty,
Statement(
UseIdent("name"),
":",
VALUE_HOOKS.expected("a value"),
)
)
class Layout(AstNode):
grammar = Sequence( grammar = Sequence(
Keyword("layout"), Keyword("layout"),
"{", "{",
Until(LayoutProperty, "}"), Until(layout_prop, "}"),
) )
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"layout",
SymbolKind.Struct,
self.range,
self.group.tokens["layout"].range,
)
@validate("layout") @validate("layout")
def container_is_widget(self): def container_is_widget(self):
validate_parent_type(self, "Gtk", "Widget", "layout properties") validate_parent_type(self, "Gtk", "Widget", "layout properties")
@ -83,9 +64,11 @@ class ExtLayout(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate layout block") self.validate_unique_in_parent("Duplicate layout block")
@docs("layout") def emit_xml(self, xml: XmlEmitter):
def ref_docs(self): xml.start_tag("layout")
return get_docs_section("Syntax ExtLayout") for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@completer( @completer(
@ -93,8 +76,11 @@ class ExtLayout(AstNode):
applies_in_subclass=("Gtk", "Widget"), applies_in_subclass=("Gtk", "Widget"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def layout_completer(lsp, ast_node, match_variables): def layout_completer(ast_node, match_variables):
yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}") yield Completion(
"layout", CompletionItemKind.Snippet,
snippet="layout {\n $0\n}"
)
@decompiler("layout") @decompiler("layout")

View file

@ -1,111 +0,0 @@
import typing as T
from blueprintcompiler.errors import T
from blueprintcompiler.lsp_utils import DocumentSymbol
from ..ast_utils import AstNode, validate
from .common import *
from .contexts import ScopeCtx
from .gobject_object import ObjectContent, validate_parent_type
from .types import TypeName
class ExtListItemFactory(AstNode):
grammar = [
UseExact("id", "template"),
Mark("typename_start"),
Optional(TypeName),
Mark("typename_end"),
ObjectContent,
]
@property
def id(self) -> str:
return "template"
@property
def signature(self) -> str:
return f"template {self.gir_class.full_name}"
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.signature,
SymbolKind.Object,
self.range,
self.group.tokens["id"].range,
)
@property
def type_name(self) -> T.Optional[TypeName]:
if len(self.children[TypeName]) == 1:
return self.children[TypeName][0]
else:
return None
@property
def gir_class(self):
if self.type_name is not None:
return self.type_name.gir_type
else:
return self.root.gir.get_type("ListItem", "Gtk")
@validate("id")
def container_is_builder_list(self):
validate_parent_type(
self,
"Gtk",
"BuilderListItemFactory",
"sub-templates",
)
@validate("id")
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate template block")
@validate("typename_start", "typename_end")
def type_is_list_item(self):
if self.type_name is not None:
if self.type_name.glib_type_name not in (
"GtkListItem",
"GtkListHeader",
"GtkColumnViewRow",
"GtkColumnViewCell",
):
raise CompileError(
f"Only Gtk.ListItem, Gtk.ListHeader, Gtk.ColumnViewRow, or Gtk.ColumnViewCell is allowed as a type here"
)
@validate("id")
def type_name_upgrade(self):
if self.type_name is None:
raise UpgradeWarning(
"Expected type name after 'template' keyword",
actions=[
CodeAction(
"Add ListItem type to template block (introduced in blueprint 0.8.0)",
"template ListItem",
)
],
)
@context(ScopeCtx)
def scope_ctx(self) -> ScopeCtx:
return ScopeCtx(node=self)
@validate()
def unique_ids(self):
self.context[ScopeCtx].validate_unique_ids()
@property
def content(self) -> ObjectContent:
return self.children[ObjectContent][0]
@property
def action_widgets(self):
# The sub-template shouldn't have its own actions, this is just here to satisfy XmlOutput._emit_object_or_template
return None
@docs("id")
def ref_docs(self):
return get_docs_section("Syntax ExtListItemFactory")

View file

@ -17,257 +17,173 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from blueprintcompiler.language.values import StringValue
from .attributes import BaseAttribute
from .gobject_object import Object, ObjectContent
from .ui import UI
from .common import * from .common import *
from .contexts import ValueTypeCtx
from .gobject_object import RESERVED_IDS
class Menu(AstNode): 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 @property
def gir_class(self): def gir_class(self):
return self.root.gir.namespaces["Gtk"].lookup_type("Gio.Menu") return self.root.gir.namespaces["Gtk"].lookup_type("Gio.Menu")
@property
def id(self) -> str:
return self.tokens["id"]
@property class MenuAttribute(BaseAttribute):
def signature(self) -> str:
if self.id:
return f"Gio.Menu {self.id}"
else:
return "Gio.Menu"
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.tokens["tag"],
SymbolKind.Object,
self.range,
self.group.tokens[self.tokens["tag"]].range,
self.id,
)
@property
def tag(self) -> str:
return self.tokens["tag"]
@property
def items(self) -> T.List[T.Union["Menu", "MenuAttribute"]]:
return self.children
@validate("menu")
def has_id(self):
if self.tokens["tag"] == "menu" and self.tokens["id"] is None:
raise CompileError("Menu requires an ID")
@validate("id")
def object_id_not_reserved(self):
if self.id in RESERVED_IDS:
raise CompileWarning(f"{self.id} may be a confusing object ID")
@docs("menu")
def ref_docs_menu(self):
return get_docs_section("Syntax Menu")
@docs("section")
def ref_docs_section(self):
return get_docs_section("Syntax Menu")
@docs("submenu")
def ref_docs_submenu(self):
return get_docs_section("Syntax Menu")
@docs("item")
def ref_docs_item(self):
if self.tokens["shorthand"]:
return get_docs_section("Syntax MenuItemShorthand")
else:
return get_docs_section("Syntax Menu")
class MenuAttribute(AstNode):
tag_name = "attribute" tag_name = "attribute"
@property @property
def name(self) -> str: def value_type(self):
return self.tokens["name"] return None
@property
def value(self) -> StringValue:
return self.children[StringValue][0]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.name,
SymbolKind.Field,
self.range,
(
self.group.tokens["name"].range
if self.group.tokens["name"]
else self.range
),
self.value.range.text,
)
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(None)
@validate("name")
def unique(self):
self.validate_unique_in_parent(
f"Duplicate attribute '{self.name}'", lambda x: x.name == self.name
)
menu_child = AnyOf() 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( menu_attribute = Group(
MenuAttribute, MenuAttribute,
[ [
UseIdent("name"), UseIdent("name"),
":", ":",
Err(StringValue, "Expected string or translated string"), VALUE_HOOKS.expected("a value"),
Match(";").expected(), Match(";").expected(),
], ]
)
menu_section = Group(
Menu,
[
Keyword("section"),
UseLiteral("tag", "section"),
Optional(UseIdent("id")),
Match("{").expected(),
Until(AnyOf(menu_child, menu_attribute), "}"),
],
)
menu_submenu = Group(
Menu,
[
Keyword("submenu"),
UseLiteral("tag", "submenu"),
Optional(UseIdent("id")),
Match("{").expected(),
Until(AnyOf(menu_child, menu_attribute), "}"),
],
) )
menu_item = Group( menu_item = Group(
Menu, Menu,
[ [
Keyword("item"), "item",
UseLiteral("tag", "item"), UseLiteral("tag", "item"),
Optional(UseIdent("id")),
Match("{").expected(), Match("{").expected(),
Until(menu_attribute, "}"), Until(menu_attribute, "}"),
], ]
) )
menu_item_shorthand = Group( menu_item_shorthand = Group(
Menu, Menu,
[ [
Keyword("item"), "item",
UseLiteral("tag", "item"), UseLiteral("tag", "item"),
UseLiteral("shorthand", True),
"(", "(",
Group( Group(
MenuAttribute, MenuAttribute,
[UseLiteral("name", "label"), StringValue], [UseLiteral("name", "label"), VALUE_HOOKS],
), ),
Optional( Optional([
[ ",",
",", Optional([
Optional( Group(
[ MenuAttribute,
Group( [UseLiteral("name", "action"), VALUE_HOOKS],
MenuAttribute,
[UseLiteral("name", "action"), StringValue],
),
Optional(
[
",",
Group(
MenuAttribute,
[UseLiteral("name", "icon"), StringValue],
),
]
),
]
), ),
] Optional([
), ",",
Group(
MenuAttribute,
[UseLiteral("name", "icon"), VALUE_HOOKS],
),
])
])
]),
Match(")").expected(), Match(")").expected(),
], ]
) )
menu_child.children = [ menu_contents.children = [
menu_section, Match("{"),
menu_submenu, Until(AnyOf(
menu_item_shorthand, menu_section,
menu_item, menu_submenu,
menu_item_shorthand,
menu_item,
menu_attribute,
), "}"),
] ]
menu: Group = Group( menu = Group(
Menu, Menu,
[ [
Keyword("menu"), "menu",
UseLiteral("tag", "menu"), UseLiteral("tag", "menu"),
Optional(UseIdent("id")), Optional(UseIdent("id")),
[ menu_contents
Match("{"),
Until(
AnyOf(
menu_child,
Fail(
menu_attribute,
"Attributes are not permitted at the top level of a menu",
),
),
"}",
),
],
], ],
) )
from .ui import UI
@completer( @completer(
applies_in=[UI], applies_in=[UI],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def menu_completer(lsp, ast_node, match_variables): def menu_completer(ast_node, match_variables):
yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}") yield Completion(
"menu", CompletionItemKind.Snippet,
snippet="menu {\n $0\n}"
)
@completer( @completer(
applies_in=[Menu], applies_in=[Menu],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def menu_content_completer(lsp, ast_node, match_variables): def menu_content_completer(ast_node, match_variables):
yield Completion( yield Completion(
"submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}" "submenu", CompletionItemKind.Snippet,
snippet="submenu {\n $0\n}"
) )
yield Completion( yield Completion(
"section", CompletionItemKind.Snippet, snippet="section {\n $0\n}" "section", CompletionItemKind.Snippet,
snippet="section {\n $0\n}"
) )
yield Completion("item", CompletionItemKind.Snippet, snippet="item {\n $0\n}")
yield Completion( yield Completion(
"item (shorthand)", "item", CompletionItemKind.Snippet,
CompletionItemKind.Snippet, snippet="item {\n $0\n}"
snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")', )
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(
yield Completion("action", CompletionItemKind.Snippet, snippet='action: "$0";') "label", CompletionItemKind.Snippet,
yield Completion("icon", CompletionItemKind.Snippet, snippet='icon: "$0";') snippet='label: $0;'
)
yield Completion(
"action", CompletionItemKind.Snippet,
snippet='action: "$0";'
)
yield Completion(
"icon", CompletionItemKind.Snippet,
snippet='icon: "$0";'
)
@decompiler("menu") @decompiler("menu")
@ -277,7 +193,6 @@ def decompile_menu(ctx, gir, id=None):
else: else:
ctx.print("menu {") ctx.print("menu {")
@decompiler("submenu") @decompiler("submenu")
def decompile_submenu(ctx, gir, id=None): def decompile_submenu(ctx, gir, id=None):
if id: if id:
@ -285,15 +200,13 @@ def decompile_submenu(ctx, gir, id=None):
else: else:
ctx.print("submenu {") ctx.print("submenu {")
@decompiler("item")
@decompiler("item", parent_tag="menu")
def decompile_item(ctx, gir, id=None): def decompile_item(ctx, gir, id=None):
if id: if id:
ctx.print(f"item {id} {{") ctx.print(f"item {id} {{")
else: else:
ctx.print("item {") ctx.print("item {")
@decompiler("section") @decompiler("section")
def decompile_section(ctx, gir, id=None): def decompile_section(ctx, gir, id=None):
if id: if id:

View file

@ -1,187 +0,0 @@
# gtk_scale.py
#
# Copyright 2023 James Westman <james@jwestman.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .common import *
from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue
class ExtScaleMark(AstNode):
grammar = [
Keyword("mark"),
Match("(").expected(),
[
Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))),
UseNumber("value"),
Optional(
[
",",
UseIdent("position"),
Optional([",", StringValue]),
]
),
],
Match(")").expected(),
]
@property
def value(self) -> float:
if self.tokens["sign"] == "-":
return -self.tokens["value"]
else:
return self.tokens["value"]
@property
def position(self) -> T.Optional[str]:
return self.tokens["position"]
@property
def label(self) -> T.Optional[StringValue]:
if len(self.children[StringValue]) == 1:
return self.children[StringValue][0]
else:
return None
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
str(self.value),
SymbolKind.Field,
self.range,
self.group.tokens["mark"].range,
self.label.string if self.label else None,
)
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
if range := self.ranges["position"]:
yield SemanticToken(
range.start,
range.end,
SemanticTokenType.EnumMember,
)
@docs("position")
def position_docs(self) -> T.Optional[str]:
if member := self.root.gir.get_type("PositionType", "Gtk").members.get(
self.position
):
return member.doc
else:
return None
@validate("position")
def validate_position(self):
positions = self.root.gir.get_type("PositionType", "Gtk").members
if self.position is not None and positions.get(self.position) is None:
raise CompileError(
f"'{self.position}' is not a member of Gtk.PositionType",
did_you_mean=(self.position, positions.keys()),
)
@docs("mark")
def ref_docs(self):
return get_docs_section("Syntax ExtScaleMarks")
class ExtScaleMarks(AstNode):
grammar = [
Keyword("marks"),
Match("[").expected(),
Until(ExtScaleMark, "]", ","),
]
@property
def marks(self) -> T.List[ExtScaleMark]:
return self.children
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"marks",
SymbolKind.Array,
self.range,
self.group.tokens["marks"].range,
)
@validate("marks")
def container_is_size_group(self):
validate_parent_type(self, "Gtk", "Scale", "scale marks")
@validate("marks")
def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate 'marks' block")
@docs("marks")
def ref_docs(self):
return get_docs_section("Syntax ExtScaleMarks")
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Scale"),
matches=new_statement_patterns,
)
def complete_marks(lsp, ast_node, match_variables):
yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]")
@completer(
applies_in=[ExtScaleMarks],
)
def complete_mark(lsp, ast_node, match_variables):
yield Completion("mark", CompletionItemKind.Keyword, snippet="mark ($0),")
@decompiler("marks")
def decompile_marks(
ctx,
gir,
):
ctx.print("marks [")
@decompiler("mark", cdata=True)
def decompile_mark(
ctx: DecompileCtx,
gir,
value,
position=None,
cdata=None,
translatable="false",
comments=None,
context=None,
):
if comments is not None:
ctx.print(f"/* Translators: {comments} */")
text = f"mark ({value}"
if position:
text += f", {position}"
elif cdata:
text += f", bottom"
if truthy(translatable):
comments, translatable = decompile_translatable(
cdata, translatable, context, comments
)
text += f", {translatable}"
text += "),"
ctx.print(text)

View file

@ -18,58 +18,32 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .common import *
from .contexts import ScopeCtx
from .gobject_object import ObjectContent, validate_parent_type from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class Widget(AstNode): class Widget(AstNode):
grammar = UseIdent("name") grammar = UseIdent("name")
@property
def name(self) -> str:
return self.tokens["name"]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.name,
SymbolKind.Field,
self.range,
self.group.tokens["name"].range,
)
def get_reference(self, _idx: int) -> T.Optional[LocationLink]:
if obj := self.context[ScopeCtx].objects.get(self.name):
return LocationLink(self.range, obj.range, obj.ranges["id"])
else:
return None
@validate("name") @validate("name")
def obj_widget(self): def obj_widget(self):
object = self.context[ScopeCtx].objects.get(self.tokens["name"]) object = self.root.objects_by_id.get(self.tokens["name"])
type = self.root.gir.get_type("Widget", "Gtk") type = self.root.gir.get_type("Widget", "Gtk")
if object is None: if object is None:
raise CompileError( raise CompileError(
f"Could not find object with ID {self.tokens['name']}", f"Could not find object with ID {self.tokens['name']}",
did_you_mean=( did_you_mean=(self.tokens['name'], self.root.objects_by_id.keys()),
self.tokens["name"],
self.context[ScopeCtx].objects.keys(),
),
) )
elif object.gir_class and not object.gir_class.assignable_to(type): elif object.gir_class and not object.gir_class.assignable_to(type):
raise CompileError( raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {type.full_name}" f"Cannot assign {object.gir_class.full_name} to {type.full_name}"
) )
@validate("name") def emit_xml(self, xml: XmlEmitter):
def unique_in_parent(self): xml.put_self_closing("widget", name=self.tokens["name"])
self.validate_unique_in_parent(
f"Object '{self.name}' is listed twice", lambda x: x.name == self.name
)
class ExtSizeGroupWidgets(AstNode): class Widgets(AstNode):
grammar = [ grammar = [
Keyword("widgets"), Keyword("widgets"),
"[", "[",
@ -77,15 +51,6 @@ class ExtSizeGroupWidgets(AstNode):
"]", "]",
] ]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"widgets",
SymbolKind.Array,
self.range,
self.group.tokens["widgets"].range,
)
@validate("widgets") @validate("widgets")
def container_is_size_group(self): def container_is_size_group(self):
validate_parent_type(self, "Gtk", "SizeGroup", "size group properties") validate_parent_type(self, "Gtk", "SizeGroup", "size group properties")
@ -94,9 +59,11 @@ class ExtSizeGroupWidgets(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate widgets block") self.validate_unique_in_parent("Duplicate widgets block")
@docs("widgets") def emit_xml(self, xml: XmlEmitter):
def ref_docs(self): xml.start_tag("widgets")
return get_docs_section("Syntax ExtSizeGroupWidgets") for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@completer( @completer(
@ -104,15 +71,5 @@ class ExtSizeGroupWidgets(AstNode):
applies_in_subclass=("Gtk", "SizeGroup"), applies_in_subclass=("Gtk", "SizeGroup"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def size_group_completer(lsp, ast_node, match_variables): def size_group_completer(ast_node, match_variables):
yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]") yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]")
@decompiler("widgets")
def size_group_decompiler(ctx, gir: gir.GirContext):
ctx.print("widgets [")
@decompiler("widget")
def widget_decompiler(ctx, gir: gir.GirContext, name: str):
ctx.print(name + ",")

View file

@ -18,29 +18,28 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .common import * from .attributes import BaseTypedAttribute
from .gobject_object import ObjectContent, validate_parent_type from .gobject_object import ObjectContent, validate_parent_type
from .values import StringValue from .values import Value, TranslatedStringValue
from .common import *
class Item(AstNode): class Item(AstNode):
grammar = StringValue grammar = VALUE_HOOKS
@property @property
def child(self) -> StringValue: def value_type(self):
return self.children[StringValue][0] return StringType()
@property def emit_xml(self, xml: XmlEmitter):
def document_symbol(self) -> DocumentSymbol: value = self.children[Value][0]
return DocumentSymbol( attrs = value.attrs if isinstance(value, TranslatedStringValue) else {}
self.child.range.text, xml.start_tag("item", **attrs)
SymbolKind.String, value.emit_xml(xml)
self.range, xml.end_tag()
self.range,
)
class ExtStringListStrings(AstNode): class Strings(AstNode):
grammar = [ grammar = [
Keyword("strings"), Keyword("strings"),
"[", "[",
@ -48,16 +47,7 @@ class ExtStringListStrings(AstNode):
"]", "]",
] ]
@property @validate("items")
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"strings",
SymbolKind.Array,
self.range,
self.group.tokens["strings"].range,
)
@validate("strings")
def container_is_string_list(self): def container_is_string_list(self):
validate_parent_type(self, "Gtk", "StringList", "StringList items") validate_parent_type(self, "Gtk", "StringList", "StringList items")
@ -65,9 +55,11 @@ class ExtStringListStrings(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate strings block") self.validate_unique_in_parent("Duplicate strings block")
@docs("strings") def emit_xml(self, xml: XmlEmitter):
def ref_docs(self): xml.start_tag("items")
return get_docs_section("Syntax ExtStringListStrings") for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@completer( @completer(
@ -75,27 +67,8 @@ class ExtStringListStrings(AstNode):
applies_in_subclass=("Gtk", "StringList"), applies_in_subclass=("Gtk", "StringList"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def strings_completer(lsp, ast_node, match_variables): def strings_completer(ast_node, match_variables):
yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]") yield Completion(
"strings", CompletionItemKind.Snippet,
snippet="strings [$0]"
@decompiler("items", parent_type="Gtk.StringList")
def decompile_strings(ctx: DecompileCtx, gir: gir.GirContext):
ctx.print("strings [")
@decompiler("item", cdata=True, parent_type="Gtk.StringList")
def decompile_item(
ctx: DecompileCtx,
gir: gir.GirContext,
translatable="false",
comments=None,
context=None,
cdata=None,
):
comments, translatable = decompile_translatable(
cdata, translatable, context, comments
) )
if comments is not None:
ctx.print(comments)
ctx.print(translatable + ",")

View file

@ -18,34 +18,18 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .common import *
from .gobject_object import ObjectContent, validate_parent_type from .gobject_object import ObjectContent, validate_parent_type
from .common import *
class StyleClass(AstNode): class StyleClass(AstNode):
grammar = UseQuoted("name") grammar = UseQuoted("name")
@property def emit_xml(self, xml):
def name(self) -> str: xml.put_self_closing("class", name=self.tokens["name"])
return self.tokens["name"]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.name,
SymbolKind.String,
self.range,
self.range,
)
@validate("name")
def unique_in_parent(self):
self.validate_unique_in_parent(
f"Duplicate style class '{self.name}'", lambda x: x.name == self.name
)
class ExtStyles(AstNode): class Styles(AstNode):
grammar = [ grammar = [
Keyword("styles"), Keyword("styles"),
"[", "[",
@ -53,15 +37,6 @@ class ExtStyles(AstNode):
"]", "]",
] ]
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
"styles",
SymbolKind.Array,
self.range,
self.group.tokens["styles"].range,
)
@validate("styles") @validate("styles")
def container_is_widget(self): def container_is_widget(self):
validate_parent_type(self, "Gtk", "Widget", "style classes") validate_parent_type(self, "Gtk", "Widget", "style classes")
@ -70,9 +45,11 @@ class ExtStyles(AstNode):
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent("Duplicate styles block") self.validate_unique_in_parent("Duplicate styles block")
@docs("styles") def emit_xml(self, xml: XmlEmitter):
def ref_docs(self): xml.start_tag("style")
return get_docs_section("Syntax ExtStyles") for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@completer( @completer(
@ -80,15 +57,14 @@ class ExtStyles(AstNode):
applies_in_subclass=("Gtk", "Widget"), applies_in_subclass=("Gtk", "Widget"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def style_completer(lsp, ast_node, match_variables): def style_completer(ast_node, match_variables):
yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]') yield Completion("styles", CompletionItemKind.Keyword, snippet="styles [\"$0\"]")
@decompiler("style") @decompiler("style")
def decompile_style(ctx, gir): def decompile_style(ctx, gir):
ctx.print(f"styles [") ctx.print(f"styles [")
@decompiler("class") @decompiler("class")
def decompile_style_class(ctx, gir, name): def decompile_style_class(ctx, gir, name):
ctx.print(f'"{name}",') ctx.print(f'"{name}",')

View file

@ -20,126 +20,52 @@
from functools import cached_property from functools import cached_property
from .common import *
from .gobject_object import Object from .gobject_object import Object
from .response_id import ExtResponse, decompile_response_type from .response_id import ResponseId
from .common import *
ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [
("Gtk", "Buildable"),
("Gio", "ListStore"),
]
class ChildInternal(AstNode):
grammar = ["internal-child", UseIdent("internal_child")]
@property
def internal_child(self) -> str:
return self.tokens["internal_child"]
class ChildType(AstNode):
grammar = UseIdent("child_type").expected("a child type")
@property
def child_type(self) -> str:
return self.tokens["child_type"]
class ChildExtension(AstNode):
grammar = ExtResponse
@property
def child(self) -> ExtResponse:
return self.children[0]
@docs()
def ref_docs(self):
return get_docs_section("Syntax ChildExtension")
class ChildAnnotation(AstNode):
grammar = ["[", AnyOf(ChildInternal, ChildExtension, ChildType), "]"]
@property
def child(self) -> T.Union[ChildInternal, ChildExtension, ChildType]:
return self.children[0]
class Child(AstNode): class Child(AstNode):
grammar = [ grammar = [
Optional(ChildAnnotation), Optional([
"[",
Optional(["internal-child", UseLiteral("internal_child", True)]),
UseIdent("child_type").expected("a child type"),
Optional(ResponseId),
"]",
]),
Object, Object,
] ]
@property
def annotation(self) -> T.Optional[ChildAnnotation]:
annotations = self.children[ChildAnnotation]
return annotations[0] if len(annotations) else None
@property
def object(self) -> Object:
return self.children[Object][0]
@validate()
def parent_can_have_child(self):
if gir_class := self.parent.gir_class:
for namespace, name in ALLOWED_PARENTS:
parent_type = self.root.gir.get_type(name, namespace)
if gir_class.assignable_to(parent_type):
break
else:
hints = [
"only Gio.ListStore or Gtk.Buildable implementors can have children"
]
if hasattr(gir_class, "properties") and "child" in gir_class.properties:
hints.append(
"did you mean to assign this object to the 'child' property?"
)
raise CompileError(
f"{gir_class.full_name} doesn't have children",
hints=hints,
)
@cached_property @cached_property
def response_id(self) -> T.Optional[ExtResponse]: def response_id(self) -> T.Optional[ResponseId]:
"""Get action widget's response ID. """Get action widget's response ID.
If child is not action widget, returns `None`. If child is not action widget, returns `None`.
""" """
if ( response_ids = self.children[ResponseId]
self.annotation is not None
and isinstance(self.annotation.child, ChildExtension) if response_ids:
and isinstance(self.annotation.child.child, ExtResponse) return response_ids[0]
):
return self.annotation.child.child
else: else:
return None return None
@validate() def emit_xml(self, xml: XmlEmitter):
def internal_child_unique(self): child_type = internal_child = None
if self.annotation is not None: if self.tokens["internal_child"]:
if isinstance(self.annotation.child, ChildInternal): internal_child = self.tokens["child_type"]
internal_child = self.annotation.child.internal_child else:
self.validate_unique_in_parent( child_type = self.tokens["child_type"]
f"Duplicate internal child '{internal_child}'", xml.start_tag("child", type=child_type, internal_child=internal_child)
lambda x: ( for child in self.children:
x.annotation child.emit_xml(xml)
and isinstance(x.annotation.child, ChildInternal) xml.end_tag()
and x.annotation.child.internal_child == internal_child
),
)
@decompiler("child", element=True) @decompiler("child")
def decompile_child(ctx, gir, element): def decompile_child(ctx, gir, type=None, internal_child=None):
if type := element["type"]: if type is not None:
if type == "action":
if decompiled := decompile_response_type(ctx.parent_node, element):
ctx.print(decompiled)
return
ctx.print(f"[{type}]") ctx.print(f"[{type}]")
elif internal_child := element["internal-child"]: elif internal_child is not None:
ctx.print(f"[internal-child {internal_child}]") ctx.print(f"[internal-child {internal_child}]")
return gir return gir

View file

@ -17,93 +17,46 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from blueprintcompiler.language.common import GirType
from ..gir import TemplateType
from .common import *
from .gobject_object import Object, ObjectContent from .gobject_object import Object, ObjectContent
from .types import ClassName, TemplateClassName from .common import *
class Template(Object): class Template(Object):
grammar = [ grammar = [
UseExact("id", "template"), "template",
to_parse_node(TemplateClassName).expected("template type"), UseIdent("name").expected("template class name"),
Optional( Optional([
[ Match(":"),
Match(":"), class_name.expected("parent class"),
to_parse_node(ClassName).expected("parent class"), ]),
]
),
ObjectContent, ObjectContent,
] ]
@property
def id(self) -> str:
return "template"
@property
def signature(self) -> str:
if self.parent_type and self.parent_type.gir_type:
return f"template {self.class_name.as_string} : {self.parent_type.gir_type.full_name}"
else:
return f"template {self.class_name.as_string}"
@property
def document_symbol(self) -> DocumentSymbol:
return DocumentSymbol(
self.signature,
SymbolKind.Object,
self.range,
self.group.tokens["id"].range,
)
@property
def gir_class(self) -> GirType:
if isinstance(self.class_name.gir_type, ExternType):
if gir := self.parent_type:
return TemplateType(self.class_name.gir_type.full_name, gir.gir_type)
return self.class_name.gir_type
@property
def parent_type(self) -> T.Optional[ClassName]:
if len(self.children[ClassName]) == 2:
return self.children[ClassName][1]
else:
return None
@validate() @validate()
def parent_only_if_extern(self): def not_abstract(self):
if not isinstance(self.class_name.gir_type, ExternType): pass # does not apply to templates
if self.parent_type is not None:
raise CompileError(
"Parent type may only be specified if the template type is extern"
)
@validate("id") @validate("name")
def unique_in_parent(self): def unique_in_parent(self):
self.validate_unique_in_parent( self.validate_unique_in_parent(f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",)
f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",
)
@docs("id") def emit_xml(self, xml: XmlEmitter):
def ref_docs(self): xml.start_tag(
return get_docs_section("Syntax Template") "template",
**{"class": self.tokens["name"]},
parent=self.gir_class or self.tokens["class_name"]
)
for child in self.children:
child.emit_xml(xml)
xml.end_tag()
@decompiler("template") @decompiler("template")
def decompile_template(ctx: DecompileCtx, gir, klass, parent=None): def decompile_template(ctx: DecompileCtx, gir, klass, parent="Widget"):
def class_name(cname: str) -> str: gir_class = ctx.type_by_cname(parent)
if gir := ctx.type_by_cname(cname): if gir_class is None:
return decompile.full_name(gir) ctx.print(f"template {klass} : .{parent} {{")
else:
return "$" + cname
if parent is None:
ctx.print(f"template {class_name(klass)} {{")
else: else:
ctx.print(f"template {class_name(klass)} : {class_name(parent)} {{") ctx.print(f"template {klass} : {decompile.full_name(gir_class)} {{")
return gir_class
return ctx.type_by_cname(klass) or ctx.type_by_cname(parent)

View file

@ -24,47 +24,37 @@ from .common import *
class GtkDirective(AstNode): class GtkDirective(AstNode):
grammar = Statement( grammar = Statement(
Match("using").err( Match("using").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"),
'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;`)"),
),
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"), UseNumberText("version").expected("a version number for GTK"),
) )
@validate("version") @validate("version")
def gtk_version(self): def gtk_version(self):
version = self.tokens["version"] if self.tokens["version"] not in ["4.0"]:
if version not in ["4.0"]:
err = CompileError("Only GTK 4 is supported") err = CompileError("Only GTK 4 is supported")
if version and version.startswith("4"): if self.tokens["version"].startswith("4"):
err.hint( err.hint("Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'.")
"Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'."
)
else: else:
err.hint("Expected 'using Gtk 4.0;'") err.hint("Expected 'using Gtk 4.0;'")
raise err raise err
try: try:
gir.get_namespace("Gtk", version) gir.get_namespace("Gtk", self.tokens["version"])
except CompileError as e: except:
raise CompileError( raise CompileError("Could not find GTK 4 introspection files. Is gobject-introspection installed?", fatal=True)
"Could not find GTK 4 introspection files. Is gobject-introspection installed?",
fatal=True,
# preserve the hints from the original error, because it contains
# useful debugging information
hints=e.hints,
)
@property @property
def gir_namespace(self): def gir_namespace(self):
# For better error handling, just assume it's 4.0 # validate the GTK version first to make sure the more specific error
return gir.get_namespace("Gtk", "4.0") # message is emitted
self.gtk_version()
return gir.get_namespace("Gtk", self.tokens["version"])
@docs()
def ref_docs(self): def emit_xml(self, xml: XmlEmitter):
return get_docs_section("Syntax GtkDecl") xml.put_self_closing("requires", lib="gtk", version=self.tokens["version"])
class Import(AstNode): class Import(AstNode):
@ -74,36 +64,16 @@ class Import(AstNode):
UseNumberText("version").expected("a version number"), UseNumberText("version").expected("a version number"),
) )
@property
def namespace(self):
return self.tokens["namespace"]
@property
def version(self):
return self.tokens["version"]
@validate("namespace", "version") @validate("namespace", "version")
def namespace_exists(self): def namespace_exists(self):
gir.get_namespace(self.namespace, self.version) gir.get_namespace(self.tokens["namespace"], self.tokens["version"])
@validate()
def unused(self):
if self.namespace not in self.root.used_imports:
raise UnusedWarning(
f"Unused import: {self.namespace}",
self.range,
actions=[
CodeAction("Remove import", "", self.range.with_trailing_newline)
],
)
@property @property
def gir_namespace(self): def gir_namespace(self):
try: try:
return gir.get_namespace(self.namespace, self.version) return gir.get_namespace(self.tokens["namespace"], self.tokens["version"])
except CompileError: except CompileError:
return None return None
@docs() def emit_xml(self, xml):
def ref_docs(self): pass
return get_docs_section("Syntax Using")

View file

@ -23,25 +23,33 @@ import typing as T
from .common import * from .common import *
class ExtResponse(AstNode): class ResponseId(AstNode):
"""Response ID of action widget.""" """Response ID of action widget."""
ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [("Gtk", "Dialog"), ("Gtk", "InfoBar")] ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [
("Gtk", "Dialog"),
("Gtk", "InfoBar")
]
grammar = [ grammar = [
Keyword("action"),
Keyword("response"), Keyword("response"),
"=", "=",
AnyOf( AnyOf(
UseIdent("response_id"), UseIdent("response_id"),
[ UseNumber("response_id")
Optional(UseExact("sign", "-")),
UseNumber("response_id"),
],
), ),
Optional([Keyword("default"), UseLiteral("is_default", True)]), 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() @validate()
def parent_has_action_widgets(self) -> None: def parent_has_action_widgets(self) -> None:
"""Chech that parent widget has allowed type.""" """Chech that parent widget has allowed type."""
@ -53,7 +61,7 @@ class ExtResponse(AstNode):
gir = self.root.gir gir = self.root.gir
for namespace, name in ExtResponse.ALLOWED_PARENTS: for namespace, name in ResponseId.ALLOWED_PARENTS:
parent_type = gir.get_type(name, namespace) parent_type = gir.get_type(name, namespace)
if container_type.assignable_to(parent_type): if container_type.assignable_to(parent_type):
break break
@ -65,10 +73,10 @@ class ExtResponse(AstNode):
@validate() @validate()
def widget_have_id(self) -> None: def widget_have_id(self) -> None:
"""Check that action widget have ID.""" """Check that action widget have ID."""
from .gtkbuilder_child import Child from .gobject_object import Object
object = self.parent_by_type(Child).object _object = self.parent.children[Object][0]
if object.id is None: if _object.tokens["id"] is None:
raise CompileError(f"Action widget must have ID") raise CompileError(f"Action widget must have ID")
@validate("response_id") @validate("response_id")
@ -81,24 +89,28 @@ class ExtResponse(AstNode):
gir = self.root.gir gir = self.root.gir
response = self.tokens["response_id"] response = self.tokens["response_id"]
if self.tokens["sign"] == "-": if isinstance(response, int):
raise CompileError("Numeric response type can't be negative") if response < 0:
raise CompileError(
if isinstance(response, float): "Numeric response type can't be negative")
elif isinstance(response, float):
raise CompileError( raise CompileError(
"Response type must be GtkResponseType member or integer," " not float" "Response type must be GtkResponseType member or integer,"
" not float"
) )
elif not isinstance(response, int): else:
responses = gir.get_type("ResponseType", "Gtk").members.keys() responses = gir.get_type("ResponseType", "Gtk").members.keys()
if response not in responses: if response not in responses:
raise CompileError(f'Response type "{response}" doesn\'t exist') raise CompileError(
f"Response type \"{response}\" doesn't exist")
@validate("default") @validate("default")
def no_multiple_default(self) -> None: def no_multiple_default(self) -> None:
"""Only one action widget in dialog can be default.""" """Only one action widget in dialog can be default."""
from .gtkbuilder_child import Child
from .gobject_object import Object from .gobject_object import Object
if not self.is_default: if not self.tokens["is_default"]:
return return
action_widgets = self.parent_by_type(Object).action_widgets action_widgets = self.parent_by_type(Object).action_widgets
@ -108,57 +120,33 @@ class ExtResponse(AstNode):
if widget.tokens["is_default"]: if widget.tokens["is_default"]:
raise CompileError("Default response is already set") raise CompileError("Default response is already set")
@property
def response_id(self) -> str:
return self.tokens["response_id"]
@property
def is_default(self) -> bool:
return self.tokens["is_default"] or False
@property @property
def widget_id(self) -> str: def widget_id(self) -> str:
"""Get action widget ID.""" """Get action widget ID."""
from .gtkbuilder_child import Child from .gobject_object import Object
object = self.parent_by_type(Child).object _object: Object = self.parent.children[Object][0]
return object.id return _object.tokens["id"]
@docs() def emit_xml(self, xml: XmlEmitter) -> None:
def ref_docs(self): """Emit nothing.
return get_docs_section("Syntax ExtResponse")
@docs("response_id") Response ID don't have to emit any XML in place,
def response_id_docs(self): but have to emit action-widget tag in separate
if enum := self.root.gir.get_type("ResponseType", "Gtk"): place (see `ResponseId.emit_action_widget`)
if member := enum.members.get(self.response_id, None): """
return member.doc
def emit_action_widget(self, xml: XmlEmitter) -> None:
"""Emit action-widget XML.
def decompile_response_type(parent_element, child_element): Must be called while <action-widgets> tag is open.
obj_id = None
for obj in child_element.children:
if obj.tag == "object":
obj_id = obj["id"]
break
if obj_id is None: For more details see `GtkDialog` and `GtkInfoBar` docs.
return None """
xml.start_tag(
for child in parent_element.children: "action-widget",
if child.tag == "action-widgets": response=self.tokens["response_id"],
for action_widget in child.children: default=self.tokens["is_default"]
if action_widget.cdata == obj_id: )
response_id = action_widget["response"] xml.put_text(self.widget_id)
is_default = ( xml.end_tag()
" default" if decompile.truthy(action_widget["default"]) else ""
)
return f"[action response={response_id}{is_default}]"
return None
@decompiler("action-widgets", skip_children=True)
def decompile_action_widgets(ctx, gir):
# This is handled in the <child> decompiler and decompile_response_type above
pass

View file

@ -1,184 +0,0 @@
# types.py
#
# Copyright 2022 James Westman <james@jwestman.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from ..gir import Class, ExternType, Interface
from .common import *
class TypeName(AstNode):
grammar = AnyOf(
[
UseIdent("namespace"),
".",
UseIdent("class_name"),
],
[
AnyOf("$", [".", UseLiteral("old_extern", True)]),
UseIdent("class_name"),
UseLiteral("extern", True),
],
UseIdent("class_name"),
)
@validate()
def old_extern(self):
if self.tokens["old_extern"]:
raise UpgradeWarning(
"Use the '$' extern syntax introduced in blueprint 0.8.0",
actions=[CodeAction("Use '$' syntax", "$" + self.tokens["class_name"])],
)
@validate("class_name")
def type_exists(self):
if not self.tokens["extern"] and self.gir_ns is not None:
self.root.gir.validate_type(
self.tokens["class_name"], self.tokens["namespace"]
)
@validate("namespace")
def gir_ns_exists(self):
if not self.tokens["extern"]:
try:
self.root.gir.validate_ns(self.tokens["namespace"])
except CompileError as e:
ns = self.tokens["namespace"]
e.actions = [
self.root.import_code_action(n, version)
for n, version in gir.get_available_namespaces()
if n == ns
]
raise e
@validate()
def deprecated(self) -> None:
if self.gir_type and self.gir_type.deprecated:
hints = []
if self.gir_type.deprecated_doc:
hints.append(self.gir_type.deprecated_doc)
raise DeprecatedWarning(
f"{self.gir_type.full_name} is deprecated",
hints=hints,
)
@property
def gir_ns(self) -> T.Optional[gir.Namespace]:
if not self.tokens["extern"]:
return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk")
return None
@property
def gir_type(self) -> gir.GirType:
if self.tokens["class_name"] and not self.tokens["extern"]:
return self.root.gir.get_type(
self.tokens["class_name"], self.tokens["namespace"]
)
return gir.ExternType(self.tokens["class_name"])
@property
def glib_type_name(self) -> str:
if gir_type := self.gir_type:
return gir_type.glib_type_name
else:
return self.tokens["class_name"]
@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_type:
return self.gir_type.doc
@property
def as_string(self) -> str:
if self.tokens["extern"]:
return "$" + self.tokens["class_name"]
elif self.tokens["namespace"]:
return f"{self.tokens['namespace']}.{self.tokens['class_name']}"
else:
return self.tokens["class_name"]
class ClassName(TypeName):
@validate("namespace", "class_name")
def gir_class_exists(self):
if (
self.gir_type is not None
and not isinstance(self.gir_type, ExternType)
and not isinstance(self.gir_type, Class)
):
if isinstance(self.gir_type, Interface):
raise CompileError(
f"{self.gir_type.full_name} is an interface, not a class"
)
else:
raise CompileError(f"{self.gir_type.full_name} is not a class")
class ConcreteClassName(ClassName):
@validate("namespace", "class_name")
def not_abstract(self):
if isinstance(self.gir_type, Class) and self.gir_type.abstract:
raise CompileError(
f"{self.gir_type.full_name} can't be instantiated because it's abstract",
hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"],
)
class TemplateClassName(ClassName):
"""Handles the special case of a template type. The old syntax uses an identifier,
which is ambiguous with the new syntax. So this class displays an appropriate
upgrade warning instead of a class not found error."""
@property
def is_legacy(self):
return (
self.tokens["extern"] is None
and self.tokens["namespace"] is None
and self.root.gir.get_type(self.tokens["class_name"], "Gtk") is None
)
@property
def gir_type(self) -> gir.GirType:
if self.is_legacy:
return gir.ExternType(self.tokens["class_name"])
else:
return super().gir_type
@validate("class_name")
def type_exists(self):
if self.is_legacy:
if type := self.root.gir.get_type_by_cname(self.tokens["class_name"]):
replacement = type.full_name
else:
replacement = "$" + self.tokens["class_name"]
raise UpgradeWarning(
"Use type syntax here (introduced in blueprint 0.8.0)",
actions=[CodeAction("Use type syntax", replace_with=replacement)],
)
if not self.tokens["extern"] and self.gir_ns is not None:
self.root.gir.validate_type(
self.tokens["class_name"], self.tokens["namespace"]
)

View file

@ -17,44 +17,32 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from functools import cached_property
from .. import gir from .. import gir
from .common import *
from .contexts import ScopeCtx
from .gobject_object import Object
from .gtk_menu import Menu, menu
from .gtkbuilder_template import Template
from .imports import GtkDirective, Import from .imports import GtkDirective, Import
from .translation_domain import TranslationDomain from .gtkbuilder_template import Template
from .types import TypeName from .common import *
class UI(AstNode): class UI(AstNode):
"""The AST node for the entire file""" """ The AST node for the entire file """
grammar = [ grammar = [
GtkDirective, GtkDirective,
ZeroOrMore(Import), ZeroOrMore(Import),
Optional(TranslationDomain), Until(AnyOf(
Until( Template,
AnyOf( OBJECT_HOOKS,
Template, ), Eof()),
menu,
Object,
),
Eof(),
),
] ]
@cached_property @property
def gir(self) -> gir.GirContext: def gir(self):
gir_ctx = gir.GirContext() gir_ctx = gir.GirContext()
self._gir_errors = [] self._gir_errors = []
try: try:
if gtk := self.children[GtkDirective][0].gir_namespace: gir_ctx.add_namespace(self.children[GtkDirective][0].gir_namespace)
gir_ctx.add_namespace(gtk)
except CompileError as e: except CompileError as e:
self._gir_errors.append(e) self._gir_errors.append(e)
@ -62,85 +50,18 @@ class UI(AstNode):
try: try:
if i.gir_namespace is not None: if i.gir_namespace is not None:
gir_ctx.add_namespace(i.gir_namespace) gir_ctx.add_namespace(i.gir_namespace)
else:
gir_ctx.not_found_namespaces.add(i.namespace)
except CompileError as e: except CompileError as e:
e.range = i.range e.start = i.group.tokens["namespace"].start
e.end = i.group.tokens["version"].end
self._gir_errors.append(e) self._gir_errors.append(e)
return gir_ctx return gir_ctx
@property
def using(self) -> T.List[Import]:
return self.children[Import]
@property @property
def gtk_decl(self) -> GtkDirective: def objects_by_id(self):
return self.children[GtkDirective][0] return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None }
@property
def translation_domain(self) -> T.Optional[TranslationDomain]:
domains = self.children[TranslationDomain]
if len(domains):
return domains[0]
else:
return None
@property
def contents(self) -> T.List[T.Union[Object, Template, Menu]]:
return [
child
for child in self.children
if isinstance(child, Object)
or isinstance(child, Template)
or isinstance(child, Menu)
]
@property
def template(self) -> T.Optional[Template]:
if len(self.children[Template]):
return self.children[Template][0]
else:
return None
def is_legacy_template(self, id: str) -> bool:
return (
id not in self.context[ScopeCtx].objects
and self.template is not None
and self.template.class_name.glib_type_name == id
)
def import_code_action(self, ns: str, version: str) -> CodeAction:
if len(self.children[Import]):
pos = self.children[Import][-1].range.end
else:
pos = self.children[GtkDirective][0].range.end
return CodeAction(
f"Import {ns} {version}",
f"\nusing {ns} {version};",
Range(pos, pos, self.group.text),
)
@cached_property
def used_imports(self) -> T.Optional[T.Set[str]]:
def _iter_recursive(node: AstNode):
yield node
for child in node.children:
if isinstance(child, AstNode):
yield from _iter_recursive(child)
result = set()
for node in _iter_recursive(self):
if isinstance(node, TypeName):
ns = node.gir_ns
if ns is not None:
result.add(ns.name)
return result
@context(ScopeCtx)
def scope_ctx(self) -> ScopeCtx:
return ScopeCtx(node=self)
@validate() @validate()
def gir_errors(self): def gir_errors(self):
@ -149,6 +70,22 @@ class UI(AstNode):
if len(self._gir_errors): if len(self._gir_errors):
raise MultipleErrors(self._gir_errors) raise MultipleErrors(self._gir_errors)
@validate() @validate()
def unique_ids(self): def unique_ids(self):
self.context[ScopeCtx].validate_unique_ids() 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()

View file

@ -17,233 +17,101 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from blueprintcompiler.gir import ArrayType
from blueprintcompiler.lsp_utils import SemanticToken
from .common import * from .common import *
from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx
from .expression import Expression
from .gobject_object import Object
from .types import TypeName
class Translated(AstNode): class Value(AstNode):
pass
class TranslatedStringValue(Value):
grammar = AnyOf( grammar = AnyOf(
["_", "(", UseQuoted("string"), ")"], [
"_",
"(",
UseQuoted("value").expected("a quoted string"),
Match(")").expected(),
],
[ [
"C_", "C_",
"(", "(",
UseQuoted("context"), UseQuoted("context").expected("a quoted string"),
",", ",",
UseQuoted("string"), UseQuoted("value").expected("a quoted string"),
")", Optional(","),
Match(")").expected(),
], ],
) )
@property @property
def string(self) -> str: def attrs(self):
return self.tokens["string"] attrs = { "translatable": "true" }
if "context" in self.tokens:
attrs["context"] = self.tokens["context"]
return attrs
@property def emit_xml(self, xml: XmlEmitter):
def translate_context(self) -> T.Optional[str]: xml.put_text(self.tokens["value"])
return self.tokens["context"]
class LiteralValue(Value):
grammar = AnyOf(
UseNumber("value"),
UseQuoted("value"),
)
def emit_xml(self, xml: XmlEmitter):
xml.put_text(self.tokens["value"])
@validate() @validate()
def validate_for_type(self) -> None: def validate_for_type(self):
expected_type = self.context[ValueTypeCtx].value_type type = self.parent.value_type
if expected_type is not None and not expected_type.assignable_to(StringType()): if isinstance(type, gir.IntType):
raise CompileError( try:
f"Cannot convert translated string to {expected_type.full_name}" int(self.tokens["value"])
) except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to integer")
@validate("context") elif isinstance(type, gir.UIntType):
def context_double_quoted(self): try:
if self.translate_context is None: int(self.tokens["value"])
return if int(self.tokens["value"]) < 0:
raise Exception()
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to unsigned integer")
if not str(self.group.tokens["context"]).startswith('"'): elif isinstance(type, gir.FloatType):
raise CompileWarning("gettext may not recognize single-quoted strings") try:
float(self.tokens["value"])
except:
raise CompileError(f"Cannot convert {self.group.tokens['value']} to float")
@validate("string") elif isinstance(type, gir.StringType):
def string_double_quoted(self):
if not str(self.group.tokens["string"]).startswith('"'):
raise CompileWarning("gettext may not recognize single-quoted strings")
@docs()
def ref_docs(self):
return get_docs_section("Syntax Translated")
class TypeLiteral(AstNode):
grammar = [
"typeof",
AnyOf(
[
"<",
to_parse_node(TypeName).expected("type name"),
Match(">").expected(),
],
[
UseExact("lparen", "("),
to_parse_node(TypeName).expected("type name"),
UseExact("rparen", ")").expected("')'"),
],
),
]
@property
def type(self):
return gir.TypeType()
@property
def type_name(self) -> TypeName:
return self.children[TypeName][0]
@validate()
def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type
if expected_type is not None and not isinstance(expected_type, gir.TypeType):
raise CompileError(f"Cannot convert GType to {expected_type.full_name}")
@validate("lparen", "rparen")
def upgrade_to_angle_brackets(self):
if self.tokens["lparen"]:
raise UpgradeWarning(
"Use angle bracket syntax introduced in blueprint 0.8.0",
actions=[
CodeAction(
"Use <> instead of ()",
f"<{self.children[TypeName][0].as_string}>",
)
],
)
@docs()
def ref_docs(self):
return get_docs_section("Syntax TypeLiteral")
class QuotedLiteral(AstNode):
grammar = UseQuoted("value")
@property
def value(self) -> str:
return self.tokens["value"]
@property
def type(self):
return gir.StringType()
@validate()
def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type
if (
isinstance(expected_type, gir.IntType)
or isinstance(expected_type, gir.UIntType)
or isinstance(expected_type, gir.FloatType)
):
raise CompileError(f"Cannot convert string to number")
elif isinstance(expected_type, gir.StringType):
pass pass
elif ( elif isinstance(type, gir.Class) or isinstance(type, gir.Interface):
isinstance(expected_type, gir.Class)
or isinstance(expected_type, gir.Interface)
or isinstance(expected_type, gir.Boxed)
):
parseable_types = [ parseable_types = [
"Gdk.Paintable", "Gdk.Paintable",
"Gdk.Texture", "Gdk.Texture",
"Gdk.Pixbuf", "Gdk.Pixbuf",
"Gio.File", "GLib.File",
"Gtk.ShortcutTrigger", "Gtk.ShortcutTrigger",
"Gtk.ShortcutAction", "Gtk.ShortcutAction",
"Gdk.RGBA",
"Gdk.ContentFormats",
"Gsk.Transform",
"GLib.Variant",
] ]
if expected_type.full_name not in parseable_types: if type.full_name not in parseable_types:
hints = [] raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}")
if isinstance(expected_type, gir.TypeType):
hints.append(f"use the typeof operator: 'typeof({self.value})'")
raise CompileError(
f"Cannot convert string to {expected_type.full_name}", hints=hints
)
elif expected_type is not None: elif type is not None:
raise CompileError(f"Cannot convert string to {expected_type.full_name}") raise CompileError(f"Cannot convert {self.group.tokens['value']} to {type.full_name}")
class NumberLiteral(AstNode):
grammar = [
Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))),
UseNumber("value"),
]
@property
def type(self) -> gir.GirType:
if isinstance(self.value, int):
return gir.IntType()
else:
return gir.FloatType()
@property
def value(self) -> T.Union[int, float]:
if self.tokens["sign"] == "-":
return -self.tokens["value"]
else:
return self.tokens["value"]
@validate()
def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type
if isinstance(expected_type, gir.IntType):
if not isinstance(self.value, int):
raise CompileError(
f"Cannot convert {self.group.tokens['value']} to integer"
)
elif isinstance(expected_type, gir.UIntType):
if self.value < 0:
raise CompileError(
f"Cannot convert -{self.group.tokens['value']} to unsigned integer"
)
elif not isinstance(expected_type, gir.FloatType) and expected_type is not None:
raise CompileError(f"Cannot convert number to {expected_type.full_name}")
class Flag(AstNode): class Flag(AstNode):
grammar = UseIdent("value") grammar = UseIdent("value")
@property
def name(self) -> str:
return self.tokens["value"]
@property
def value(self) -> T.Optional[str]:
type = self.context[ValueTypeCtx].value_type
if not isinstance(type, Enumeration):
return None
elif member := type.members.get(self.name):
return member.nick
else:
return None
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
yield SemanticToken(
self.group.tokens["value"].start,
self.group.tokens["value"].end,
SemanticTokenType.EnumMember,
)
@docs() @docs()
def docs(self): def docs(self):
type = self.context[ValueTypeCtx].value_type type = self.parent.parent.value_type
if not isinstance(type, Enumeration): if not isinstance(type, Enumeration):
return return
if member := type.members.get(self.tokens["value"]): if member := type.members.get(self.tokens["value"]):
@ -251,298 +119,81 @@ class Flag(AstNode):
@validate() @validate()
def validate_for_type(self): def validate_for_type(self):
expected_type = self.context[ValueTypeCtx].value_type type = self.parent.parent.value_type
if ( if isinstance(type, gir.Bitfield) and self.tokens["value"] not in type.members:
isinstance(expected_type, gir.Bitfield)
and self.tokens["value"] not in expected_type.members
):
raise CompileError( raise CompileError(
f"{self.tokens['value']} is not a member of {expected_type.full_name}", f"{self.tokens['value']} is not a member of {type.full_name}",
did_you_mean=(self.tokens["value"], expected_type.members.keys()), did_you_mean=(self.tokens['value'], type.members.keys()),
) )
@validate()
def unique(self):
self.validate_unique_in_parent(
f"Duplicate flag '{self.name}'", lambda x: x.name == self.name
)
class FlagsValue(Value):
class Flags(AstNode): grammar = [Flag, "|", Delimited(Flag, "|")]
grammar = [Flag, "|", Flag, ZeroOrMore(["|", Flag])]
@property
def flags(self) -> T.List[Flag]:
return self.children
@validate() @validate()
def validate_for_type(self) -> None: def parent_is_bitfield(self):
expected_type = self.context[ValueTypeCtx].value_type type = self.parent.value_type
if expected_type is not None and not isinstance(expected_type, gir.Bitfield): if type is not None and not isinstance(type, gir.Bitfield):
raise CompileError(f"{expected_type.full_name} is not a bitfield type") raise CompileError(f"{type.full_name} is not a bitfield type")
@docs() def emit_xml(self, xml: XmlEmitter):
def ref_docs(self): xml.put_text("|".join([flag.tokens["value"] for flag in self.children[Flag]]))
return get_docs_section("Syntax Flags")
class IdentLiteral(AstNode): class IdentValue(Value):
grammar = UseIdent("value") grammar = UseIdent("value")
@property def emit_xml(self, xml: XmlEmitter):
def ident(self) -> str: if isinstance(self.parent.value_type, gir.Enumeration):
return self.tokens["value"] xml.put_text(self.parent.value_type.members[self.tokens["value"]].nick)
@property
def type(self) -> T.Optional[gir.GirType]:
# If the expected type is known, then use that. Otherwise, guess.
if expected_type := self.context[ValueTypeCtx].value_type:
return expected_type
elif self.ident in ["true", "false"]:
return gir.BoolType()
elif object := self.context[ScopeCtx].objects.get(self.ident):
return object.gir_class
elif self.root.is_legacy_template(self.ident):
return self.root.template.class_name.gir_type
else: else:
return None xml.put_text(self.tokens["value"])
@validate() @validate()
def validate_for_type(self) -> None: def validate_for_type(self):
expected_type = self.context[ValueTypeCtx].value_type type = self.parent.value_type
if isinstance(expected_type, gir.BoolType):
if self.ident not in ["true", "false"]:
raise CompileError(f"Expected 'true' or 'false' for boolean value")
elif isinstance(expected_type, gir.Enumeration): if isinstance(type, gir.Enumeration) or isinstance(type, gir.Bitfield):
if self.ident not in expected_type.members: if self.tokens["value"] not in type.members:
raise CompileError( raise CompileError(
f"{self.ident} is not a member of {expected_type.full_name}", f"{self.tokens['value']} is not a member of {type.full_name}",
did_you_mean=(self.ident, list(expected_type.members.keys())), did_you_mean=(self.tokens['value'], type.members.keys()),
) )
elif self.root.is_legacy_template(self.ident): elif isinstance(type, gir.BoolType):
raise UpgradeWarning( if self.tokens["value"] not in ["true", "false"]:
"Use 'template' instead of the class name (introduced in 0.8.0)", raise CompileError(
actions=[CodeAction("Use 'template'", "template")], f"Expected 'true' or 'false' for boolean value",
) did_you_mean=(self.tokens['value'], ["true", "false"]),
)
elif expected_type is not None or self.context[ValueTypeCtx].must_infer_type: elif type is not None:
object = self.context[ScopeCtx].objects.get(self.ident) object = self.root.objects_by_id.get(self.tokens["value"])
if object is None: if object is None:
if self.ident == "null":
if not self.context[ValueTypeCtx].allow_null:
raise CompileError("null is not permitted here")
elif self.ident == "item":
if not self.context[ExprValueCtx]:
raise CompileError(
'"item" can only be used in an expression literal'
)
elif self.ident not in ["true", "false"]:
raise CompileError(
f"Could not find object with ID {self.ident}",
did_you_mean=(
self.ident,
self.context[ScopeCtx].objects.keys(),
),
)
elif (
expected_type is not None
and object.gir_class is not None
and not object.gir_class.assignable_to(expected_type)
):
raise CompileError( raise CompileError(
f"Cannot assign {object.gir_class.full_name} to {expected_type.full_name}" 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() @docs()
def docs(self) -> T.Optional[str]: def docs(self):
expected_type = self.context[ValueTypeCtx].value_type type = self.parent.value_type
if isinstance(expected_type, gir.BoolType): if isinstance(type, gir.Enumeration) or isinstance(type, gir.Bitfield):
return None if member := type.members.get(self.tokens["value"]):
elif isinstance(expected_type, gir.Enumeration):
if member := expected_type.members.get(self.ident):
return member.doc return member.doc
else: else:
return expected_type.doc return type.doc
elif self.ident == "null" and self.context[ValueTypeCtx].allow_null: elif isinstance(type, gir.GirNode):
return None return type.doc
elif object := self.context[ScopeCtx].objects.get(self.ident):
return f"```\n{object.signature}\n```"
elif self.root.is_legacy_template(self.ident):
return f"```\n{self.root.template.signature}\n```"
else:
return None
def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: def get_semantic_tokens(self) -> T.Iterator[SemanticToken]:
type = self.context[ValueTypeCtx].value_type if isinstance(self.parent.value_type, gir.Enumeration):
if isinstance(type, gir.Enumeration):
token = self.group.tokens["value"] token = self.group.tokens["value"]
yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember) yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember)
def get_reference(self, _idx: int) -> T.Optional[LocationLink]:
ref = self.context[ScopeCtx].objects.get(self.ident)
if ref is None and self.root.is_legacy_template(self.ident):
ref = self.root.template
if ref:
return LocationLink(self.range, ref.range, ref.ranges["id"])
else:
return None
class Literal(AstNode):
grammar = AnyOf(
TypeLiteral,
QuotedLiteral,
NumberLiteral,
IdentLiteral,
)
@property
def value(
self,
) -> T.Union[TypeLiteral, QuotedLiteral, NumberLiteral, IdentLiteral]:
return self.children[0]
class ObjectValue(AstNode):
grammar = Object
@property
def object(self) -> Object:
return self.children[Object][0]
@validate()
def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type
if (
expected_type is not None
and self.object.gir_class is not None
and not self.object.gir_class.assignable_to(expected_type)
):
raise CompileError(
f"Cannot assign {self.object.gir_class.full_name} to {expected_type.full_name}"
)
class ExprValue(AstNode):
grammar = [Keyword("expr"), Expression]
@property
def expression(self) -> Expression:
return self.children[Expression][0]
@validate("expr")
def validate_for_type(self) -> None:
expected_type = self.parent.context[ValueTypeCtx].value_type
expr_type = self.root.gir.get_type("Expression", "Gtk")
if expected_type is not None and not expected_type.assignable_to(expr_type):
raise CompileError(
f"Cannot convert Gtk.Expression to {expected_type.full_name}"
)
@docs("expr")
def ref_docs(self):
return get_docs_section("Syntax ExprValue")
@context(ExprValueCtx)
def expr_literal(self):
return ExprValueCtx()
@context(ValueTypeCtx)
def value_type(self):
return ValueTypeCtx(None, must_infer_type=True)
class Value(AstNode):
grammar = AnyOf(Translated, Flags, Literal)
@property
def child(
self,
) -> T.Union[Translated, Flags, Literal]:
return self.children[0]
class ArrayValue(AstNode):
grammar = ["[", Delimited(Value, ","), "]"]
@validate()
def validate_for_type(self) -> None:
expected_type = self.gir_type
if expected_type is not None and not isinstance(expected_type, gir.ArrayType):
raise CompileError(f"Cannot assign array to {expected_type.full_name}")
if expected_type is not None and not isinstance(
expected_type.inner, StringType
):
raise CompileError("Only string arrays are supported")
@validate()
def validate_invalid_newline(self) -> None:
expected_type = self.gir_type
if isinstance(expected_type, gir.ArrayType) and isinstance(
expected_type.inner, StringType
):
errors = []
for value in self.values:
if isinstance(value.child, Literal) and isinstance(
value.child.value, QuotedLiteral
):
quoted_literal = value.child.value
literal_value = quoted_literal.value
# literal_value can be None if there's an invalid escape sequence
if literal_value is not None and "\n" in literal_value:
errors.append(
CompileError(
"String literals inside arrays can't contain newlines",
range=quoted_literal.range,
)
)
elif isinstance(value.child, Translated):
errors.append(
CompileError(
"Arrays can't contain translated strings",
range=value.child.range,
)
)
if len(errors) > 0:
raise MultipleErrors(errors)
@property
def values(self) -> T.List[Value]:
return self.children
@property
def gir_type(self):
return self.parent.context[ValueTypeCtx].value_type
@context(ValueTypeCtx)
def child_value(self):
if self.gir_type is None or not isinstance(self.gir_type, ArrayType):
return ValueTypeCtx(None)
else:
return ValueTypeCtx(self.gir_type.inner)
class StringValue(AstNode):
grammar = AnyOf(Translated, QuotedLiteral)
@property
def child(
self,
) -> T.Union[Translated, QuotedLiteral]:
return self.children[0]
@property
def string(self) -> str:
if isinstance(self.child, Translated):
return self.child.string
else:
return self.child.value
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(StringType())

View file

@ -18,94 +18,69 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import json
import sys
import traceback
import typing as T import typing as T
from difflib import SequenceMatcher import json, sys, traceback
from . import decompiler, formatter, parser, tokenizer, utils, xml_reader
from .ast_utils import AstNode
from .completions import complete from .completions import complete
from .errors import CompileError, MultipleErrors from .errors import PrintableError, CompileError, MultipleErrors
from .lsp_utils import * from .lsp_utils import *
from .outputs.xml import XmlOutput from . import tokenizer, parser, utils, xml_reader
from .tokenizer import Token
def printerr(*args, **kwargs): def command(json_method):
print(*args, file=sys.stderr, **kwargs)
def command(json_method: str):
def decorator(func): def decorator(func):
func._json_method = json_method func._json_method = json_method
return func return func
return decorator return decorator
class OpenFile: class OpenFile:
def __init__(self, uri: str, text: str, version: int) -> None: def __init__(self, uri, text, version):
self.uri = uri self.uri = uri
self.text = text self.text = text
self.version = version self.version = version
self.ast: T.Optional[AstNode] = None self.ast = None
self.tokens: T.Optional[list[Token]] = None self.tokens = None
self._update() self._update()
def apply_changes(self, changes) -> None: def apply_changes(self, changes):
for change in changes: for change in changes:
if "range" not in change: start = utils.pos_to_idx(change["range"]["start"]["line"], change["range"]["start"]["character"], self.text)
self.text = change["text"] end = utils.pos_to_idx(change["range"]["end"]["line"], change["range"]["end"]["character"], self.text)
continue
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.text = self.text[:start] + change["text"] + self.text[end:]
self._update() self._update()
def _update(self) -> None: def _update(self):
self.diagnostics: list[CompileError] = [] self.diagnostics = []
try: try:
self.tokens = tokenizer.tokenize(self.text) self.tokens = tokenizer.tokenize(self.text)
self.ast, errors, warnings = parser.parse(self.tokens) self.ast, errors, warnings = parser.parse(self.tokens)
self.diagnostics += warnings self.diagnostics += warnings
if errors is not None: if errors is not None:
self.diagnostics += errors.errors self.diagnostics += errors.errors
self.diagnostics += self.ast.errors
except MultipleErrors as e: except MultipleErrors as e:
self.diagnostics += e.errors self.diagnostics += e.errors
except CompileError as e: except CompileError as e:
self.diagnostics.append(e) self.diagnostics.append(e)
def calc_semantic_tokens(self) -> T.List[int]:
if self.ast is None:
return []
def calc_semantic_tokens(self) -> T.List[int]:
tokens = list(self.ast.get_semantic_tokens()) tokens = list(self.ast.get_semantic_tokens())
token_lists = [ token_lists = [
[ [
*utils.idx_to_pos(token.start, self.text), # line and column *utils.idx_to_pos(token.start, self.text), # line and column
token.end - token.start, # length token.end - token.start, # length
token.type, token.type,
0, # token modifiers 0, # token modifiers
] ] for token in tokens]
for token in tokens
]
# convert line, column numbers to deltas # convert line, column numbers to deltas
for a, b in zip(token_lists[-2::-1], token_lists[:0:-1]): for i, token_list in enumerate(token_lists[1:]):
b[0] -= a[0] token_list[0] -= token_lists[i][0]
if b[0] == 0: if token_list[0] == 0:
b[1] -= a[1] token_list[1] -= token_lists[i][1]
# flatten the list # flatten the list
return [x for y in token_lists for x in y] return [x for y in token_lists for x in y]
@ -114,11 +89,10 @@ class OpenFile:
class LanguageServer: class LanguageServer:
commands: T.Dict[str, T.Callable] = {} commands: T.Dict[str, T.Callable] = {}
def __init__(self): def __init__(self, logfile=None):
self.client_capabilities = {} self.client_capabilities = {}
self.client_supports_completion_choice = False self._open_files: {str: OpenFile} = {}
self._open_files: T.Dict[str, OpenFile] = {} self.logfile = logfile
self._exited = False
def run(self): def run(self):
# Read <doc> tags from gir files. During normal compilation these are # Read <doc> tags from gir files. During normal compilation these are
@ -126,7 +100,7 @@ class LanguageServer:
xml_reader.PARSE_GIR.add("doc") xml_reader.PARSE_GIR.add("doc")
try: try:
while not self._exited: while True:
line = "" line = ""
content_len = -1 content_len = -1
while content_len == -1 or (line != "\n" and line != "\r\n"): while content_len == -1 or (line != "\n" and line != "\r\n"):
@ -136,7 +110,7 @@ class LanguageServer:
if line.startswith("Content-Length:"): if line.startswith("Content-Length:"):
content_len = int(line.split("Content-Length:")[1].strip()) content_len = int(line.split("Content-Length:")[1].strip())
line = sys.stdin.buffer.read(content_len).decode() line = sys.stdin.buffer.read(content_len).decode()
printerr("input: " + line) self._log("input: " + line)
data = json.loads(line) data = json.loads(line)
method = data.get("method") method = data.get("method")
@ -146,89 +120,61 @@ class LanguageServer:
if method in self.commands: if method in self.commands:
self.commands[method](self, id, params) self.commands[method](self, id, params)
except Exception as e: except Exception as e:
printerr(traceback.format_exc()) self._log(traceback.format_exc())
def _send(self, data): def _send(self, data):
data["jsonrpc"] = "2.0" data["jsonrpc"] = "2.0"
line = json.dumps(data, separators=(",", ":")) line = json.dumps(data, separators=(",", ":")) + "\r\n"
printerr("output: " + line) self._log("output: " + line)
sys.stdout.write( sys.stdout.write(f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}")
f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}"
)
sys.stdout.flush() sys.stdout.flush()
def _send_error(self, id, code, message, data=None): def _log(self, msg):
self._send( if self.logfile is not None:
{ self.logfile.write(str(msg))
"id": id, self.logfile.write("\n")
"error": { self.logfile.flush()
"code": code,
"message": message,
"data": data,
},
}
)
def _send_response(self, id, result): def _send_response(self, id, result):
self._send( self._send({
{ "id": id,
"id": id, "result": result,
"result": result, })
}
)
def _send_notification(self, method, params): def _send_notification(self, method, params):
self._send( self._send({
{ "method": method,
"method": method, "params": params,
"params": params, })
}
)
@command("initialize") @command("initialize")
def initialize(self, id, params): def initialize(self, id, params):
from . import main from . import main
self.client_capabilities = params.get("capabilities", {}) self.client_capabilities = params.get("capabilities")
self.client_supports_completion_choice = params.get("clientInfo", {}).get( self._send_response(id, {
"name" "capabilities": {
) in ["Visual Studio Code", "VSCodium"] "textDocumentSync": {
self._send_response( "openClose": True,
id, "change": TextDocumentSyncKind.Incremental,
{
"capabilities": {
"textDocumentSync": {
"openClose": True,
"change": TextDocumentSyncKind.Incremental,
},
"semanticTokensProvider": {
"legend": {
"tokenTypes": ["enumMember"],
"tokenModifiers": [],
},
"full": True,
},
"completionProvider": {},
"codeActionProvider": {},
"hoverProvider": True,
"documentSymbolProvider": True,
"definitionProvider": True,
"documentFormattingProvider": True,
}, },
"serverInfo": { "semanticTokensProvider": {
"name": "Blueprint", "legend": {
"version": main.VERSION, "tokenTypes": ["enumMember"],
},
"full": True,
}, },
"completionProvider": {},
"codeActionProvider": {},
"hoverProvider": True,
}, },
) "serverInfo": {
"name": "Blueprint",
@command("shutdown") "version": main.VERSION,
def shutdown(self, id, params): },
self._send_response(id, None) })
@command("exit")
def exit(self, id, params):
self._exited = True
@command("textDocument/didOpen") @command("textDocument/didOpen")
def didOpen(self, id, params): def didOpen(self, id, params):
@ -255,23 +201,14 @@ class LanguageServer:
@command("textDocument/hover") @command("textDocument/hover")
def hover(self, id, params): def hover(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]] open_file = self._open_files[params["textDocument"]["uri"]]
docs = open_file.ast and open_file.ast.get_docs( docs = open_file.ast and open_file.ast.get_docs(utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text))
utils.pos_to_idx(
params["position"]["line"],
params["position"]["character"],
open_file.text,
)
)
if docs: if docs:
self._send_response( self._send_response(id, {
id, "contents": {
{ "kind": "markdown",
"contents": { "value": docs,
"kind": "markdown", }
"value": docs, })
}
},
)
else: else:
self._send_response(id, None) self._send_response(id, None)
@ -283,218 +220,70 @@ class LanguageServer:
self._send_response(id, []) self._send_response(id, [])
return return
idx = utils.pos_to_idx( idx = utils.pos_to_idx(params["position"]["line"], params["position"]["character"], open_file.text)
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])
completions = complete(self, open_file.ast, open_file.tokens, idx)
self._send_response(
id, [completion.to_json(True) for completion in completions]
)
@command("textDocument/formatting")
def formatting(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
if open_file.text is None:
self._send_error(id, ErrorCode.RequestFailed, "Document is not open")
return
try:
formatted_blp = formatter.format(
open_file.text,
params["options"]["tabSize"],
params["options"]["insertSpaces"],
)
except PrintableError:
self._send_error(id, ErrorCode.RequestFailed, "Could not format document")
return
lst = []
for tag, i1, i2, j1, j2 in SequenceMatcher(
None, open_file.text, formatted_blp
).get_opcodes():
if tag in ("replace", "insert", "delete"):
lst.append(
TextEdit(
Range(i1, i2, open_file.text),
"" if tag == "delete" else formatted_blp[j1:j2],
).to_json()
)
self._send_response(id, lst)
@command("textDocument/x-blueprint-compile")
def compile(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
if open_file.ast is None:
self._send_error(id, ErrorCode.RequestFailed, "Document is not open")
return
xml = None
try:
output = XmlOutput()
xml = output.emit(open_file.ast, indent=2, generated_notice=False)
except:
printerr(traceback.format_exc())
self._send_error(id, ErrorCode.RequestFailed, "Could not compile document")
return
self._send_response(id, {"xml": xml})
@command("x-blueprint/decompile")
def decompile(self, id, params):
text = params.get("text")
blp = None
if text.strip() == "":
blp = ""
printerr("Decompiled to empty blueprint because input was empty")
else:
try:
blp = decompiler.decompile_string(text)
except decompiler.UnsupportedError as e:
self._send_error(id, ErrorCode.RequestFailed, e.message)
return
except:
printerr(traceback.format_exc())
self._send_error(id, ErrorCode.RequestFailed, "Invalid input")
return
self._send_response(id, {"blp": blp})
@command("textDocument/semanticTokens/full") @command("textDocument/semanticTokens/full")
def semantic_tokens(self, id, params): def semantic_tokens(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]] open_file = self._open_files[params["textDocument"]["uri"]]
self._send_response( self._send_response(id, {
id, "data": open_file.calc_semantic_tokens(),
{ })
"data": open_file.calc_semantic_tokens(),
},
)
@command("textDocument/codeAction") @command("textDocument/codeAction")
def code_actions(self, id, params): def code_actions(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]] open_file = self._open_files[params["textDocument"]["uri"]]
range = Range( range_start = utils.pos_to_idx(params["range"]["start"]["line"], params["range"]["start"]["character"], open_file.text)
utils.pos_to_idx( range_end = utils.pos_to_idx(params["range"]["end"]["line"], params["range"]["end"]["character"], open_file.text)
params["range"]["start"]["line"],
params["range"]["start"]["character"],
open_file.text,
),
utils.pos_to_idx(
params["range"]["end"]["line"],
params["range"]["end"]["character"],
open_file.text,
),
open_file.text,
)
actions = [ actions = [
{ {
"title": action.title, "title": action.title,
"kind": "quickfix", "kind": "quickfix",
"diagnostics": [self._create_diagnostic(open_file.uri, diagnostic)], "diagnostics": [self._create_diagnostic(open_file.text, open_file.uri, diagnostic)],
"edit": { "edit": {
"changes": { "changes": {
open_file.uri: [ open_file.uri: [{
{ "range": utils.idxs_to_range(diagnostic.start, diagnostic.end, open_file.text),
"range": ( "newText": action.replace_with
action.edit_range.to_json() }]
if action.edit_range
else diagnostic.range.to_json()
),
"newText": action.replace_with,
}
]
} }
}, }
} }
for diagnostic in open_file.diagnostics for diagnostic in open_file.diagnostics
if range.overlaps(diagnostic.range) if not (diagnostic.end < range_start or diagnostic.start > range_end)
for action in diagnostic.actions for action in diagnostic.actions
] ]
self._send_response(id, actions) self._send_response(id, actions)
@command("textDocument/documentSymbol")
def document_symbols(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
symbols = open_file.ast.get_document_symbols()
def to_json(symbol: DocumentSymbol):
result = {
"name": symbol.name,
"kind": symbol.kind,
"range": symbol.range.to_json(),
"selectionRange": symbol.selection_range.to_json(),
"children": [to_json(child) for child in symbol.children],
}
if symbol.detail is not None:
result["detail"] = symbol.detail
return result
self._send_response(id, [to_json(symbol) for symbol in symbols])
@command("textDocument/definition")
def definition(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
idx = utils.pos_to_idx(
params["position"]["line"], params["position"]["character"], open_file.text
)
definition = open_file.ast.get_reference(idx)
if definition is None:
self._send_response(id, None)
else:
self._send_response(
id,
definition.to_json(open_file.uri),
)
def _send_file_updates(self, open_file: OpenFile): def _send_file_updates(self, open_file: OpenFile):
self._send_notification( self._send_notification("textDocument/publishDiagnostics", {
"textDocument/publishDiagnostics", "uri": open_file.uri,
{ "diagnostics": [self._create_diagnostic(open_file.text, open_file.uri, err) for err in open_file.diagnostics],
"uri": open_file.uri, })
"diagnostics": [
self._create_diagnostic(open_file.uri, err)
for err in open_file.diagnostics
],
},
)
def _create_diagnostic(self, uri: str, err: CompileError):
message = err.message
assert err.range is not None
for hint in err.hints:
message += "\nhint: " + hint
def _create_diagnostic(self, text, uri, err):
result = { result = {
"range": err.range.to_json(), "range": utils.idxs_to_range(err.start, err.end, text),
"message": message, "message": err.message,
"severity": ( "severity": DiagnosticSeverity.Warning if isinstance(err, CompileWarning) else DiagnosticSeverity.Error,
DiagnosticSeverity.Warning
if isinstance(err, CompileWarning)
else DiagnosticSeverity.Error
),
} }
if isinstance(err, DeprecatedWarning):
result["tags"] = [DiagnosticTag.Deprecated]
if isinstance(err, UnusedWarning):
result["tags"] = [DiagnosticTag.Unnecessary]
if len(err.references) > 0: if len(err.references) > 0:
result["relatedInformation"] = [ result["relatedInformation"] = [
{ {
"location": { "location": {
"uri": uri, "uri": uri,
"range": ref.range.to_json(), "range": utils.idxs_to_range(ref.start, ref.end, text),
}, },
"message": ref.message, "message": ref.message
} }
for ref in err.references for ref in err.references
] ]
@ -506,3 +295,4 @@ for name in dir(LanguageServer):
item = getattr(LanguageServer, name) item = getattr(LanguageServer, name)
if callable(item) and hasattr(item, "_json_method"): if callable(item) and hasattr(item, "_json_method"):
LanguageServer.commands[item._json_method] = item LanguageServer.commands[item._json_method] = item

View file

@ -18,14 +18,11 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from dataclasses import dataclass
import enum import enum
import json
import os
import typing as T import typing as T
from dataclasses import dataclass, field
from .errors import * from .errors import *
from .tokenizer import Range
from .utils import * from .utils import *
@ -34,16 +31,13 @@ class TextDocumentSyncKind(enum.IntEnum):
Full = 1 Full = 1
Incremental = 2 Incremental = 2
class CompletionItemTag(enum.IntEnum): class CompletionItemTag(enum.IntEnum):
Deprecated = 1 Deprecated = 1
class InsertTextFormat(enum.IntEnum): class InsertTextFormat(enum.IntEnum):
PlainText = 1 PlainText = 1
Snippet = 2 Snippet = 2
class CompletionItemKind(enum.IntEnum): class CompletionItemKind(enum.IntEnum):
Text = 1 Text = 1
Method = 2 Method = 2
@ -72,21 +66,15 @@ class CompletionItemKind(enum.IntEnum):
TypeParameter = 25 TypeParameter = 25
class ErrorCode(enum.IntEnum):
RequestFailed = -32803
@dataclass @dataclass
class Completion: class Completion:
label: str label: str
kind: CompletionItemKind kind: CompletionItemKind
signature: T.Optional[str] = None signature: T.Optional[str] = None
deprecated: bool = False deprecated: bool = False
sort_text: T.Optional[str] = None
docs: T.Optional[str] = None docs: T.Optional[str] = None
text: T.Optional[str] = None text: T.Optional[str] = None
snippet: T.Optional[str] = None snippet: T.Optional[str] = None
detail: T.Optional[str] = None
def to_json(self, snippets: bool): def to_json(self, snippets: bool):
insert_text = self.text or self.label insert_text = self.text or self.label
@ -99,23 +87,16 @@ class Completion:
"label": self.label, "label": self.label,
"kind": self.kind, "kind": self.kind,
"tags": [CompletionItemTag.Deprecated] if self.deprecated else None, "tags": [CompletionItemTag.Deprecated] if self.deprecated else None,
# https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemLabelDetails "detail": self.signature,
"labelDetails": ({"detail": self.signature} if self.signature else None), "documentation": {
"documentation": ( "kind": "markdown",
{ "value": self.docs,
"kind": "markdown", } if self.docs else None,
"value": self.docs,
}
if self.docs
else None
),
"deprecated": self.deprecated, "deprecated": self.deprecated,
"sortText": self.sort_text,
"insertText": insert_text, "insertText": insert_text,
"insertTextFormat": insert_text_format, "insertTextFormat": insert_text_format,
"detail": self.detail if self.detail else None,
} }
return {k: v for k, v in result.items() if v is not None} return { k: v for k, v in result.items() if v is not None }
class SemanticTokenType(enum.IntEnum): class SemanticTokenType(enum.IntEnum):
@ -129,100 +110,9 @@ class DiagnosticSeverity(enum.IntEnum):
Hint = 4 Hint = 4
class DiagnosticTag(enum.IntEnum):
Unnecessary = 1
Deprecated = 2
@dataclass @dataclass
class SemanticToken: class SemanticToken:
start: int start: int
end: int end: int
type: SemanticTokenType type: SemanticTokenType
class SymbolKind(enum.IntEnum):
File = 1
Module = 2
Namespace = 3
Package = 4
Class = 5
Method = 6
Property = 7
Field = 8
Constructor = 9
Enum = 10
Interface = 11
Function = 12
Variable = 13
Constant = 14
String = 15
Number = 16
Boolean = 17
Array = 18
Object = 19
Key = 20
Null = 21
EnumMember = 22
Struct = 23
Event = 24
Operator = 25
TypeParameter = 26
@dataclass
class DocumentSymbol:
name: str
kind: SymbolKind
range: Range
selection_range: Range
detail: T.Optional[str] = None
children: T.List["DocumentSymbol"] = field(default_factory=list)
@dataclass
class LocationLink:
origin_selection_range: Range
target_range: Range
target_selection_range: Range
def to_json(self, target_uri: str):
return {
"originSelectionRange": self.origin_selection_range.to_json(),
"targetUri": target_uri,
"targetRange": self.target_range.to_json(),
"targetSelectionRange": self.target_selection_range.to_json(),
}
@dataclass
class TextEdit:
range: Range
newText: str
def to_json(self):
return {"range": self.range.to_json(), "newText": self.newText}
_docs_sections: T.Optional[dict[str, T.Any]] = None
def get_docs_section(section_name: str) -> T.Optional[str]:
global _docs_sections
if _docs_sections is None:
try:
with open(
os.path.join(os.path.dirname(__file__), "reference_docs.json")
) as f:
_docs_sections = json.load(f)
except FileNotFoundError:
_docs_sections = {}
if section := _docs_sections.get(section_name):
content = section["content"]
link = section["link"]
content += f"\n\n---\n\n[Online documentation]({link})"
return content
else:
return None

View file

@ -18,23 +18,16 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import argparse
import difflib
import os
import sys
import typing as T import typing as T
import argparse, json, os, sys
from . import formatter, interactive_port, parser, tokenizer from .errors import PrintableError, report_bug, MultipleErrors
from .decompiler import decompile_string
from .errors import CompileError, CompilerBugError, PrintableError, report_bug
from .gir import add_typelib_search_path
from .lsp import LanguageServer from .lsp import LanguageServer
from .outputs import XmlOutput from . import parser, tokenizer, decompiler, interactive_port
from .utils import Colors from .utils import Colors
from .xml_emitter import XmlEmitter
VERSION = "uninstalled" VERSION = "uninstalled"
LIBDIR = None
class BlueprintApp: class BlueprintApp:
def main(self): def main(self):
@ -42,82 +35,19 @@ class BlueprintApp:
self.subparsers = self.parser.add_subparsers(metavar="command") self.subparsers = self.parser.add_subparsers(metavar="command")
self.parser.set_defaults(func=self.cmd_help) self.parser.set_defaults(func=self.cmd_help)
compile = self.add_subcommand( compile = self.add_subcommand("compile", "Compile blueprint files", self.cmd_compile)
"compile", "Compile blueprint files", self.cmd_compile
)
compile.add_argument("--output", dest="output", default="-") compile.add_argument("--output", dest="output", default="-")
compile.add_argument("--typelib-path", nargs="?", action="append") compile.add_argument("input", metavar="filename", default=sys.stdin, type=argparse.FileType('r'))
compile.add_argument(
"input", metavar="filename", default=sys.stdin, type=argparse.FileType("r")
)
batch_compile = self.add_subcommand( batch_compile = self.add_subcommand("batch-compile", "Compile many blueprint files at once", self.cmd_batch_compile)
"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("output_dir", metavar="output-dir")
batch_compile.add_argument("input_dir", metavar="input-dir") batch_compile.add_argument("input_dir", metavar="input-dir")
batch_compile.add_argument("--typelib-path", nargs="?", action="append") batch_compile.add_argument("inputs", nargs="+", metavar="filenames", default=sys.stdin, type=argparse.FileType('r'))
batch_compile.add_argument(
"inputs",
nargs="+",
metavar="filenames",
default=sys.stdin,
type=argparse.FileType("r"),
)
format = self.add_subcommand(
"format", "Format given blueprint files", self.cmd_format
)
format.add_argument(
"-f",
"--fix",
help="Apply the edits to the files",
default=False,
action="store_true",
)
format.add_argument(
"-t",
"--tabs",
help="Use tabs instead of spaces",
default=False,
action="store_true",
)
format.add_argument(
"-s",
"--spaces-num",
help="How many spaces should be used per indent",
default=2,
type=int,
)
format.add_argument(
"-n",
"--no-diff",
help="Do not print a full diff of the changes",
default=False,
action="store_true",
)
format.add_argument(
"inputs",
nargs="+",
metavar="filenames",
)
decompile = self.add_subcommand(
"decompile", "Convert .ui XML files to blueprint", self.cmd_decompile
)
decompile.add_argument("--output", dest="output", default="-")
decompile.add_argument("--typelib-path", nargs="?", action="append")
decompile.add_argument(
"input", metavar="filename", default=sys.stdin, type=argparse.FileType("r")
)
port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port) port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port)
lsp = self.add_subcommand( lsp = self.add_subcommand("lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp)
"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) self.add_subcommand("help", "Show this message", self.cmd_help)
@ -135,19 +65,18 @@ class BlueprintApp:
except: except:
report_bug() report_bug()
def add_subcommand(self, name: str, help: str, func):
def add_subcommand(self, name, help, func):
parser = self.subparsers.add_parser(name, help=help) parser = self.subparsers.add_parser(name, help=help)
parser.set_defaults(func=func) parser.set_defaults(func=func)
return parser return parser
def cmd_help(self, opts): def cmd_help(self, opts):
self.parser.print_help() self.parser.print_help()
def cmd_compile(self, opts):
if opts.typelib_path != None:
for typelib_path in opts.typelib_path:
add_typelib_search_path(typelib_path)
def cmd_compile(self, opts):
data = opts.input.read() data = opts.input.read()
try: try:
xml, warnings = self._compile(data) xml, warnings = self._compile(data)
@ -161,24 +90,17 @@ class BlueprintApp:
with open(opts.output, "w") as file: with open(opts.output, "w") as file:
file.write(xml) file.write(xml)
except PrintableError as e: except PrintableError as e:
e.pretty_print(opts.input.name, data, stream=sys.stderr) e.pretty_print(opts.input.name, data)
sys.exit(1) sys.exit(1)
def cmd_batch_compile(self, opts):
if opts.typelib_path != None:
for typelib_path in opts.typelib_path:
add_typelib_search_path(typelib_path)
def cmd_batch_compile(self, opts):
for file in opts.inputs: for file in opts.inputs:
data = file.read() data = file.read()
file_abs = os.path.abspath(file.name)
input_dir_abs = os.path.abspath(opts.input_dir)
try: try:
if not os.path.commonpath([file_abs, input_dir_abs]): if not os.path.commonpath([file.name, opts.input_dir]):
print( print(f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}")
f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}"
)
sys.exit(1) sys.exit(1)
xml, warnings = self._compile(data) xml, warnings = self._compile(data)
@ -189,8 +111,9 @@ class BlueprintApp:
path = os.path.join( path = os.path.join(
opts.output_dir, opts.output_dir,
os.path.relpath( os.path.relpath(
os.path.splitext(file.name)[0] + ".ui", opts.input_dir os.path.splitext(file.name)[0] + ".ui",
), opts.input_dir
)
) )
os.makedirs(os.path.dirname(path), exist_ok=True) os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as file: with open(path, "w") as file:
@ -199,157 +122,29 @@ class BlueprintApp:
e.pretty_print(file.name, data) e.pretty_print(file.name, data)
sys.exit(1) sys.exit(1)
def cmd_format(self, opts):
input_files = []
missing_files = []
panic = False
formatted_files = 0
skipped_files = 0
for path in opts.inputs:
if os.path.isfile(path):
input_files.append(path)
elif os.path.isdir(path):
for root, subfolders, files in os.walk(path):
for file in files:
if file.endswith(".blp"):
input_files.append(os.path.join(root, file))
else:
missing_files.append(path)
for file in input_files:
with open(file, "r+") as file:
data = file.read()
errored = False
try:
self._compile(data)
except:
errored = True
formatted_str = formatter.format(data, opts.spaces_num, not opts.tabs)
if data != formatted_str:
happened = "Would format"
if opts.fix and not errored:
file.seek(0)
file.truncate()
file.write(formatted_str)
happened = "Formatted"
if not opts.no_diff:
diff_lines = []
a_lines = data.splitlines(keepends=True)
b_lines = formatted_str.splitlines(keepends=True)
for line in difflib.unified_diff(
a_lines, b_lines, fromfile=file.name, tofile=file.name, n=5
):
# Work around https://bugs.python.org/issue2142
# See:
# https://www.gnu.org/software/diffutils/manual/html_node/Incomplete-Lines.html
if line[-1] == "\n":
diff_lines.append(line)
else:
diff_lines.append(line + "\n")
diff_lines.append("\\ No newline at end of file\n")
print("".join(diff_lines))
to_print = Colors.BOLD
if errored:
to_print += f"{Colors.RED}Skipped {file.name}: Will not overwrite file with compile errors"
panic = True
skipped_files += 1
else:
to_print += f"{happened} {file.name}"
formatted_files += 1
print(to_print)
print(Colors.CLEAR)
missing_num = len(missing_files)
summary = ""
if missing_num > 0:
print(
f"{Colors.BOLD}{Colors.RED}Could not find files:{Colors.CLEAR}{Colors.BOLD}"
)
for path in missing_files:
print(f" {path}")
print(Colors.CLEAR)
panic = True
if len(input_files) == 0:
print(f"{Colors.RED}No Blueprint files found")
sys.exit(1)
def would_be(verb):
return verb if opts.fix else f"would be {verb}"
def how_many(count, bold=True):
string = f"{Colors.BLUE}{count} {'files' if count != 1 else 'file'}{Colors.CLEAR}"
return Colors.BOLD + string + Colors.BOLD if bold else Colors.CLEAR + string
if formatted_files > 0:
summary += f"{how_many(formatted_files)} {would_be('formatted')}, "
panic = panic or not opts.fix
left_files = len(input_files) - formatted_files - skipped_files
summary += f"{how_many(left_files, False)} {would_be('left unchanged')}"
if skipped_files > 0:
summary += f", {how_many(skipped_files)} {would_be('skipped')}"
if missing_num > 0:
summary += f", {how_many(missing_num)} not found"
print(summary + Colors.CLEAR)
if panic:
sys.exit(1)
def cmd_decompile(self, opts):
if opts.typelib_path != None:
for typelib_path in opts.typelib_path:
add_typelib_search_path(typelib_path)
data = opts.input.read()
try:
decompiled = decompile_string(data)
if opts.output == "-":
print(decompiled)
else:
with open(opts.output, "w") as file:
file.write(decompiled)
except PrintableError as e:
e.pretty_print(opts.input.name, data, stream=sys.stderr)
sys.exit(1)
def cmd_lsp(self, opts): def cmd_lsp(self, opts):
langserv = LanguageServer() langserv = LanguageServer(opts.logfile)
langserv.run() langserv.run()
def cmd_port(self, opts): def cmd_port(self, opts):
interactive_port.run(opts) interactive_port.run(opts)
def _compile(self, data: str) -> T.Tuple[str, T.List[CompileError]]:
def _compile(self, data: str) -> T.Tuple[str, T.List[PrintableError]]:
tokens = tokenizer.tokenize(data) tokens = tokenizer.tokenize(data)
ast, errors, warnings = parser.parse(tokens) ast, errors, warnings = parser.parse(tokens)
if errors: if errors:
raise errors raise errors
if ast is None: if len(ast.errors):
raise CompilerBugError() raise MultipleErrors(ast.errors)
formatter = XmlOutput() return ast.generate(), warnings
return formatter.emit(ast), warnings
def main(version, libdir): def main(version):
global VERSION, LIBDIR global VERSION
VERSION, LIBDIR = version, libdir VERSION = version
BlueprintApp().main() BlueprintApp().main()

View file

@ -1,9 +0,0 @@
from ..language import UI
class OutputFormat:
def emit(self, ui: UI) -> str:
raise NotImplementedError()
from .xml import XmlOutput

View file

@ -1,426 +0,0 @@
import typing as T
from ...language import *
from .. import OutputFormat
from .xml_emitter import XmlEmitter
class XmlOutput(OutputFormat):
def emit(self, ui: UI, indent=2, generated_notice=True) -> str:
xml = XmlEmitter(indent, generated_notice)
self._emit_ui(ui, xml)
return xml.result
def _emit_ui(self, ui: UI, xml: XmlEmitter):
if domain := ui.translation_domain:
xml.start_tag("interface", domain=domain.domain)
else:
xml.start_tag("interface")
self._emit_gtk_directive(ui.gtk_decl, xml)
for x in ui.contents:
if isinstance(x, Template):
self._emit_template(x, xml)
elif isinstance(x, Object):
self._emit_object(x, xml)
elif isinstance(x, Menu):
self._emit_menu(x, xml)
else:
raise CompilerBugError()
xml.end_tag()
def _emit_gtk_directive(self, gtk: GtkDirective, xml: XmlEmitter):
xml.put_self_closing("requires", lib="gtk", version=gtk.gir_namespace.version)
def _emit_template(self, template: Template, xml: XmlEmitter):
xml.start_tag(
"template", **{"class": template.gir_class}, parent=template.parent_type
)
self._emit_object_or_template(template, xml)
xml.end_tag()
def _emit_object(self, obj: Object, xml: XmlEmitter):
xml.start_tag(
"object",
**{"class": obj.class_name},
id=obj.id,
)
self._emit_object_or_template(obj, xml)
xml.end_tag()
def _emit_object_or_template(
self, obj: T.Union[Object, Template, ExtListItemFactory], xml: XmlEmitter
):
for child in obj.content.children:
if isinstance(child, Property):
self._emit_property(child, xml)
elif isinstance(child, Signal):
self._emit_signal(child, xml)
elif isinstance(child, Child):
self._emit_child(child, xml)
else:
self._emit_extensions(child, xml)
# List action widgets
action_widgets = obj.action_widgets
if action_widgets:
xml.start_tag("action-widgets")
for action_widget in action_widgets:
xml.start_tag(
"action-widget",
response=action_widget.response_id,
default=action_widget.is_default or None,
)
xml.put_text(action_widget.widget_id)
xml.end_tag()
xml.end_tag()
def _emit_menu(self, menu: Menu, xml: XmlEmitter):
xml.start_tag(menu.tag, id=menu.id)
for child in menu.items:
if isinstance(child, Menu):
self._emit_menu(child, xml)
elif isinstance(child, MenuAttribute):
xml.start_tag(
"attribute",
name=child.name,
**self._translated_string_attrs(child.value.child),
)
xml.put_text(child.value.string)
xml.end_tag()
else:
raise CompilerBugError()
xml.end_tag()
def _emit_property(self, property: Property, xml: XmlEmitter):
value = property.value
props: T.Dict[str, T.Optional[str]] = {
"name": property.name,
}
if isinstance(value, Value):
child = value.child
if isinstance(child, Translated):
xml.start_tag(
"property", **props, **self._translated_string_attrs(child)
)
xml.put_text(child.string)
xml.end_tag()
else:
xml.start_tag("property", **props)
self._emit_value(value, xml)
xml.end_tag()
elif isinstance(value, Binding):
if simple := value.simple_binding:
props["bind-source"] = self._object_id(value, simple.source)
props["bind-property"] = simple.property_name
flags = []
if not simple.no_sync_create:
flags.append("sync-create")
if simple.inverted:
flags.append("invert-boolean")
if simple.bidirectional:
flags.append("bidirectional")
props["bind-flags"] = "|".join(flags) or None
xml.put_self_closing("property", **props)
else:
xml.start_tag("binding", **props)
self._emit_expression(value.expression, xml)
xml.end_tag()
elif isinstance(value, ExprValue):
xml.start_tag("property", **props)
self._emit_expression(value.expression, xml)
xml.end_tag()
elif isinstance(value, ObjectValue):
xml.start_tag("property", **props)
self._emit_object(value.object, xml)
xml.end_tag()
elif isinstance(value, ArrayValue):
xml.start_tag("property", **props)
values = list(value.values)
for value in values[:-1]:
self._emit_value(value, xml)
xml.put_text("\n")
self._emit_value(values[-1], xml)
xml.end_tag()
else:
raise CompilerBugError()
def _translated_string_attrs(
self, translated: T.Optional[T.Union[QuotedLiteral, Translated]]
) -> T.Dict[str, T.Optional[str]]:
if translated is None:
return {}
elif isinstance(translated, QuotedLiteral):
return {}
else:
return {"translatable": "yes", "context": translated.translate_context}
def _emit_signal(self, signal: Signal, xml: XmlEmitter):
name = signal.name
if signal.detail_name:
name += "::" + signal.detail_name
xml.put_self_closing(
"signal",
name=name,
handler=signal.handler,
swapped=signal.is_swapped,
after=signal.is_after or None,
object=(
self._object_id(signal, signal.object_id) if signal.object_id else None
),
)
def _emit_child(self, child: Child, xml: XmlEmitter):
child_type = internal_child = None
if child.annotation is not None:
annotation = child.annotation.child
if isinstance(annotation, ChildType):
child_type = annotation.child_type
elif isinstance(annotation, ChildInternal):
internal_child = annotation.internal_child
elif isinstance(annotation, ChildExtension):
child_type = "action"
else:
raise CompilerBugError()
xml.start_tag("child", type=child_type, internal_child=internal_child)
self._emit_object(child.object, xml)
xml.end_tag()
def _emit_literal(self, literal: Literal, xml: XmlEmitter):
value = literal.value
if isinstance(value, IdentLiteral):
value_type = value.context[ValueTypeCtx].value_type
if isinstance(value_type, gir.BoolType):
xml.put_text(value.ident)
elif isinstance(value_type, gir.Enumeration):
xml.put_text(str(value_type.members[value.ident].value))
else:
xml.put_text(self._object_id(value, value.ident))
elif isinstance(value, TypeLiteral):
xml.put_text(value.type_name.glib_type_name)
else:
if isinstance(value.value, float) and value.value == int(value.value):
xml.put_text(int(value.value))
else:
xml.put_text(value.value)
def _emit_value(self, value: Value, xml: XmlEmitter):
if isinstance(value.child, Literal):
self._emit_literal(value.child, xml)
elif isinstance(value.child, Flags):
xml.put_text(
"|".join([str(flag.value or flag.name) for flag in value.child.flags])
)
else:
raise CompilerBugError()
def _emit_expression(self, expression: Expression, xml: XmlEmitter):
self._emit_expression_part(expression.last, xml)
def _emit_expression_part(self, expression: ExprBase, xml: XmlEmitter):
if isinstance(expression, LiteralExpr):
self._emit_literal_expr(expression, xml)
elif isinstance(expression, LookupOp):
self._emit_lookup_op(expression, xml)
elif isinstance(expression, Expression):
self._emit_expression(expression, xml)
elif isinstance(expression, CastExpr):
self._emit_cast_expr(expression, xml)
elif isinstance(expression, ClosureExpr):
self._emit_closure_expr(expression, xml)
else:
raise CompilerBugError()
def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter):
if expr.is_this:
return
if expr.is_object:
xml.start_tag("constant")
else:
xml.start_tag("constant", type=expr.type)
self._emit_literal(expr.literal, xml)
xml.end_tag()
def _emit_lookup_op(self, expr: LookupOp, xml: XmlEmitter):
xml.start_tag("lookup", name=expr.property_name, type=expr.lhs.type)
self._emit_expression_part(expr.lhs, xml)
xml.end_tag()
def _emit_cast_expr(self, expr: CastExpr, xml: XmlEmitter):
self._emit_expression_part(expr.lhs, xml)
def _emit_closure_expr(self, expr: ClosureExpr, xml: XmlEmitter):
xml.start_tag("closure", function=expr.closure_name, type=expr.type)
for arg in expr.args:
self._emit_expression_part(arg.expr, xml)
xml.end_tag()
def _emit_attribute(
self,
tag: str,
attr: str,
name: str,
value: T.Union[Value, StringValue],
xml: XmlEmitter,
):
attrs = {attr: name}
if isinstance(value.child, Translated):
xml.start_tag(tag, **attrs, **self._translated_string_attrs(value.child))
xml.put_text(value.child.string)
xml.end_tag()
elif isinstance(value.child, QuotedLiteral):
xml.start_tag(tag, **attrs)
xml.put_text(value.child.value)
xml.end_tag()
else:
xml.start_tag(tag, **attrs)
self._emit_value(value, xml)
xml.end_tag()
def _emit_extensions(self, extension, xml: XmlEmitter):
if isinstance(extension, ExtAccessibility):
xml.start_tag("accessibility")
for property in extension.properties:
for val in property.values:
self._emit_attribute(
property.tag_name, "name", property.name, val, xml
)
xml.end_tag()
elif isinstance(extension, AdwBreakpointCondition):
xml.start_tag("condition")
xml.put_text(extension.condition)
xml.end_tag()
elif isinstance(extension, AdwBreakpointSetters):
for setter in extension.setters:
if setter.value is None:
continue
attrs = {}
if isinstance(setter.value.child, Translated):
attrs = self._translated_string_attrs(setter.value.child)
xml.start_tag(
"setter",
object=self._object_id(setter, setter.object_id),
property=setter.property_name,
**attrs,
)
if isinstance(setter.value.child, Translated):
xml.put_text(setter.value.child.string)
elif (
isinstance(setter.value.child, Literal)
and isinstance(setter.value.child.value, IdentLiteral)
and setter.value.child.value.ident == "null"
and setter.context[ScopeCtx].objects.get("null") is None
):
pass
else:
self._emit_value(setter.value, xml)
xml.end_tag()
elif isinstance(extension, Filters):
xml.start_tag(extension.tokens["tag_name"])
for prop in extension.children:
xml.start_tag(prop.tokens["tag_name"])
xml.put_text(prop.tokens["name"])
xml.end_tag()
xml.end_tag()
elif isinstance(extension, ExtComboBoxItems):
xml.start_tag("items")
for prop in extension.children:
self._emit_attribute("item", "id", prop.name, prop.value, xml)
xml.end_tag()
elif isinstance(extension, ExtLayout):
xml.start_tag("layout")
for prop in extension.children:
self._emit_attribute("property", "name", prop.name, prop.value, xml)
xml.end_tag()
elif isinstance(extension, ExtAdwResponseDialog):
xml.start_tag("responses")
for response in extension.responses:
xml.start_tag(
"response",
id=response.id,
**self._translated_string_attrs(response.value.child),
enabled=None if response.enabled else "false",
appearance=response.appearance,
)
xml.put_text(response.value.string)
xml.end_tag()
xml.end_tag()
elif isinstance(extension, ExtScaleMarks):
xml.start_tag("marks")
for mark in extension.marks:
label = mark.label.child if mark.label is not None else None
xml.start_tag(
"mark",
value=mark.value,
position=mark.position,
**self._translated_string_attrs(label),
)
if mark.label is not None:
xml.put_text(mark.label.string)
xml.end_tag()
xml.end_tag()
elif isinstance(extension, ExtStringListStrings):
xml.start_tag("items")
for string in extension.children:
value = string.child
xml.start_tag("item", **self._translated_string_attrs(value.child))
xml.put_text(value.string)
xml.end_tag()
xml.end_tag()
elif isinstance(extension, ExtListItemFactory):
child_xml = XmlEmitter(generated_notice=False)
child_xml.start_tag("interface")
child_xml.start_tag("template", **{"class": extension.gir_class})
self._emit_object_or_template(extension, child_xml)
child_xml.end_tag()
child_xml.end_tag()
xml.start_tag("property", name="bytes")
xml.put_cdata(child_xml.result)
xml.end_tag()
elif isinstance(extension, ExtStyles):
xml.start_tag("style")
for style in extension.children:
xml.put_self_closing("class", name=style.name)
xml.end_tag()
elif isinstance(extension, ExtSizeGroupWidgets):
xml.start_tag("widgets")
for prop in extension.children:
xml.put_self_closing("widget", name=prop.name)
xml.end_tag()
else:
raise CompilerBugError()
def _object_id(self, node: AstNode, id: str) -> str:
if id == "template" and node.context[ScopeCtx].template is not None:
return node.context[ScopeCtx].template.gir_class.glib_type_name
else:
return id

View file

@ -17,35 +17,30 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
"""Utilities for parsing an AST from a token stream.""" """ Utilities for parsing an AST from a token stream. """
import typing as T import typing as T
from collections import defaultdict
from enum import Enum from enum import Enum
from . import utils from .errors import assert_true, CompilerBugError, CompileError, CompileWarning, UnexpectedTokenError
from .ast_utils import AstNode from .tokenizer import Token, TokenType
from .errors import (
CompileError,
CompilerBugError,
CompileWarning,
UnexpectedTokenError,
assert_true,
)
from .tokenizer import Range, Token, TokenType
SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE] SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE]
class ParseResult(Enum): class ParseResult(Enum):
"""Represents the result of parsing. The extra EMPTY result is necessary """ Represents the result of parsing. The extra EMPTY result is necessary
to avoid freezing the parser: imagine a ZeroOrMore node containing a node to avoid freezing the parser: imagine a ZeroOrMore node containing a node
that can match empty. It will repeatedly match empty and never advance that can match empty. It will repeatedly match empty and never advance
the parser. So, ZeroOrMore stops when a failed *or empty* match is the parser. So, ZeroOrMore stops when a failed *or empty* match is
made.""" made. """
SUCCESS = 0 SUCCESS = 0
FAILURE = 1 FAILURE = 1
EMPTY = 2 EMPTY = 2
def matched(self): def matched(self):
return self == ParseResult.SUCCESS return self == ParseResult.SUCCESS
@ -58,82 +53,75 @@ class ParseResult(Enum):
class ParseGroup: class ParseGroup:
"""A matching group. Match groups have an AST type, children grouped by """ 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 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 be converted to AST nodes by passing the children and key=value pairs to
the AST node constructor.""" the AST node constructor. """
def __init__(self, ast_type: T.Type[AstNode], start: int, text: str): def __init__(self, ast_type, start: int):
self.ast_type = ast_type self.ast_type = ast_type
self.children: T.List[ParseGroup] = [] self.children: T.List[ParseGroup] = []
self.keys: T.Dict[str, T.Any] = {} self.keys: T.Dict[str, T.Any] = {}
self.tokens: T.Dict[str, T.Optional[Token]] = {} self.tokens: T.Dict[str, Token] = {}
self.ranges: T.Dict[str, Range] = {}
self.start = start self.start = start
self.end: T.Optional[int] = None self.end = None
self.incomplete = False self.incomplete = False
self.text = text
def add_child(self, child: "ParseGroup"): def add_child(self, child):
self.children.append(child) self.children.append(child)
def set_val(self, key: str, val: T.Any, token: T.Optional[Token]): def set_val(self, key, val, token):
assert_true(key not in self.keys) assert_true(key not in self.keys)
self.keys[key] = val self.keys[key] = val
self.tokens[key] = token self.tokens[key] = token
if token:
self.set_range(key, token.range)
def set_range(self, key: str, range: Range):
assert_true(key not in self.ranges)
self.ranges[key] = range
def to_ast(self): def to_ast(self):
"""Creates an AST node from the match group.""" """ Creates an AST node from the match group. """
children = [child.to_ast() for child in self.children] children = [child.to_ast() for child in self.children]
try: try:
return self.ast_type(self, children, self.keys, incomplete=self.incomplete) return self.ast_type(self, children, self.keys, incomplete=self.incomplete)
except TypeError: # pragma: no cover except TypeError as e:
raise CompilerBugError( raise CompilerBugError(f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace.")
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: class ParseContext:
"""Contains the state of the parser.""" """ Contains the state of the parser. """
def __init__(self, tokens: T.List[Token], text: str, index=0): def __init__(self, tokens, index=0):
self.tokens = tokens self.tokens = list(tokens)
self.text = text
self.binding_power = 0
self.index = index self.index = index
self.start = index self.start = index
self.group: T.Optional[ParseGroup] = None self.group = None
self.group_keys: T.Dict[str, T.Tuple[T.Any, T.Optional[Token]]] = {} self.group_keys = {}
self.group_children: T.List[ParseGroup] = [] self.group_children = []
self.group_ranges: T.Dict[str, Range] = {} self.last_group = None
self.last_group: T.Optional[ParseGroup] = None
self.group_incomplete = False self.group_incomplete = False
self.errors: T.List[CompileError] = [] self.errors = []
self.warnings: T.List[CompileWarning] = [] self.warnings = []
def create_child(self) -> "ParseContext":
"""Creates a new ParseContext at this context's position. The new 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 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 new context will be applied to "self". If parsing fails, the new
context will be discarded.""" context will be discarded. """
ctx = ParseContext(self.tokens, self.text, self.index) ctx = ParseContext(self.tokens, self.index)
ctx.errors = self.errors ctx.errors = self.errors
ctx.warnings = self.warnings ctx.warnings = self.warnings
ctx.binding_power = self.binding_power
return ctx return ctx
def apply_child(self, other: "ParseContext"): def apply_child(self, other):
"""Applies a child context to this context.""" """ Applies a child context to this context. """
if other.group is not None: if other.group is not None:
# If the other context had a match group, collect all the matched # If the other context had a match group, collect all the matched
@ -142,8 +130,6 @@ class ParseContext:
other.group.set_val(key, val, token) other.group.set_val(key, val, token)
for child in other.group_children: for child in other.group_children:
other.group.add_child(child) other.group.add_child(child)
for key, range in other.group_ranges.items():
other.group.set_range(key, range)
other.group.end = other.tokens[other.index - 1].end other.group.end = other.tokens[other.index - 1].end
other.group.incomplete = other.group_incomplete other.group.incomplete = other.group_incomplete
self.group_children.append(other.group) self.group_children.append(other.group)
@ -152,7 +138,6 @@ class ParseContext:
# its matched values # its matched values
self.group_keys = {**self.group_keys, **other.group_keys} self.group_keys = {**self.group_keys, **other.group_keys}
self.group_children += other.group_children self.group_children += other.group_children
self.group_ranges = {**self.group_ranges, **other.group_ranges}
self.group_incomplete |= other.group_incomplete self.group_incomplete |= other.group_incomplete
self.index = other.index self.index = other.index
@ -163,50 +148,43 @@ class ParseContext:
elif other.last_group: elif other.last_group:
self.last_group = other.last_group self.last_group = other.last_group
def start_group(self, ast_type: T.Type[AstNode]):
"""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, self.text)
def set_group_val(self, key: str, value: T.Any, token: T.Optional[Token]): def start_group(self, ast_type):
"""Sets a matched key=value pair on the current match group.""" """ 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) assert_true(key not in self.group_keys)
self.group_keys[key] = (value, token) self.group_keys[key] = (value, token)
def set_mark(self, key: str):
"""Sets a zero-length range on the current match group at the current position."""
self.group_ranges[key] = Range(
self.tokens[self.index].start, self.tokens[self.index].start, self.text
)
def set_group_incomplete(self): def set_group_incomplete(self):
"""Marks the current match group as incomplete (it could not be fully """ Marks the current match group as incomplete (it could not be fully
parsed, but the parser recovered).""" parsed, but the parser recovered). """
self.group_incomplete = True self.group_incomplete = True
def skip(self): def skip(self):
"""Skips whitespace and comments.""" """ Skips whitespace and comments. """
while ( while self.index < len(self.tokens) and self.tokens[self.index].type in SKIP_TOKENS:
self.index < len(self.tokens)
and self.tokens[self.index].type in SKIP_TOKENS
):
self.index += 1 self.index += 1
def next_token(self) -> Token: def next_token(self) -> Token:
"""Advances the token iterator and returns the next token.""" """ Advances the token iterator and returns the next token. """
self.skip() self.skip()
token = self.tokens[self.index] token = self.tokens[self.index]
self.index += 1 self.index += 1
return token return token
def peek_token(self) -> Token: def peek_token(self) -> Token:
"""Returns the next token without advancing the iterator.""" """ Returns the next token without advancing the iterator. """
self.skip() self.skip()
token = self.tokens[self.index] token = self.tokens[self.index]
return token return token
def skip_unexpected_token(self): def skip_unexpected_token(self):
"""Skips a token and logs an "unexpected token" error.""" """ Skips a token and logs an "unexpected token" error. """
self.skip() self.skip()
start = self.tokens[self.index].start start = self.tokens[self.index].start
@ -214,24 +192,22 @@ class ParseContext:
self.skip() self.skip()
end = self.tokens[self.index - 1].end end = self.tokens[self.index - 1].end
if ( if (len(self.errors)
len(self.errors) and isinstance((err := self.errors[-1]), UnexpectedTokenError)
and isinstance((err := self.errors[-1]), UnexpectedTokenError) and err.end == start):
and err.range.end == start err.end = end
):
err.range.end = end
else: else:
self.errors.append(UnexpectedTokenError(Range(start, end, self.text))) self.errors.append(UnexpectedTokenError(start, end))
def is_eof(self) -> bool: def is_eof(self) -> Token:
return self.index >= len(self.tokens) or self.peek_token().type == TokenType.EOF return self.index >= len(self.tokens) or self.peek_token().type == TokenType.EOF
class ParseNode: class ParseNode:
"""Base class for the nodes in the parser tree.""" """ Base class for the nodes in the parser tree. """
def parse(self, ctx: ParseContext) -> ParseResult: def parse(self, ctx: ParseContext) -> ParseResult:
"""Attempts to match the ParseNode at the context's current location.""" """ Attempts to match the ParseNode at the context's current location. """
start_idx = ctx.index start_idx = ctx.index
inner_ctx = ctx.create_child() inner_ctx = ctx.create_child()
@ -247,45 +223,65 @@ class ParseNode:
def _parse(self, ctx: ParseContext) -> bool: def _parse(self, ctx: ParseContext) -> bool:
raise NotImplementedError() raise NotImplementedError()
def err(self, message: str) -> "Err": def err(self, message):
"""Causes this ParseNode to raise an exception if it fails to parse. """ Causes this ParseNode to raise an exception if it fails to parse.
This prevents the parser from backtracking, so you should understand This prevents the parser from backtracking, so you should understand
what it does and how the parser works before using it.""" what it does and how the parser works before using it. """
return Err(self, message) return Err(self, message)
def expected(self, expect: str) -> "Err": def expected(self, expect):
"""Convenience method for err().""" """ Convenience method for err(). """
return self.err("Expected " + expect) 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): class Err(ParseNode):
"""ParseNode that emits a compile error if it fails to parse.""" """ ParseNode that emits a compile error if it fails to parse. """
def __init__(self, child, message: str): def __init__(self, child, message):
self.child = to_parse_node(child) self.child = to_parse_node(child)
self.message = message self.message = message
def _parse(self, ctx: ParseContext): def _parse(self, ctx):
if self.child.parse(ctx).failed(): if self.child.parse(ctx).failed():
start_idx = ctx.start start_idx = ctx.start
while ctx.tokens[start_idx].type in SKIP_TOKENS: while ctx.tokens[start_idx].type in SKIP_TOKENS:
start_idx += 1 start_idx += 1
start_token = ctx.tokens[start_idx]
raise CompileError( start_token = ctx.tokens[start_idx]
self.message, Range(start_token.start, start_token.start, ctx.text) 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 return True
class Fail(ParseNode): class Fail(ParseNode):
"""ParseNode that emits a compile error if it parses successfully.""" """ ParseNode that emits a compile error if it parses successfully. """
def __init__(self, child, message: str): def __init__(self, child, message):
self.child = to_parse_node(child) self.child = to_parse_node(child)
self.message = message self.message = message
def _parse(self, ctx: ParseContext): def _parse(self, ctx):
if self.child.parse(ctx).succeeded(): if self.child.parse(ctx).succeeded():
start_idx = ctx.start start_idx = ctx.start
while ctx.tokens[start_idx].type in SKIP_TOKENS: while ctx.tokens[start_idx].type in SKIP_TOKENS:
@ -293,16 +289,13 @@ class Fail(ParseNode):
start_token = ctx.tokens[start_idx] start_token = ctx.tokens[start_idx]
end_token = ctx.tokens[ctx.index] end_token = ctx.tokens[ctx.index]
raise CompileError( raise CompileError(self.message, start_token.start, end_token.end)
self.message, Range.join(start_token.range, end_token.range)
)
return True return True
class Group(ParseNode): class Group(ParseNode):
"""ParseNode that creates a match group.""" """ ParseNode that creates a match group. """
def __init__(self, ast_type, child):
def __init__(self, ast_type: T.Type[AstNode], child):
self.ast_type = ast_type self.ast_type = ast_type
self.child = to_parse_node(child) self.child = to_parse_node(child)
@ -313,8 +306,7 @@ class Group(ParseNode):
class Sequence(ParseNode): class Sequence(ParseNode):
"""ParseNode that attempts to match all of its children in sequence.""" """ ParseNode that attempts to match all of its children in sequence. """
def __init__(self, *children): def __init__(self, *children):
self.children = [to_parse_node(child) for child in children] self.children = [to_parse_node(child) for child in children]
@ -326,9 +318,8 @@ class Sequence(ParseNode):
class Statement(ParseNode): class Statement(ParseNode):
"""ParseNode that attempts to match all of its children in sequence. If any """ 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.""" child raises an error, the error will be logged but parsing will continue. """
def __init__(self, *children): def __init__(self, *children):
self.children = [to_parse_node(child) for child in children] self.children = [to_parse_node(child) for child in children]
@ -344,23 +335,21 @@ class Statement(ParseNode):
token = ctx.peek_token() token = ctx.peek_token()
if str(token) != ";": if str(token) != ";":
ctx.errors.append(CompileError("Expected `;`", token.range)) ctx.errors.append(CompileError("Expected `;`", token.start, token.end))
else: else:
ctx.next_token() ctx.next_token()
return True return True
class AnyOf(ParseNode): class AnyOf(ParseNode):
"""ParseNode that attempts to match exactly one of its children. Child """ ParseNode that attempts to match exactly one of its children. Child
nodes are attempted in order.""" nodes are attempted in order. """
def __init__(self, *children): def __init__(self, *children):
self.children = children self.children = children
@property @property
def children(self): def children(self):
return self._children return self._children
@children.setter @children.setter
def children(self, children): def children(self, children):
self._children = [to_parse_node(child) for child in children] self._children = [to_parse_node(child) for child in children]
@ -373,18 +362,14 @@ class AnyOf(ParseNode):
class Until(ParseNode): class Until(ParseNode):
"""ParseNode that repeats its child until a delimiting token is found. If """ 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 the child does not match, one token is skipped and the match is attempted
again.""" again. """
def __init__(self, child, delimiter):
def __init__(self, child, delimiter, between_delimiter=None):
self.child = to_parse_node(child) self.child = to_parse_node(child)
self.delimiter = to_parse_node(delimiter) self.delimiter = to_parse_node(delimiter)
self.between_delimiter = (
to_parse_node(between_delimiter) if between_delimiter is not None else None
)
def _parse(self, ctx: ParseContext): def _parse(self, ctx):
while not self.delimiter.parse(ctx).succeeded(): while not self.delimiter.parse(ctx).succeeded():
if ctx.is_eof(): if ctx.is_eof():
return False return False
@ -392,17 +377,6 @@ class Until(ParseNode):
try: try:
if not self.child.parse(ctx).matched(): if not self.child.parse(ctx).matched():
ctx.skip_unexpected_token() ctx.skip_unexpected_token()
if (
self.between_delimiter is not None
and not self.between_delimiter.parse(ctx).succeeded()
):
if self.delimiter.parse(ctx).succeeded():
return True
else:
if ctx.is_eof():
return False
ctx.skip_unexpected_token()
except CompileError as e: except CompileError as e:
ctx.errors.append(e) ctx.errors.append(e)
ctx.next_token() ctx.next_token()
@ -411,13 +385,13 @@ class Until(ParseNode):
class ZeroOrMore(ParseNode): class ZeroOrMore(ParseNode):
"""ParseNode that matches its child any number of times (including zero """ 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 times). It cannot fail to parse. If its child raises an exception, one token
will be skipped and parsing will continue.""" will be skipped and parsing will continue. """
def __init__(self, child): def __init__(self, child):
self.child = to_parse_node(child) self.child = to_parse_node(child)
def _parse(self, ctx): def _parse(self, ctx):
while True: while True:
try: try:
@ -429,9 +403,8 @@ class ZeroOrMore(ParseNode):
class Delimited(ParseNode): class Delimited(ParseNode):
"""ParseNode that matches its first child any number of times (including zero """ ParseNode that matches its first child any number of times (including zero
times) with its second child in between and optionally at the end.""" times) with its second child in between and optionally at the end. """
def __init__(self, child, delimiter): def __init__(self, child, delimiter):
self.child = to_parse_node(child) self.child = to_parse_node(child)
self.delimiter = to_parse_node(delimiter) self.delimiter = to_parse_node(delimiter)
@ -443,9 +416,8 @@ class Delimited(ParseNode):
class Optional(ParseNode): class Optional(ParseNode):
"""ParseNode that matches its child zero or one times. It cannot fail to """ ParseNode that matches its child zero or one times. It cannot fail to
parse.""" parse. """
def __init__(self, child): def __init__(self, child):
self.child = to_parse_node(child) self.child = to_parse_node(child)
@ -455,25 +427,23 @@ class Optional(ParseNode):
class Eof(ParseNode): class Eof(ParseNode):
"""ParseNode that matches an EOF token.""" """ ParseNode that matches an EOF token. """
def _parse(self, ctx: ParseContext) -> bool: def _parse(self, ctx: ParseContext) -> bool:
token = ctx.next_token() token = ctx.next_token()
return token.type == TokenType.EOF return token.type == TokenType.EOF
class Match(ParseNode): class Match(ParseNode):
"""ParseNode that matches the given literal token.""" """ ParseNode that matches the given literal token. """
def __init__(self, op):
def __init__(self, op: str):
self.op = op self.op = op
def _parse(self, ctx: ParseContext) -> bool: def _parse(self, ctx: ParseContext) -> bool:
token = ctx.next_token() token = ctx.next_token()
return str(token) == self.op return str(token) == self.op
def expected(self, expect: T.Optional[str] = None): def expected(self, expect: str = None):
"""Convenience method for err().""" """ Convenience method for err(). """
if expect is None: if expect is None:
return self.err(f"Expected '{self.op}'") return self.err(f"Expected '{self.op}'")
else: else:
@ -481,10 +451,9 @@ class Match(ParseNode):
class UseIdent(ParseNode): class UseIdent(ParseNode):
"""ParseNode that matches any identifier and sets it in a key=value pair on """ ParseNode that matches any identifier and sets it in a key=value pair on
the containing match group.""" the containing match group. """
def __init__(self, key):
def __init__(self, key: str):
self.key = key self.key = key
def _parse(self, ctx: ParseContext): def _parse(self, ctx: ParseContext):
@ -497,10 +466,9 @@ class UseIdent(ParseNode):
class UseNumber(ParseNode): class UseNumber(ParseNode):
"""ParseNode that matches a number and sets it in a key=value pair on """ ParseNode that matches a number and sets it in a key=value pair on
the containing match group.""" the containing match group. """
def __init__(self, key):
def __init__(self, key: str):
self.key = key self.key = key
def _parse(self, ctx: ParseContext): def _parse(self, ctx: ParseContext):
@ -509,15 +477,16 @@ class UseNumber(ParseNode):
return False return False
number = token.get_number() number = token.get_number()
if number % 1.0 == 0:
number = int(number)
ctx.set_group_val(self.key, number, token) ctx.set_group_val(self.key, number, token)
return True return True
class UseNumberText(ParseNode): class UseNumberText(ParseNode):
"""ParseNode that matches a number, but sets its *original text* it in a """ ParseNode that matches a number, but sets its *original text* it in a
key=value pair on the containing match group.""" key=value pair on the containing match group. """
def __init__(self, key):
def __init__(self, key: str):
self.key = key self.key = key
def _parse(self, ctx: ParseContext): def _parse(self, ctx: ParseContext):
@ -530,10 +499,9 @@ class UseNumberText(ParseNode):
class UseQuoted(ParseNode): class UseQuoted(ParseNode):
"""ParseNode that matches a quoted string and sets it in a key=value pair """ ParseNode that matches a quoted string and sets it in a key=value pair
on the containing match group.""" on the containing match group. """
def __init__(self, key):
def __init__(self, key: str):
self.key = key self.key = key
def _parse(self, ctx: ParseContext): def _parse(self, ctx: ParseContext):
@ -541,28 +509,20 @@ class UseQuoted(ParseNode):
if token.type != TokenType.QUOTED: if token.type != TokenType.QUOTED:
return False return False
unescaped = None string = (str(token)[1:-1]
.replace("\\n", "\n")
try: .replace("\\\"", "\"")
unescaped = utils.unescape_quote(str(token)) .replace("\\\\", "\\")
except utils.UnescapeError as e: .replace("\\'", "\'"))
start = ctx.tokens[ctx.index - 1].start ctx.set_group_val(self.key, string, token)
range = Range(start + e.start, start + e.end, ctx.text)
ctx.errors.append(
CompileError(f"Invalid escape sequence '{range.text}'", range)
)
ctx.set_group_val(self.key, unescaped, token)
return True return True
class UseLiteral(ParseNode): class UseLiteral(ParseNode):
"""ParseNode that doesn't match anything, but rather sets a static key=value """ 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: pair on the containing group. Useful for, e.g., property and signal flags:
`Sequence(Keyword("swapped"), UseLiteral("swapped", True))`""" `Sequence(Keyword("swapped"), UseLiteral("swapped", True))` """
def __init__(self, key, literal):
def __init__(self, key: str, literal: T.Any):
self.key = key self.key = key
self.literal = literal self.literal = literal
@ -571,24 +531,10 @@ class UseLiteral(ParseNode):
return True return True
class UseExact(ParseNode):
"""Matches the given identifier and sets it as a named token."""
def __init__(self, key: str, string: str):
self.key = key
self.string = string
def _parse(self, ctx: ParseContext):
token = ctx.next_token()
ctx.set_group_val(self.key, self.string, token)
return str(token) == self.string
class Keyword(ParseNode): class Keyword(ParseNode):
"""Matches the given identifier and sets it as a named token, with the name """ Matches the given identifier and sets it as a named token, with the name
being the identifier itself.""" being the identifier itself. """
def __init__(self, kw):
def __init__(self, kw: str):
self.kw = kw self.kw = kw
self.set_token = True self.set_token = True
@ -598,15 +544,6 @@ class Keyword(ParseNode):
return str(token) == self.kw return str(token) == self.kw
class Mark(ParseNode):
def __init__(self, key: str):
self.key = key
def _parse(self, ctx: ParseContext):
ctx.set_mark(self.key)
return True
def to_parse_node(value) -> ParseNode: def to_parse_node(value) -> ParseNode:
if isinstance(value, str): if isinstance(value, str):
return Match(value) return Match(value)

View file

@ -19,29 +19,20 @@
from .errors import MultipleErrors, PrintableError from .errors import MultipleErrors, PrintableError
from .language import OBJECT_CONTENT_HOOKS, UI, Template
from .parse_tree import * from .parse_tree import *
from .parser_utils import *
from .tokenizer import TokenType from .tokenizer import TokenType
from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI
def parse( def parse(tokens) -> T.Tuple[UI, T.Optional[MultipleErrors], T.List[PrintableError]]:
tokens: T.List[Token], """ Parses a list of tokens into an abstract syntax tree. """
) -> T.Tuple[T.Optional[UI], T.Optional[MultipleErrors], T.List[CompileError]]:
"""Parses a list of tokens into an abstract syntax tree."""
try: ctx = ParseContext(tokens)
original_text = tokens[0].string if len(tokens) else "" AnyOf(UI).parse(ctx)
ctx = ParseContext(tokens, original_text)
AnyOf(UI).parse(ctx)
assert ctx.last_group is not None ast_node = ctx.last_group.to_ast() if ctx.last_group else None
ast_node = ctx.last_group.to_ast() errors = MultipleErrors(ctx.errors) if len(ctx.errors) else None
warnings = ctx.warnings
errors = [*ctx.errors, *ast_node.errors] return (ast_node, errors, warnings)
warnings = [*ctx.warnings, *ast_node.warnings]
return (ast_node, MultipleErrors(errors) if len(errors) else None, warnings)
except MultipleErrors as e:
return (None, e, [])
except CompileError as e:
return (None, MultipleErrors([e]), [])

View file

@ -1,6 +1,6 @@
# translation_domain.py # parser_utils.py
# #
# Copyright 2022 James Westman <james@jwestman.net> # Copyright 2021 James Westman <james@jwestman.net>
# #
# This file is free software; you can redistribute it and/or modify it # This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as # under the terms of the GNU Lesser General Public License as
@ -17,19 +17,20 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .common import *
from .parse_tree import *
class TranslationDomain(AstNode): class_name = AnyOf(
grammar = Statement( [
"translation-domain", UseIdent("namespace"),
UseQuoted("domain"), ".",
) UseIdent("class_name"),
],
@property [
def domain(self): ".",
return self.tokens["domain"] UseIdent("class_name"),
UseLiteral("ignore_gir", True),
@docs() ],
def ref_docs(self): UseIdent("class_name"),
return get_docs_section("Syntax TranslationDomain") )

View file

@ -18,80 +18,66 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import re
import typing as T import typing as T
from dataclasses import dataclass import re
from enum import Enum from enum import Enum
from . import utils from .errors import CompileError
class TokenType(Enum): class TokenType(Enum):
EOF = 0 EOF = 0
IDENT = 1 IDENT = 1
QUOTED = 2 QUOTED = 2
NUMBER = 3 NUMBER = 3
OP = 4 OP = 4
WHITESPACE = 5 WHITESPACE = 5
COMMENT = 6 COMMENT = 6
PUNCTUATION = 7 PUNCTUATION = 7
_tokens = [ _tokens = [
(TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"), (TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"),
(TokenType.QUOTED, r'"(\\(.|\n)|[^\\"\n])*"'), (TokenType.QUOTED, r'"(\\"|[^"\n])*"'),
(TokenType.QUOTED, r"'(\\(.|\n)|[^\\'\n])*'"), (TokenType.QUOTED, r"'(\\'|[^'\n])*'"),
(TokenType.NUMBER, r"0x[A-Za-z0-9_]+"), (TokenType.NUMBER, r"0x[A-Fa-f0-9_]+"),
(TokenType.NUMBER, r"[\d_]+(\.[\d_]+)?"), (TokenType.NUMBER, r"[-+]?[\d_]*\d(\.[\d_]*\d)?"),
(TokenType.NUMBER, r"\.[\d_]+"), (TokenType.NUMBER, r"[-+]?\.[\d_]*\d"),
(TokenType.WHITESPACE, r"\s+"), (TokenType.WHITESPACE, r"\s+"),
(TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"), (TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"),
(TokenType.COMMENT, r"\/\/[^\n]*"), (TokenType.COMMENT, r"\/\/[^\n]*"),
(TokenType.OP, r"\$|<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"), (TokenType.OP, r"<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"),
(TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"), (TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"),
] ]
_TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens] _TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens]
class Token: class Token:
def __init__(self, type: TokenType, start: int, end: int, string: str): def __init__(self, type, start, end, string):
self.type = type self.type = type
self.start = start self.start = start
self.end = end self.end = end
self.string = string self.string = string
def __str__(self) -> str: def __str__(self):
return self.string[self.start : self.end] return self.string[self.start:self.end]
@property
def range(self) -> "Range":
return Range(self.start, self.end, self.string)
def get_number(self) -> T.Union[int, float]:
from .errors import CompileError, CompilerBugError
def get_number(self):
if self.type != TokenType.NUMBER: if self.type != TokenType.NUMBER:
raise CompilerBugError() return None
string = str(self).replace("_", "") string = str(self).replace("_", "")
try: if string.startswith("0x"):
if string.startswith("0x"): return int(string, 16)
return int(string, 16) else:
elif "." in string: return float(string.replace("_", ""))
return float(string)
else:
return int(string)
except:
raise CompileError(f"{str(self)} is not a valid number literal", self.range)
def _tokenize(ui_ml: str): def _tokenize(ui_ml: str):
from .errors import CompileError
i = 0 i = 0
while i < len(ui_ml): while i < len(ui_ml):
matched = False matched = False
for type, regex in _TOKENS: for (type, regex) in _TOKENS:
match = regex.match(ui_ml, i) match = regex.match(ui_ml, i)
if match is not None: if match is not None:
@ -101,55 +87,10 @@ def _tokenize(ui_ml: str):
break break
if not matched: if not matched:
raise CompileError( raise CompileError("Could not determine what kind of syntax is meant here", i, i)
"Could not determine what kind of syntax is meant here",
Range(i, i, ui_ml),
)
yield Token(TokenType.EOF, i, i, ui_ml) yield Token(TokenType.EOF, i, i, ui_ml)
def tokenize(data: str) -> T.List[Token]: def tokenize(data: str) -> T.List[Token]:
return list(_tokenize(data)) return list(_tokenize(data))
@dataclass
class Range:
start: int
end: int
original_text: str
@property
def length(self) -> int:
return self.end - self.start
@property
def text(self) -> str:
return self.original_text[self.start : self.end]
@property
def with_trailing_newline(self) -> "Range":
if len(self.original_text) > self.end and self.original_text[self.end] == "\n":
return Range(self.start, self.end + 1, self.original_text)
else:
return self
@staticmethod
def join(a: T.Optional["Range"], b: T.Optional["Range"]) -> T.Optional["Range"]:
if a is None:
return b
if b is None:
return a
return Range(min(a.start, b.start), max(a.end, b.end), a.original_text)
def __contains__(self, other: T.Union[int, "Range"]) -> bool:
if isinstance(other, int):
return self.start <= other <= self.end
else:
return self.start <= other.start and self.end >= other.end
def to_json(self):
return utils.idxs_to_range(self.start, self.end, self.original_text)
def overlaps(self, other: "Range") -> bool:
return not (self.end < other.start or self.start > other.end)

View file

@ -1,322 +0,0 @@
# typelib.py
#
# Copyright 2022 James Westman <james@jwestman.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import math
import mmap
import os
import sys
import typing as T
from ctypes import *
from .errors import CompilerBugError
BLOB_TYPE_STRUCT = 3
BLOB_TYPE_BOXED = 4
BLOB_TYPE_ENUM = 5
BLOB_TYPE_FLAGS = 6
BLOB_TYPE_OBJECT = 7
BLOB_TYPE_INTERFACE = 8
TYPE_VOID = 0
TYPE_BOOLEAN = 1
TYPE_INT8 = 2
TYPE_UINT8 = 3
TYPE_INT16 = 4
TYPE_UINT16 = 5
TYPE_INT32 = 6
TYPE_UINT32 = 7
TYPE_INT64 = 8
TYPE_UINT64 = 9
TYPE_FLOAT = 10
TYPE_DOUBLE = 11
TYPE_GTYPE = 12
TYPE_UTF8 = 13
TYPE_FILENAME = 14
TYPE_ARRAY = 15
TYPE_INTERFACE = 16
TYPE_GLIST = 17
TYPE_GSLIST = 18
TYPE_GHASH = 19
TYPE_ERROR = 20
TYPE_UNICHAR = 21
class Field:
def __init__(self, offset: int, type: str, shift=0, mask=None):
self._offset = offset
self._type = type
if not mask or sys.byteorder == "little":
self._shift = shift
elif self._type == "u8" or self._type == "i8":
self._shift = 8 - (shift + mask)
elif self._type == "u16" or self._type == "i16":
self._shift = 16 - (shift + mask)
else:
self._shift = 32 - (shift + mask)
self._mask = (1 << mask) - 1 if mask else None
self._name = f"{offset}__{type}__{shift}__{mask}"
def __get__(self, typelib: "Typelib", _objtype=None):
if typelib is None:
return self
def shift_mask(n):
n = n >> self._shift
if self._mask:
n = n & self._mask
return n
tl = typelib[self._offset]
if self._type == "u8":
return shift_mask(tl.u8)
elif self._type == "u16":
return shift_mask(tl.u16)
elif self._type == "u32":
return shift_mask(tl.u32)
elif self._type == "i8":
return shift_mask(tl.i8)
elif self._type == "i16":
return shift_mask(tl.i16)
elif self._type == "i32":
return shift_mask(tl.i32)
elif self._type == "pointer":
return tl.header[tl.u32]
elif self._type == "offset":
return tl
elif self._type == "string":
return tl.string
elif self._type == "dir_entry":
return tl.header.dir_entry(tl.u16)
else:
raise CompilerBugError(self._type)
class Typelib:
AS_DIR_ENTRY = Field(0, "dir_entry")
HEADER_N_ENTRIES = Field(0x14, "u16")
HEADER_N_LOCAL_ENTRIES = Field(0x16, "u16")
HEADER_DIRECTORY = Field(0x18, "pointer")
HEADER_N_ATTRIBUTES = Field(0x1C, "u32")
HEADER_ATTRIBUTES = Field(0x20, "pointer")
HEADER_DEPENDENCIES = Field(0x24, "pointer")
HEADER_NAMESPACE = Field(0x2C, "string")
HEADER_NSVERSION = Field(0x30, "string")
HEADER_ENTRY_BLOB_SIZE = Field(0x3C, "u16")
HEADER_FUNCTION_BLOB_SIZE = Field(0x3E, "u16")
HEADER_CALLBACK_BLOB_SIZE = Field(0x40, "u16")
HEADER_SIGNAL_BLOB_SIZE = Field(0x42, "u16")
HEADER_ARG_BLOB_SIZE = Field(0x46, "u16")
HEADER_PROPERTY_BLOB_SIZE = Field(0x48, "u16")
HEADER_FIELD_BLOB_SIZE = Field(0x4A, "u16")
HEADER_VALUE_BLOB_SIZE = Field(0x4C, "u16")
HEADER_ATTRIBUTE_BLOB_SIZE = Field(0x4E, "u16")
HEADER_ENUM_BLOB_SIZE = Field(0x56, "u16")
HEADER_OBJECT_BLOB_SIZE = Field(0x5A, "u16")
HEADER_INTERFACE_BLOB_SIZE = Field(0x5C, "u16")
DIR_ENTRY_BLOB_TYPE = Field(0x0, "u16")
DIR_ENTRY_LOCAL = Field(0x2, "u16", 0, 1)
DIR_ENTRY_NAME = Field(0x4, "string")
DIR_ENTRY_OFFSET = Field(0x8, "pointer")
DIR_ENTRY_NAMESPACE = Field(0x8, "string")
ARG_NAME = Field(0x0, "string")
ARG_TYPE = Field(0xC, "u32")
SIGNATURE_RETURN_TYPE = Field(0x0, "u32")
SIGNATURE_N_ARGUMENTS = Field(0x6, "u16")
SIGNATURE_ARGUMENTS = Field(0x8, "offset")
ATTR_OFFSET = Field(0x0, "u32")
ATTR_NAME = Field(0x4, "string")
ATTR_VALUE = Field(0x8, "string")
TYPE_BLOB_TAG = Field(0x0, "u8", 3, 5)
TYPE_BLOB_INTERFACE = Field(0x2, "dir_entry")
TYPE_BLOB_ARRAY_INNER = Field(0x4, "u32")
BLOB_NAME = Field(0x4, "string")
STRUCT_DEPRECATED = Field(0x2, "u16", 0, 1)
ENUM_DEPRECATED = Field(0x2, "u16", 0, 1)
ENUM_GTYPE_NAME = Field(0x8, "string")
ENUM_N_VALUES = Field(0x10, "u16")
ENUM_N_METHODS = Field(0x12, "u16")
ENUM_VALUES = Field(0x18, "offset")
INTERFACE_DEPRECATED = Field(0x2, "u16", 0, 1)
INTERFACE_GTYPE_NAME = Field(0x8, "string")
INTERFACE_N_PREREQUISITES = Field(0x12, "u16")
INTERFACE_N_PROPERTIES = Field(0x14, "u16")
INTERFACE_N_METHODS = Field(0x16, "u16")
INTERFACE_N_SIGNALS = Field(0x18, "u16")
INTERFACE_N_VFUNCS = Field(0x1A, "u16")
INTERFACE_N_CONSTANTS = Field(0x1C, "u16")
INTERFACE_PREREQUISITES = Field(0x28, "offset")
OBJ_DEPRECATED = Field(0x02, "u16", 0, 1)
OBJ_ABSTRACT = Field(0x02, "u16", 1, 1)
OBJ_FUNDAMENTAL = Field(0x02, "u16", 2, 1)
OBJ_FINAL = Field(0x02, "u16", 3, 1)
OBJ_GTYPE_NAME = Field(0x08, "string")
OBJ_PARENT = Field(0x10, "dir_entry")
OBJ_GTYPE_STRUCT = Field(0x12, "string")
OBJ_N_INTERFACES = Field(0x14, "u16")
OBJ_N_FIELDS = Field(0x16, "u16")
OBJ_N_PROPERTIES = Field(0x18, "u16")
OBJ_N_METHODS = Field(0x1A, "u16")
OBJ_N_SIGNALS = Field(0x1C, "u16")
OBJ_N_VFUNCS = Field(0x1E, "u16")
OBJ_N_CONSTANTS = Field(0x20, "u16")
OBJ_N_FIELD_CALLBACKS = Field(0x22, "u16")
PROP_NAME = Field(0x0, "string")
PROP_DEPRECATED = Field(0x4, "u32", 0, 1)
PROP_READABLE = Field(0x4, "u32", 1, 1)
PROP_WRITABLE = Field(0x4, "u32", 2, 1)
PROP_CONSTRUCT = Field(0x4, "u32", 3, 1)
PROP_CONSTRUCT_ONLY = Field(0x4, "u32", 4, 1)
PROP_TYPE = Field(0xC, "u32")
SIGNAL_DEPRECATED = Field(0x0, "u16", 0, 1)
SIGNAL_DETAILED = Field(0x0, "u16", 5, 1)
SIGNAL_NAME = Field(0x4, "string")
SIGNAL_SIGNATURE = Field(0xC, "pointer")
VALUE_NAME = Field(0x4, "string")
VALUE_VALUE = Field(0x8, "i32")
def __init__(self, typelib_file, offset: int):
self._typelib_file = typelib_file
self._offset = offset
def __getitem__(self, index: int):
return Typelib(self._typelib_file, self._offset + index)
def attr(self, name):
return self.header.attr(self._offset, name)
@property
def header(self) -> "TypelibHeader":
return TypelibHeader(self._typelib_file)
@property
def u8(self) -> int:
"""Gets the 8-bit unsigned int at this location."""
return self._int(1, False)
@property
def u16(self) -> int:
"""Gets the 16-bit unsigned int at this location."""
return self._int(2, False)
@property
def u32(self) -> int:
"""Gets the 32-bit unsigned int at this location."""
return self._int(4, False)
@property
def i8(self) -> int:
"""Gets the 8-bit unsigned int at this location."""
return self._int(1, True)
@property
def i16(self) -> int:
"""Gets the 16-bit unsigned int at this location."""
return self._int(2, True)
@property
def i32(self) -> int:
"""Gets the 32-bit unsigned int at this location."""
return self._int(4, True)
@property
def string(self) -> T.Optional[str]:
"""Interprets the 32-bit unsigned int at this location as a pointer
within the typelib file, and returns the null-terminated string at that
pointer."""
loc = self.u32
if loc == 0:
return None
end = self._typelib_file.find(b"\0", loc)
return self._typelib_file[loc:end].decode("utf-8")
def _int(self, size, signed) -> int:
return int.from_bytes(
self._typelib_file[self._offset : self._offset + size],
sys.byteorder,
signed=signed,
)
class TypelibHeader(Typelib):
def __init__(self, typelib_file):
super().__init__(typelib_file, 0)
def dir_entry(self, index) -> T.Optional[Typelib]:
if index == 0:
return None
else:
return self.HEADER_DIRECTORY[(index - 1) * self.HEADER_ENTRY_BLOB_SIZE]
def attr(self, offset, name):
lower = 0
upper = self.HEADER_N_ATTRIBUTES
attr_size = self.HEADER_ATTRIBUTE_BLOB_SIZE
attrs = self.HEADER_ATTRIBUTES
mid = 0
while lower <= upper:
mid = math.floor((upper + lower) / 2)
attr = attrs[mid * attr_size]
if attr.ATTR_OFFSET < offset:
lower = mid + 1
elif attr.ATTR_OFFSET > offset:
upper = mid - 1
else:
while mid >= 0 and attrs[(mid - 1) * attr_size].ATTR_OFFSET == offset:
mid -= 1
break
if attrs[mid * attr_size].ATTR_OFFSET != offset:
# no match found
return None
while attrs[mid * attr_size].ATTR_OFFSET == offset:
if attrs[mid * attr_size].ATTR_NAME == name:
return attrs[mid * attr_size].ATTR_VALUE
mid += 1
return None
def attr_by_index(self, index):
pass
@property
def dir_entries(self):
return [self.dir_entry(i) for i in range(self[0x16].u16)]
def load_typelib(path: str) -> Typelib:
with open(path, "rb") as f:
return Typelib(f.read(), 0)

View file

@ -18,20 +18,18 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T import typing as T
from dataclasses import dataclass
class Colors: class Colors:
RED = "\033[91m" RED = '\033[91m'
GREEN = "\033[92m" GREEN = '\033[92m'
YELLOW = "\033[33m" YELLOW = '\033[33m'
PURPLE = "\033[35m" FAINT = '\033[2m'
FAINT = "\033[2m" BOLD = '\033[1m'
BOLD = "\033[1m" BLUE = '\033[34m'
BLUE = "\033[34m" UNDERLINE = '\033[4m'
UNDERLINE = "\033[4m" NO_UNDERLINE = '\033[24m'
NO_UNDERLINE = "\033[24m" CLEAR = '\033[0m'
CLEAR = "\033[0m"
def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]: def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]:
@ -58,34 +56,29 @@ def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]:
cost = 1 cost = 1
else: else:
cost = 2 cost = 2
distances[i][j] = min( distances[i][j] = min(distances[i-1][j] + 2, distances[i][j-1] + 2, distances[i-1][j-1] + cost)
distances[i - 1][j] + 2,
distances[i][j - 1] + 2,
distances[i - 1][j - 1] + cost,
)
return distances[m - 1][n - 1] return distances[m-1][n-1]
distances = [(option, levenshtein(word, option)) for option in options] distances = [(option, levenshtein(word, option)) for option in options]
closest = min(distances, key=lambda item: item[1]) closest = min(distances, key=lambda item:item[1])
if closest[1] <= 5: if closest[1] <= 5:
return closest[0] return closest[0]
return None return None
def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]: def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]:
if idx == 0 or len(text) == 0: if idx == 0:
return (0, 0) return (0, 0)
line_num = text.count("\n", 0, idx) + 1 sp = text[:idx].splitlines(keepends=True)
col_num = idx - text.rfind("\n", 0, idx) - 1 line_num = len(sp)
col_num = len(sp[-1])
return (line_num - 1, col_num) return (line_num - 1, col_num)
def pos_to_idx(line: int, col: int, text: str) -> int: def pos_to_idx(line: int, col: int, text: str) -> int:
lines = text.splitlines(keepends=True) lines = text.splitlines(keepends=True)
return sum([len(line) for line in lines[:line]]) + col return sum([len(line) for line in lines[:line]]) + col
def idxs_to_range(start: int, end: int, text: str): def idxs_to_range(start: int, end: int, text: str):
start_l, start_c = idx_to_pos(start, text) start_l, start_c = idx_to_pos(start, text)
end_l, end_c = idx_to_pos(end, text) end_l, end_c = idx_to_pos(end, text)
@ -99,58 +92,3 @@ def idxs_to_range(start: int, end: int, text: str):
"character": end_c, "character": end_c,
}, },
} }
@dataclass
class UnescapeError(Exception):
start: int
end: int
def escape_quote(string: str) -> str:
return (
'"'
+ (
string.replace("\\", "\\\\")
.replace('"', '\\"')
.replace("\n", "\\n")
.replace("\t", "\\t")
)
+ '"'
)
def unescape_quote(string: str) -> str:
string = string[1:-1]
REPLACEMENTS = {
"\n": "\n",
"\\": "\\",
"n": "\n",
"t": "\t",
'"': '"',
"'": "'",
}
result = ""
i = 0
while i < len(string):
c = string[i]
if c == "\\":
i += 1
if i >= len(string):
from .errors import CompilerBugError
raise CompilerBugError()
if r := REPLACEMENTS.get(string[i]):
result += r
else:
raise UnescapeError(i, i + 2)
else:
result += c
i += 1
return result

View file

@ -17,32 +17,19 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from xml.sax import saxutils
from blueprintcompiler.gir import GirType from xml.sax import saxutils
from blueprintcompiler.language.types import ClassName from . import gir
class XmlEmitter: class XmlEmitter:
def __init__(self, indent=2, generated_notice=True): def __init__(self, indent=2):
self.indent = indent self.indent = indent
self.result = '<?xml version="1.0" encoding="UTF-8"?>' self.result = '<?xml version="1.0" encoding="UTF-8"?>'
if generated_notice:
self.result += (
"\n"
"<!--\n"
"DO NOT EDIT!\n"
"This file was @generated by blueprint-compiler. Instead, edit the\n"
"corresponding .blp file and regenerate this file with blueprint-compiler.\n"
"-->"
)
self._tag_stack = [] self._tag_stack = []
self._needs_newline = False self._needs_newline = False
def start_tag( def start_tag(self, tag, **attrs):
self, tag, **attrs: T.Union[str, GirType, ClassName, bool, None, float]
):
self._indent() self._indent()
self.result += f"<{tag}" self.result += f"<{tag}"
for key, val in attrs.items(): for key, val in attrs.items():
@ -68,23 +55,16 @@ class XmlEmitter:
self.result += f"</{tag}>" self.result += f"</{tag}>"
self._needs_newline = True self._needs_newline = True
def put_text(self, text: T.Union[str, int, float]): def put_text(self, text):
self.result += saxutils.escape(str(text)) self.result += saxutils.escape(str(text))
self._needs_newline = False self._needs_newline = False
def put_cdata(self, text: str):
text = text.replace("]]>", "]]]]><![CDATA[>")
self.result += f"<![CDATA[{text}]]>"
self._needs_newline = False
def _indent(self): def _indent(self):
if self.indent is not None: if self.indent is not None:
self.result += "\n" + " " * (self.indent * len(self._tag_stack)) self.result += "\n" + " " * (self.indent * len(self._tag_stack))
def _to_string(self, val): def _to_string(self, val):
if isinstance(val, GirType): if isinstance(val, gir.GirType):
return val.glib_type_name
elif isinstance(val, ClassName):
return val.glib_type_name return val.glib_type_name
else: else:
return str(val) return str(val)

View file

@ -18,82 +18,76 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from collections import defaultdict from collections import defaultdict
from functools import cached_property from functools import cached_property
import typing as T
from xml import sax from xml import sax
# To speed up parsing, we ignore all tags except these # To speed up parsing, we ignore all tags except these
PARSE_GIR = set( PARSE_GIR = set([
[ "repository", "namespace", "class", "interface", "property", "glib:signal",
"repository", "include", "implements", "type", "parameter", "parameters", "enumeration",
"namespace", "member", "bitfield",
"class", ])
"interface",
"property",
"glib:signal",
"include",
"implements",
"type",
"parameter",
"parameters",
"enumeration",
"member",
"bitfield",
]
)
class Element: class Element:
def __init__(self, tag: str, attrs: T.Dict[str, str]): def __init__(self, tag, attrs: T.Dict[str, str]):
self.tag = tag self.tag = tag
self.attrs = attrs self.attrs = attrs
self.children: T.List["Element"] = [] self.children: T.Dict[str, T.List["Element"]] = defaultdict(list)
self.cdata_chunks: T.List[str] = [] self.cdata_chunks: T.List[str] = []
@cached_property @cached_property
def cdata(self): def cdata(self):
return "".join(self.cdata_chunks) return ''.join(self.cdata_chunks)
def get_elements(self, name: str) -> T.List["Element"]: def get_elements(self, name) -> T.List["Element"]:
return [child for child in self.children if child.tag == name] return self.children.get(name, [])
def __getitem__(self, key: str): def __getitem__(self, key):
return self.attrs.get(key) return self.attrs.get(key)
class Handler(sax.handler.ContentHandler): class Handler(sax.handler.ContentHandler):
def __init__(self): def __init__(self, parse_type):
self.root = None self.root = None
self.stack = [] self.stack = []
self.skipping = 0
self._interesting_elements = parse_type
def startElement(self, name, attrs): 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()) element = Element(name, attrs.copy())
if len(self.stack): if len(self.stack):
last = self.stack[-1] last = self.stack[-1]
last.children.append(element) last.children[name].append(element)
else: else:
self.root = element self.root = element
self.stack.append(element) self.stack.append(element)
def endElement(self, name): def endElement(self, name):
self.stack.pop() 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): def characters(self, content):
self.stack[-1].cdata_chunks.append(content) if not self.skipping:
self.stack[-1].cdata_chunks.append(content)
def parse(filename): def parse(filename, parse_type=None):
parser = sax.make_parser() parser = sax.make_parser()
handler = Handler() handler = Handler(parse_type)
parser.setContentHandler(handler) parser.setContentHandler(handler)
parser.parse(filename) parser.parse(filename)
return handler.root return handler.root
def parse_string(xml):
handler = Handler()
parser = sax.parseString(xml, handler)
return handler.root

View file

@ -1,13 +1,9 @@
FROM fedora:latest FROM fedora:latest
RUN dnf install -y meson gcc g++ python3-pip gobject-introspection-devel \ RUN dnf install -y meson python3-pip gtk4-devel gobject-introspection-devel libadwaita-devel python3-devel
python3-devel python3-gobject git diffutils xorg-x11-server-Xvfb \ RUN pip3 install furo mypy sphinx coverage
appstream-devel dbus-x11 "dnf-command(builddep)" glslc
RUN dnf build-dep -y gtk4 libadwaita
RUN pip3 install furo mypy sphinx coverage black isort
COPY install_deps.sh .
RUN ./install_deps.sh
# The version on PyPI is very old and doesn't install. Use the upstream package registry instead. # The version on PyPI is very old and doesn't install. Use the upstream package registry instead.
RUN pip install pythonfuzz --extra-index-url https://gitlab.com/api/v4/projects/19904939/packages/pypi/simple RUN pip install pythonfuzz --extra-index-url https://gitlab.com/api/v4/projects/19904939/packages/pypi/simple
RUN dnf install -y git

View file

@ -1,26 +0,0 @@
#!/bin/bash
set -e
echo "===== Install GTK ====="
git clone --depth=1 https://gitlab.gnome.org/GNOME/gtk.git
cd gtk
meson setup builddir \
--prefix=/usr \
-Ddocumentation=true \
-Dbuild-demos=false \
-Dbuild-examples=false \
-Dbuild-tests=false \
-Dbuild-testsuite=false
ninja -C builddir install
cd -
rm -rf gtk
echo "===== Install libadwaita ====="
git clone --depth=1 https://gitlab.gnome.org/GNOME/libadwaita.git
cd libadwaita
meson builddir \
--prefix=/usr
ninja -C builddir install
cd -
rm -rf libadwaita

View file

@ -1,24 +0,0 @@
.experimental-admonition {
display: flex;
align-items: center;
}
.experimental-admonition img {
width: 64px;
}
p.grammar-block {
font-family: var(--font-stack--monospace);
white-space: pre;
overflow: auto;
font-size: var(--code-font-size);
padding: .625rem .875rem;
line-height: 1.5;
background: #f8f8f8;
border-radius: .2rem;
}
body:not([data-theme="light"]) .grammar-block {
background: #202020;
color: #d0d0d0;
}

View file

@ -1,139 +0,0 @@
#!/usr/bin/env python3
import json
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
__all__ = ["get_docs_section"]
DOCS_ROOT = "https://gnome.pages.gitlab.gnome.org/blueprint-compiler"
sections: dict[str, "Section"] = {}
@dataclass
class Section:
link: str
lines: str
def to_json(self):
return {
"content": rst_to_md(self.lines),
"link": self.link,
}
def load_reference_docs():
for filename in Path(os.path.dirname(__file__), "reference").glob("*.rst"):
with open(filename) as f:
section_name = None
lines = []
def close_section():
if section_name:
html_file = re.sub(r"\.rst$", ".html", filename.name)
anchor = re.sub(r"[^a-z0-9]+", "-", section_name.lower())
link = f"{DOCS_ROOT}/reference/{html_file}#{anchor}"
sections[section_name] = Section(link, lines)
for line in f:
if m := re.match(r"\.\.\s+_(.*):", line):
close_section()
section_name = m.group(1)
lines = []
else:
lines.append(line)
close_section()
# This isn't a comprehensive rST to markdown converter, it just needs to handle the
# small subset of rST used in the reference docs.
def rst_to_md(lines: list[str]) -> str:
result = ""
def rst_to_md_inline(line):
line = re.sub(r"``(.*?)``", r"`\1`", line)
line = re.sub(
r":ref:`(.*?)<(.*?)>`",
lambda m: f"[{m.group(1)}]({sections[m.group(2)].link})",
line,
)
line = re.sub(r"`([^`]*?) <([^`>]*?)>`_", r"[\1](\2)", line)
return line
i = 0
n = len(lines)
heading_levels = {}
def print_block(lang: str = "", code: bool = True, strip_links: bool = False):
nonlocal result, i
block = ""
while i < n:
line = lines[i].rstrip()
if line.startswith(" "):
line = line[3:]
elif line != "":
break
if strip_links:
line = re.sub(r":ref:`(.*?)<(.*?)>`", r"\1", line)
if not code:
line = rst_to_md_inline(line)
block += line + "\n"
i += 1
if code:
result += f"```{lang}\n{block.strip()}\n```\n\n"
else:
result += block
while i < n:
line = lines[i].rstrip()
i += 1
if line == ".. rst-class:: grammar-block":
print_block("text", strip_links=True)
elif line == ".. code-block:: blueprint":
print_block("blueprint")
elif line == ".. note::":
result += "#### Note\n"
print_block(code=False)
elif m := re.match(r"\.\. image:: (.*)", line):
result += f"![{m.group(1)}]({DOCS_ROOT}/_images/{m.group(1)})\n"
elif i < n and re.match(r"^((-+)|(~+)|(\++))$", lines[i]):
level_char = lines[i][0]
if level_char not in heading_levels:
heading_levels[level_char] = max(heading_levels.values(), default=1) + 1
result += (
"#" * heading_levels[level_char] + " " + rst_to_md_inline(line) + "\n"
)
i += 1
else:
result += rst_to_md_inline(line) + "\n"
return result
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: collect_sections.py <output_file>")
sys.exit(1)
outfile = sys.argv[1]
load_reference_docs()
# print the sections to a json file
with open(outfile, "w") as f:
json.dump(
{name: section.to_json() for name, section in sections.items()},
f,
indent=2,
sort_keys=True,
)

View file

@ -17,9 +17,9 @@
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = "Blueprint" project = 'Blueprint'
copyright = "2021-2023, James Westman" copyright = '2021, James Westman'
author = "James Westman" author = 'James Westman'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
@ -27,15 +27,16 @@ author = "James Westman"
# Add any Sphinx extension module names here, as strings. They can be # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [] extensions = [
]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path. # This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
@ -43,11 +44,9 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
# #
html_theme = "furo" html_theme = 'furo'
# Add any paths that contain custom static files (such as style sheets) here, # 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, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"] html_static_path = ['_static']
html_css_files = ["styles.css"]

420
docs/examples.rst Normal file
View file

@ -0,0 +1,420 @@
========
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;
}
Referencing a template object
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to use a template object elsewhere in your blueprint, you can use
the template class name as the object ID:
.. code-block::
template MyAppWindow : ApplicationWindow { }
Gtk.Label {
visible: bind MyAppWindow.visible;
}
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 {}
}

View file

@ -1,163 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="128"
height="128"
viewBox="0 0 33.866666 33.866666"
version="1.1"
id="svg145234"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
sodipodi:docname="beaker.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview145236"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="2.3786088"
inkscape:cx="113.72194"
inkscape:cy="67.26621"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid145800" />
</sodipodi:namedview>
<defs
id="defs145231">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1082">
<path
sodipodi:nodetypes="ccsccccccccccssccccccsccccc"
inkscape:connector-curvature="0"
id="path1084"
d="m 59.306656,179.84113 c -1.539651,-0.0204 -2.554714,0.50293 -3.259361,1.28705 -0.704636,0.78405 -1.080179,1.88075 -1.080179,2.99185 0,1.1112 0.375543,2.20784 1.080179,2.99194 0.704647,0.78412 1.71971,1.30878 3.259361,1.28701 0.596478,-0.008 1.084356,0.47321 1.084359,1.06974 v 31.27782 c 4e-6,0.2286 -0.07322,0.45119 -0.208928,0.63515 l -29.921313,40.60614 c -5.03e-4,7.6e-4 -0.001,0.001 -0.002,0.002 -1.584091,2.39264 -1.933179,5.40515 -1.95979,7.96457 v 0.004 c -2.1e-5,0.002 2.1e-5,0.004 0,0.006 0.005,8.86974 7.174927,16.0356 16.046057,16.0356 H 97.83189 c 8.8738,0 16.04477,-7.17027 16.04594,-16.04397 -0.007,-2.82242 -0.49265,-5.62802 -1.97021,-7.97704 -9.97128,-13.53236 -19.94162,-27.0653 -29.91287,-40.59772 -0.13575,-0.18403 -0.20895,-0.40672 -0.20888,-0.6354 v -31.27771 c 1e-5,-0.59653 0.48788,-1.07782 1.08436,-1.06973 2.40019,0.0329 4.33954,-1.87832 4.33954,-4.27897 0,-2.40065 -1.93937,-4.31292 -4.33954,-4.27891 -0.005,3e-5 -0.01,3e-5 -0.0145,0 H 59.321313 c -0.0049,3e-5 -0.0097,3e-5 -0.01456,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#99c1f1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
</clipPath>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter1221"
x="-0.12864004"
width="1.2572789"
y="-0.10374497"
height="1.2074448">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="4.1703178"
id="feGaussianBlur1223" />
</filter>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient1205"
id="radialGradient1271"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.9534006,0,0,0.87645096,-163.67294,29.472168)"
cx="41.498363"
cy="254.6526"
fx="41.498363"
fy="254.6526"
r="34.247517" />
<linearGradient
inkscape:collect="always"
id="linearGradient1205">
<stop
style="stop-color:#33d17a;stop-opacity:1"
offset="0"
id="stop1201" />
<stop
style="stop-color:#3584e4;stop-opacity:1"
offset="1"
id="stop1203" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient1013"
id="radialGradient1273"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(9.0225063,0,0,6.9495164,-619.87454,-1384.9283)"
cx="62"
cy="229.19627"
fx="62"
fy="229.19627"
r="16.015505" />
<linearGradient
inkscape:collect="always"
id="linearGradient1013">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop1009" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop1011" />
</linearGradient>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g425"
transform="matrix(0.26458333,0,0,0.26458333,32.647151,-39.952084)"
style="display:inline;enable-background:new">
<g
transform="translate(-130.47915,-20)"
clip-path="url(#clipPath1082)"
id="g1261">
<path
sodipodi:nodetypes="ccsccccccccccssccccccsccccc"
inkscape:connector-curvature="0"
id="path1255"
d="m 59.306656,179.84113 c -1.539651,-0.0204 -2.554714,0.50293 -3.259361,1.28705 -0.704636,0.78405 -1.080179,1.88075 -1.080179,2.99185 0,1.1112 0.375543,2.20784 1.080179,2.99194 0.704647,0.78412 1.71971,1.30878 3.259361,1.28701 0.596478,-0.008 1.084356,0.47321 1.084359,1.06974 v 31.27782 c 4e-6,0.2286 -0.07322,0.45119 -0.208928,0.63515 l -29.921313,40.60614 c -5.03e-4,7.6e-4 -0.001,0.001 -0.002,0.002 -1.584091,2.39264 -1.933179,5.40515 -1.95979,7.96457 v 0.004 c -2.1e-5,0.002 2.1e-5,0.004 0,0.006 0.005,8.86974 7.174927,16.0356 16.046057,16.0356 H 97.83189 c 8.8738,0 16.04477,-7.17027 16.04594,-16.04397 -0.007,-2.82242 -0.49265,-5.62802 -1.97021,-7.97704 -9.97128,-13.53236 -19.94162,-27.0653 -29.91287,-40.59772 -0.13575,-0.18403 -0.20895,-0.40672 -0.20888,-0.6354 v -31.27771 c 1e-5,-0.59653 0.48788,-1.07782 1.08436,-1.06973 2.40019,0.0329 4.33954,-1.87832 4.33954,-4.27897 0,-2.40065 -1.93937,-4.31292 -4.33954,-4.27891 -0.005,3e-5 -0.01,3e-5 -0.0145,0 H 59.321313 c -0.0049,3e-5 -0.0097,3e-5 -0.01456,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.322;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#99c1f1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.81;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#1c71d8;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.881889;filter:url(#filter1221);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 59.306656,179.84113 c -1.539651,-0.0204 -2.554714,0.50293 -3.259361,1.28705 -0.704636,0.78405 -1.080179,1.88075 -1.080179,2.99185 0,1.1112 0.375543,2.20784 1.080179,2.99194 0.704647,0.78412 1.71971,1.30878 3.259361,1.28701 0.596478,-0.008 1.084356,0.47321 1.084359,1.06974 v 31.27782 c 4e-6,0.2286 -0.07322,0.45119 -0.208928,0.63515 l -29.921313,40.60614 c -5.03e-4,7.6e-4 -0.001,0.001 -0.002,0.002 -1.584091,2.39264 -1.933179,5.40515 -1.95979,7.96457 v 0.004 c -2.1e-5,0.002 2.1e-5,0.004 0,0.006 0.005,8.86974 7.174927,16.0356 16.046057,16.0356 H 97.83189 c 8.8738,0 16.04477,-7.17027 16.04594,-16.04397 -0.007,-2.82242 -0.49265,-5.62802 -1.97021,-7.97704 -9.97128,-13.53236 -19.94162,-27.0653 -29.91287,-40.59772 -0.13575,-0.18403 -0.20895,-0.40672 -0.20888,-0.6354 v -31.27771 c 1e-5,-0.59653 0.48788,-1.07782 1.08436,-1.06973 2.40019,0.0329 4.33954,-1.87832 4.33954,-4.27897 0,-2.40065 -1.93937,-4.31292 -4.33954,-4.27891 -0.005,3e-5 -0.01,3e-5 -0.0145,0 H 59.321313 c -0.0049,3e-5 -0.0097,3e-5 -0.01456,0 z"
id="path1257"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccsccccccccccssccccccsccccc" />
<path
d="m 79,221 h 10 m -10,-4 h 10 m -10,-4 h 10 m -14,-4 h 10 m -6,-4 h 10 m -10,-4 h 10 m -10,-4 h 10 m -14,-4 h 10"
style="fill:none;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.519685"
id="path1259"
inkscape:connector-curvature="0" />
</g>
<path
sodipodi:nodetypes="cccsscccccccc"
inkscape:connector-curvature="0"
id="path1263"
d="m -34.29794,233.66755 8.04173,10.91453 c 0.74491,1.13854 1.09081,3.07356 1.11345,5.02175 -0.009,5.8719 -4.66171,10.51971 -10.53703,10.51971 h -47.42097 c -5.87918,0 -10.53438,-4.65437 -10.53704,-10.53357 0.006,-2.0657 0.38011,-3.80592 1.16367,-5.07197 2.665,-3.61676 5.32998,-7.23362 7.99498,-10.85045 10.97111,12.38168 41.40432,-11.28458 50.18121,0 z m 8.04173,10.90414 c 0.0181,0.0184 0.48159,0.49211 0.74461,0.76019 -0.11172,-0.0775 -0.28578,-0.19921 -0.28572,-0.19914 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#1c71d8;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#radialGradient1271);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m -84.48262,233.66755 -8.04173,10.91453 c -0.7449,1.13854 -1.09081,3.07356 -1.11345,5.02175 0.009,5.8719 4.66171,10.51971 10.53703,10.51971 h 47.42097 c 5.87918,0 10.53438,-4.65437 10.53704,-10.53357 -0.006,-2.0657 -0.38011,-3.80592 -1.16366,-5.07197 -2.66501,-3.61676 -5.32999,-7.23362 -7.99499,-10.85045 -10.97111,12.38168 -41.40431,-11.28458 -50.18121,0 z m -8.04173,10.90414 c -0.0181,0.0184 -0.48159,0.49211 -0.74461,0.76019 0.11172,-0.0775 0.28578,-0.19921 0.28572,-0.19914 z"
id="path1265"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccsscccccccc" />
<path
sodipodi:nodetypes="cssccccsscc"
inkscape:connector-curvature="0"
id="path1267"
d="M -16.64126,250.7207 C -17.04348,259.23501 -24.03184,266 -32.64712,266 h -53.48633 c -8.54507,0 -15.493,-6.65433 -15.99804,-15.07031 -0.0161,0.34274 -0.0454,0.69541 -0.0488,1.02539 v 0.004 c -2e-5,0.002 2e-5,0.004 0,0.006 0.005,8.8695 7.17574,16.03492 16.04687,16.03492 h 53.48633 c 8.8738,0 16.04375,-7.17122 16.04492,-16.04492 -10e-4,-0.41261 -0.016,-0.82386 -0.0391,-1.23438 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.623;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#1a5fb4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
inkscape:connector-curvature="0"
id="path1269"
d="m -66.48305,207.95117 a 4.0004,4.0004 0 0 0 -3.49219,2.10547 l -20,36 a 4.0004,4.0004 0 1 0 6.99219,3.88672 l 20,-36 a 4.0004,4.0004 0 0 0 -3.5,-5.99219 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.48;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#radialGradient1273);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 17 KiB

View file

@ -12,12 +12,11 @@ a module in your flatpak manifest:
{ {
"name": "blueprint-compiler", "name": "blueprint-compiler",
"buildsystem": "meson", "buildsystem": "meson",
"cleanup": ["*"],
"sources": [ "sources": [
{ {
"type": "git", "type": "git",
"url": "https://gitlab.gnome.org/GNOME/blueprint-compiler", "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler",
"tag": "v0.16.0" "branch": "main"
} }
] ]
} }

View file

@ -1,14 +1,6 @@
Overview Overview
======== ========
.. warning::
.. container:: experimental-admonition
.. image:: experimental.svg
**Blueprint is still experimental.** Future versions may have breaking changes, and most GTK tutorials use XML syntax.
Blueprint is a markup language and compiler for GTK 4 user interfaces. Blueprint is a markup language and compiler for GTK 4 user interfaces.
.. toctree:: .. toctree::
@ -18,15 +10,14 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces.
setup setup
translations translations
flatpak flatpak
reference/index examples
packaging
.. code-block:: blueprint .. code-block::
using Gtk 4.0; using Gtk 4.0;
template $MyAppWindow: ApplicationWindow { template MyAppWindow : ApplicationWindow {
default-width: 600; default-width: 600;
default-height: 300; default-height: 300;
title: _("Hello, Blueprint!"); title: _("Hello, Blueprint!");
@ -35,7 +26,7 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces.
HeaderBar {} HeaderBar {}
Label { Label {
label: bind template.main_text; label: bind MyAppWindow.main_text;
} }
} }
@ -53,96 +44,31 @@ Features
- **Concise syntax.** No more clumsy XML! Blueprint is designed from the ground - **Concise syntax.** No more clumsy XML! Blueprint is designed from the ground
up to match GTK's widget model, including templates, child types, signal up to match GTK's widget model, including templates, child types, signal
handlers, and menus. handlers, and menus.
- **Easy to learn.** The syntax should be very familiar to most people. Take a look at the :doc:`reference <reference/index>` to get started. - **Easy to learn.** The syntax should be very familiar to most people. Scroll
- **Modern tooling.** Blueprint ships a `Language Server <https://microsoft.github.io/language-server-protocol/>`_ for IDE integration. through the :doc:`examples page <examples>` for a quick overview of the whole
language.
Links - **Modern tooling.** IDE integration for `GNOME Builder <https://developer.gnome.org/documentation/introduction/builder.html>`_
----- is in progress, and a VS Code extension is also planned.
- `Source code <https://gitlab.gnome.org/GNOME/blueprint-compiler>`_
- `Workbench <https://github.com/sonnyp/Workbench>`_ lets you try, preview and export Blueprint
- `GNOME Builder <https://developer.gnome.org/documentation/introduction/builder.html>`_ provides builtin support
- `Vim syntax highlighting plugin by thetek42 <https://github.com/thetek42/vim-blueprint-syntax>`_
- `Vim syntax highlighting plugin by gabmus <https://gitlab.com/gabmus/vim-blueprint>`_
- `GNU Emacs major mode by DrBluefall <https://github.com/DrBluefall/blueprint-mode>`_
- `Visual Studio Code plugin by bodil <https://github.com/bodil/vscode-blueprint>`_
History
-------
1. `Simplify our UI declarative language, a strawman proposal <https://discourse.gnome.org/t/simplify-our-ui-declarative-language-a-strawman-proposal/2913>`_
2. `A Markup Language for GTK <https://www.jwestman.net/2021/10/22/a-markup-language-for-gtk.html>`_
3. `Introducing Blueprint: A New Way to Craft User Interfaces <https://www.jwestman.net/2021/12/02/introducing-blueprint-a-new-way-to-craft-user-interfaces.html>`_
4. `Next Steps for Blueprint <https://www.jwestman.net/2022/04/12/next-steps-for-blueprint.html>`_
Built with Blueprint Built with Blueprint
-------------------- --------------------
- `AdwSteamGtk <https://github.com/Foldex/AdwSteamGtk>`_
- `Blurble <https://gitlab.gnome.org/World/Blurble>`_
- `Bottles <https://github.com/bottlesdevs/Bottles>`_
- `Cartridges <https://github.com/kra-mo/cartridges>`_
- `Cassette <https://gitlab.gnome.org/Rirusha/Cassette>`_
- `Cavalier <https://github.com/NickvisionApps/Cavalier>`_
- `Chance <https://zelikos.dev/apps/rollit>`_
- `Commit <https://github.com/sonnyp/Commit/>`_
- `Confy <https://confy.kirgroup.net/>`_
- `Cozy <https://github.com/geigi/cozy>`_
- `Daikhan <https://github.com/flathub/io.gitlab.daikhan.stable>`_
- `Damask <https://gitlab.gnome.org/subpop/damask>`_
- `Denaro <https://github.com/NickvisionApps/Denaro>`_
- `Design <https://github.com/dubstar-04/Design>`_
- `Dev Toolbox <https://github.com/aleiepure/devtoolbox>`_
- `Dialect <https://github.com/dialect-app/dialect>`_
- `Diccionario de la Lengua <https://codeberg.org/rafaelmardojai/diccionario-lengua>`_
- `Doggo <https://gitlab.gnome.org/sungsphinx/Doggo>`_
- `Dosage <https://github.com/diegopvlk/Dosage>`_
- `Dynamic Wallpaper <https://github.com/dusansimic/dynamic-wallpaper>`_
- `Extension Manager <https://github.com/mjakeman/extension-manager>`_ - `Extension Manager <https://github.com/mjakeman/extension-manager>`_
- `Eyedropper <https://github.com/FineFindus/eyedropper>`_
- `favagtk <https://gitlab.gnome.org/johannesjh/favagtk>`_
- `Feeds <https://gitlab.gnome.org/World/gfeeds>`_ - `Feeds <https://gitlab.gnome.org/World/gfeeds>`_
- `File Shredder <https://github.com/ADBeveridge/raider>`_
- `Flare <https://gitlab.com/schmiddi-on-mobile/flare>`_
- `Flowtime <https://github.com/Diego-Ivan/Flowtime>`_
- `Fretboard <https://github.com/bragefuglseth/fretboard>`_
- `Frog <https://github.com/TenderOwl/Frog>`_
- `Geopard <https://github.com/ranfdev/Geopard>`_ - `Geopard <https://github.com/ranfdev/Geopard>`_
- `Giara <https://gitlab.gnome.org/World/giara>`_ - `Giara <https://gitlab.gnome.org/World/giara>`_
- `Girens <https://gitlab.gnome.org/tijder/girens>`_
- `Gradience <https://github.com/GradienceTeam/Gradience>`_
- `Graphs <https://gitlab.gnome.org/World/Graphs>`_
- `Health <https://gitlab.gnome.org/World/Health>`_ - `Health <https://gitlab.gnome.org/World/Health>`_
- `HydraPaper <https://gitlab.com/gabmus/HydraPaper>`_ - `HydraPaper <https://gitlab.com/gabmus/HydraPaper>`_
- `Identity <https://gitlab.gnome.org/YaLTeR/identity>`_ - `Identity <https://gitlab.gnome.org/YaLTeR/identity>`_
- `Jogger <https://codeberg.org/baarkerlounger/jogger>`_
- `Junction <https://github.com/sonnyp/Junction/>`_
- `Komikku <https://codeberg.org/valos/Komikku>`_
- `Letterpress <https://gitlab.gnome.org/World/Letterpress>`_
- `Login Manager Settings <https://github.com/realmazharhussain/gdm-settings>`_
- `Maniatic Launcher <https://github.com/santiagocezar/maniatic-launcher/>`_
- `Master Key <https://gitlab.com/guillermop/master-key/>`_
- `Misson Center <https://github.com/flathub/io.missioncenter.MissionCenter>`_
- `NewCaw <https://github.com/CodedOre/NewCaw>`_
- `Paper <https://gitlab.com/posidon_software/paper>`_
- `Paper Plane <https://github.com/paper-plane-developers/paper-plane>`_
- `Parabolic <https://github.com/NickvisionApps/Parabolic>`_
- `Passes <https://github.com/pablo-s/passes>`_
- `Pipeline <https://gitlab.com/schmiddi-on-mobile/pipeline>`_
- `Playhouse <https://github.com/sonnyp/Playhouse>`_
- `Plitki <https://github.com/YaLTeR/plitki>`_ - `Plitki <https://github.com/YaLTeR/plitki>`_
- `Raider <https://github.com/ADBeveridge/raider>`_
- `Retro <https://github.com/sonnyp/Retro>`_
- `Solanum <https://gitlab.gnome.org/World/Solanum>`_ - `Solanum <https://gitlab.gnome.org/World/Solanum>`_
- `Sudoku Solver <https://gitlab.com/cyberphantom52/sudoku-solver>`_
- `Swatch <https://gitlab.gnome.org/GabMus/swatch>`_
- `Switcheroo <https://gitlab.com/adhami3310/Switcheroo>`_
- `Tagger <https://github.com/NickvisionApps/Tagger>`_
- `Tangram <https://github.com/sonnyp/Tangram/>`_
- `Text Pieces <https://github.com/liferooter/textpieces>`_ - `Text Pieces <https://github.com/liferooter/textpieces>`_
- `Upscaler <https://gitlab.gnome.org/World/Upscaler>`_
- `Video Trimmer <https://gitlab.gnome.org/YaLTeR/video-trimmer>`_ - `Video Trimmer <https://gitlab.gnome.org/YaLTeR/video-trimmer>`_
- `Webfont Kit Generator <https://github.com/rafaelmardojai/webfont-kit-generator>`_
- `WhatIP <https://gitlab.gnome.org/GabMus/whatip>`_ - `WhatIP <https://gitlab.gnome.org/GabMus/whatip>`_
- `Who Wants To Be a Millionaire <https://github.com/martinszeltins/who-wants-to-be-a-millionaire/>`_
- `Workbench <https://github.com/sonnyp/Workbench>`_ - `Workbench <https://github.com/sonnyp/Workbench>`_
Links
-----
- `Source code <https://gitlab.gnome.org/jwestman/blueprint-compiler>`_
- `Vim syntax highlighting plugin <https://github.com/thetek42/vim-blueprint-syntax>`_

View file

@ -3,17 +3,9 @@ if get_option('docs')
sphinx = find_program(['sphinx-build-3', 'sphinx-build'], required: true) sphinx = find_program(['sphinx-build-3', 'sphinx-build'], required: true)
custom_target('docs', custom_target('docs',
command: [sphinx, '-b', 'html', '-c', meson.current_source_dir(), meson.current_source_dir(), '@OUTPUT@'], command: [sphinx, '-b', 'html', '-c', meson.current_source_dir(), meson.current_source_dir(), '@OUTPUT@'],
output: 'en', output: 'en',
build_always_stale: true, build_by_default: true
) )
endif endif
custom_target('reference_docs.json',
output: 'reference_docs.json',
command: [meson.current_source_dir() / 'collect-sections.py', '@OUTPUT@'],
build_always_stale: true,
install: true,
install_dir: py.get_install_dir() / 'blueprintcompiler',
)

View file

@ -1,36 +0,0 @@
====================
For Distro Packagers
====================
blueprint-compiler is a build tool that converts UI definitions written in
Blueprint into XML files that are installed with the app and that GTK can read.
So for most applications that use blueprint-compiler, it is a build dependency.
It is a Python program, but like most GNOME-related projects, it uses
`Meson <https://mesonbuild.com>`_ as its build system.
GObject Introspection
~~~~~~~~~~~~~~~~~~~~~
Blueprint files can import GObject Introspection namespaces like this:
.. code-block:: blueprint
using Gtk 4.0;
using Adw 1;
To compile a blueprint file, ``.typelib`` files for all of the imported
namespaces must be installed. All blueprint files must import Gtk 4.0, so
``Gtk-4.0.typelib`` is effectively a runtime dependency of blueprint-compiler.
blueprint-compiler also depends on pygobject, because it uses GIRepository
to determine the search path for typelib files.
So, if a package uses blueprint-compiler, its build dependencies should include
the typelib files for any namespaces imported in its blueprint files. (Note
that many apps also have the same typelib files as runtime dependencies,
separately from blueprint).
In addition, the blueprint language server uses ``.gir`` files to provide
documentation on hover. Some distros package these files separately from the
main package (e.g. in a ``-devel`` package). The language server will not crash
if these files are not present, but for a good user experience you should make
sure they are installed.

View file

@ -1,188 +0,0 @@
===========
Diagnostics
===========
.. _Diagnostic abstract_class:
abstract_class
--------------
Objects can't be created from abstract classes. Abstract classes are used as base classes for other classes, but they don't have functionality on their own. You may want to use a non-abstract subclass instead.
.. _Diagnostic bad_syntax:
bad_syntax
----------
The tokenizer encountered an unexpected sequence of characters that aren't part of any known blueprint syntax.
.. _Diagnostic child_not_accepted:
child_not_accepted
------------------
The parent class does not have child objects (it does not implement `Gtk.Buildable <https://docs.gtk.org/gtk4/iface.Buildable.html>`_ and is not a subclass of `Gio.ListStore <https://docs.gtk.org/gio/class.ListStore.html>`_). Some classes use properties instead of children to add widgets. Check the parent class's documentation.
.. _Diagnostic conversion_error:
conversion_error
----------------
The value's type cannot be converted to the target type.
Subclasses may be converted to their superclasses, but not vice versa. A type that implements an interface can be converted to that interface's type. Many boxed types can be parsed from strings in a type-specific way.
.. _Diagnostic expected_bool:
expected_bool
-------------
A boolean value was expected, but the value is not ``true`` or ``false``.
.. _Diagnostic extension_not_repeatable:
extension_not_repeatable
------------------------
This extension can't be used more than once in an object.
.. _Diagnostic extension_wrong_parent_type:
extension_wrong_parent_type
---------------------------
No extension with the given name exists for this object's class (or, for a :ref:`child extension<Syntax ChildExtension>`, the parent class).
.. _Diagnostic invalid_number_literal:
invalid_number_literal
----------------------
The tokenizer encountered what it thought was a number, but it couldn't parse it as a number.
.. _Diagnostic member_dne:
member_dne
----------
The value is being interpreted as a member of an enum or flag type, but that type doesn't have a member with the given name.
.. _Diagnostic missing_gtk_declaration:
missing_gtk_declaration
-----------------------
All blueprint files must start with a GTK declaration, e.g. ``using Gtk 4.0;``.
.. _Diagnostic multiple_templates:
multiple_templates
------------------
Only one :ref:`template<Syntax Template>` is allowed per blueprint file, but there are multiple. The template keyword indicates which object is the one being instantiated.
.. _Diagnostic namespace_not_found:
namespace_not_found
--------------------
The ``.typelib`` files for the given namespace could not be found. There are several possibilities:
* There is a typo in the namespace name, e.g. ``Adwaita`` instead of ``Adw``
* The version number is incorrect, e.g. ``Adw 1.0`` instead of ``Adw 1``. The library's documentation will tell you the correct version number to use.
* The packages for the library are not installed. On some distributions, the ``.typelib`` file is in a separate package from the main library, such as a ``-devel`` package.
* There is an issue with the path to the typelib file. The ``GI_TYPELIB_PATH`` environment variable can be used to add additional paths to search.
.. _Diagnostic namespace_not_imported:
namespace_not_imported
----------------------
The given namespace was not imported at the top of the file. Importing the namespace is necessary because it tells blueprint-compiler which version of the library to use.
.. _Diagnostic object_dne:
object_dne
----------
No object with the given ID exists in the current scope.
.. _Diagnostic property_dne:
property_dne
------------
The class or interface doesn't have a property with the given name.
.. _Diagnostic property_convert_error:
property_convert_error
----------------------
The value given for the property can't be converted to the property's type.
.. _Diagnostic property_construct_only:
property_construct_only
-----------------------
The property can't be bound because it is a construct-only property, meaning it can only be set once when the object is first constructed. Binding it to an expression could cause its value to change later.
.. _Diagnostic property_read_only:
property_read_only
------------------
This property can't be set because it is marked as read-only.
.. _Diagnostic signal_dne:
signal_dne
----------
The class or interface doesn't have a signal with the given name.
.. _Diagnostic type_dne:
type_dne
--------
The given type doesn't exist in the namespace.
.. _Diagnostic type_not_a_class:
type_not_a_class
----------------
The given type exists in the namespace, but it isn't a class. An object's type must be a concrete (not abstract) class, not an interface or boxed type.
.. _Diagnostic version_conflict:
version_conflict
----------------
This error occurs when two versions of a namespace are imported (possibly transitively) in the same file. For example, this will cause a version conflict:
.. code-block:: blueprint
using Gtk 4.0;
using Gtk 3.0;
But so will this:
.. code-block:: blueprint
using Gtk 4.0;
using Handy 1;
because libhandy imports ``Gtk 3.0``.
.. _Diagnostic wrong_compiler_version:
wrong_compiler_version
----------------------
This version of blueprint-compiler is for GTK 4 blueprints only. Future GTK versions will use different versions of blueprint-compiler.

View file

@ -1,88 +0,0 @@
=======================
Document Root & Imports
=======================
.. _Syntax Root:
Document Root
-------------
.. rst-class:: grammar-block
Root = :ref:`GtkDecl<Syntax GtkDecl>` (:ref:`Using<Syntax Using>`)* (:ref:`TranslationDomain<Syntax TranslationDomain>`)? ( :ref:`Template<Syntax Template>` | :ref:`Menu<Syntax Menu>` | :ref:`Object<Syntax Object>` )* EOF
A blueprint document consists of a :ref:`GTK declaration<Syntax GtkDecl>`, one or more :ref:`imports<Syntax Using>`, and a list of :ref:`objects<Syntax Object>` and/or a :ref:`template<Syntax Template>`.
Example
~~~~~~~
.. code-block:: blueprint
// Gtk Declaration
using Gtk 4.0;
// Import Statement
using Adw 1;
// Object
Window my_window {}
.. _Syntax GtkDecl:
GTK Declaration
---------------
.. rst-class:: grammar-block
GtkDecl = 'using' 'Gtk' '4.0' ';'
Every blueprint file begins with the line ``using Gtk 4.0;``, which declares the target GTK version for the file. Tools that read blueprint files should verify that they support the declared version.
Example
~~~~~~~
.. code-block:: blueprint
using Gtk 4.0;
.. _Syntax Using:
GObject Introspection Imports
-----------------------------
.. rst-class:: grammar-block
Using = 'using' <namespace::ref:`IDENT<Syntax IDENT>`> <version::ref:`NUMBER<Syntax NUMBER>`> ';'
To use classes and types from namespaces other than GTK itself, those namespaces must be imported at the top of the file. This tells the compiler what version of the namespace to import.
You'll need the GIR name and version, not the package name and not the exact version number. These are listed at the top of each library's documentation homepage:
.. image:: gir-namespace.png
The compiler requires typelib files for these libraries to be installed. They are usually installed with the library, but on some distros, you may need to install the package that provides ``{namespace}-{version}.typelib`` (e.g. ``Adw-1.typelib``).
Example
~~~~~~~
.. code-block:: blueprint
// Import libadwaita
using Adw 1;
.. _Syntax TranslationDomain:
Translation Domain
------------------
.. rst-class:: grammar-block
TranslationDomain = 'translation-domain' <domain::ref:`QUOTED<Syntax QUOTED>`> ';'
The translation domain is used to look up translations for translatable strings in the blueprint file. If no translation domain is specified, strings will be looked up in the program's global domain.
See `Gtk.Builder:translation-domain <https://docs.gtk.org/gtk4/property.Builder.translation-domain.html>`_ for more information.

View file

@ -1,112 +0,0 @@
===========
Expressions
===========
Expressions make your user interface code *reactive*. This means when your
application's data changes, the user interface reacts to the change
automatically.
.. code-block:: blueprint
label: bind template.account.username;
/* ^ ^ ^
| creates lookup expressions that are re-evaluated when
| the account's username *or* the account itself changes
|
binds the `label` property to the expression's output
*/
When a value is bound to an expression using the ``bind`` keyword, the binding
monitors all the object properties that are inputs to the expression, and
reevaluates it if any of them change.
This is a powerful tool for ensuring consistency and simplifying your code.
Rather than pushing changes to the user interface wherever they may occur,
you can define your data model with GObject and let GTK take care of the rest.
.. _Syntax Expression:
Expressions
-----------
.. rst-class:: grammar-block
Expression = ( :ref:`ClosureExpression<Syntax ClosureExpression>` | :ref:`Literal<Syntax Literal>` | ( '(' Expression ')' ) ) ( :ref:`LookupExpression<Syntax LookupExpression>` | :ref:`CastExpression<Syntax CastExpression>` )*
.. note::
The grammar above is designed to eliminate `left recursion <https://en.wikipedia.org/wiki/Left_recursion>`_, which can make parsing more complex. In this format, an expression consists of a prefix (such as a literal value or closure invocation) followed by zero or more infix or suffix operators.
Expressions are composed of property lookups and/or closures. Property lookups are the inputs to the expression, and closures provided in application code can perform additional calculations on those inputs.
.. _Syntax LookupExpression:
Lookups
-------
.. rst-class:: grammar-block
LookupExpression = '.' <property::ref:`IDENT<Syntax IDENT>`>
Lookup expressions perform a GObject property lookup on the preceding expression. They are recalculated whenever the property changes, using the `notify signal <https://docs.gtk.org/gobject/signal.Object.notify.html>`_.
The type of a property expression is the type of the property it refers to.
.. _Syntax ClosureExpression:
Closures
--------
.. rst-class:: grammar-block
ClosureExpression = '$' <name::ref:`IDENT<Syntax IDENT>`> '(' ( :ref:`Expression<Syntax Expression>` ),* ')'
Closure expressions allow you to perform additional calculations that aren't supported in blueprint by writing those calculations as application code. These application-defined functions are created in the same way as :ref:`signal handlers<Syntax Signal>`.
Expressions are only reevaluated when their inputs change. Because blueprint doesn't manage a closure's application code, it can't tell what changes might affect the result. Therefore, closures must be *pure*, or deterministic. They may only calculate the result based on their immediate inputs, not properties of their inputs or outside variables.
Blueprint doesn't know the closure's return type, so closure expressions must be cast to the correct return type using a :ref:`cast expression<Syntax CastExpression>`.
.. _Syntax CastExpression:
Casts
-----
.. rst-class:: grammar-block
CastExpression = 'as' '<' :ref:`TypeName<Syntax TypeName>` '>'
Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it. This is necessary for closures and for properties of application-defined types.
Example
~~~~~~~
.. code-block:: blueprint
// Cast the result of the closure so blueprint knows it's a string
label: bind $format_bytes(template.file-size) as <string>
.. _Syntax ExprValue:
Expression Values
-----------------
.. rst-class:: grammar-block
ExprValue = 'expr' :ref:`Expression<Syntax Expression>`
Some APIs take *an expression itself*--not its result--as a property value. For example, `Gtk.BoolFilter <https://docs.gtk.org/gtk4/class.BoolFilter.html>`_ has an ``expression`` property of type `Gtk.Expression <https://docs.gtk.org/gtk4/class.Expression.html>`_. This expression is evaluated for every item in a list model to determine whether the item should be filtered.
To define an expression for such a property, use ``expr`` instead of ``bind``. Inside the expression, you can use the ``item`` keyword to refer to the item being evaluated. You must cast the item to the correct type using the ``as`` keyword, and you can only use ``item`` in a property lookup--you may not pass it to a closure.
Example
~~~~~~~
.. code-block:: blueprint
BoolFilter {
expression: expr item as <$UserAccount>.active;
}

View file

@ -1,374 +0,0 @@
==========
Extensions
==========
.. _Syntax Extension:
Properties are the main way to set values on objects, but they are limited by the GObject type system in what values they can accept. Some classes, therefore, have specialized syntax for certain features.
.. note::
Extensions are a feature of ``Gtk.Buildable``--see `Gtk.Buildable.custom_tag_start() <https://docs.gtk.org/gtk4/vfunc.Buildable.custom_tag_start.html>`_ for internal details.
Because they aren't part of the type system, they aren't present in typelib files like properties and signals are. Therefore, if a library adds a new extension, syntax for it must be added to Blueprint manually. If there's a commonly used extension that isn't supported by Blueprint, please `file an issue <https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues>`_.
.. rst-class:: grammar-block
Extension = :ref:`ExtAccessibility<Syntax ExtAccessibility>`
| :ref:`ExtAdwAlertDialog<Syntax ExtAdwAlertDialog>`
| :ref:`ExtAdwMessageDialog<Syntax ExtAdwMessageDialog>`
| :ref:`ExtAdwBreakpoint<Syntax ExtAdwBreakpoint>`
| :ref:`ExtComboBoxItems<Syntax ExtComboBoxItems>`
| :ref:`ExtFileFilterMimeTypes<Syntax ExtFileFilter>`
| :ref:`ExtFileFilterPatterns<Syntax ExtFileFilter>`
| :ref:`ExtFileFilterSuffixes<Syntax ExtFileFilter>`
| :ref:`ExtLayout<Syntax ExtLayout>`
| :ref:`ExtListItemFactory<Syntax ExtListItemFactory>`
| :ref:`ExtSizeGroupWidgets<Syntax ExtSizeGroupWidgets>`
| :ref:`ExtStringListStrings<Syntax ExtStringListStrings>`
| :ref:`ExtStyles<Syntax ExtStyles>`
.. _Syntax ExtAccessibility:
Accessibility Properties
------------------------
.. rst-class:: grammar-block
ExtAccessibility = 'accessibility' '{' ExtAccessibilityProp* '}'
ExtAccessibilityProp = <name::ref:`IDENT<Syntax IDENT>`> ':' (:ref:`Value <Syntax Value>` | ('[' (:ref: Value <Syntax Value>),* ']') ) ';'
Valid in any `Gtk.Widget <https://docs.gtk.org/gtk4/class.Widget.html>`_.
The ``accessibility`` block defines values relevant to accessibility software. The property names and acceptable values are described in the `Gtk.AccessibleRelation <https://docs.gtk.org/gtk4/enum.AccessibleRelation.html>`_, `Gtk.AccessibleState <https://docs.gtk.org/gtk4/enum.AccessibleState.html>`_, and `Gtk.AccessibleProperty <https://docs.gtk.org/gtk4/enum.AccessibleProperty.html>`_ enums.
.. note::
Relations which allow for a list of values, for example `labelled-by`, must be given as a single relation with a list of values instead of duplicating the relation like done in Gtk.Builder.
.. _Syntax ExtAdwBreakpoint:
Adw.Breakpoint
--------------
.. rst-class:: grammar-block
ExtAdwBreakpointCondition = 'condition' '(' <condition::ref:`QUOTED<Syntax QUOTED>`> ')'
ExtAdwBreakpoint = 'setters' '{' ExtAdwBreakpointSetter* '}'
ExtAdwBreakpointSetter = <object::ref:`IDENT<Syntax IDENT>`> '.' <property::ref:`IDENT<Syntax IDENT>`> ':' :ref:`Value <Syntax Value>` ';'
Valid in `Adw.Breakpoint <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Breakpoint.html>`_.
Defines the condition for a breakpoint and the properties that will be set at that breakpoint. See the documentation for `Adw.Breakpoint <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Breakpoint.html>`_.
.. note::
The `Adw.Breakpoint:condition <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/property.Breakpoint.condition.html>`_ property has type `Adw.BreakpointCondition <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/struct.BreakpointCondition.html>`_, which GtkBuilder doesn't know how to parse from a string. Therefore, the ``condition`` syntax is used instead.
.. _Syntax ExtAdwAlertDialog:
Adw.AlertDialog Responses
----------------------------
.. rst-class:: grammar-block
ExtAdwAlertDialog = 'responses' '[' (ExtAdwAlertDialogResponse),* ']'
ExtAdwAlertDialogResponse = <id::ref:`IDENT<Syntax IDENT>`> ':' :ref:`StringValue<Syntax StringValue>` ExtAdwAlertDialogFlag*
ExtAdwAlertDialogFlag = 'destructive' | 'suggested' | 'disabled'
Valid in `Adw.AlertDialog <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.AlertDialog.html>`_.
The ``responses`` block defines the buttons that will be added to the dialog. The ``destructive`` or ``suggested`` flag sets the appearance of the button, and the ``disabled`` flag can be used to disable the button.
.. code-block:: blueprint
using Adw 1;
Adw.AlertDialog {
responses [
cancel: _("Cancel"),
delete: _("Delete") destructive,
save: "Save" suggested,
wipeHardDrive: "Wipe Hard Drive" destructive disabled,
]
}
.. _Syntax ExtAdwMessageDialog:
Adw.MessageDialog Responses
----------------------------
.. rst-class:: grammar-block
ExtAdwMessageDialog = 'responses' '[' (ExtAdwMessageDialogResponse),* ']'
ExtAdwMessageDialogResponse = <id::ref:`IDENT<Syntax IDENT>`> ':' :ref:`StringValue<Syntax StringValue>` ExtAdwMessageDialogFlag*
ExtAdwMessageDialogFlag = 'destructive' | 'suggested' | 'disabled'
Valid in `Adw.MessageDialog <https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.MessageDialog.html>`_.
The ``responses`` block defines the buttons that will be added to the dialog. The ``destructive`` or ``suggested`` flag sets the appearance of the button, and the ``disabled`` flag can be used to disable the button.
.. code-block:: blueprint
using Adw 1;
Adw.MessageDialog {
responses [
cancel: _("Cancel"),
delete: _("Delete") destructive,
save: "Save" suggested,
wipeHardDrive: "Wipe Hard Drive" destructive disabled,
]
}
.. _Syntax ExtComboBoxItems:
Gtk.ComboBoxText Items
----------------------
.. rst-class:: grammar-block
ExtComboBoxItems = 'items' '[' (ExtComboBoxItem),* ']'
ExtComboBoxItem = ( <id::ref:`IDENT<Syntax IDENT>`> ':' )? :ref:`StringValue<Syntax StringValue>`
Valid in `Gtk.ComboBoxText <https://docs.gtk.org/gtk4/class.ComboBoxText.html>`_, which is deprecated as of Gtk 4.10.
The ``items`` block defines the items that will be added to the combo box. The optional ID can be used to refer to the item rather than its label.
.. code-block:: blueprint
ComboBoxText {
items [
item1: "Item 1",
item2: "Item 2",
item3: "Item 3",
]
}
.. _Syntax ExtFileFilter:
Gtk.FileFilter Filters
----------------------
.. rst-class:: grammar-block
ExtFileFilterMimeTypes = 'mime-types' '[' (ExtFileFilterItem),* ']'
ExtFileFilterPatterns = 'patterns' '[' (ExtFileFilterItem),* ']'
ExtFileFilterSuffixes = 'suffixes' '[' (ExtFileFilterItem),* ']'
ExtFileFilterItem = <item::ref:`QUOTED<Syntax QUOTED>`>
Valid in `Gtk.FileFilter <https://docs.gtk.org/gtk4/class.FileFilter.html>`_.
The ``mime-types``, ``patterns``, and ``suffixes`` blocks define the items that will be added to the file filter. The ``mime-types`` block accepts mime types (including wildcards for subtypes, such as ``image/*``). The ``patterns`` block accepts glob patterns, and the ``suffixes`` block accepts file extensions.
.. code-block:: blueprint
FileFilter {
mime-types [ "text/plain", "image/*" ]
patterns [ "*.txt" ]
suffixes [ "png", "jpg" ]
}
.. _Syntax ExtLayout:
Widget Layouts
--------------
.. rst-class:: grammar-block
ExtLayout = 'layout' '{' ExtLayoutProp* '}'
ExtLayoutProp = <name::ref:`IDENT<Syntax IDENT>`> ':' :ref:`Value<Syntax Value>` ';'
Valid in `Gtk.Widget <https://docs.gtk.org/gtk4/class.Widget.html>`_.
The ``layout`` block describes how the widget should be positioned within its parent. The available properties depend on the parent widget's layout manager.
.. code-block:: blueprint
Grid {
Button {
layout {
column: 0;
row: 0;
}
}
Button {
layout {
column: 1;
row: 0;
}
}
Button {
layout {
column: 0;
row: 1;
row-span: 2;
}
}
}
.. _Syntax ExtListItemFactory:
Gtk.BuilderListItemFactory Templates
------------------------------------
.. rst-class:: grammar-block
ExtListItemFactory = 'template' :ref:`TypeName<Syntax TypeName>` :ref:`ObjectContent<Syntax Object>`
Valid in `Gtk.BuilderListItemFactory <https://docs.gtk.org/gtk4/class.BuilderListItemFactory.html>`_.
The ``template`` block defines the template that will be used to create list items. This block is unique within Blueprint because it defines a completely separate sub-blueprint which is used to create each list item. The sub-blueprint may not reference objects in the main blueprint or vice versa.
The template type must be `Gtk.ListItem <https://docs.gtk.org/gtk4/class.ListItem.html>`_, `Gtk.ColumnViewRow <https://docs.gtk.org/gtk4/class.ColumnViewRow.html>`_, or `Gtk.ColumnViewCell <https://docs.gtk.org/gtk4/class.ColumnViewCell.html>`_. The template object can be referenced with the ``template`` keyword.
.. code-block:: blueprint
ListView {
factory: BuilderListItemFactory {
template ListItem {
child: Label {
label: bind template.item as <StringObject>.string;
};
}
};
model: NoSelection {
model: StringList {
strings [ "Item 1", "Item 2", "Item 3" ]
};
};
}
.. _Syntax ExtScaleMarks:
Gtk.Scale Marks
---------------
.. rst-class:: grammar-block
ExtScaleMarks = 'marks' '[' (ExtScaleMark),* ']'
ExtScaleMark = 'mark' '(' ( '-' | '+' )? <value::ref:`NUMBER<Syntax NUMBER>`> ( ',' <position::ref:`IDENT<Syntax IDENT>`> ( ',' :ref:`StringValue<Syntax StringValue>` )? )? ')'
Valid in `Gtk.Scale <https://docs.gtk.org/gtk4/class.Scale.html>`_.
The ``marks`` block defines the marks on a scale. A single ``mark`` has up to three arguments: a value, an optional position, and an optional label. The position can be ``left``, ``right``, ``top``, or ``bottom``. The label may be translated.
.. _Syntax ExtSizeGroupWidgets:
Gtk.SizeGroup Widgets
---------------------
.. rst-class:: grammar-block
ExtSizeGroupWidgets = 'widgets' '[' (ExtSizeGroupWidget),* ']'
ExtSizeGroupWidget = <id::ref:`IDENT<Syntax IDENT>`>
Valid in `Gtk.SizeGroup <https://docs.gtk.org/gtk4/class.SizeGroup.html>`_.
The ``widgets`` block defines the widgets that will be added to the size group.
.. code-block:: blueprint
Box {
Button button1 {}
Button button2 {}
}
SizeGroup {
widgets [button1, button2]
}
.. _Syntax ExtStringListStrings:
Gtk.StringList Strings
----------------------
.. rst-class:: grammar-block
ExtStringListStrings = 'strings' '[' (ExtStringListItem),* ']'
ExtStringListItem = :ref:`StringValue<Syntax StringValue>`
Valid in `Gtk.StringList <https://docs.gtk.org/gtk4/class.StringList.html>`_.
The ``strings`` block defines the strings in the string list.
.. code-block:: blueprint
StringList {
strings ["violin", "guitar", _("harp")]
}
.. _Syntax ExtStyles:
CSS Styles
----------
.. rst-class:: grammar-block
ExtStyles = 'styles' '[' ExtStylesProp* ']'
ExtStylesProp = <name::ref:`QUOTED<Syntax QUOTED>`>
Valid in any `Gtk.Widget <https://docs.gtk.org/gtk4/class.Widget.html>`_.
The ``styles`` block defines CSS classes that will be added to the widget.
.. code-block:: blueprint
Button {
styles ["suggested-action"]
}
.. _Syntax ChildExtension:
Child Extensions
----------------
.. rst-class:: grammar-block
ChildExtension = :ref:`ExtResponse<Syntax ExtResponse>`
Child extensions are similar to regular extensions, but they apply to a child of the object rather than the object itself. They are used to add properties to child widgets of a container, such as the buttons in a `Gtk.Dialog <https://docs.gtk.org/gtk4/class.Dialog.html>`_. The child extension takes the place of a child type inside the square brackets.
Currently, the only child extension is :ref:`ExtResponse<Syntax ExtResponse>`.
.. _Syntax ExtResponse:
Dialog & InfoBar Responses
--------------------------
.. rst-class:: grammar-block
ExtResponse = 'action' 'response' '=' ( <name::ref:`IDENT<Syntax IDENT>`> | <id::ref:`NUMBER<Syntax NUMBER>`> ) 'default'?
Valid as a child extension for children of `Gtk.Dialog <https://docs.gtk.org/gtk4/class.Dialog.html>`_ or `Gtk.InfoBar <https://docs.gtk.org/gtk4/class.InfoBar.html>`_, which are both deprecated as of Gtk 4.10.
The ``action response`` extension sets the ``action`` child type for the child and sets the child's integer response type. The response type may be either a member of the `Gtk.ResponseType <https://docs.gtk.org/gtk4/enum.ResponseType.html>`_ enum or a positive, application-defined integer.
No more than one child of a dialog or infobar may have the ``default`` flag.
.. code-block:: blueprint
Dialog {
[action response=ok default]
Button {}
[action response=cancel]
Button {}
[action response=1]
Button {}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -1,50 +0,0 @@
================
Syntax Reference
================
This is the official specification of the blueprint format.
The grammar is expressed as a `parsing expression grammar <https://en.wikipedia.org/wiki/Parsing_expression_grammar>`_. This has two important implications: the parser will never backtrack, and alternation (e.g. a ``|`` in the specification) will always take the *first* branch that matches, even if that causes an error later. These properties make PEGs both unambiguous and simple to implement in code.
Blueprint uses C-style line comments (``// comment for the rest of the line``) and block comments (``/* multiline comment... */``).
Wherever commas are used as delimiters in repetition (expressed in this reference as ``( <rule> ),*``), the trailing comma is permitted and optional.
.. toctree::
:maxdepth: 1
document_root
objects
templates
values
expressions
menus
extensions
diagnostics
Tokens
------
.. _Syntax IDENT:
IDENT
~~~~~
An identifier starts with an ASCII underscore ``_`` or letter ``[A-Za-z]`` and consists of ASCII underscores, letters, digits ``[0-9]``, and dashes ``-``. Dashes are included for historical reasons, since GObject properties and signals are traditionally kebab-case.
.. _Syntax NUMBER:
NUMBER
~~~~~~
Numbers begin with an ASCII digit and consist of ASCII digits, underscores, dots ``.``, and letters (for radix pre-/suffixes). More than one dot in a number is not allowed. Underscores are permitted for increased readability, and are ignored.
Hexadecimal numbers may be specified using the ``0x`` prefix and may use uppercase or lowercase letters, or a mix. Hexadecimal values may not have a fractional part. They are generally converted to decimal in the output.
.. _Syntax QUOTED:
QUOTED
~~~~~~
Quotes begin with an ASCII single quote ``'`` or double quote ``"`` and end with the same character they started with. An ASCII backslash ``\`` begins an escape sequence; this allows newlines ``\n``, tabs ``\t``, and quotes ``\'``, ``\"`` to be inserted. It also allows multiline strings by escaping a newline character, which will be ignored.

View file

@ -1,62 +0,0 @@
=====
Menus
=====
.. _Syntax Menu:
Menus
-----
.. rst-class:: grammar-block
Menu = 'menu' <id::ref:`IDENT<Syntax IDENT>`>? '{' MenuChild* '}'
MenuChild = ( MenuSection | MenuSubmenu | :ref:`MenuItemShorthand<Syntax MenuItemShorthand>` | MenuItem )
MenuSection = 'section' <id::ref:`IDENT<Syntax IDENT>`>? '{' ( MenuChild | MenuAttribute )* '}'
MenuSubmenu = 'submenu' <id::ref:`IDENT<Syntax IDENT>`>? '{' ( MenuChild | MenuAttribute )* '}'
MenuItem = 'item' '{' MenuAttribute* '}'
MenuAttribute = <name::ref:`IDENT<Syntax IDENT>`> ':' :ref:`StringValue<Syntax StringValue>` ';'
Menus, such as the application menu, are defined using the ``menu`` keyword. Menus have the type `Gio.MenuModel <https://docs.gtk.org/gio/class.MenuModel.html>`_ and can be referenced by ID. They cannot be defined inline.
Example
~~~~~~~
.. code-block:: blueprint
menu my_menu {
submenu {
label: _("File");
item {
label: _("New");
action: "app.new";
icon: "document-new-symbolic";
}
}
}
MenuButton {
menu-model: my_menu;
}
.. _Syntax MenuItemShorthand:
Item Shorthand
--------------
.. rst-class:: grammar-block
MenuItemShorthand = 'item' '(' :ref:`StringValue<Syntax StringValue>` ( ',' ( :ref:`StringValue<Syntax StringValue>` ( ',' :ref:`StringValue<Syntax StringValue>`? )? )? )? ')'
The most common menu attributes are ``label``, ``action``, and ``icon``. Because they're so common, Blueprint provides a shorter syntax for menu items with just these properties.
Example
~~~~~~~
.. code-block:: blueprint
menu {
item ("label")
item ("label", "action")
item ("label", "action", "icon")
}

View file

@ -1,190 +0,0 @@
=======
Objects
=======
.. _Syntax Object:
Objects
-------
.. rst-class:: grammar-block
Object = :ref:`TypeName<Syntax TypeName>` <id::ref:`IDENT<Syntax IDENT>`>? ObjectContent
ObjectContent = '{' (:ref:`Signal<Syntax Signal>` | :ref:`Property<Syntax Property>` | :ref:`Extension<Syntax Extension>` | :ref:`Child<Syntax Child>`)* '}'
Objects are the basic building blocks of a GTK user interface. Your widgets are all objects, as are some other features such as list models.
Optionally, objects may have an ID to provide a handle for other parts of the blueprint and your code to access objects.
.. note::
Object IDs must be unique within their scope. The document root is a scope, but :ref:`sub-templates<Syntax ExtListItemFactory>` have their own, isolated scope.
Example
~~~~~~~
.. code-block:: blueprint
Label label1 {
label: "Hello, world!";
}
Label label2 {
label: bind-property file.name;
}
.. _Syntax TypeName:
Type Names
----------
.. rst-class:: grammar-block
TypeName = TypeNameFull | TypeNameExternal | TypeNameShort
TypeNameFull = <namespace::ref:`IDENT<Syntax IDENT>`> '.' <name::ref:`IDENT<Syntax IDENT>`>
TypeNameExternal = '$' <name::ref:`IDENT<Syntax IDENT>`>
TypeNameShort = <name::ref:`IDENT<Syntax IDENT>`>
There are three forms of type names: full, short, and external. Full type names take the form ``{namespace}.{name}``, e.g. ``Gtk.ApplicationWindow`` or ``Adw.Leaflet``. Because GTK types are so common, the Gtk namespace may be omitted, shortening ``Gtk.ApplicationWindow`` to just ``ApplicationWindow``.
External type names refer to types defined in your application. They are prefixed with ``$`` and do not have a dot between the namespace and class name. In fact, anywhere a ``$`` is used in a blueprint, it refers to something that must be defined in your application code.
.. _Syntax Property:
Properties
----------
.. rst-class:: grammar-block
Property = <name::ref:`IDENT<Syntax IDENT>`> ':' ( :ref:`Binding<Syntax Binding>` | :ref:`ExprValue<Syntax ExprValue>` | :ref:`ObjectValue<Syntax ObjectValue>` | :ref:`Value<Syntax Value>` ) ';'
Properties specify the details of each object, like a label's text, an image's icon name, or the margins on a container.
Most properties are static and directly specified in the blueprint, but properties can also be bound to a data model using the ``bind`` or ``bind-property`` keywords.
A property's value can be another object, either inline or referenced by ID.
Example
~~~~~~~
.. code-block:: blueprint
Label {
label: "text";
}
Button {
/* Inline object value. Notice the semicolon after the object. */
child: Image {
/* ... */
};
}
.. _Syntax Signal:
Signal Handlers
---------------
.. rst-class:: grammar-block
Signal = <name::ref:`IDENT<Syntax IDENT>`> ('::' <detail::ref:`IDENT<Syntax IDENT>`>)? '=>' '$' <handler::ref:`IDENT<Syntax IDENT>`> '(' <object::ref:`IDENT<Syntax IDENT>`>? ')' (SignalFlag)* ';'
SignalFlag = 'after' | 'swapped' | 'not-swapped'
Signals are one way to respond to user input (another is `actions <https://docs.gtk.org/gtk4/actions.html>`_, which use the `action-name property <https://docs.gtk.org/gtk4/property.Actionable.action-name.html>`_).
Signals provide a handle for your code to listen to events in the UI. The handler name is prefixed with ``$`` to indicate that it's an external symbol which needs to be provided by your code; if it isn't, things might not work correctly, or at all.
Optionally, you can provide an object ID to use when connecting the signal.
The ``swapped`` flag is used to swap the order of the object and userdata arguments in C applications. If an object argument is specified, then this is the default behavior, so the ``not-swapped`` flag can be used to prevent the swap.
Example
~~~~~~~
.. code-block:: blueprint
Button {
clicked => $on_button_clicked();
}
.. _Syntax Child:
Children
--------
.. rst-class:: grammar-block
Child = ChildAnnotation? :ref:`Object<Syntax Object>`
ChildAnnotation = '[' ( ChildInternal | :ref:`ChildExtension<Syntax ChildExtension>` | ChildType ) ']'
ChildInternal = 'internal-child' <internal-child::ref:`IDENT<Syntax IDENT>`>
ChildType = <child_type::ref:`IDENT<Syntax IDENT>`>
Some objects can have children. This defines the hierarchical structure of a user interface: containers contain widgets, which can be other containers, and so on.
Child annotations are defined by the parent widget. Some widgets, such as `HeaderBar <https://docs.gtk.org/gtk4/class.HeaderBar.html>`_, have "child types" which allow different child objects to be treated in different ways. Some, such as `Dialog <https://docs.gtk.org/gtk4/class.Dialog.html>`_ and `InfoBar <https://docs.gtk.org/gtk4/class.InfoBar.html>`_, define child :ref:`extensions<Syntax ChildExtension>`, which provide more detailed information about the child.
Internal children are a special case. Rather than creating a new object, children marked with ``[internal-child <name>]`` modify an existing object provided by the parent. This is used, for example, for the ``content_area`` of a `Dialog <https://docs.gtk.org/gtk4/class.Dialog.html>`_.
.. note::
The objects at the root of a blueprint cannot have child annotations, since there is no root widget for them to be a child of.
.. note::
Some widgets, like `Button <https://docs.gtk.org/gtk4/class.Button.html>`_, use a property to set their child instead. Widgets added in this way don't have child annotations.
Examples
~~~~~~~~
Add children to a container
+++++++++++++++++++++++++++
.. code-block:: blueprint
Button {
Image {}
}
Child types
+++++++++++
.. code-block:: blueprint
HeaderBar {
[start]
Label {
}
[end]
Button {
}
}
Child extensions
++++++++++++++++
.. code-block:: blueprint
Dialog {
// Here, a child extension annotation defines the button's response.
[action response=cancel]
Button {}
}
Internal children
+++++++++++++++++
.. code-block:: blueprint
Dialog {
[internal-child content_area]
Box {
// Unlike most objects in a blueprint, this internal-child widget
// represents the properties, signal handlers, children, and extensions
// of an existing Box created by the Dialog, not a new Box created by
// the blueprint.
}
}

View file

@ -1,96 +0,0 @@
===================
Composite Templates
===================
.. _Syntax Template:
Composite Templates
-------------------
.. rst-class:: grammar-block
Template = 'template' :ref:`TypeName<Syntax TypeName>` ( ':' :ref:`TypeName<Syntax TypeName>` )? :ref:`ObjectContent<Syntax Object>`
Widget subclassing is one of the primary techniques for structuring an application. For example, a maps app might have a `Gtk.ApplicationWindow <https://docs.gtk.org/gtk4/class.ApplicationWindow.html>`_ subclass, ``MapsApplicationWindow``, that implements the functionality of its main window. But a maps app has a lot of functionality, so the headerbar might be split into its own `Gtk.HeaderBar <https://docs.gtk.org/gtk4/class.HeaderBar.html>`_ subclass, ``MapsHeaderBar``, for the sake of organization.
You could implement this with the following blueprint:
.. code-block:: blueprint
using Gtk 4.0;
$MapsApplicationWindow window {
$MapsHeaderBar {
/* probably a lot of buttons ... */
}
$MapsMainView {
/* a lot more UI definitions ... */
}
}
There are two problems with this approach:
1. The widget code may be organized neatly into different files, but the UI is not. This blueprint contains the entire UI definition for the app.
2. Widgets aren't in control of their own contents. It shouldn't be up to the caller to construct a widget using the correct blueprint--that's an implementation detail of the widget.
We can solve these problems by giving each widget its own blueprint file, which we reference in the widget's constructor. Then, whenever the widget is instantiated (by another blueprint, or by the application), it will get all the children and properties defined in its blueprint.
For this to work, we need to specify in the blueprint which object is the one being instantiated. We do this with a template block:
.. code-block:: blueprint
using Gtk 4.0;
template $MapsHeaderBar : Gtk.HeaderBar {
/* probably a lot of buttons ... */
}
Gio.ListStore bookmarked_places_store {
/* This isn't the object being instantiated, just an auxillary object. GTK knows this because it isn't the
one marked with 'template'. */
}
This blueprint can only be used by the ``MapsHeaderBar`` constructor. Instantiating it with ``Gtk.Builder`` won't work since it needs an existing, under-construction ``MapsHeaderBar`` to use for the template object. The ``template`` block must be at the top level of the file (not nested within another object) and only one is allowed per file.
This ``MapsHeaderBar`` class, along with its blueprint template, can then be referenced in another blueprint:
.. code-block:: blueprint
using Gtk 4.0;
ApplicationWindow {
$MapsHeaderBar {
/* Nothing needed here, the widgets are in the MapsHeaderBar template. */
}
}
Type & Parent Parameters
~~~~~~~~~~~~~~~~~~~~~~~~
The type name that directly follows the ``template`` keyword is the type of the template class. In most cases, this will be an extern type starting with ``$`` and matching the class name in the application code. Templates for use in a `Gtk.BuilderListItemFactory <https://docs.gtk.org/gtk4/class.BuilderListItemFactory.html>`_ use ``ListItem`` as the type name instead.
The parent type is optional, and may only be present if the template type is extern. It enables limited type checking for the properties and signals of the template object.
Referencing a Template
----------------------
To reference the template object in a binding or expression, use the ``template`` keyword:
.. code-block:: blueprint
template $MyTemplate {
prop1: "Hello, world!";
prop2: bind template.prop1;
}
Language Implementations
------------------------
- **C** ``gtk_widget_class_set_template ()``: https://docs.gtk.org/gtk4/class.Widget.html#building-composite-widgets-from-template-xml
- **gtk-rs** ``#[template]``: https://gtk-rs.org/gtk4-rs/stable/latest/book/composite_templates.html
- **GJS** ``GObject.registerClass()``: https://gjs.guide/guides/gtk/3/14-templates.html
- **PyGObject** ``@Gtk.Template``: https://pygobject.gnome.org/guide/gtk_template.html

View file

@ -1,181 +0,0 @@
======
Values
======
.. _Syntax Value:
Values
------
.. rst-class:: grammar-block
Value = :ref:`Translated<Syntax Translated>` | :ref:`Flags<Syntax Flags>` | :ref:`Literal<Syntax Literal>`
.. _Syntax Literal:
Literals
--------
.. rst-class:: grammar-block
Literal = :ref:`TypeLiteral<Syntax TypeLiteral>` | QuotedLiteral | NumberLiteral | IdentLiteral
QuotedLiteral = <value::ref:`QUOTED<Syntax QUOTED>`>
NumberLiteral = ( '-' | '+' )? <value::ref:`NUMBER<Syntax NUMBER>`>
IdentLiteral = <ident::ref:`IDENT<Syntax IDENT>`>
Literals are used to specify values for properties. They can be strings, numbers, references to objects, ``null``, types, boolean values, or enum members.
.. _Syntax TypeLiteral:
Type Literals
-------------
.. rst-class:: grammar-block
TypeLiteral = 'typeof' '<' :ref:`TypeName<Syntax TypeName>` '>'
Sometimes, you need to specify a type as a value. For example, when creating a list store, you may need to specify the type of the items in the list store. This is done using a ``typeof<>`` literal.
The type of a ``typeof<>`` literal is `GType <https://docs.gtk.org/gobject/alias.Type.html>`_, GObject's "meta-type" for type information.
Example
~~~~~~~
.. code-block:: blueprint
Gio.ListStore {
item-type: typeof<GObject.Object>;
}
.. _Syntax Flags:
Flags
-----
.. rst-class:: grammar-block
Flags = <first::ref:`IDENT<Syntax IDENT>`> '|' ( <rest::ref:`IDENT<Syntax IDENT>`> )|+
Flags are used to specify a set of options. One or more of the available flag values may be specified, and they are combined using ``|``.
Example
~~~~~~~
.. code-block:: blueprint
Adw.TabView {
shortcuts: control_tab | control_shift_tab;
}
.. _Syntax Translated:
Translated Strings
------------------
.. rst-class:: grammar-block
Translated = ( '_' '(' <string::ref:`QUOTED<Syntax QUOTED>`> ')' ) | ( '\C_' '(' <context::ref:`QUOTED<Syntax QUOTED>`> ',' <string::ref:`QUOTED<Syntax QUOTED>`> ')' )
Use ``_("...")`` to mark strings as translatable. You can put a comment for translators on the line above if needed.
.. code-block:: blueprint
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:: blueprint
Gtk.Label label {
/* Translators: This is a section in the preferences window */
label: C_("preferences window", "Hello, world!");
}
.. _Syntax Binding:
Bindings
--------
.. rst-class:: grammar-block
Binding = 'bind' :ref:`Expression<Syntax Expression>` (BindingFlag)*
BindingFlag = 'inverted' | 'bidirectional' | 'no-sync-create'
Bindings keep a property updated as other properties change. They can be used to keep the UI in sync with application data, or to connect two parts of the UI.
The simplest bindings connect to a property of another object in the blueprint. When that other property changes, the bound property updates as well. More advanced bindings can do multi-step property lookups and can even call application code to compute values. See :ref:`the expressions page<Syntax Expression>`.
Simple Bindings
~~~~~~~~~~~~~~~
A binding that consists of a source object and a single lookup is called a "simple binding". These are implemented using `GObject property bindings <https://docs.gtk.org/gobject/method.Object.bind_property.html>`_ and support a few flags:
- ``inverted``: For boolean properties, the target is set to the inverse of the source property.
- ``bidirectional``: The binding is two-way, so changes to the target property will also update the source property.
- ``no-sync-create``: Normally, when a binding is created, the target property is immediately updated with the current value of the source property. This flag disables that behavior, and the bound property will be updated the next time the source property changes.
Complex Bindings
~~~~~~~~~~~~~~~~
Bindings with more complex expressions are implemented with `Gtk.Expression <https://docs.gtk.org/gtk4/class.Expression.html>`_. These bindings do not support flags.
Example
~~~~~~~
.. code-block:: blueprint
/* Use bindings to show a label when a switch
* is active, without any application code */
Switch show_label {}
Label {
visible: bind show_label.active;
label: _("I'm a label that's only visible when the switch is enabled!");
}
.. _Syntax ObjectValue:
Object Values
-------------
.. rst-class:: grammar-block
ObjectValue = :ref:`Object<Syntax Object>`
The value of a property can be an object, specified inline. This is particularly useful for widgets that use a ``child`` property rather than a list of child widgets. Objects constructed in this way can even have IDs and be referenced in other places in the blueprint.
Such objects cannot have child annotations because they aren't, as far as blueprint is concerned, children of another object.
.. _Syntax StringValue:
String Values
-------------
.. rst-class:: grammar-block
StringValue = :ref:`Translated<Syntax Translated>` | :ref:`QuotedLiteral<Syntax Literal>`
Menus, as well as some :ref:`extensions<Syntax Extension>`, have properties that can only be string literals or translated strings.
.. _Syntax ArrayValue:
Array Values
-------------
.. rst-class:: grammar-block
ArrayValue = '[' (:ref:`StringValue<Syntax StringValue>`),* ']'
For now, it only supports :ref:`Strings<Syntax StringValue>`. This is because Gtk.Builder only supports string arrays.

View file

@ -8,7 +8,7 @@ Setting up Blueprint on a new or existing project
Using the porting tool Using the porting tool
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
Clone `blueprint-compiler <https://gitlab.gnome.org/GNOME/blueprint-compiler>`_ Clone `blueprint-compiler <https://gitlab.gnome.org/jwestman/blueprint-compiler>`_
from source. You can install it using ``meson _build`` and ``ninja -C _build install``, from source. You can install it using ``meson _build`` and ``ninja -C _build install``,
or you can leave it uninstalled. or you can leave it uninstalled.
@ -29,7 +29,7 @@ blueprint-compiler works as a meson subproject.
[wrap-git] [wrap-git]
directory = blueprint-compiler directory = blueprint-compiler
url = https://gitlab.gnome.org/GNOME/blueprint-compiler.git url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git
revision = main revision = main
depth = 1 depth = 1

View file

@ -5,7 +5,7 @@ Translations
Blueprint files can be translated with xgettext. To mark a string as translated, Blueprint files can be translated with xgettext. To mark a string as translated,
use the following syntax: use the following syntax:
.. code-block:: blueprint .. code-block::
_("translated string") _("translated string")
@ -24,8 +24,6 @@ If you're using Meson's `i18n module <https://mesonbuild.com/i18n-module.html#i1
i18n.gettext('package name', preset: 'glib') i18n.gettext('package name', preset: 'glib')
You must use double quotes for the translated strings in order for gettext to recognize them. Newer versions of blueprint will warn you if you use single quotes.
Contexts Contexts
-------- --------
@ -36,7 +34,7 @@ conflicts. Two strings that are the same in English, but appear in different
contexts, might be different in another language! To disambiguate, use ``C_`` contexts, might be different in another language! To disambiguate, use ``C_``
instead of ``_`` and add a context string as the first argument: instead of ``_`` and add a context string as the first argument:
.. code-block:: blueprint .. code-block::
C_("shortcuts window", "Quit") C_("shortcuts window", "Quit")

View file

@ -1,22 +0,0 @@
default: black isort
# Format with black formatter
black:
black ./
# Sort imports using isort
isort:
isort ./ --profile black
# Run all tests
test: mypy unittest
# Check typings with mypy
mypy:
mypy --python-version=3.9 blueprintcompiler/
# Test code with unittest
unittest:
python3 -m unittest

View file

@ -1,14 +1,14 @@
project('blueprint-compiler', project('blueprint-compiler',
version: '0.16.0', version: '0.2.0',
) )
subdir('docs')
prefix = get_option('prefix') prefix = get_option('prefix')
datadir = join_paths(prefix, get_option('datadir')) datadir = join_paths(prefix, get_option('datadir'))
py = import('python').find_installation('python3') py = import('python').find_installation('python3')
subdir('docs')
configure_file( configure_file(
input: 'blueprint-compiler.pc.in', input: 'blueprint-compiler.pc.in',
output: 'blueprint-compiler.pc', output: 'blueprint-compiler.pc',
@ -17,29 +17,22 @@ configure_file(
install_dir: join_paths(datadir, 'pkgconfig'), install_dir: join_paths(datadir, 'pkgconfig'),
) )
config = configuration_data({
'VERSION': meson.project_version(),
'LIBDIR': get_option('prefix') / get_option('libdir'),
})
if meson.is_subproject()
config.set('MODULE_PATH', meson.current_source_dir())
else
config.set('MODULE_PATH', py.get_install_dir())
endif
blueprint_compiler = configure_file( blueprint_compiler = configure_file(
input: 'blueprint-compiler.py', input: 'blueprint-compiler.py',
output: 'blueprint-compiler', output: 'blueprint-compiler',
configuration: config, configuration: {
'VERSION': meson.project_version(),
},
install: not meson.is_subproject(), install: not meson.is_subproject(),
install_dir: get_option('bindir'), install_dir: get_option('bindir'),
) )
if meson.is_subproject() # Don't use the output configure_file here--that file is in the build directory
meson.override_find_program('blueprint-compiler', blueprint_compiler) # and won't be able to find the python modules in the source directory.
else meson.override_find_program('blueprint-compiler', find_program('blueprint-compiler.py'))
install_subdir('blueprintcompiler', install_dir: py.get_install_dir())
if not meson.is_subproject()
install_subdir('blueprintcompiler', install_dir: datadir / 'blueprint-compiler')
endif endif
subdir('tests') subdir('tests')

View file

@ -1,4 +0,0 @@
using Gtk 4.0;
//comment
// Trailing whitespace:
//

View file

@ -1,4 +0,0 @@
using Gtk 4.0;
// comment
// Trailing whitespace:
//

View file

@ -1,69 +0,0 @@
using Gtk 4.0;
using Adw 1;
template $MyTemplate: Label {
/**
* A list of strings.
*/
StringList {
// comment
strings [
"Hello",
C_("Greeting", "World"),
]
}
object: Button {
label: "Click me";
};
flags: a | b;
[child]
Label {}
[child]
Label label2 {}
// Single line comment.
/**
* Multiline comment.
*/
// Single line comment.
value: bind (1.0) as <double>;
as: 1;
signal => $on_signal() after; // Inline comment
type_value: typeof<$MyTemplate>;
}
Dialog {
[action response=ok]
$MyButton {}
}
menu menu {
item ("test")
item {
label: "test";
}
item ("test")
}
Adw.MessageDialog {
responses [
save: "Save" suggested disabled,
]
}
Adw.Breakpoint {
condition ("width < 100")
setters {
label2.label: _("Hello, world!");
label2.visible: false;
label2.extra-menu: null;
}
}

View file

@ -1 +0,0 @@
using Gtk 4.0;using Adw 1;Overlay{Label label{label:_("'Hello World!' \"\n\t\"");}[overlay]Button{notify::icon-name=>$on_icon_name_changed(label)swapped;styles["destructive"]}visible:bind $isVisible(label.visible,my-menu)as<bool>;width-request:bind label.width-request no-sync-create;}menu my-menu{item(_("Label"), "action-name", "icon-name")item{action:"win.format";}}

View file

@ -1,40 +0,0 @@
using Gtk 4.0;
using Adw 1;
Overlay {
Label
label
{
label
:
_
(
"'Hello World!' \"\n\t\""
)
;
}
[
overlay
] Button
{ notify
:: icon-name
=> $ on_icon_name_changed ( label )
swapped ;
styles
[ "destructive" ]
}
visible
: bind $ isVisible ( label.visible ,
my-menu ) as
< bool > ; width-request : bind label . width-request no-sync-create ; }
menu my-menu
{ item ( _ ( "Label" ) , "action-name" , "icon-name" ) item { action : "win.format" ; } }

View file

@ -1,21 +0,0 @@
using Gtk 4.0;
Box {
styles []
}
Box {
styles ["a"]
}
Box {
styles ["a",]
}
Box {
styles ["a", "b"]
}
Box {
styles ["a", "b",]
}

View file

@ -1,31 +0,0 @@
using Gtk 4.0;
Box {
styles []
}
Box {
styles [
"a",
]
}
Box {
styles [
"a",
]
}
Box {
styles [
"a",
"b",
]
}
Box {
styles [
"a",
"b",
]
}

View file

@ -1,28 +0,0 @@
using Gtk 4.0;
using Adw 1;
Overlay {
Label label {
label: _("'Hello World!' \"\n\t\"");
}
[overlay]
Button {
notify::icon-name => $on_icon_name_changed(label) swapped;
styles [
"destructive",
]
}
visible: bind $isVisible(label.visible, my-menu) as <bool>;
width-request: bind label.width-request no-sync-create;
}
menu my-menu {
item (_("Label"), "action-name", "icon-name")
item {
action: "win.format";
}
}

View file

@ -1,5 +0,0 @@
using Gtk 4.0;
Label {
label: "\"'\'\t\n\\'";
}

View file

@ -1,5 +0,0 @@
using Gtk 4.0;
Label {
label: "\"'\'\t\n\\'";
}

View file

@ -1,22 +1,13 @@
import os import os, sys
import sys
from pythonfuzz.main import PythonFuzz from pythonfuzz.main import PythonFuzz
from blueprintcompiler.outputs.xml import XmlOutput
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from blueprintcompiler import decompiler, gir, parser, tokenizer, utils from blueprintcompiler import tokenizer, parser, decompiler
from blueprintcompiler.completions import complete from blueprintcompiler.completions import complete
from blueprintcompiler.errors import ( from blueprintcompiler.errors import PrintableError, MultipleErrors, CompileError, CompilerBugError
CompileError,
CompilerBugError,
MultipleErrors,
PrintableError,
)
from blueprintcompiler.tokenizer import Token, TokenType, tokenize from blueprintcompiler.tokenizer import Token, TokenType, tokenize
from blueprintcompiler import utils
@PythonFuzz @PythonFuzz
def fuzz(buf): def fuzz(buf):
@ -26,9 +17,8 @@ def fuzz(buf):
tokens = tokenizer.tokenize(blueprint) tokens = tokenizer.tokenize(blueprint)
ast, errors, warnings = parser.parse(tokens) ast, errors, warnings = parser.parse(tokens)
xml = XmlOutput() if errors is None and len(ast.errors) == 0:
if errors is None and ast is not None: actual = ast.generate()
xml.emit(ast)
except CompilerBugError as e: except CompilerBugError as e:
raise e raise e
except PrintableError: except PrintableError:
@ -36,10 +26,5 @@ def fuzz(buf):
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
if __name__ == "__main__": if __name__ == "__main__":
# Make sure Gtk 4.0 is accessible, otherwise every test will fail on that
# and nothing interesting will be tested
gir.get_namespace("Gtk", "4.0")
fuzz() fuzz()

View file

@ -1 +1 @@
test('tests', py, args: ['-m', 'unittest'], workdir: meson.project_source_root()) test('tests', py, args: ['-m', 'unittest'], workdir: meson.source_root())

View file

@ -1,9 +0,0 @@
using Gtk 4.0;
Box {
accessibility {
label: _("Hello, world!");
labelled-by: [];
checked: true;
}
}

View file

@ -1 +0,0 @@
6,5,11,'labelled-by' may not be empty

View file

@ -1,15 +0,0 @@
using Gtk 4.0;
Box {
accessibility {
label: _("Hello, world!");
active-descendant: [my_label1, my_label2, my_label3];
checked: true;
}
}
Label my_label1 {}
Label my_label2 {}
Label my_label3 {}

View file

@ -1 +0,0 @@
6,5,17,'active-descendant' does not allow a list of values

View file

@ -1 +1 @@
5,18,1,Cannot convert number to Gtk.Orientation 5,18,1,Cannot convert 1 to Gtk.Orientation

Some files were not shown because too many files have changed in this diff Show more