pages.py 6.28 KB
Newer Older
Matthieu Boileau's avatar
Matthieu Boileau committed
1
2
3
4
5
6
7
8
import nbformat
import re
import logging
import markdown
from pathlib import Path
from pprint import pformat
import jinja2
from bs4 import BeautifulSoup
9
import frontmatter
10
import sys
Matthieu Boileau's avatar
Matthieu Boileau committed
11
12

log = logging.getLogger(__name__)
13
logging.basicConfig(level=logging.INFO)
Matthieu Boileau's avatar
Matthieu Boileau committed
14
15


16
17
class Page:
    """An abstract class for a static web page"""
Matthieu Boileau's avatar
Matthieu Boileau committed
18

19
20
    html_template = ''
    html = ''
Matthieu Boileau's avatar
Matthieu Boileau committed
21
22

    def __init__(self, nbcourse):
23
        conf = nbcourse.conf.copy()
24
        self.html_path = conf['output_path'] / self.html
Matthieu Boileau's avatar
Matthieu Boileau committed
25
26
        self.title = conf['title']
        self.template_path = conf['template_path']
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
        self.variables = conf
        self.notebooks = nbcourse.notebooks

    def _render_template(self):
        """Return html rendered from template and variables"""

        # Inject variables into html_template
        env = jinja2.Environment(loader=jinja2.FileSystemLoader(
                                 self.template_path.as_posix()))
        template = env.get_template(self.html_template)
        html_out = template.render(self.variables)
        with open(self.html_path, 'w') as f:
            f.write(html_out)

    def render(self):
        """Render html using templating"""
        raise NotImplementedError()


class HomePage(Page):
    """A class to handle a homepage to be rendered"""

Matthieu Boileau's avatar
Matthieu Boileau committed
49
    html_template = "index.html.j2"
50
51
52
53
54
55
56
    html = "index.html"
    name = 'home'
    parent = None
    parent_name = ''

    def __init__(self, nbcourse):
        super().__init__(nbcourse)
Matthieu Boileau's avatar
Matthieu Boileau committed
57
58
        self.chapters = {}

Matthieu Boileau's avatar
Matthieu Boileau committed
59
60
    @staticmethod
    def _get_chapter(path: Path):
Matthieu Boileau's avatar
Matthieu Boileau committed
61
62
63
64
65
        """Get chapter from notebook file (source: bookbook)"""

        chapter_no = int(re.match(r'(\d+)\-', path.stem).group(1))
        nb = nbformat.read(str(path), as_version=4)

66
67
68
69
        if nb.cells[0].cell_type != 'markdown':
            log.error(f"The first cell of the notebook {path} should be Markdown type " + 
                      f"({nb.cells[0].cell_type} instead).")
            sys.exit()
Matthieu Boileau's avatar
Matthieu Boileau committed
70
71
72
73
74
75
        lines = nb.cells[0].source.splitlines()
        if lines[0].startswith('# '):
            header = lines[0][2:]
        elif len(lines) > 1 and lines[1].startswith('==='):
            header = lines[0]
        else:
76
77
            log.error(f"No heading found in {path}")
            sys.exit()
Matthieu Boileau's avatar
Matthieu Boileau committed
78
79
80
81
82
83
84
85
86
87

        assert path.suffix == '.ipynb', path

        return {'number': chapter_no,
                'title': header,
                'filename': path.stem}

    def _get_variables(self):
        """Set some variables from conf dictionnary and notebooks files"""

88
89
90
        def build_link_name(link_type: str, suffix: str):
            """Build archive or pdf book links from slug name"""
            if link_type in self.variables['links']:
Matthieu Boileau's avatar
Fix #22    
Matthieu Boileau committed
91
92
93
94
95
96
97
98
                try:
                    # For example look for yaml entry: `book: file: filename.pdf`
                    self.variables['links'][link_type]['target'] = \
                        self.variables[link_type]['file']
                except KeyError:
                    # Use <slug_title>.pdf
                    self.variables['links'][link_type]['target'] = \
                        self.variables['slug_title'] + suffix
99

Matthieu Boileau's avatar
Matthieu Boileau committed
100
101
102
103
        # Get chapters from notebook files
        chapters = []
        for nbfile in self.notebooks:
            chapter = self._get_chapter(nbfile)
Matthieu Boileau's avatar
Matthieu Boileau committed
104
105
106
107
108
            try:
                chapter['preview_only'] = chapter['number'] in \
                    self.variables['chapter_preview_only']
            except KeyError:
                chapter['preview_only'] = False
Matthieu Boileau's avatar
Matthieu Boileau committed
109
110
111
            chapters.append(chapter)
        chapters.sort(key=lambda chapter: chapter['number'])
        self.variables.update({'chapters': chapters})
112
113
114
        build_link_name('book', '.pdf')
        build_link_name('archive', '.zip')

115
116
        log.debug("Homepage template variables:")
        log.debug(pformat(self.variables))
Matthieu Boileau's avatar
Matthieu Boileau committed
117
118
119
120
121
122
123
124
125

    def _render_template(self):
        """Return html rendered from template and variables"""

        # Inject variables into html_template
        env = jinja2.Environment(loader=jinja2.FileSystemLoader(
                                 self.template_path.as_posix()))
        template = env.get_template(self.html_template)
        html_out = template.render(self.variables)
126
        with open(self.html_path, 'w') as f:
Matthieu Boileau's avatar
Matthieu Boileau committed
127
128
129
130
131
132
133
134
            f.write(html_out)

    def render(self):
        """Render html using templating"""
        self._get_variables()
        self._render_template()


135
class MarkdownPage(Page):
Matthieu Boileau's avatar
Matthieu Boileau committed
136

Matthieu Boileau's avatar
Matthieu Boileau committed
137
    html_template = "page.html.j2"
Matthieu Boileau's avatar
Matthieu Boileau committed
138

139
    def __init__(self, nbcourse, src: Path):
Matthieu Boileau's avatar
Matthieu Boileau committed
140
        super().__init__(nbcourse)
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
        self.src = src
        self.html = Path(self.src.name).with_suffix('.html')
        self.html_path = nbcourse.conf['output_path'] / self.html

        with open(self.src, 'r') as md_file:
            metadata, self.md_content = frontmatter.parse(md_file.read())
        self.title = metadata['title']
        self.parent_name = metadata.get('parent', 'home')
        self.name = self.src.name
        self.parent = None  # to be populate later

    def get_menu_list(self):
        """Return a list to be displayed as a top menu"""
        # At least current page
        menu = [(self.title, self.html)]
        page = self  # Initialize with current page
        # Ascend to parent page
        while page.parent.parent:
            menu.append((page.parent.title, page.parent.html))
            page = page.parent
        menu.reverse()
        return menu

    def render(self, parent):
Matthieu Boileau's avatar
Matthieu Boileau committed
165
166
        """Render markdown file into html file adding ToC"""

167
        self.parent = parent
Matthieu Boileau's avatar
Matthieu Boileau committed
168
        pattern = re.compile(r'^pages\/(.*)\.md$')
Matthieu Boileau's avatar
Matthieu Boileau committed
169
170
171
172
173

        def markdown2html():
            """Return html from markdown file"""
            md = markdown.Markdown(extensions=['fenced_code', 'codehilite',
                                               'toc'])
174
            body = md.convert(self.md_content)
Matthieu Boileau's avatar
Matthieu Boileau committed
175
176
177
178
179
180
181
182
183
184
185
186
187

            # Transform link to file.md into link to file.html
            soup = BeautifulSoup(body, 'html.parser')
            for link in soup.findAll(['a']):
                href = link.get('href')
                link['href'] = pattern.sub(r'\1.html', href)

            return soup.prettify(), md.toc

        body, toc = markdown2html()
        self.variables = {'title': self.title,
                          'article_content': body,
                          'toc': toc,
188
                          'menu': self.get_menu_list()}
Matthieu Boileau's avatar
Matthieu Boileau committed
189
        self._render_template()