1#!/usr/bin/env python
2
3# This Source Code Form is subject to the terms of the Mozilla Public
4# License, v. 2.0. If a copy of the MPL was not distributed with this
5# file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
7import re
8import os
9import sys
10
11from io import BytesIO
12
13GECKO_DIR = os.path.dirname(__file__.replace("\\", "/"))
14sys.path.insert(0, os.path.join(os.path.dirname(GECKO_DIR), "properties"))
15
16import build
17
18
19# Matches lines like `GK_ATOM(foo, "foo", 0x12345678, true, nsStaticAtom, PseudoElementAtom)`.
20PATTERN = re.compile(
21    '^GK_ATOM\(([^,]*),[^"]*"([^"]*)",\s*(0x[0-9a-f]+),\s*[^,]*,\s*([^,]*),\s*([^)]*)\)',
22    re.MULTILINE,
23)
24FILE = "include/nsGkAtomList.h"
25
26
27def map_atom(ident):
28    if ident in {
29        "box",
30        "loop",
31        "match",
32        "mod",
33        "ref",
34        "self",
35        "type",
36        "use",
37        "where",
38        "in",
39    }:
40        return ident + "_"
41    return ident
42
43
44class Atom:
45    def __init__(self, ident, value, hash, ty, atom_type):
46        self.ident = "nsGkAtoms_{}".format(ident)
47        self.original_ident = ident
48        self.value = value
49        self.hash = hash
50        # The Gecko type: "nsStaticAtom", "nsCSSPseudoElementStaticAtom", or
51        # "nsAnonBoxPseudoStaticAtom".
52        self.ty = ty
53        # The type of atom: "Atom", "PseudoElement", "NonInheritingAnonBox",
54        # or "InheritingAnonBox".
55        self.atom_type = atom_type
56
57        if (
58            self.is_pseudo_element()
59            or self.is_anon_box()
60            or self.is_tree_pseudo_element()
61        ):
62            self.pseudo_ident = (ident.split("_", 1))[1]
63
64        if self.is_anon_box():
65            assert self.is_inheriting_anon_box() or self.is_non_inheriting_anon_box()
66
67    def type(self):
68        return self.ty
69
70    def capitalized_pseudo(self):
71        return self.pseudo_ident[0].upper() + self.pseudo_ident[1:]
72
73    def is_pseudo_element(self):
74        return self.atom_type == "PseudoElementAtom"
75
76    def is_anon_box(self):
77        if self.is_tree_pseudo_element():
78            return False
79        return self.is_non_inheriting_anon_box() or self.is_inheriting_anon_box()
80
81    def is_non_inheriting_anon_box(self):
82        assert not self.is_tree_pseudo_element()
83        return self.atom_type == "NonInheritingAnonBoxAtom"
84
85    def is_inheriting_anon_box(self):
86        if self.is_tree_pseudo_element():
87            return False
88        return self.atom_type == "InheritingAnonBoxAtom"
89
90    def is_tree_pseudo_element(self):
91        return self.value.startswith(":-moz-tree-")
92
93
94def collect_atoms(objdir):
95    atoms = []
96    path = os.path.abspath(os.path.join(objdir, FILE))
97    print("cargo:rerun-if-changed={}".format(path))
98    with open(path) as f:
99        content = f.read()
100        for result in PATTERN.finditer(content):
101            atoms.append(
102                Atom(
103                    result.group(1),
104                    result.group(2),
105                    result.group(3),
106                    result.group(4),
107                    result.group(5),
108                )
109            )
110    return atoms
111
112
113class FileAvoidWrite(BytesIO):
114    """File-like object that buffers output and only writes if content changed."""
115
116    def __init__(self, filename):
117        BytesIO.__init__(self)
118        self.name = filename
119
120    def write(self, buf):
121        if isinstance(buf, str):
122            buf = buf.encode("utf-8")
123        BytesIO.write(self, buf)
124
125    def close(self):
126        buf = self.getvalue()
127        BytesIO.close(self)
128        try:
129            with open(self.name, "rb") as f:
130                old_content = f.read()
131                if old_content == buf:
132                    print("{} is not changed, skip".format(self.name))
133                    return
134        except IOError:
135            pass
136        with open(self.name, "wb") as f:
137            f.write(buf)
138
139    def __enter__(self):
140        return self
141
142    def __exit__(self, type, value, traceback):
143        if not self.closed:
144            self.close()
145
146
147PRELUDE = """
148/* This Source Code Form is subject to the terms of the Mozilla Public
149 * License, v. 2.0. If a copy of the MPL was not distributed with this
150 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
151
152// Autogenerated file created by components/style/gecko/regen_atoms.py.
153// DO NOT EDIT DIRECTLY
154"""[
155    1:
156]
157
158RULE_TEMPLATE = """
159    ("{atom}") => {{{{
160        #[allow(unsafe_code)] #[allow(unused_unsafe)]
161        unsafe {{ $crate::string_cache::Atom::from_index_unchecked({index}) }}
162    }}}};
163"""[
164    1:
165]
166
167MACRO_TEMPLATE = """
168/// Returns a static atom by passing the literal string it represents.
169#[macro_export]
170macro_rules! atom {{
171{body}\
172}}
173"""
174
175
176def write_atom_macro(atoms, file_name):
177    with FileAvoidWrite(file_name) as f:
178        f.write(PRELUDE)
179        macro_rules = [
180            RULE_TEMPLATE.format(atom=atom.value, name=atom.ident, index=i)
181            for (i, atom) in enumerate(atoms)
182        ]
183        f.write(MACRO_TEMPLATE.format(body="".join(macro_rules)))
184
185
186def write_pseudo_elements(atoms, target_filename):
187    pseudos = []
188    for atom in atoms:
189        if (
190            atom.type() == "nsCSSPseudoElementStaticAtom"
191            or atom.type() == "nsCSSAnonBoxPseudoStaticAtom"
192        ):
193            pseudos.append(atom)
194
195    pseudo_definition_template = os.path.join(
196        GECKO_DIR, "pseudo_element_definition.mako.rs"
197    )
198    print("cargo:rerun-if-changed={}".format(pseudo_definition_template))
199    contents = build.render(pseudo_definition_template, PSEUDOS=pseudos)
200
201    with FileAvoidWrite(target_filename) as f:
202        f.write(contents)
203
204
205def generate_atoms(dist, out):
206    atoms = collect_atoms(dist)
207    write_atom_macro(atoms, os.path.join(out, "atom_macro.rs"))
208    write_pseudo_elements(atoms, os.path.join(out, "pseudo_element_definition.rs"))
209
210
211if __name__ == "__main__":
212    if len(sys.argv) != 3:
213        print("Usage: {} dist out".format(sys.argv[0]))
214        exit(2)
215    generate_atoms(sys.argv[1], sys.argv[2])
216