By design, a Zim wiki file's Attachment Directory should + * have the same path of the wiki file itself, without the .txt + * suffix.
+ * + *Here, a difference from Zim is that any file can derive a + * properly named Attachment Directory, as long as that file is + * with a suffix.
+ * + * @param currentPage Current wiki file's path. + * @return {@code currentPage}'s path without suffix. + * + * @see GsFileUtils#getFilenameWithoutExtension(File) + */ + public static File findAttachmentDir(File currentPage) { + return GsFileUtils.join(currentPage.getParentFile(), GsFileUtils.getFilenameWithoutExtension(currentPage)); + } + + /** + * Return a Notebook's Root Directory. + * + *By design ({@code shouldDynamicallyDetermineRoot = true}), + * a Zim Notebook's Root Directory is the closest ancestor of the + * {@code currentPage} with a notebook.zim file in it.
+ * + *Here, a difference from Zim is that if a notebook.zim file + * can't be located, a {@code currentPage}'s current directory is + * taken as Root Directory.
+ * + *WARNING: Removing a notebook.zim file from the Notebook or + * changing the value of {@code notebookRootDir}, may switch to a + * Root Directory different than where the Notebook was organized + * originally.
+ * + * @param notebookRootDir Root Directory when {@code shouldDynamicallyDetermineRoot == false}. + * @param currentPage Current wiki file's path used to determine the current directory. + * @param shouldDynamicallyDetermineRoot If {@code true}, return the closest ancestor of the + * {@code currentPage} with a notebook.zim file in it, or fall back to the current directory + * on failure. If {@code false}, return {@code notebookRootDir}. + * @return Identified Notebook's Root Directory. + * + * @see WikitextLinkResolver#findNotebookRootDir(File) + */ + public static File findNotebookRootDir(File notebookRootDir, File currentPage, boolean shouldDynamicallyDetermineRoot) { + if (shouldDynamicallyDetermineRoot) { + notebookRootDir = findNotebookRootDir(currentPage); + if (notebookRootDir == null) { + notebookRootDir = currentPage.getParentFile(); + } + } + return notebookRootDir; + } + + /** + * Return a system file's path as wiki attachment's path. + * + *If both {@code file} and {@code currentPage} are children + * of the same Notebook's Root Directory, return a path relative + * to the {@code currentPage}'s Attachment Directory, otherwise, + * return the original {@code file}'s path.
+ * + *Here, a difference from Zim is to always consider as Root + * Directory the {@code currentPage}'s directory before anything + * else.
+ * + * @param file System file's path to resolve as wiki attachment's path. + * @param notebookRootDir Root Directory when {@code shouldDynamicallyDetermineRoot == false}. + * @param currentPage Current wiki file's path used to determine the current Attachment Directory. + * @param shouldDynamicallyDetermineRoot If {@code true}, the Root Directory is the closest ancestor + * of the {@code currentPage} with a notebook.zim file in it, or the {@code currentPage}'s directory + * on failure. If {@code false}, the Root Directory is {@code notebookRootDir}. In either cases, a + * {@code currentPage}'s directory is always considered as Root Directory before anything else. + * @return {@code file}'s path relative to the {@code currentPage}'s Attachment Directory, when both + * {@code file} and {@code currentPage} are children of the identified Notebook's Root Directory, or + * the original {@code file}'s path otherwise. + * + * @see WikitextLinkResolver#findAttachmentDir(File) + * @see WikitextLinkResolver#findNotebookRootDir(File, File, boolean) + */ + public static String resolveSystemFilePath(File file, File notebookRootDir, File currentPage, boolean shouldDynamicallyDetermineRoot) { + final File currentDir = currentPage.getParentFile(); + notebookRootDir = findNotebookRootDir(notebookRootDir, currentPage, shouldDynamicallyDetermineRoot); + + if (GsFileUtils.isChild(currentDir, file) || + (GsFileUtils.isChild(notebookRootDir, file) && GsFileUtils.isChild(notebookRootDir, currentPage))) { + final File attachmentDir = findAttachmentDir(currentPage); + String path = GsFileUtils.relativePath(attachmentDir, file); + + // Zim prefixes also children of the Attachment Directory. + if (file.toString().endsWith("/" + path)) { + path = "./" + path; + } + + return path; + } + + return file.toString(); + } + + /** + * Return a wiki attachment's path as system absolute path. + * + *If {@code path} can be resolved as a relative path to the + * {@code currentPage}'s Attachment Directory, return the result + * of {@code path} as a system absolute path. Otherwise, return + * the original {@code path}.
+ * + *Here, a difference from Zim is to always consider as Root + * Directory the {@code currentPage}'s directory before anything + * else.
+ * + * @param path Path that might be relative to the {@code currentPage}'s Attachment Directory. + * @param notebookRootDir Root Directory when {@code shouldDynamicallyDetermineRoot == false}. + * @param currentPage Current wiki file's path used to determine the current Attachment Directory. + * @param shouldDynamicallyDetermineRoot If {@code true}, the Root Directory is the closest ancestor + * of the {@code currentPage} with a notebook.zim file in it, or the {@code currentPage}'s directory + * on failure. If {@code false}, the Root Directory is {@code notebookRootDir}. In either cases, a + * {@code currentPage}'s directory is always considered as Root Directory before anything else. + * @return {@code path} as a system absolute path, if it can be resolved as a relative path to the + * {@code currentPage}'s Attachment Directory, or the original {@code path} otherwise. + * + * @see WikitextLinkResolver#findAttachmentDir(File) + * @see WikitextLinkResolver#resolveSystemFilePath(File, File, File, boolean) + */ + public static String resolveAttachmentPath(String path, File notebookRootDir, File currentPage, boolean shouldDynamicallyDetermineRoot) { + if (path.startsWith("./") || path.startsWith("../")) { + final File attachmentDir = findAttachmentDir(currentPage); + final File file = new File(attachmentDir, path).getAbsoluteFile(); + final String resolved = resolveSystemFilePath(file, notebookRootDir, currentPage, shouldDynamicallyDetermineRoot); + + if (path.equals(resolved)) { + try { + return file.getCanonicalPath(); + } catch (IOException e) { + return file.toString(); + } + } + } + + return path; + } } diff --git a/app/src/main/java/net/gsantner/markor/format/wikitext/WikitextTextConverter.java b/app/src/main/java/net/gsantner/markor/format/wikitext/WikitextTextConverter.java index 82a3716ad5..9b1e8247e5 100644 --- a/app/src/main/java/net/gsantner/markor/format/wikitext/WikitextTextConverter.java +++ b/app/src/main/java/net/gsantner/markor/format/wikitext/WikitextTextConverter.java @@ -22,7 +22,10 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileReader; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.util.Arrays; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -155,7 +158,49 @@ private String convertImage(File file, String fullMatch) { String currentPageFileName = file.getName(); String currentPageFolderName = currentPageFileName.replaceFirst(".txt$", ""); String markdownPathToImage = FilenameUtils.concat(currentPageFolderName, imagePathFromPageFolder); - return "![" + file.getName() + "](" + markdownPathToImage + ")"; + + // Zim may insert in the image link, after a '?' character, the 'id', 'width', + // 'height', 'type', and 'href' tags, separating them with a '&' character, so + // you may not want to use '?' and '&' as directory or file name: + // https://github.com/zim-desktop-wiki/zim-desktop-wiki/blob/c88cf3cb53896bf272e87704826b77e82eddb3ef/zim/formats/__init__.py#L903 + final int pos = markdownPathToImage.indexOf("?"); + if (pos != -1) { + final String image = markdownPathToImage.substring(0, pos); + final String[] options = markdownPathToImage.substring(pos + 1).split("&"); + String link = null; // or [![name](image)](link) + StringBuilder attributes = new StringBuilder(); // + // The 'type' tag is for backward compatibility of image generators before + // Zim version 0.70. Here, it probably may be ignored: + // https://github.com/zim-desktop-wiki/zim-desktop-wiki/blob/c88cf3cb53896bf272e87704826b77e82eddb3ef/zim/formats/wiki.py#586 + final Pattern tags = Pattern.compile("(id|width|height|href)=(.+)", Pattern.CASE_INSENSITIVE); + for (String item : options) { + final Matcher data = tags.matcher(item); + if (data.matches()) { + final String key = Objects.requireNonNull(data.group(1)).toLowerCase(); + String value = data.group(2); + try { + value = URLDecoder.decode(value, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + if (key.equals("href")) { + link = value; + } else { + attributes.append(String.format("%s=\"%s\" ", key, value)); + } + } + } + String html = String.format("", image, currentPageFileName, attributes); + if (link != null) { + AppSettings settings = ApplicationObject.settings(); + File notebookDir = settings.getNotebookDirectory(); + link = WikitextLinkResolver.resolveAttachmentPath(link, notebookDir, file, settings.isWikitextDynamicNotebookRootEnabled()); + html = String.format("%s", link, html); + } + return html; + } + + return String.format("![%s](%s)", currentPageFileName, markdownPathToImage); } @Override diff --git a/app/src/main/java/net/gsantner/markor/frontend/AttachLinkOrFileDialog.java b/app/src/main/java/net/gsantner/markor/frontend/AttachLinkOrFileDialog.java index 7e3316a133..ff7a0cf2c2 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/AttachLinkOrFileDialog.java +++ b/app/src/main/java/net/gsantner/markor/frontend/AttachLinkOrFileDialog.java @@ -27,6 +27,8 @@ import net.gsantner.markor.R; import net.gsantner.markor.format.FormatRegistry; import net.gsantner.markor.format.markdown.MarkdownActionButtons; +import net.gsantner.markor.format.markdown.MarkdownSyntaxHighlighter; +import net.gsantner.markor.format.wikitext.WikitextLinkResolver; import net.gsantner.markor.frontend.filebrowser.MarkorFileBrowserFactory; import net.gsantner.markor.frontend.filesearch.FileSearchDialog; import net.gsantner.markor.frontend.filesearch.FileSearchEngine; @@ -61,7 +63,7 @@ private static String getLinkFormat(final int textFormatId) { if (textFormatId == FormatRegistry.FORMAT_MARKDOWN) { return "[%TITLE%](%LINK%)"; } else if (textFormatId == FormatRegistry.FORMAT_WIKITEXT) { - return "{{%LINK%|%TITLE%}}"; + return "[[LINK|TITLE]]"; } else if (textFormatId == FormatRegistry.FORMAT_ASCIIDOC) { return "link:%LINK%[%TITLE%]"; } else if (textFormatId == FormatRegistry.FORMAT_TODOTXT) { @@ -72,6 +74,9 @@ private static String getLinkFormat(final int textFormatId) { } private static String getAudioFormat(final int textFormatId) { + if (textFormatId == FormatRegistry.FORMAT_WIKITEXT) { + return "[[LINK|TITLE]]"; + } return ""; } @@ -304,35 +309,72 @@ private static void insertItem( // If path is not under notebook, copy it to the res folder File file = new File(path); - if (!GsFileUtils.isChild(_appSettings.getNotebookDirectory(), file)) { - final File local = GsFileUtils.findNonConflictingDest(attachmentDir, file.getName()); - attachmentDir.mkdirs(); - GsFileUtils.copyFile(file, local); - file = local; - } - // Pull the appropriate title - String title = ""; - if (nameEdit != null) { - title = nameEdit.getText().toString(); - } + if (textFormatId == FormatRegistry.FORMAT_WIKITEXT) { + final File notebookDir = _appSettings.getNotebookDirectory(); + final boolean shouldDynamicallyDetermineRoot = _appSettings.isWikitextDynamicNotebookRootEnabled(); + path = WikitextLinkResolver.resolveSystemFilePath(file, notebookDir, currentFile, shouldDynamicallyDetermineRoot); + if (path.startsWith("/")) { + final File zimAttachmentDir = WikitextLinkResolver.findAttachmentDir(currentFile); + final File local = GsFileUtils.findNonConflictingDest(zimAttachmentDir, file.getName()); + zimAttachmentDir.mkdirs(); + GsFileUtils.copyFile(file, local); + file = local; + path = WikitextLinkResolver.resolveSystemFilePath(file, notebookDir, currentFile, shouldDynamicallyDetermineRoot); + } + insertLink.callback(path, path); + } else { + if (!GsFileUtils.isChild(_appSettings.getNotebookDirectory(), file)) { + final File local = GsFileUtils.findNonConflictingDest(attachmentDir, file.getName()); + attachmentDir.mkdirs(); + GsFileUtils.copyFile(file, local); + file = local; + } - if (GsTextUtils.isNullOrEmpty(title)) { - title = GsFileUtils.getFilenameWithoutExtension(file); - } + // Pull the appropriate title + String title = ""; + if (nameEdit != null) { + title = nameEdit.getText().toString(); + } - insertLink.callback(title, GsFileUtils.relativePath(currentFile, file)); + if (GsTextUtils.isNullOrEmpty(title)) { + title = GsFileUtils.getFilenameWithoutExtension(file); + } + insertLink.callback(title, GsFileUtils.relativePath(currentFile, file)); + } }; final MarkorContextUtils cu = new MarkorContextUtils(activity); final GsCallback.a1