diff --git a/app/src/main/java/net/gsantner/markor/activity/SettingsActivity.java b/app/src/main/java/net/gsantner/markor/activity/SettingsActivity.java index 2f9b65348..ee729fdaf 100644 --- a/app/src/main/java/net/gsantner/markor/activity/SettingsActivity.java +++ b/app/src/main/java/net/gsantner/markor/activity/SettingsActivity.java @@ -235,10 +235,26 @@ protected void onPreferenceChanged(final SharedPreferences prefs, final String k @Override @SuppressWarnings({"ConstantConditions", "ConstantIfStatement", "StatementWithEmptyBody"}) public Boolean onPreferenceClicked(Preference preference, String key, int keyResId) { + final FragmentManager fragManager = getActivity().getSupportFragmentManager(); switch (keyResId) { + case R.string.pref_key__snippet_directory_path: { + MarkorFileBrowserFactory.showFolderDialog(new GsFileBrowserOptions.SelectionListenerAdapter() { + @Override + public void onFsViewerSelected(String request, File file, final Integer lineNumber) { + _appSettings.setSnippetDirectory(file); + doUpdatePreferences(); + } + + @Override + public void onFsViewerConfig(GsFileBrowserOptions.Options dopt) { + dopt.titleText = R.string.snippet_directory; + dopt.rootFolder = _appSettings.getNotebookDirectory(); + } + }, fragManager, getActivity()); + return true; + } case R.string.pref_key__notebook_directory: { - FragmentManager fragManager = getActivity().getSupportFragmentManager(); MarkorFileBrowserFactory.showFolderDialog(new GsFileBrowserOptions.SelectionListenerAdapter() { @Override public void onFsViewerSelected(String request, File file, final Integer lineNumber) { @@ -256,7 +272,6 @@ public void onFsViewerConfig(GsFileBrowserOptions.Options dopt) { return true; } case R.string.pref_key__quicknote_filepath: { - FragmentManager fragManager = getActivity().getSupportFragmentManager(); MarkorFileBrowserFactory.showFileDialog(new GsFileBrowserOptions.SelectionListenerAdapter() { @Override public void onFsViewerSelected(String request, File file, final Integer lineNumber) { @@ -274,7 +289,6 @@ public void onFsViewerConfig(GsFileBrowserOptions.Options dopt) { return true; } case R.string.pref_key__todo_filepath: { - FragmentManager fragManager = getActivity().getSupportFragmentManager(); MarkorFileBrowserFactory.showFileDialog(new GsFileBrowserOptions.SelectionListenerAdapter() { @Override public void onFsViewerSelected(String request, File file, final Integer lineNumber) { diff --git a/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java b/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java index 968c0ded6..29ff25fe7 100644 --- a/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java +++ b/app/src/main/java/net/gsantner/markor/format/ActionButtonBase.java @@ -599,14 +599,17 @@ protected final boolean runCommonAction(final @StringRes int action) { } case R.string.abid_common_insert_snippet: { MarkorDialogFactory.showInsertSnippetDialog(_activity, (snip) -> { - _hlEditor.insertOrReplaceTextOnCursor(TextViewUtils.interpolateEscapedDateTime(snip)); + _hlEditor.insertOrReplaceTextOnCursor(TextViewUtils.interpolateSnippet(snip, _document.getTitle(), TextViewUtils.getSelectedText(_hlEditor))); _lastSnip = snip; }); return true; } case R.string.abid_common_open_link_browser: { - String url; - if ((url = GsTextUtils.tryExtractUrlAroundPos(text.toString(), _hlEditor.getSelectionStart())) != null) { + final int sel = TextViewUtils.getSelection(_hlEditor)[0]; + final String line = TextViewUtils.getSelectedLines(_hlEditor, sel); + final int cursor = sel - TextViewUtils.getLineStart(_hlEditor.getText(), sel); + String url = GsTextUtils.tryExtractUrlAroundPos(line, cursor); + if (url != null) { if (url.endsWith(")")) { url = url.substring(0, url.length() - 1); } @@ -689,7 +692,7 @@ protected final boolean runCommonLongPressAction(@StringRes int action) { } case R.string.abid_common_insert_snippet: { if (!TextUtils.isEmpty(_lastSnip)) { - _hlEditor.insertOrReplaceTextOnCursor(TextViewUtils.interpolateEscapedDateTime(_lastSnip)); + _hlEditor.insertOrReplaceTextOnCursor(TextViewUtils.interpolateSnippet(_lastSnip, _document.getTitle(), TextViewUtils.getSelectedText(_hlEditor))); } return true; } diff --git a/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownActionButtons.java b/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownActionButtons.java index 572d1d207..0473ab2bc 100644 --- a/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownActionButtons.java +++ b/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownActionButtons.java @@ -15,21 +15,31 @@ import androidx.annotation.StringRes; import net.gsantner.markor.R; +import net.gsantner.markor.activity.DocumentActivity; import net.gsantner.markor.format.ActionButtonBase; import net.gsantner.markor.frontend.MarkorDialogFactory; import net.gsantner.markor.frontend.textview.AutoTextFormatter; import net.gsantner.markor.frontend.textview.TextViewUtils; import net.gsantner.markor.model.Document; +import net.gsantner.opoc.util.GsContextUtils; +import net.gsantner.opoc.util.GsFileUtils; +import java.io.File; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; +import java.util.regex.Pattern; public class MarkdownActionButtons extends ActionButtonBase { - private Set _disabledHeadings = new HashSet<>(); + // Group 1 matches text, group 2 matches path + private static final Pattern MARKDOWN_LINK = Pattern.compile("\\[([^\\]]*)\\]\\(([^)]+)\\)"); + + private static final Pattern WEB_URL = Pattern.compile("https?://[^\\s/$.?#].[^\\s]*"); + + private final Set _disabledHeadings = new HashSet<>(); public MarkdownActionButtons(@NonNull Context context, Document document) { super(context, document); @@ -139,6 +149,11 @@ public boolean onActionClick(final @StringRes int action) { MarkorDialogFactory.showInsertTableRowDialog(getActivity(), false, this::insertTableRow); return true; } + case R.string.abid_common_open_link_browser: { + if (followLinkUnderCursor()) { + return true; + } + } default: { return runCommonAction(action); } @@ -168,6 +183,34 @@ public boolean onActionLongClick(final @StringRes int action) { } } + private boolean followLinkUnderCursor() { + final int sel = TextViewUtils.getSelection(_hlEditor)[0]; + if (sel < 0) { + return false; + } + + final String line = TextViewUtils.getSelectedLines(_hlEditor, sel); + final int cursor = sel - TextViewUtils.getLineStart(_hlEditor.getText(), sel); + + final Matcher m = MARKDOWN_LINK.matcher(line); + while (m.find()) { + final String group = m.group(2); + if (m.start() <= cursor && m.end() > cursor && group != null) { + if (WEB_URL.matcher(group).matches()) { + GsContextUtils.instance.openWebpageInExternalBrowser(getActivity(), group); + return true; + } else { + final File f = GsFileUtils.makeAbsolute(group, _document.getFile().getParentFile()); + if (GsFileUtils.canCreate(f)) { + DocumentActivity.handleFileClick(getActivity(), f, null); + return true; + } + } + } + } + return false; + } + private void insertTableRow(int cols, boolean isHeaderEnabled) { StringBuilder sb = new StringBuilder(); _hlEditor.requestFocus(); diff --git a/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java b/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java index 60646ec27..c783423ac 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java +++ b/app/src/main/java/net/gsantner/markor/frontend/MarkorDialogFactory.java @@ -793,7 +793,7 @@ public static void showInsertSnippetDialog(final Activity activity, final GsCall dopt.data = data; dopt.isSearchEnabled = true; dopt.titleText = R.string.insert_snippet; - dopt.messageText = Html.fromHtml("" + as().getSnippetsFolder().getAbsolutePath() + ""); + dopt.messageText = Html.fromHtml("" + as().getSnippetsDirectory().getAbsolutePath() + ""); dopt.positionCallback = (ind) -> callback.callback(GsFileUtils.readTextFileFast(texts.get(data.get(ind.get(0)))).first); GsSearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt); } diff --git a/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java b/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java index be58b69b3..fc4b55781 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java +++ b/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java @@ -267,56 +267,49 @@ private void callback(boolean ok, File file) { // // ----- // if you use Androidstudio/IntelliJ copy file content into t = "". Android studio takes care of escaping - @SuppressLint("TrulyRandom") - private Pair getTemplateContent(final Spinner templateSpinner, final File basedir, final String filename, final boolean encrypt) { - String t = null; - switch (templateSpinner.getSelectedItemPosition()) { + private String getTemplateByPos( + final int spinnerPos, + final String fileName, + final File basedir + ) { + switch (spinnerPos) { case 1: { - t = "# Markdown Reference\nAutomatically generate _table of contents_ by checking the option here: `Settings > Format > Markdown`.\n\n## H2 Header\n### H3 header\n#### H4 Header\n##### H5 Header\n###### H6 Header\n\n\n\n## Format Text\n\n*Italic emphasis* , _Alternative italic emphasis_\n\n**Bold emphasis** , __Alternative bold emphasis__\n\n~~Strikethrough~~\n\nBreak line (two spaces at end of line) \n\n> Block quote\n\n`Inline code`\n\n```\nCode blocks\nare\nawesome\n```\n\n\n \n## Lists\n### Ordered & unordered\n\n* Unordered list\n* ...with asterisk/star\n* Test\n\n- Another unordered list\n- ...with hyphen/minus\n- Test\n\n1. Ordered list\n2. Test\n3. Test\n4. Test\n\n- Nested lists\n * Unordered nested list\n * Test\n * Test\n * Test\n- Ordered nested list\n 1. Test\n 2. Test\n 3. Test\n 4. Test\n- Double-nested unordered list\n - Test\n - Unordered\n - Test a\n - Test b\n - Ordered\n 1. Test 1\n 2. Test 2\n\n### Checklist\n* [ ] Salad\n* [x] Potatoes\n\n1. [x] Clean\n2. [ ] Cook\n\n\n\n## Links\n[Link](https://duckduckgo.com/)\n\n[File in same folder as the document.](markor-markdown-reference.md) Use %20 for spaces!\n\n\n\n## Tables\n\n| Left aligned | Middle aligned | Right aligned |\n| :--------------- | :------------------: | -----------------: |\n| Test | Test | Test |\n| Test | Test | Test |\n\n÷÷÷÷\n\nShorter | Table | Syntax\n:---: | ---: | :---\nTest | Test | Test\nTest | Test | Test\n\n\n\n\n\n## Math (KaTeX)\nSee [reference](https://katex.org/docs/supported.html) & [examples](https://github.com/waylonflinn/markdown-it-katex/blob/master/README.md). Enable by checking Math at `Settings > Markdown`.\n\n### Math inline\n\n$ I = \\frac V R $\n\n### Math block\n\n$$\\begin{array}{c} \nabla \\times \\vec{\\mathbf{B}} -\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{E}}}{\\partial t} & = \\frac{4\\pi}{c}\\vec{\\mathbf{j}} \nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\ \nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\ \nabla \\cdot \\vec{\\mathbf{B}} & = 0 \\end{array}$$\n\n\n$$\\frac{k_t}{k_e} = \\sqrt{2}$$\n\n\n\n## Format Text (continued)\n\n### Text color\n\nText with background color / highlight\n\nText foreground color\n\nText with colored outline / Text with colored outline\n\n\n### Text sub & superscript\n\nUnderline\n\nThe Subway sandwich was super\n\nSuper special characters: ⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁺ ⁻ ⁼ ⁽ ⁾ ⁿ ™ ® ℠\n\n### Text positioning\n
\n\ntext on the **right**\n\n
\n\n
\n\ntext in the **center** \n(one empy line above and below \nrequired for Markdown support OR markdown='1')\n\n
\n\n### Block Text\n\n
\nlorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. \n
\n\n### Dropdown\n\n
Click to Expand/Collapse\n\nExpanded content. Shows up and keeps visible when clicking expand. Hide again by clicking the dropdown button again.\n\n
\n\n\n### Break page\nTo break the page (/start a new page), put the div below into a own line.\nRelevant for creating printable pages from the document (Print / PDF).\n\n
\n\n\n\n\n## Multimedia\n\n### Images\n![Image](file:///android_asset/img/schindelpattern.jpg)\n\n### Videos\n**Youtube** [Welcome to Upper Austria](https://www.youtube.com/watch?v=RJREFH7Lmm8)\n\n\n**Peertube** [Road in the wood](https://open.tube/videos/watch/8116312a-dbbd-43a3-9260-9ea6367c72fc)\n
\n\n\n\n### Audio & Music\n**Web audio** [Guifrog - Xia Yu](https://www.freemusicarchive.org/music/Guifrog/Xia_Yu)\n\n\n**Local audio** Yellowcard - Lights up in the sky\n\n\n## Charts / Graphs / Diagrams (mermaidjs)\nPie, flow, sequence, class, state, ER \nSee also: mermaidjs [live editor](https://mermaid-js.github.io/mermaid-live-editor/).\n\n```mermaid\ngraph LR\n A[Square Rect] -- Link text --> B((Circle))\n A --> C(Round Rect)\n B --> D{Rhombus}\n C --> D\n```\n\n\n\n## Admonition Extension\nCreate block-styled side content. \nUse one of these qualifiers to select the icon and the block color: abstract, summary, tldr, bug, danger, error, example, snippet, failure, fail, missing, question, help, faq, info, todo, note, seealso, quote, cite, success, check, done, tip, hint, important, warning, caution, attention.\n\n!!! warning 'Optional Title'\n Block-Styled Side Content with **Markdown support**\n\n!!! info ''\n No-Heading Content\n\n??? bug 'Collapsed by default'\n Collapsible Block-Styled Side Content\n\n???+ example 'Open by default'\n Collapsible Block-Styled Side Content\n\n------------------\n\nThis Markdown reference file was created for the [Markor](https://github.com/gsantner/markor) project by [Gregor Santner](https://github.com/gsanter) and is licensed [Creative Commons Zero 1.0](https://creativecommons.org/publicdomain/zero/1.0/legalcode) (public domain). File revision 3.\n\n------------------\n\n\n"; - break; + return "# Markdown Reference\nAutomatically generate _table of contents_ by checking the option here: `Settings > Format > Markdown`.\n\n## H2 Header\n### H3 header\n#### H4 Header\n##### H5 Header\n###### H6 Header\n\n\n\n## Format Text\n\n*Italic emphasis* , _Alternative italic emphasis_\n\n**Bold emphasis** , __Alternative bold emphasis__\n\n~~Strikethrough~~\n\nBreak line (two spaces at end of line) \n\n> Block quote\n\n`Inline code`\n\n```\nCode blocks\nare\nawesome\n```\n\n\n \n## Lists\n### Ordered & unordered\n\n* Unordered list\n* ...with asterisk/star\n* Test\n\n- Another unordered list\n- ...with hyphen/minus\n- Test\n\n1. Ordered list\n2. Test\n3. Test\n4. Test\n\n- Nested lists\n * Unordered nested list\n * Test\n * Test\n * Test\n- Ordered nested list\n 1. Test\n 2. Test\n 3. Test\n 4. Test\n- Double-nested unordered list\n - Test\n - Unordered\n - Test a\n - Test b\n - Ordered\n 1. Test 1\n 2. Test 2\n\n### Checklist\n* [ ] Salad\n* [x] Potatoes\n\n1. [x] Clean\n2. [ ] Cook\n\n\n\n## Links\n[Link](https://duckduckgo.com/)\n\n[File in same folder as the document.](markor-markdown-reference.md) Use %20 for spaces!\n\n\n\n## Tables\n\n| Left aligned | Middle aligned | Right aligned |\n| :--------------- | :------------------: | -----------------: |\n| Test | Test | Test |\n| Test | Test | Test |\n\n÷÷÷÷\n\nShorter | Table | Syntax\n:---: | ---: | :---\nTest | Test | Test\nTest | Test | Test\n\n\n\n\n\n## Math (KaTeX)\nSee [reference](https://katex.org/docs/supported.html) & [examples](https://github.com/waylonflinn/markdown-it-katex/blob/master/README.md). Enable by checking Math at `Settings > Markdown`.\n\n### Math inline\n\n$ I = \\frac V R $\n\n### Math block\n\n$$\\begin{array}{c} \nabla \\times \\vec{\\mathbf{B}} -\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{E}}}{\\partial t} & = \\frac{4\\pi}{c}\\vec{\\mathbf{j}} \nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\ \nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\ \nabla \\cdot \\vec{\\mathbf{B}} & = 0 \\end{array}$$\n\n\n$$\\frac{k_t}{k_e} = \\sqrt{2}$$\n\n\n\n## Format Text (continued)\n\n### Text color\n\nText with background color / highlight\n\nText foreground color\n\nText with colored outline / Text with colored outline\n\n\n### Text sub & superscript\n\nUnderline\n\nThe Subway sandwich was super\n\nSuper special characters: ⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁺ ⁻ ⁼ ⁽ ⁾ ⁿ ™ ® ℠\n\n### Text positioning\n
\n\ntext on the **right**\n\n
\n\n
\n\ntext in the **center** \n(one empy line above and below \nrequired for Markdown support OR markdown='1')\n\n
\n\n### Block Text\n\n
\nlorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. \n
\n\n### Dropdown\n\n
Click to Expand/Collapse\n\nExpanded content. Shows up and keeps visible when clicking expand. Hide again by clicking the dropdown button again.\n\n
\n\n\n### Break page\nTo break the page (/start a new page), put the div below into a own line.\nRelevant for creating printable pages from the document (Print / PDF).\n\n
\n\n\n\n\n## Multimedia\n\n### Images\n![Image](file:///android_asset/img/schindelpattern.jpg)\n\n### Videos\n**Youtube** [Welcome to Upper Austria](https://www.youtube.com/watch?v=RJREFH7Lmm8)\n\n\n**Peertube** [Road in the wood](https://open.tube/videos/watch/8116312a-dbbd-43a3-9260-9ea6367c72fc)\n
\n\n\n\n### Audio & Music\n**Web audio** [Guifrog - Xia Yu](https://www.freemusicarchive.org/music/Guifrog/Xia_Yu)\n\n\n**Local audio** Yellowcard - Lights up in the sky\n\n\n## Charts / Graphs / Diagrams (mermaidjs)\nPie, flow, sequence, class, state, ER \nSee also: mermaidjs [live editor](https://mermaid-js.github.io/mermaid-live-editor/).\n\n```mermaid\ngraph LR\n A[Square Rect] -- Link text --> B((Circle))\n A --> C(Round Rect)\n B --> D{Rhombus}\n C --> D\n```\n\n\n\n## Admonition Extension\nCreate block-styled side content. \nUse one of these qualifiers to select the icon and the block color: abstract, summary, tldr, bug, danger, error, example, snippet, failure, fail, missing, question, help, faq, info, todo, note, seealso, quote, cite, success, check, done, tip, hint, important, warning, caution, attention.\n\n!!! warning 'Optional Title'\n Block-Styled Side Content with **Markdown support**\n\n!!! info ''\n No-Heading Content\n\n??? bug 'Collapsed by default'\n Collapsible Block-Styled Side Content\n\n???+ example 'Open by default'\n Collapsible Block-Styled Side Content\n\n------------------\n\nThis Markdown reference file was created for the [Markor](https://github.com/gsantner/markor) project by [Gregor Santner](https://github.com/gsanter) and is licensed [Creative Commons Zero 1.0](https://creativecommons.org/publicdomain/zero/1.0/legalcode) (public domain). File revision 3.\n\n------------------\n\n\n"; } case 2: { - t = "(A) Call Mom @mobile +family\n(A) Schedule annual checkup +health\n(A) Urgently buy milk @shop\n(B) Outline chapter 5 +novel @computer\n(C) Add cover sheets @work +myproject\nPlan backyard herb garden @home\nBuy salad @shop\nWrite blog post @pc\nInstall Markor @mobile\n2019-06-24 scan photos @home +blog\n2019-06-25 draw diagram @work \nx This has been done @home +renovations"; - break; + return "(A) Call Mom @mobile +family\n(A) Schedule annual checkup +health\n(A) Urgently buy milk @shop\n(B) Outline chapter 5 +novel @computer\n(C) Add cover sheets @work +myproject\nPlan backyard herb garden @home\nBuy salad @shop\nWrite blog post @pc\nInstall Markor @mobile\n2019-06-24 scan photos @home +blog\n2019-06-25 draw diagram @work \nx This has been done @home +renovations"; } case 3: { - t = "---\nlayout: post\ntags: []\ncategories: []\n#date: 2019-06-25 13:14:15\n#excerpt: ''\n#image: 'BASEURL/assets/blog/img/.png'\n#description:\n#permalink:\ntitle: 'title'\n---\n\n\n"; - break; + return "---\nlayout: post\ntags: []\ncategories: []\n#date: 2019-06-25 13:14:15\n#excerpt: ''\n#image: 'BASEURL/assets/blog/img/.png'\n#description:\n#permalink:\ntitle: 'title'\n---\n\n\n"; } case 4: { - t = "# Title\n## Description\n\n![Text](picture.png)\n\n### Ingredients\n\n| Ingredient | Amount |\n|:--------------|:-------|\n| 1 | 1 |\n| 2 | 2 |\n| 3 | 3 |\n| 4 | 4 |\n\n\n### Preparation\n\n1. Text\n2. Text\n\n"; - break; + return "# Title\n## Description\n\n![Text](picture.png)\n\n### Ingredients\n\n| Ingredient | Amount |\n|:--------------|:-------|\n| 1 | 1 |\n| 2 | 2 |\n| 3 | 3 |\n| 4 | 4 |\n\n\n### Preparation\n\n1. Text\n2. Text\n\n"; } case 5: { - t = "---\nclass: beamer\n---\n\n-----------------\n# Cool presentation\n\n## Abed Nadir\n\n{{ post.date_today }}\n\n\n\n\n-----------------\n\n## Slide title\n\n\n1. All Markdown features of Markor are **supported** for Slides too ~~strikeout~~ _italic_ `code`\n2. Start new slides with 3 more hyphens (---) separated by empty lines\n3. End last slide with hyphens too\n4. Slide backgrounds can be configured using CSS, for all and individual slides\n5. Print / PDF export in landscape mode\n6. Create title only slides (like first slide) by starting the slide (line after `---`) with title `# title`\n\n\n-----------------\n## Slide with centered image\n* Images can be centered by adding 'imghcenter' in alt text & grown to page size with 'imgbig'\n* Example: `![text imghcenter imgbig text](a.jpg)`\n\n![imghcenter imgbig](file:///android_asset/img/flowerfield.jpg)\n\n\n\n\n-----------------\n## Page with gradient background\n* and a picture\n* configure background color/image with CSS .slide_p4 { } (4 = the slide page number)\n\n![pic](file:///android_asset/img/flowerfield.jpg)\n\n\n\n\n-----------------\n## Page with image background\n* containing text and a table\n\n| Left aligned | Middle aligned | Right aligned |\n| :------------------- | :----------------------: | --------------------: |\n| Test | Test | Test |\n| Test | Test | Test |\n\n\n\n\n\n-----------------\n"; - break; + return "---\nclass: beamer\n---\n\n-----------------\n# Cool presentation\n\n## Abed Nadir\n\n{{ post.date_today }}\n\n\n\n\n-----------------\n\n## Slide title\n\n\n1. All Markdown features of Markor are **supported** for Slides too ~~strikeout~~ _italic_ `code`\n2. Start new slides with 3 more hyphens (---) separated by empty lines\n3. End last slide with hyphens too\n4. Slide backgrounds can be configured using CSS, for all and individual slides\n5. Print / PDF export in landscape mode\n6. Create title only slides (like first slide) by starting the slide (line after `---`) with title `# title`\n\n\n-----------------\n## Slide with centered image\n* Images can be centered by adding 'imghcenter' in alt text & grown to page size with 'imgbig'\n* Example: `![text imghcenter imgbig text](a.jpg)`\n\n![imghcenter imgbig](file:///android_asset/img/flowerfield.jpg)\n\n\n\n\n-----------------\n## Page with gradient background\n* and a picture\n* configure background color/image with CSS .slide_p4 { } (4 = the slide page number)\n\n![pic](file:///android_asset/img/flowerfield.jpg)\n\n\n\n\n-----------------\n## Page with image background\n* containing text and a table\n\n| Left aligned | Middle aligned | Right aligned |\n| :------------------- | :----------------------: | --------------------: |\n| Test | Test | Test |\n| Test | Test | Test |\n\n\n\n\n\n-----------------\n"; } case 6: { - t = "Content-Type: text/x-zim-wiki\nWiki-Format: zim 0.4\nCreation-Date: 2019-01-28T20:53:47+01:00\n\n====== Zim Wiki ======\nLet me try to gather a list of the formatting options Zim provides.\n\n====== Head 1 ======\n\n===== Head 2 =====\n\n==== Head 3 ====\n\n=== Head 4 ===\n\n== Head 5 ==\n\n**Bold**\n//italics//\n__marked (yellow Background)__\n~~striked~~\n\n* Unordered List\n* second item\n * [[Sub-Item]]\n * Subsub-Item\n * and one more sub\n* Back to first indent level\n\n1. ordered list\n2. second item\n a. item 2a\n 1. Item 2a1\n 2. Item 2a2\n b. item 2b\n 1. 2b1\n a. 2b1a\n3. an so on...\n\n[ ] Checklist\n[ ] unchecked item\n[*] checked item\n[x] crossed item\n[>] Item marked with a yellow left-to-right-arrow\n[ ] another unchecked item\n\n\nThis ist ''preformatted text'' inline.\n\n'''\nThis is a preformatted text block.\nIt spans multiple lines.\nAnd it's visually indented.\n'''\n\nWe also have _{subscript} and ^{superscript}.\n\nIt seems there is no way to combine those styles.\n//**this is simply italic**// and you can see the asterisks.\n**//This is simply bold//** and you can see the slashes.\n__**This is simply marked yellow**__ and you can see the asterisks.\n\nThis is a web link: [[https://github.com/gsantner/markor|Markor on Github]]\nLinks inside the Zim Wiki project can be made by simply using the [[Page Name]] in double square brackets.\nThis my also contain some hierarchy information, like [[Folder:Subfolder:Document Name]]\n\n\nThis zim wiki reference file was created for the [[https://github.com/gsantner/markor|Markor]] project by [[https://github.com/gsantner|Gregor Santner]] and is licensed [[https://creativecommons.org/publicdomain/zero/1.0/legalcode|Creative Commons Zero 1.0]] (public domain). File revision 1."; - break; + return "Content-Type: text/x-zim-wiki\nWiki-Format: zim 0.4\nCreation-Date: 2019-01-28T20:53:47+01:00\n\n====== Zim Wiki ======\nLet me try to gather a list of the formatting options Zim provides.\n\n====== Head 1 ======\n\n===== Head 2 =====\n\n==== Head 3 ====\n\n=== Head 4 ===\n\n== Head 5 ==\n\n**Bold**\n//italics//\n__marked (yellow Background)__\n~~striked~~\n\n* Unordered List\n* second item\n * [[Sub-Item]]\n * Subsub-Item\n * and one more sub\n* Back to first indent level\n\n1. ordered list\n2. second item\n a. item 2a\n 1. Item 2a1\n 2. Item 2a2\n b. item 2b\n 1. 2b1\n a. 2b1a\n3. an so on...\n\n[ ] Checklist\n[ ] unchecked item\n[*] checked item\n[x] crossed item\n[>] Item marked with a yellow left-to-right-arrow\n[ ] another unchecked item\n\n\nThis ist ''preformatted text'' inline.\n\n'''\nThis is a preformatted text block.\nIt spans multiple lines.\nAnd it's visually indented.\n'''\n\nWe also have _{subscript} and ^{superscript}.\n\nIt seems there is no way to combine those styles.\n//**this is simply italic**// and you can see the asterisks.\n**//This is simply bold//** and you can see the slashes.\n__**This is simply marked yellow**__ and you can see the asterisks.\n\nThis is a web link: [[https://github.com/gsantner/markor|Markor on Github]]\nLinks inside the Zim Wiki project can be made by simply using the [[Page Name]] in double square brackets.\nThis my also contain some hierarchy information, like [[Folder:Subfolder:Document Name]]\n\n\nThis zim wiki reference file was created for the [[https://github.com/gsantner/markor|Markor]] project by [[https://github.com/gsantner|Gregor Santner]] and is licensed [[https://creativecommons.org/publicdomain/zero/1.0/legalcode|Creative Commons Zero 1.0]] (public domain). File revision 1."; } case 7: { - t = WikitextActionButtons.createWikitextHeaderAndTitleContents(filename.replaceAll("(\\.((zim)|(txt)))*$", "").trim().replace(' ', '_'), new Date(), getResources().getString(R.string.created)); - break; + return WikitextActionButtons.createWikitextHeaderAndTitleContents(fileName.replaceAll("(\\.((zim)|(txt)))*$", "").trim().replace(' ', '_'), new Date(), getResources().getString(R.string.created)); } case 8: { - t = TextViewUtils.interpolateEscapedDateTime("---\ntags: []\ncreated: '`yyyy-MM-dd`'\ntitle: ''\n---\n\n"); + final String header = TextViewUtils.interpolateEscapedDateTime("---\ntags: []\ncreated: '`yyyy-MM-dd`'\ntitle: ''\n---\n\n"); if (basedir != null && new File(basedir.getParentFile(), ".notabledir").exists()) { - t = t.replace("created:", "modified:"); + return header.replace("created:", "modified:"); } - break; + return header; } case 9: { - t = "source:\ncategory:\ntag:\n------------\n"; - break; + return "source:\ncategory:\ntag:\n------------\n"; } case 10: { - t = "= My Title\n:page-subtitle: This is a subtitle\n:page-last-updated: 2029-01-01\n:page-tags: ['AsciiDoc', 'Markor', 'open source']\n:toc: auto\n:toclevels: 2\n// :page-description: the optional description\n// This should match the structure on the jekyll server:\n:imagesdir: ../assets/img/blog\n\nifndef::env-site[]\n\n// on the jekyll server, the :page-subtitle: is displayed below the title.\n// but it is not shown, when rendered in html5, and the site is rendered in html5, when working locally\n// so we show it additionally only, when we work locally\n// https://docs.asciidoctor.org/asciidoc/latest/document/subtitle/\n\n[discrete] \n=== {page-subtitle}\n\nendif::env-site[]\n\n// local testing:\n:imagesdir: ../app/src/main/assets/img\n\nimage::flowerfield.jpg[]\n\nimage::schindelpattern.jpg[Schindelpattern,200]\n\nbefore inline picture image:schindelpattern.jpg[Schindelpattern,50] and after inline picture\n"; - break; + return "= My Title\n:page-subtitle: This is a subtitle\n:page-last-updated: 2029-01-01\n:page-tags: ['AsciiDoc', 'Markor', 'open source']\n:toc: auto\n:toclevels: 2\n// :page-description: the optional description\n// This should match the structure on the jekyll server:\n:imagesdir: ../assets/img/blog\n\nifndef::env-site[]\n\n// on the jekyll server, the :page-subtitle: is displayed below the title.\n// but it is not shown, when rendered in html5, and the site is rendered in html5, when working locally\n// so we show it additionally only, when we work locally\n// https://docs.asciidoctor.org/asciidoc/latest/document/subtitle/\n\n[discrete] \n=== {page-subtitle}\n\nendif::env-site[]\n\n// local testing:\n:imagesdir: ../app/src/main/assets/img\n\nimage::flowerfield.jpg[]\n\nimage::schindelpattern.jpg[Schindelpattern,200]\n\nbefore inline picture image:schindelpattern.jpg[Schindelpattern,50] and after inline picture\n"; } case 11: { // sample.csv - t = "# this is a comment in csv file\n" + + return "# this is a comment in csv file\n" + "\n" + "# below is the header\n" + "number;text;finishing date\n" + @@ -330,33 +323,45 @@ private Pair getTemplateContent(final Spinner templateSpinner, "# use \"...\" if the column is multiline\n" + "3;\"multi\n" + " line\";2059-12-24\n"; - break; } - default: { - final Map snippets = MarkorDialogFactory.getSnippets(ApplicationObject.settings()); - if (templateSpinner.getSelectedItem() instanceof String && snippets.containsKey((String) templateSpinner.getSelectedItem())) { - t = TextViewUtils.interpolateEscapedDateTime(GsFileUtils.readTextFileFast(snippets.get((String) templateSpinner.getSelectedItem())).first); - break; - } + } + return null; + } + + private Pair getTemplateContent( + final Spinner templateSpinner, + final File basedir, + final String filename, + final boolean encrypt + ) { + + String template = getTemplateByPos(templateSpinner.getSelectedItemPosition(), filename, basedir); + if (template == null) { + final Map snippets = MarkorDialogFactory.getSnippets(ApplicationObject.settings()); + final Object sel = templateSpinner.getSelectedItem(); + if (sel instanceof String && snippets.containsKey((String) sel)) { + final String t = GsFileUtils.readTextFileFast(snippets.get((String) sel)).first; + final String title = GsFileUtils.getNameWithoutExtension(filename); + template = TextViewUtils.interpolateSnippet(t, title, ""); } } - if (t == null) { - return new Pair<>(null, -1); + if (template == null) { + return Pair.create(null, -1); } - final int startingIndex = t.indexOf(HighlightingEditor.PLACE_CURSOR_HERE_TOKEN); - t = t.replace(HighlightingEditor.PLACE_CURSOR_HERE_TOKEN, ""); + final int startingIndex = template.indexOf(HighlightingEditor.PLACE_CURSOR_HERE_TOKEN); + template = template.replace(HighlightingEditor.PLACE_CURSOR_HERE_TOKEN, ""); // Has no utility in a new file - t = t.replace(HighlightingEditor.INSERT_SELECTION_HERE_TOKEN, ""); + template = template.replace(HighlightingEditor.INSERT_SELECTION_HERE_TOKEN, ""); final byte[] bytes; if (encrypt && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final char[] pass = ApplicationObject.settings().getDefaultPassword(); - bytes = new JavaPasswordbasedCryption(Build.VERSION.SDK_INT, new SecureRandom()).encrypt(t, pass); + bytes = new JavaPasswordbasedCryption(Build.VERSION.SDK_INT, new SecureRandom()).encrypt(template, pass); } else { - bytes = t.getBytes(); + bytes = template.getBytes(); } return Pair.create(bytes, startingIndex); diff --git a/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java b/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java index 7b828ffe5..67020097c 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java +++ b/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java @@ -406,7 +406,6 @@ public void insertOrReplaceTextOnCursor(final String newText) { final Editable edit = getText(); if (edit != null && newText != null) { - // TODO - should consider moving any snippet specific logic out of here // Fill in any instances of selection final int[] sel = TextViewUtils.getSelection(this); final CharSequence selected = TextViewUtils.toString(edit, sel[0], sel[1]); diff --git a/app/src/main/java/net/gsantner/markor/frontend/textview/TextViewUtils.java b/app/src/main/java/net/gsantner/markor/frontend/textview/TextViewUtils.java index 1f75c1827..e8793065d 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/textview/TextViewUtils.java +++ b/app/src/main/java/net/gsantner/markor/frontend/textview/TextViewUtils.java @@ -37,6 +37,7 @@ import java.util.List; import java.util.Locale; import java.util.TreeSet; +import java.util.UUID; @SuppressWarnings({"CharsetObjectCanBeUsed", "WeakerAccess", "unused"}) public final class TextViewUtils extends GsTextUtils { @@ -132,6 +133,15 @@ public static int[] getSelection(final CharSequence text) { } } + public static String getSelectedText(final CharSequence text) { + final int[] sel = getSelection(text); + return (sel[0] >= 0 && sel[1] >= 0) ? text.subSequence(sel[0], sel[1]).toString() : ""; + } + + public static String getSelectedText(final TextView text) { + return getSelectedText(text.getText()); + } + public static int[] getLineSelection(final CharSequence text, final int[] sel) { return sel != null && sel.length >= 2 ? new int[]{getLineStart(text, sel[0]), getLineEnd(text, sel[1])} : null; } @@ -144,11 +154,28 @@ public static int[] getLineSelection(final CharSequence seq) { return getLineSelection(seq, getSelection(seq)); } + + /** Get lines of text in which sel[0] -> sel[1] is contained **/ + public static String getSelectedLines(final TextView text, final int... sel) { + return getSelectedLines(text.getText(), sel); + } + public static String getSelectedLines(final CharSequence seq) { - final int[] sel = getLineSelection(seq); - return seq.subSequence(sel[0], sel[1]).toString(); + return getSelectedLines(seq, getSelection(seq)); + } + + /** Get lines of text in which sel[0] -> sel[1] is contained **/ + public static String getSelectedLines(final CharSequence seq, final int... sel) { + if (sel == null || sel.length == 0) { + return ""; + } + + final int start = Math.min(Math.max(sel[0], 0), seq.length()); + final int end = Math.min(Math.max(start, sel[sel.length - 1]), seq.length()); + return seq.subSequence(getLineStart(seq, start), getLineEnd(seq, end)).toString(); } + /** * Convert a char index to a line index + offset from end of line * @@ -370,20 +397,50 @@ public static void setSelectionAndShow(final EditText edit, final int... sel) { } edit.setSelection(start, end); - edit.post(() -> showSelection(edit, start, end)); + edit.postDelayed(() -> showSelection(edit, start, end), 250); }); } } + /** + * Snippets are evaluated in the following order: + * 1. {{*}} style placeholders are replaced (except {{cursor}}) + * 2. Time formats within backticks are interpolated + * 3. {{cursor}} tokens are replaced with HighlightingEditor.PLACE_CURSOR_HERE_TOKEN + * + * @param text Text to be interpolated + * @param title Title of note (for {{title}}) + * @param selectedText Currently selected text + */ + public static String interpolateSnippet(String text, final String title, final String selectedText) { + final long current = System.currentTimeMillis(); + final String time = GsContextUtils.instance.formatDateTime((Locale) null, "HH:mm", current); + final String date = GsContextUtils.instance.formatDateTime((Locale) null, "yyyy-MM-dd", current); + + // Replace placeholders + text = text + .replace("{{time}}", time) + .replace("{{date}}", date) + .replace("{{title}}", title) + .replace("{{sel}}", selectedText) + .replace("{{cursor}}", HighlightingEditor.PLACE_CURSOR_HERE_TOKEN); + + while (text.contains("{{uuid}}")) { + text = text.replaceFirst("\\{\\{uuid\\}\\}", UUID.randomUUID().toString()); + } + + return interpolateEscapedDateTime(text); + } + // Search for matching pairs of backticks // interpolate contents of backtick pair as SimpleDateFormat - public static String interpolateEscapedDateTime(final String snip) { + public static String interpolateEscapedDateTime(final String text) { final StringBuilder interpolated = new StringBuilder(); final StringBuilder temp = new StringBuilder(); boolean isEscaped = false; boolean inDate = false; - for (int i = 0; i < snip.length(); i++) { - final char c = snip.charAt(i); + for (int i = 0; i < text.length(); i++) { + final char c = text.charAt(i); if (c == '\\' && !isEscaped) { isEscaped = true; } else if (isEscaped) { diff --git a/app/src/main/java/net/gsantner/markor/model/AppSettings.java b/app/src/main/java/net/gsantner/markor/model/AppSettings.java index 17b2bc895..18130f77d 100644 --- a/app/src/main/java/net/gsantner/markor/model/AppSettings.java +++ b/app/src/main/java/net/gsantner/markor/model/AppSettings.java @@ -112,8 +112,14 @@ public File getDefaultTodoFile() { return new File(getDefaultNotebookFile(), rstr(R.string.todo_default_filename)); } - public File getSnippetsFolder() { - return new File(getNotebookDirectory(), ".app/snippets"); + public File getSnippetsDirectory() { + final File _default = new File(getNotebookDirectory(), ".app/snippets"); + final File snf = new File(getString(R.string.pref_key__quicknote_filepath, _default.getAbsolutePath())); + return snf.isDirectory() && snf.canRead() ? snf : _default; + } + + public void setSnippetDirectory(final File folder) { + setString(R.string.pref_key__snippet_directory_path, folder.getAbsolutePath()); } public String getFontFamily() { diff --git a/app/src/main/java/net/gsantner/opoc/format/GsTextUtils.java b/app/src/main/java/net/gsantner/opoc/format/GsTextUtils.java index 1dc9b74cc..b6f3d48dc 100644 --- a/app/src/main/java/net/gsantner/opoc/format/GsTextUtils.java +++ b/app/src/main/java/net/gsantner/opoc/format/GsTextUtils.java @@ -37,21 +37,19 @@ public class GsTextUtils { * @param pos Position to start searching from (backwards) * @return Extracted URL or {@code null} if none found */ - public static String tryExtractUrlAroundPos(String text, int pos) { + public static String tryExtractUrlAroundPos(final String text, int pos) { pos = Math.min(Math.max(0, pos), text.length() - 1); - if (pos >= 0 && pos < text.length()) { - int begin = Math.max(text.lastIndexOf("https://", pos), text.lastIndexOf("http://", pos)); - if (begin >= 0) { - int end = text.length(); - for (String check : new String[]{"\n", " ", "\t", "\r", ")", "|"}) { - if ((pos = text.indexOf(check, begin)) > begin && pos < end) { - end = pos; - } + int begin = Math.max(text.lastIndexOf("https://", pos), text.lastIndexOf("http://", pos)); + if (begin >= 0) { + int end = text.length(); + for (final String check : new String[]{"\n", " ", "\t", "\r", ")", "|"}) { + if ((pos = text.indexOf(check, begin)) > begin && pos < end) { + end = pos; } + } - if ((end - begin) > 5 && end > 5) { - return text.substring(begin, end).replaceAll("[\\]=%>}]+$", ""); - } + if ((end - begin) > 5) { + return text.substring(begin, end).replaceAll("[\\]=%>}]+$", ""); } } return null; diff --git a/app/src/main/java/net/gsantner/opoc/util/GsFileUtils.java b/app/src/main/java/net/gsantner/opoc/util/GsFileUtils.java index 4a96ee141..dcca29597 100644 --- a/app/src/main/java/net/gsantner/opoc/util/GsFileUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/GsFileUtils.java @@ -16,6 +16,8 @@ import android.text.TextUtils; import android.util.Pair; +import androidx.annotation.Nullable; + import net.gsantner.opoc.format.GsTextUtils; import java.io.BufferedInputStream; @@ -645,9 +647,12 @@ public static boolean fileExists(final File checkFile, boolean... caseInsensitiv // Get the title of the file public static String getFilenameWithoutExtension(final File file) { - final String name = file.getName(); - final int doti = name.lastIndexOf("."); - return (doti < 0) ? name : name.substring(0, doti); + return getNameWithoutExtension(file.getName()); + } + + public static String getNameWithoutExtension(final String fileName) { + final int doti = fileName.lastIndexOf("."); + return (doti < 0) ? fileName : fileName.substring(0, doti); } /// Get the file extension of the file, including dot @@ -824,4 +829,20 @@ public static void copyUriToFile(final Context context, final Uri source, final } catch (IOException ignored) { } } + + public static File makeAbsolute(final String path, final File base) { + return path != null ? makeAbsolute(new File(path.trim()), base) : null; + } + + public static File makeAbsolute(final File file, final File base) { + if (file == null) { + return null; + } else if (file.isAbsolute()) { + return file; + } else if (base != null) { + return new File(base, file.getPath()).getAbsoluteFile(); + } else { + return null; + } + } } diff --git a/app/src/main/res/drawable/baseline_snippet_folder_24.xml b/app/src/main/res/drawable/baseline_snippet_folder_24.xml new file mode 100644 index 000000000..e23e57cf3 --- /dev/null +++ b/app/src/main/res/drawable/baseline_snippet_folder_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/string-not_translatable.xml b/app/src/main/res/values/string-not_translatable.xml index 25ef56a5d..9adcd8d42 100644 --- a/app/src/main/res/values/string-not_translatable.xml +++ b/app/src/main/res/values/string-not_translatable.xml @@ -206,6 +206,7 @@ work. If not, see . pref_key__basic_color_scheme_sepia pref_key__keep_screen_on pref_key__is_disallow_screenshots + pref_key__snippet_directory_path abid_common_delete_lines abid_common_new_line_below diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ab0743f8a..47e825e36 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -463,6 +463,7 @@ work. If not, see . Subfolder under the current folder where attachments will be saved. If left blank, attachments will be saved in the current folder. Auto-format Insert snippet + Snippet / Template directory Error: Could not open file. Error encountered: Text copied to clipboard. Files with fewer than %d characters not saved automatically in order to prevent data loss. diff --git a/app/src/main/res/xml/preferences_master.xml b/app/src/main/res/xml/preferences_master.xml index 8de931e48..473e99f52 100644 --- a/app/src/main/res/xml/preferences_master.xml +++ b/app/src/main/res/xml/preferences_master.xml @@ -172,6 +172,11 @@ android:key="@string/pref_key__attachment_folder_name" android:title="@string/attachment_folder_name" /> + +