diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index b7f219b87..c5b31d812 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -74,6 +74,9 @@ body padding 5px border-radius 5px justify-content left + .audio-player + width: 100% + margin-bottom: 1em li label.taskListItem margin-left -1.8em diff --git a/browser/lib/markdown-it-audio.js b/browser/lib/markdown-it-audio.js new file mode 100644 index 000000000..5c27f626c --- /dev/null +++ b/browser/lib/markdown-it-audio.js @@ -0,0 +1,53 @@ +'use strict' +import { normalizeReference } from 'markdown-it/lib/common/utils' +module.exports = function audioPlugin (md) { + function audio (state, startLine) { + // match @[](src.mp3) or @[refsrc] + const audioSouceRegex = /^@\[.*\]\((.*?)\)/ + const audioReferenceSourceRegex = /^@\[(.*?)\]/ + const start = state.bMarks[startLine] + const end = state.eMarks[startLine] + let token = null + + // if it's indented more than 3 spaces, it should be a code block + if (state.sCount[startLine] - state.blkIndent >= 4) { return false } + + // Audio must be at start of input or the previous line must be blank. + if (startLine !== 0) { + const prevLineStartPos = state.bMarks[startLine - 1] + state.tShift[startLine - 1] + const prevLineMaxPos = state.eMarks[startLine - 1] + if (prevLineMaxPos > prevLineStartPos) return false + } + + let match = audioSouceRegex.exec(state.src.slice(start, end)) + if (!match || match.length < 2) { + match = audioReferenceSourceRegex.exec(state.src.slice(start, end)) + } + if (!match || match.length < 2) { + return false + } + let src = match[1] + // this is a reference link + if (!src.endsWith('.mp3') && !src.endsWith('.wav') && !src.endsWith('.ogg')) { + if (typeof state.env.references === 'undefined') { + return false + } + src = state.env.references[normalizeReference(src)].href + } + token = state.push('audio') + state.line = startLine + 1 + token.src = src + return true + } + + function audioRender (tokens, idx) { + const token = tokens[idx] + return `` + } + + md.block.ruler.before('fence', 'audio', audio, { + alt: ['paragraph', 'reference', 'blockquote', 'list'] + }) + + md.renderer.rules['audio'] = audioRender +} diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 13ef758a4..dd319a855 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -133,6 +133,7 @@ class Markdown { this.md.use(require('markdown-it-sup')) this.md.use(require('./markdown-it-deflist')) this.md.use(require('./markdown-it-frontmatter')) + this.md.use(require('./markdown-it-audio')) this.md.use(require('./markdown-it-fence'), { chart: token => { diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index c193eaf2e..9272b12c5 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -11,6 +11,11 @@ import i18n from 'browser/lib/i18n' const STORAGE_FOLDER_PLACEHOLDER = ':storage' const DESTINATION_FOLDER = 'attachments' const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep) +// file type of attachments +const FILE_TYPES = { + IMAGE: 'image', + AUDIO: 'audio' +} /** * @description * Create a Image element to get the real size of image. @@ -227,7 +232,7 @@ function migrateAttachments (markdownContent, storagePath, noteKey) { * @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths. */ function fixLocalURLS (renderedHTML, storagePath) { - return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?"', 'g'), function (match) { + return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?["|\']', 'g'), function (match) { var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g') return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER)) }) @@ -237,11 +242,20 @@ function fixLocalURLS (renderedHTML, storagePath) { * @description Generates the markdown code for a given attachment * @param {String} fileName Name of the attachment * @param {String} path Path of the attachment - * @param {Boolean} showPreview Indicator whether the generated markdown should show a preview of the image. Note that at the moment only previews for images are supported + * @param {Boolean} showPreview Indicator whether the generated markdown should show a preview of the attachment. + * @param {String} previewType Name of the type of attachment to preview * @returns {String} Generated markdown code */ -function generateAttachmentMarkdown (fileName, path, showPreview) { - return `${showPreview ? '!' : ''}[${fileName}](${path})` +function generateAttachmentMarkdown (fileName, path, showPreview, previewType) { + if (!showPreview) { + return `[${fileName}](${path})` + } + switch (previewType) { + case FILE_TYPES.IMAGE: + return `` + case FILE_TYPES.AUDIO: + return `@[${fileName}](${path})` + } } /** @@ -256,22 +270,40 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) { const file = dropEvent.dataTransfer.files[0] const filePath = file.path const originalFileName = path.basename(filePath) - const fileType = file['type'] - const isImage = fileType.startsWith('image') + const fileType = getFileType(file['type']) let promise - if (isImage) { + if (fileType === FILE_TYPES.IMAGE) { promise = fixRotate(file).then(base64data => { return copyAttachment({type: 'base64', data: base64data, sourceFilePath: filePath}, storageKey, noteKey) }) } else { promise = copyAttachment(filePath, storageKey, noteKey) } + // generate markdown syntax part promise.then((fileName) => { - const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), isImage) - codeEditor.insertAttachmentMd(imageMd) + let shouldShowPreview = false + // whenever there's a new type of attachment can be preview, add it to this list + const canPreviewTypes = [FILE_TYPES.IMAGE, FILE_TYPES.AUDIO] + if (canPreviewTypes.indexOf(fileType) !== -1) { + shouldShowPreview = true + } + const md = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), shouldShowPreview, fileType) + codeEditor.insertAttachmentMd(md) }) } +function getFileType (type) { + const types = Object.keys(FILE_TYPES) + for (let i = 0; i < types.length; i++) { + const currentTypeName = types[i] + const currentTypeValue = FILE_TYPES[currentTypeName] + if (type.startsWith(currentTypeValue)) { + return currentTypeValue + } + } + return 'unknown' +} + /** * @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code * @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code diff --git a/tests/dataApi/attachmentManagement.test.js b/tests/dataApi/attachmentManagement.test.js index a4cc80820..a74834906 100644 --- a/tests/dataApi/attachmentManagement.test.js +++ b/tests/dataApi/attachmentManagement.test.js @@ -206,11 +206,14 @@ it('should replace the ":storage" path with the actual storage path when they ha expect(actual).toEqual(expectedOutput) }) -it('should test that generateAttachmentMarkdown works correct both with previews and without', function () { +it('should test that generateAttachmentMarkdown works correct both with different previews and without', function () { const fileName = 'fileName' const path = 'path' let expected = `` - let actual = systemUnderTest.generateAttachmentMarkdown(fileName, path, true) + let actual = systemUnderTest.generateAttachmentMarkdown(fileName, path, true, 'image') + expect(actual).toEqual(expected) + expected = `@[${fileName}](${path})` + actual = systemUnderTest.generateAttachmentMarkdown(fileName, path, true, 'audio') expect(actual).toEqual(expected) expected = `[${fileName}](${path})` actual = systemUnderTest.generateAttachmentMarkdown(fileName, path, false) diff --git a/tests/fixtures/markdowns.js b/tests/fixtures/markdowns.js index 0ee809095..3bb0a5bf8 100644 --- a/tests/fixtures/markdowns.js +++ b/tests/fixtures/markdowns.js @@ -50,6 +50,18 @@ const smartQuotes = 'This is a "QUOTE".' const breaks = 'This is the first line.\nThis is the second line.' +const audio = ` +@[audio.mp3] + +Audio must be at start of input or the previous line must be blank. +@[notaudio.mp3] + +@[audio](audio.mp3) + +[ref]: audio.mp3 + +@[ref] +` const abbrevations = ` ## abbr @@ -115,5 +127,6 @@ export default { subTexts, supTexts, deflists, - shortcuts + shortcuts, + audio } diff --git a/tests/lib/markdown-test.js b/tests/lib/markdown-test.js index 46ae59410..36429bd8c 100644 --- a/tests/lib/markdown-test.js +++ b/tests/lib/markdown-test.js @@ -44,6 +44,11 @@ test('Markdown.render() should render line breaks correctly', t => { t.snapshot(renderedNonBreaks) }) +test('Markdown.render() should render audio correctly', t => { + const rendered = md.render(markdownFixtures.audio) + t.snapshot(rendered) +}) + test('Markdown.render() should renders abbrevations correctly', t => { const rendered = md.render(markdownFixtures.abbrevations) t.snapshot(rendered) diff --git a/tests/lib/snapshots/markdown-test.js.md b/tests/lib/snapshots/markdown-test.js.md index eefb232c3..50837febd 100644 --- a/tests/lib/snapshots/markdown-test.js.md +++ b/tests/lib/snapshots/markdown-test.js.md @@ -4,6 +4,14 @@ The actual snapshot is saved in `markdown-test.js.snap`. Generated by [AVA](https://ava.li). +## Markdown.render() should render audio correctly + +> Snapshot 1 + + `
Audio must be at start of input or the previous line must be blank.
␊
+ @[notaudio.mp3]