-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathcssterm.py
277 lines (211 loc) · 9.44 KB
/
cssterm.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# Copyright (C) 2021
# Author: Kacper Sokol <ks1591@my.bristol.ac.uk>
# License: new BSD
"""
Implements the `cssterm` directive for Jupyter Book and Sphinx.
"""
import os
import sys
from docutils import nodes
from docutils.parsers.rst import Directive
import sphinx_term
DEPENDENCIES = { # See sphinx_term/_static/README.md for more info
# jQuery (MIT): https://github.com/jquery/jquery
'jquery.js': 'https://code.jquery.com/jquery-latest.min.js'
}
STATIC_CSS_FILES = ['cssterm/css/cssterm.css']
STATIC_JS_FILES = ['cssterm/scripts/cssterm.js']
STATIC_FILES = STATIC_CSS_FILES + STATIC_JS_FILES
REFNAME = 'terminal box'
if sys.version_info >= (3, 0):
unicode = str
#### cssterm directive ########################################################
class cssterm_anchor(nodes.General, nodes.Element):
"""A `docutils` node anchoring cssterm boxes."""
def visit_cssterm_anchor_node(self, node):
"""Builds an opening HTML tag for cssterm anchors."""
self.body.append(self.starttag(node, 'div'))
def depart_cssterm_anchor_node(self, node):
"""Builds a closing HTML tag for cssterm anchors."""
self.body.append('</div>\n')
def visit_cssterm_anchor_node_(self, node):
"""Builds a prefix for embedding cssterm anchors in LaTeX and raw text."""
raise NotImplemented
def depart_cssterm_anchor_node_(self, node):
"""Builds a postfix for embedding cssterm anchors in LaTeX and raw text."""
raise NotImplemented
class cssterm_box(nodes.literal_block, nodes.Element):
"""A `docutils` node holding cssterm boxes."""
def visit_cssterm_box_node(self, node):
"""Builds an opening HTML tag for cssterm boxes."""
self.body.append(self.starttag(node, 'div', CLASS='cssterm'))
def depart_cssterm_box_node(self, node):
"""Builds a closing HTML tag for cssterm boxes."""
self.body.append('</div>\n')
def visit_cssterm_box_node_(self, node):
"""Builds a prefix for embedding cssterm boxes in LaTeX and raw text."""
raise NotImplemented
def depart_cssterm_box_node_(self, node):
"""Builds a postfix for embedding cssterm boxes in LaTeX and raw text. """
raise NotImplemented
class CSSterm(Directive):
"""
Defines the `cssterm` directive that builds cssterm boxes.
The `cssterm` directive is of the form::
.. cssterm:: cssterm:1.2.3 (required)
If loaded from an external file, the box id needs to be a terminal
transcript file name **with** the `cssterm:` prefix and **without**
the `.log` extension, located in a single directory.
The directory is given to Sphinx via the `sphinx_term_cssterm_dir`
config setting.
If this parameter is not set, terminal box content must be provided
explicitly.
This Sphinx extension monitors the terminal transcript files for changes
and regenerates the content pages that use them if a change is detected.
"""
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
has_content = True
option_spec = {}
def run(self):
"""Builds a cssterm box."""
env = self.state.document.settings.env
# retrieve the path to the directory holding the code files
st_term_dir = env.config.sphinx_term_cssterm_dir
assert isinstance(st_term_dir, str) or st_term_dir is None
# get the terminal file name for this particular cssterm box
assert len(self.arguments) == 1, (
'Just one argument -- terminal block id (possibly encoding the '
'code file name -- expected')
term_filename_id = self.arguments[0]
assert term_filename_id.startswith('cssterm:'), (
'The terminal box label ({}) must start with the "cssterm:" '
'prefix.'.format(term_filename_id))
assert not term_filename_id.endswith('.log'), (
'The terminal box label ({}) must not end with the ".log" '
'extension prefix.'.format(term_filename_id))
# add the .log extension as it is missing
term_filename = '{}.log'.format(term_filename_id[8:])
# if the content is given explicitly, use it instead of loading a file
if self.content:
contents = '\n'.join(self.content)
else:
localised_directory = sphinx_term.localise_term_directory(
env.srcdir,
st_term_dir,
('sphinx_term_cssterm_dir', 'cssterm box content'))
# compose the full path to the code file and ensure it exists
path_localised = os.path.join(localised_directory, term_filename)
# path_original = os.path.join(st_term_dir, term_filename)
sphinx_term.file_exists(path_localised)
# memorise the association between the document (a content source
# file) and the terminal box -- this is used for watching for
# terminal file updates
env.note_dependency(path_localised)
# read in the terminal file
with open(path_localised, 'r') as f:
contents = f.read().strip('\n')
# create a cssterm node
box = cssterm_box(contents.strip(), contents,
ids=['{}-box'.format(
nodes.make_id(term_filename_id))],
label=term_filename_id)
# create anchor
anchor = cssterm_anchor()
# assign label and id (`ids=[nodes.make_id(term_filename_id)]`)
self.options['name'] = term_filename_id
self.add_name(anchor)
# insert the terminal box node into the anchor node
anchor += box
return [anchor]
def assign_reference_title(app, document):
"""
Update the labels record of the standard environment to allow referencing
cssterm boxes.
"""
# get the standard domain
domain = app.env.get_domain('std')
# go through every cssterm box
for node in document.traverse(cssterm_anchor):
# every cssterm box must have exactly one name starting with 'cssterm:'
assert node['names']
assert len(node['names']) == 1
node_name = node['names'][0]
assert node_name.startswith('cssterm:'), (
'cssterm box ids must start with cssterm:')
refname = REFNAME
# every cssterm box has a single id
assert len(node['ids']) == 1
node_id = node['ids'][0]
# get the document name
docname = app.env.docname
# every cssterm box should **already** be referenceable without a title
assert node_name in domain.anonlabels
assert domain.anonlabels[node_name] == (docname, node_id)
# allow this cssterm box to be referenced with the default
# 'terminal box' stub (REFNAME)
domain.labels[node_name] = (docname, node_id, refname)
#### Extension setup ##########################################################
def include_static_files(app):
"""
Copies the static files required by this extension.
(Attached to the `builder-inited` Sphinx event.)
"""
for file_name in STATIC_FILES:
file_path = sphinx_term.get_static_path(file_name)
if file_path not in app.config.html_static_path:
app.config.html_static_path.append(file_path)
def load_static_files(app, pagename, templatename, context, doctree):
"""Includes cssterm static files only on pages that use the module."""
# only go through non-empty documents
if doctree is None:
return
# get cssterm boxes
cssterm_boxes = doctree.traverse(cssterm_box)
# skip pages without at least one cssterm box
if not cssterm_boxes:
return
# ensure that custom files were included
for css_file in STATIC_CSS_FILES:
_css_file = os.path.basename(css_file)
if not sphinx_term.is_css_registered(app, _css_file):
app.add_css_file(_css_file)
for js_file in STATIC_JS_FILES:
_js_file = os.path.basename(js_file)
if not sphinx_term.is_js_registered(app, _js_file):
app.add_js_file(_js_file)
# add external dependencies
script_files = [os.path.basename(i) for i in context['script_files']]
for stub, path in DEPENDENCIES.items():
if sphinx_term.is_js_registered(app, path) or stub in script_files:
continue
app.add_js_file(path)
def setup(app):
"""
Sets up the Sphinx extension for the `cssterm` directive.
"""
# register two Sphinx config values used for the extension
app.add_config_value('sphinx_term_cssterm_dir', None, 'env')
# register the custom docutils nodes with Sphinx
app.add_node(
cssterm_box,
html=(visit_cssterm_box_node, depart_cssterm_box_node),
latex=(visit_cssterm_box_node_, depart_cssterm_box_node_),
text=(visit_cssterm_box_node_, depart_cssterm_box_node_)
)
app.add_node(
cssterm_anchor,
html=(visit_cssterm_anchor_node, depart_cssterm_anchor_node),
latex=(visit_cssterm_anchor_node_, depart_cssterm_anchor_node_),
text=(visit_cssterm_anchor_node_, depart_cssterm_anchor_node_)
)
# register the custom role and directives with Sphinx
app.add_directive('cssterm', CSSterm)
# connect custom hooks to the Sphinx build process
app.connect('doctree-read', assign_reference_title)
# ...ensure the required static files are **copied** into the build
app.connect('builder-inited', include_static_files)
# ...ensure that relevant html output pages **load** the static files
app.connect('html-page-context', load_static_files)
return {'version': sphinx_term.VERSION}