Skip to content

blag.blag

blag's core methods.

build(args)

Build the site.

This is blag's main method that builds the site, generates the feed etc.

Parameters:

Name Type Description Default
args Namespace
required
Source code in blag/blag.py
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
278
279
280
281
282
283
def build(args: argparse.Namespace) -> None:
    """Build the site.

    This is blag's main method that builds the site, generates the feed
    etc.

    Parameters
    ----------
    args

    """
    os.makedirs(f"{args.output_dir}", exist_ok=True)
    convertibles = []
    for root, dirnames, filenames in os.walk(args.input_dir):
        for filename in filenames:
            rel_src = os.path.relpath(
                f"{root}/{filename}", start=args.input_dir
            )
            # all non-markdown files are just copied over, the markdown
            # files are converted to html
            if rel_src.endswith(".md"):
                rel_dst = rel_src
                rel_dst = rel_dst[:-3] + ".html"
                convertibles.append((rel_src, rel_dst))
            else:
                shutil.copy(
                    f"{args.input_dir}/{rel_src}",
                    f"{args.output_dir}/{rel_src}",
                )
        for dirname in dirnames:
            # all directories are copied into the output directory
            path = os.path.relpath(f"{root}/{dirname}", start=args.input_dir)
            os.makedirs(f"{args.output_dir}/{path}", exist_ok=True)

    # copy static files over
    logger.info("Copying static files.")
    if os.path.exists(args.static_dir):
        shutil.copytree(args.static_dir, args.output_dir, dirs_exist_ok=True)

    config = get_config("config.ini")

    env = environment_factory(args.template_dir, dict(site=config))

    try:
        page_template = env.get_template("page.html")
        article_template = env.get_template("article.html")
        index_template = env.get_template("index.html")
        archive_template = env.get_template("archive.html")
        tags_template = env.get_template("tags.html")
        tag_template = env.get_template("tag.html")
    except TemplateNotFound as exc:
        tmpl = os.path.join(blag.__path__[0], "templates")
        logger.error(
            f'Template "{exc.name}" not found in {args.template_dir}! '
            "Consider running `blag quickstart` or copying the "
            f"missing template from {tmpl}."
        )

        sys.exit(1)

    articles, pages = process_markdown(
        convertibles,
        args.input_dir,
        args.output_dir,
        page_template,
        article_template,
    )

    generate_feed(
        articles,
        args.output_dir,
        base_url=config["base_url"],
        blog_title=config["title"],
        blog_description=config["description"],
        blog_author=config["author"],
    )
    generate_index(articles, index_template, args.output_dir)
    generate_archive(articles, archive_template, args.output_dir)
    generate_tags(articles, tags_template, tag_template, args.output_dir)

environment_factory(template_dir, globals_=None)

Environment factory.

Creates a Jinja2 Environment with the templates from template_dir loaded. If globals are provided, they are attached to the environment and thus available to all contexts.

Parameters:

Name Type Description Default
template_dir str

directory containing the templates

required
globals_ dict[str, object] | None
None

Returns:

Type Description
Environment
Source code in blag/blag.py
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
def environment_factory(
    template_dir: str,
    globals_: dict[str, object] | None = None,
) -> Environment:
    """Environment factory.

    Creates a Jinja2 Environment with the templates from `template_dir` loaded.
    If `globals` are provided, they are attached to the environment and thus
    available to all contexts.

    Parameters
    ----------
    template_dir
        directory containing the templates
    globals_

    Returns
    -------
    jinja2.Environment

    """
    env = Environment(loader=FileSystemLoader(template_dir))
    if globals_:
        env.globals = globals_
    return env

generate_archive(articles, template, output_dir)

Generate the archive page.

This is used for the full archive.

Parameters:

Name Type Description Default
articles list[tuple[str, dict[str, Any]]]

List of articles. Each article has the destination path and a dictionary with the content.

required
template Template
required
output_dir str
required
Source code in blag/blag.py
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
def generate_archive(
    articles: list[tuple[str, dict[str, Any]]],
    template: Template,
    output_dir: str,
) -> None:
    """Generate the archive page.

    This is used for the full archive.

    Parameters
    ----------
    articles
        List of articles. Each article has the destination path and a
        dictionary with the content.
    template
    output_dir

    """
    archive = []
    for dst, context in articles:
        entry = context.copy()
        entry["dst"] = dst
        archive.append(entry)

    result = template.render(dict(archive=archive))
    with open(f"{output_dir}/archive.html", "w") as fh:
        fh.write(result)

generate_feed(articles, output_dir, base_url, blog_title, blog_description, blog_author)

Generate Atom feed.

Parameters:

Name Type Description Default
articles list[tuple[str, dict[str, Any]]]

list of relative output path and article dictionary

required
output_dir str

where the feed is stored

required
base_url str

base url

required
blog_title str

blog title

required
blog_description str

blog description

required
blog_author str

blog author

required
Source code in blag/blag.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
def generate_feed(
    articles: list[tuple[str, dict[str, Any]]],
    output_dir: str,
    base_url: str,
    blog_title: str,
    blog_description: str,
    blog_author: str,
) -> None:
    """Generate Atom feed.

    Parameters
    ----------
    articles
        list of relative output path and article dictionary
    output_dir
        where the feed is stored
    base_url
        base url
    blog_title
        blog title
    blog_description
        blog description
    blog_author
        blog author

    """
    logger.info("Generating Atom feed.")
    feed = feedgenerator.Atom1Feed(
        link=base_url,
        title=blog_title,
        description=blog_description,
        feed_url=base_url + "atom.xml",
    )

    for dst, context in articles:
        # if article has a description, use that. otherwise fall back to
        # the title
        description = context.get("description", context["title"])

        feed.add_item(
            title=context["title"],
            author_name=blog_author,
            link=base_url + dst,
            description=description,
            content=context["content"],
            pubdate=context["date"],
        )

    with open(f"{output_dir}/atom.xml", "w") as fh:
        feed.write(fh, encoding="utf8")

generate_index(articles, template, output_dir)

Generate the index page.

This is used for the index (i.e. landing) page.

Parameters:

Name Type Description Default
articles list[tuple[str, dict[str, Any]]]

List of articles. Each article has the destination path and a dictionary with the content.

required
template Template
required
output_dir str
required
Source code in blag/blag.py
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
def generate_index(
    articles: list[tuple[str, dict[str, Any]]],
    template: Template,
    output_dir: str,
) -> None:
    """Generate the index page.

    This is used for the index (i.e. landing) page.

    Parameters
    ----------
    articles
        List of articles. Each article has the destination path and a
        dictionary with the content.
    template
    output_dir

    """
    archive = []
    for dst, context in articles:
        entry = context.copy()
        entry["dst"] = dst
        archive.append(entry)

    result = template.render(dict(archive=archive))
    with open(f"{output_dir}/index.html", "w") as fh:
        fh.write(result)

generate_tags(articles, tags_template, tag_template, output_dir)

Generate the tags page.

Parameters:

Name Type Description Default
articles list[tuple[str, dict[str, Any]]]

List of articles. Each article has the destination path and a dictionary with the content.

required
tags_template Template
required
tag_template Template
required
output_dir str
required
Source code in blag/blag.py
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
def generate_tags(
    articles: list[tuple[str, dict[str, Any]]],
    tags_template: Template,
    tag_template: Template,
    output_dir: str,
) -> None:
    """Generate the tags page.

    Parameters
    ----------
    articles
        List of articles. Each article has the destination path and a
        dictionary with the content.
    tags_template, tag_template
    output_dir

    """
    logger.info("Generating Tag-pages.")
    os.makedirs(f"{output_dir}/tags", exist_ok=True)
    # get tags number of occurrences
    all_tags: dict[str, int] = {}
    for _, context in articles:
        tags: list[str] = context.get("tags", [])
        for tag in tags:
            all_tags[tag] = all_tags.get(tag, 0) + 1
    # sort by occurrence
    taglist: list[tuple[str, int]] = sorted(
        all_tags.items(), key=lambda x: x[1], reverse=True
    )

    result = tags_template.render(dict(tags=taglist))
    with open(f"{output_dir}/tags/index.html", "w") as fh:
        fh.write(result)

    # get tags and archive per tag
    all_tags2: dict[str, list[dict[str, Any]]] = {}
    for dst, context in articles:
        tags = context.get("tags", [])
        for tag in tags:
            archive: list[dict[str, Any]] = all_tags2.get(tag, [])
            entry = context.copy()
            entry["dst"] = dst
            archive.append(entry)
            all_tags2[tag] = archive

    for tag, archive in all_tags2.items():
        result = tag_template.render(dict(archive=archive, tag=tag))
        with open(f"{output_dir}/tags/{tag}.html", "w") as fh:
            fh.write(result)

get_config(configfile)

Load site configuration from configfile.

Parameters:

Name Type Description Default
configfile str

path to configuration file

required

Returns:

Type Description
SectionProxy
Source code in blag/blag.py
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
def get_config(configfile: str) -> configparser.SectionProxy:
    """Load site configuration from configfile.

    Parameters
    ----------
    configfile
        path to configuration file


    Returns
    -------
    configparser.SectionProxy

    """
    config = configparser.ConfigParser()
    config.read(configfile)
    # check for the mandatory options
    for value in "base_url", "title", "description", "author":
        try:
            config["main"][value]
        except Exception:
            print(f"{value} is missing in {configfile}!")
            sys.exit(1)

    if not config["main"]["base_url"].endswith("/"):
        logger.warning("base_url does not end with a slash, adding it.")
        config["main"]["base_url"] += "/"

    return config["main"]

main(arguments=None)

Run the CLI.

This method parses the CLI arguments and executes the respective commands.

Parameters:

Name Type Description Default
arguments list[str] | None

optional parameters, used for testing

None
Source code in blag/blag.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def main(arguments: list[str] | None = None) -> None:
    """Run the CLI.

    This method parses the CLI arguments and executes the respective
    commands.

    Parameters
    ----------
    arguments
        optional parameters, used for testing

    """
    args = parse_args(arguments)
    # set loglevel
    if args.verbose:
        logger.setLevel(logging.DEBUG)
    logger.debug(f"This is blag {__VERSION__}.")
    args.func(args)

parse_args(args=None)

Parse command line arguments.

Parameters:

Name Type Description Default
args list[str] | None

optional parameters, used for testing

None

Returns:

Type Description
Namespace
Source code in blag/blag.py
 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
def parse_args(args: list[str] | None = None) -> argparse.Namespace:
    """Parse command line arguments.

    Parameters
    ----------
    args
        optional parameters, used for testing

    Returns
    -------
    arparse.Namespace

    """
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--version",
        action="version",
        version="%(prog)s " + __VERSION__,
    )
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="Verbose output.",
    )

    commands = parser.add_subparsers(dest="command")
    commands.required = True

    build_parser = commands.add_parser(
        "build",
        help="Build website.",
    )
    build_parser.set_defaults(func=build)
    build_parser.add_argument(
        "-i",
        "--input-dir",
        default="content",
        help="Input directory (default: content)",
    )
    build_parser.add_argument(
        "-o",
        "--output-dir",
        default="build",
        help="Ouptut directory (default: build)",
    )
    build_parser.add_argument(
        "-t",
        "--template-dir",
        default="templates",
        help="Template directory (default: templates)",
    )
    build_parser.add_argument(
        "-s",
        "--static-dir",
        default="static",
        help="Static directory (default: static)",
    )

    quickstart_parser = commands.add_parser(
        "quickstart",
        help="Quickstart blag, creating necessary configuration.",
    )
    quickstart_parser.set_defaults(func=quickstart)

    serve_parser = commands.add_parser(
        "serve",
        help="Start development server.",
    )
    serve_parser.set_defaults(func=serve)
    serve_parser.add_argument(
        "-i",
        "--input-dir",
        default="content",
        help="Input directory (default: content)",
    )
    serve_parser.add_argument(
        "-o",
        "--output-dir",
        default="build",
        help="Ouptut directory (default: build)",
    )
    serve_parser.add_argument(
        "-t",
        "--template-dir",
        default="templates",
        help="Template directory (default: templates)",
    )
    serve_parser.add_argument(
        "-s",
        "--static-dir",
        default="static",
        help="Static directory (default: static)",
    )

    return parser.parse_args(args)

process_markdown(convertibles, input_dir, output_dir, page_template, article_template)

Process markdown files.

This method processes the convertibles, converts them to html and saves them to the respective destination paths.

If a markdown file has a date metadata field it will be recognized as article otherwise as page.

Articles are sorted by date in descending order.

Parameters:

Name Type Description Default
convertibles list[tuple[str, str]]

relative paths to markdown- (src) html- (dest) files

required
input_dir str
required
output_dir str
required
page_template Template

templates for pages and articles

required
archive_template Template

templates for pages and articles

required

Returns:

Type Description
(list[tuple[str, dict[str, Any]]], list[tuple[str, dict[str, Any]]])

articles and pages, articles are sorted by date in descending order.

Source code in blag/blag.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
def process_markdown(
    convertibles: list[tuple[str, str]],
    input_dir: str,
    output_dir: str,
    page_template: Template,
    article_template: Template,
) -> tuple[list[tuple[str, dict[str, Any]]], list[tuple[str, dict[str, Any]]]]:
    """Process markdown files.

    This method processes the convertibles, converts them to html and
    saves them to the respective destination paths.

    If a markdown file has a `date` metadata field it will be recognized
    as article otherwise as page.

    Articles are sorted by date in descending order.

    Parameters
    ----------
    convertibles
        relative paths to markdown- (src) html- (dest) files
    input_dir
    output_dir
    page_template, archive_template
        templates for pages and articles

    Returns
    -------
    list[tuple[str, dict[str, Any]]], list[tuple[str, dict[str, Any]]]
        articles and pages, articles are sorted by date in descending order.

    """
    logger.info("Converting Markdown files...")
    md = markdown_factory()

    articles = []
    pages = []
    for src, dst in convertibles:
        logger.debug(f"Processing {src}")

        with open(f"{input_dir}/{src}") as fh:
            body = fh.read()

        content, meta = convert_markdown(md, body)

        context = dict(content=content)
        context.update(meta)

        # if markdown has date in meta, we treat it as a blog article,
        # everything else are just pages
        if meta and "date" in meta:
            articles.append((dst, context))
            result = article_template.render(context)
        else:
            pages.append((dst, context))
            result = page_template.render(context)
        with open(f"{output_dir}/{dst}", "w") as fh_dest:
            fh_dest.write(result)

    # sort articles by date, descending
    articles = sorted(articles, key=lambda x: x[1]["date"], reverse=True)
    return articles, pages