Skip to content

Commit

Permalink
Support multi-page diagrams
Browse files Browse the repository at this point in the history
* Introduce a new Source type for keeping track of source file and page
  index pairs. We'll have to keep track of these during on_post_page()
  for use in on_post_build().
* Work around the Files object not being available during
  rewrite_image_embeds() by resolving the source's relative path within
  the documentation directory from the parent page's destination.
* Update the filename matching during rewrite_image_embeds() to handle
  URL anchors specifying the page index.
* Include the page index value in cache filenames.

Refs #4
  • Loading branch information
LukeCarrier committed Dec 24, 2019
1 parent 3401a36 commit 7220e0e
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 66 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Locate `draw.io` binary on `PATH` based on platform
* Better handle missing `draw.io` binary
* Added support for multi-page documents
* Clean up the code and write some unit tests

## 0.3.1: fix some more stuff
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ plugins:
sources: '*.drawio'
```
## Usage
With the plugin configured, you can now proceed to embed images by simply embedding the `*.drawio` diagram file as you would with any image file:

```markdown
![My alt text](my-diagram.drawio)
```

If you're working with multi-page documents, append the index of the page as an anchor in the URL:

```markdown
![Page 1](my-diagram.drawio#0)
```

The plugin will export the diagram to the `format` specified in your configuration and will rewrite the `<img>` tag in the generated page to match. To speed up your documentation rebuilds, the generated output will be placed into `cache_dir` and then copied to the desired destination. The cached images will only be updated if the source diagram's modification date is newer than the cached export.

## Hacking

To get completion working in your editor, set up a virtual environment in the root of this repository and install MkDocs:
Expand Down
2 changes: 1 addition & 1 deletion mkdocsdrawioexporter/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .exporter import DrawIoExporter
from .exporter import DrawIoExporter, Source
from .plugin import DrawIoExporterPlugin
99 changes: 82 additions & 17 deletions mkdocsdrawioexporter/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,61 @@
import sys


class Source:
"""Diagram source.
Sources are pairs of filenames and page indices which can be exported to
produce a static image. The relative path of the source within the
documentation directory must be resolved after instantiation due to MkDocs's
design.
"""

source_embed = None
"""Path of the embedded resource, relative to parent page.
:type: str
"""

source_rel = None
"""Path of the source, relative to the documentation directory.
:type: str
"""

page_index = None
"""Page index within the document.
:type: int"""

def __init__(self, source_embed, page_index):
"""Initialise a Source.
:param str source_embed: Path of the embedded resource.
:param int page_index: Page index within the document.
"""
self.source_embed = source_embed
self.page_index = page_index

def __eq__(self, other):
return self.source_rel == other.source_rel \
and self.page_index == other.page_index

def __hash__(self):
return hash((
'source_rel', self.source_rel,
'page_index', self.page_index,
))

def resolve_rel_path(self, page_dest_path):
"""Resolve the path of the source, relative to the documentation directory.
:param str page_dest_path: The destination path of the parent page.
"""
self.source_rel = os.path.normpath(os.path.join(
os.path.dirname(page_dest_path),
self.source_embed))


class DrawIoExporter:
"""Draw.io Exporter.
Expand Down Expand Up @@ -105,21 +160,25 @@ def rewrite_image_embeds(self, output_content, image_re, sources, format):
:param str format: Desired export format.
:return str: Rewritten content.
"""
content_sources = []

def replace(match):
if fnmatch.fnmatch(match.group(2), sources):
return '{}{}.{}{}'.format(match.group(1), match.group(2), format, match.group(3))
try:
filename, page_index = match.group(2).rsplit('#', 1)
except ValueError:
filename = match.group(2)
page_index = 0

if fnmatch.fnmatch(filename, sources):
content_sources.append(Source(filename, page_index))

return '{}{}-{}.{}{}'.format(
match.group(1), filename, page_index, format, match.group(3))
else:
return match.group(0)
return image_re.sub(replace, output_content)
output_content = image_re.sub(replace, output_content)

def match_source_files(self, files, sources):
"""Locate files matching the source glob.
:param list(mkdocs.structure.File) files: Files to filter.
:param str sources: Sources glob to filter by.
:return list(mkdocs.structure.File): Filtered files.
"""
return [f for f in files if fnmatch.fnmatch(f.src_path, sources)]
return (output_content, content_sources)

def filter_cache_files(self, files, cache_dir):
"""Remove cache files from the generated output.
Expand All @@ -130,11 +189,12 @@ def filter_cache_files(self, files, cache_dir):
"""
return [f for f in files if not f.abs_src_path.startswith(cache_dir)]

def ensure_file_cached(self, source, source_rel, drawio_executable, cache_dir, format):
def ensure_file_cached(self, source, source_rel, page_index, drawio_executable, cache_dir, format):
"""Ensure cached copy of output exists.
:param str source: Source path, absolute.
:param str source_rel: Source path, relative to docs directory.
:param int page_index: Page index, numbered from zero.
:param str drawio_executable: Path to the configured Draw.io executable.
:param str cache_dir: Export cache directory.
:param str format: Desired export format.
Expand All @@ -144,26 +204,29 @@ def ensure_file_cached(self, source, source_rel, drawio_executable, cache_dir, f
self.log.warn('Skipping build of "{}" as Draw.io executable not available'.format(source))
return

cache_filename = self.make_cache_filename(source_rel, cache_dir)
cache_filename = self.make_cache_filename(source_rel, page_index, cache_dir)
if self.use_cached_file(source, cache_filename):
self.log.debug('Source file appears unchanged; using cached copy from "{}"'.format(cache_filename))
else:
self.log.debug('Exporting "{}" to "{}"'.format(source, cache_filename))
exit_status = self.export_file(source, cache_filename, drawio_executable, format)
exit_status = self.export_file(source, page_index, cache_filename, drawio_executable, format)
if exit_status != 0:
self.log.error('Export failed with exit status {}'.format(exit_status))
return

return cache_filename

def make_cache_filename(self, source, cache_dir):
def make_cache_filename(self, source, page_index, cache_dir):
"""Make the cached filename.
:param str source: Source path, relative to the docs directory.
:param int page_index: Page index, numbered from zero.
:param str cache_dir: Export cache directory.
:return str: Resulting filename.
"""
return os.path.join(cache_dir, hashlib.sha1(source.encode('utf-8')).hexdigest())
basename = '{}-{}'.format(
hashlib.sha1(source.encode('utf-8')).hexdigest(), page_index)
return os.path.join(cache_dir, basename)

def use_cached_file(self, source, cache_filename):
"""Is the cached copy up to date?
Expand All @@ -175,10 +238,11 @@ def use_cached_file(self, source, cache_filename):
return os.path.exists(cache_filename) \
and os.path.getmtime(cache_filename) >= os.path.getmtime(source)

def export_file(self, source, dest, drawio_executable, format):
def export_file(self, source, page_index, dest, drawio_executable, format):
"""Export an individual file.
:param str source: Source path, absolute.
:param int page_index: Page index, numbered from zero.
:param str dest: Destination path, within cache.
:param str drawio_executable: Path to the configured Draw.io executable.
:param str format: Desired export format.
Expand All @@ -187,6 +251,7 @@ def export_file(self, source, dest, drawio_executable, format):
cmd = [
drawio_executable,
'--export', source,
'--page-index', str(page_index),
'--output', dest,
'--format', format,
]
Expand Down
34 changes: 22 additions & 12 deletions mkdocsdrawioexporter/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from mkdocs.structure.files import Files
from mkdocs.utils import copy_file, string_types

from .exporter import DrawIoExporter
from .exporter import DrawIoExporter, Source


log = mkdocs.plugins.log.getChild('drawio-exporter')
Expand All @@ -31,7 +31,7 @@ class DrawIoExporterPlugin(mkdocs.plugins.BasePlugin):

exporter = None

source_files = []
sources = []
image_re = None

def on_config(self, config):
Expand All @@ -49,27 +49,37 @@ def on_config(self, config):
log.debug('Using Draw.io executable "{}", cache directory "{}" and image regular expression "{}"'.format(
self.config['drawio_executable'], self.config['cache_dir'], self.config['image_re']))

def on_post_page(self, output_content, **kwargs):
return self.exporter.rewrite_image_embeds(
def on_post_page(self, output_content, page, **kwargs):
output_content, content_sources = self.exporter.rewrite_image_embeds(
output_content, self.image_re, self.config['sources'],
self.config['format'])

def on_files(self, files, config):
self.source_files = self.exporter.match_source_files(files, self.config['sources'])
log.debug('Found {} source files matching glob "{}"'.format(
len(self.source_files), self.config['sources']))
for source in content_sources:
source.resolve_rel_path(page.file.dest_path)
self.sources += content_sources

return output_content

def on_files(self, files, config):
keep = self.exporter.filter_cache_files(files, self.config['cache_dir'])
log.debug('{} files left after excluding cache'.format(len(keep)))

return Files(keep)

def on_post_build(self, config):
for f in self.source_files:
abs_dest_path = f.abs_dest_path + '.' + self.config['format']
sources = set(self.sources)
log.debug('Found {} unique sources in {} total embeds'.format(len(sources), len(self.sources)))
self.sources = []

for source in sources:
dest_rel_path = '{}-{}.{}'.format(
source.source_rel, source.page_index, self.config['format'])
abs_src_path = os.path.join(config['docs_dir'], source.source_rel)
abs_dest_path = os.path.join(config['site_dir'], dest_rel_path)
cache_filename = self.exporter.ensure_file_cached(
f.abs_src_path, f.src_path, self.config['drawio_executable'],
self.config['cache_dir'], self.config['format'])
abs_src_path, source.source_rel, source.page_index,
self.config['drawio_executable'], self.config['cache_dir'],
self.config['format'])

try:
copy_file(cache_filename, abs_dest_path)
Expand Down
Loading

0 comments on commit 7220e0e

Please sign in to comment.