@@ -29,9 +29,14 @@ def __init__(self):
2929 self .path : Path = None
3030 self .blog : frontmatter .Post = frontmatter .Post (content = "" )
3131 self .uploaded_images : dict [str , Media ] = {}
32+ self .uploaded_audio : dict [str , Medium ] = {}
3233 self .markdown_image_pattern = re .compile (
3334 r'\!\[(?P<alt_text>[^]]*)\]\((?P<url>.*?)(?P<caption>\s*"[^"]*?")?\)'
3435 )
36+ # An audio player directive on its own line: `::: audio path/to/clip.mp3`
37+ self .audio_directive_pattern = re .compile (
38+ r"^[ \t]*:::[ \t]+audio[ \t]+(?P<url>\S+)[ \t]*$" , re .MULTILINE
39+ )
3540
3641 @staticmethod
3742 def load (path : str ) -> "Blog" :
@@ -306,7 +311,19 @@ def replace_references(match: re.Match):
306311 return f""
307312 return match .group (0 )
308313
314+ def replace_audio_directive (match : re .Match ):
315+ url = match .group ("url" )
316+ audio = self .uploaded_audio .get (url )
317+ src = audio .url if audio else url
318+ # raw block-level HTML; python-markdown passes it through untouched,
319+ # and html_to_gutenberg turns it into a wp:audio block.
320+ return (
321+ f'<figure class="wp-block-audio">'
322+ f'<audio controls src="{ src } "></audio></figure>'
323+ )
324+
309325 content = self .markdown_image_pattern .sub (replace_references , self .content )
326+ content = self .audio_directive_pattern .sub (replace_audio_directive , content )
310327 html = markdown (
311328 content , extensions = ["fenced_code" , "attr_list" , "tables" , "footnotes" ],
312329 )
@@ -395,9 +412,41 @@ def upload_local_images(self, wp: Wordpress):
395412 slug = self .slug + "-" + re .sub (r"[/\.\\]+" , "-" , path .stem .strip ("-" ))
396413 self .uploaded_images [filename ] = wp .upload_media (slug , path )
397414
415+ @property
416+ def local_audio_references (self ) -> set [str ]:
417+ """
418+ Local audio files referenced by `::: audio <path>` directives.
419+
420+ >>> b = Blog()
421+ >>> b.content = "## H\\ n\\ n::: audio images/clip.mp3\\ n\\ ntext\\ n"
422+ >>> sorted(b.local_audio_references)
423+ ['images/clip.mp3']
424+ """
425+ return set (
426+ filter (
427+ lambda u : urlparse (u ).scheme in ["" , "file" ],
428+ map (
429+ lambda m : m .group ("url" ),
430+ re .finditer (self .audio_directive_pattern , self .content ),
431+ ),
432+ )
433+ )
434+
435+ def upload_local_audio (self , wp : Wordpress ):
436+ self .uploaded_audio = {}
437+ for filename in self .local_audio_references :
438+ path = Path (self .dir ).joinpath (filename )
439+ if not path .exists ():
440+ logging .warning ("%s does not exist" , path )
441+ continue
442+
443+ slug = self .slug + "-" + re .sub (r"[/\.\\]+" , "-" , path .stem .strip ("-" ))
444+ self .uploaded_audio [filename ] = wp .upload_media (slug , path )
445+
398446 def to_wordpress (self , wp : Wordpress ) -> dict :
399447 author = wp .get_unique_user_by_name (self .author , self .email , self .author_id )
400448 self .upload_local_images (wp )
449+ self .upload_local_audio (wp )
401450 result = {
402451 "title" : self .title ,
403452 "slug" : self .slug ,
0 commit comments