diff --git a/.project b/.project index d293667..ad015f1 100644 --- a/.project +++ b/.project @@ -7,5 +7,6 @@ + org.eclipse.m2e.core.maven2Nature diff --git a/README.md b/README.md index ed55460..14e0c00 100644 --- a/README.md +++ b/README.md @@ -62,14 +62,23 @@ Increase version ### History -- 1.0 +- 1.0 Initial release - 1.1 (24 Feb 2014) by Telmo Brugnara @tbrugz #40 - - Rich color preferences #35 #37 + - Rich color preferences #35 #37 - 1.2 (Jan 2015) by Olivier Martin @oliviermartin #52 - Update preview when the file is saved #48 - MultiMarkdown metadata #49 - GitHub code blocks #50 - detecting links #51 - open GFM View from Markdown View #53 +- 1.3 (Nov 2016) by Gerald Rosenberg + - Update viewer to recognize stylesheets + - Add preference choice to select between + - a default stylesheet + - multiple builtin stylesheets + - stylesheets located on the platform filesystem + - Close viewer when editor closes + - Enabled updates to occur without changing the viewer vertical document position + - Updated to Java 1.8/Neon/Tycho 0.24 with-Eclipse logo diff --git a/feature/feature.xml b/feature/feature.xml index e8b26aa..6dc7dbc 100644 --- a/feature/feature.xml +++ b/feature/feature.xml @@ -2,8 +2,8 @@ + version="1.3.0.qualifier" + provider-name="Winterwell Associates Ltd"> Support for editing the text-markup language Markdown. Also provides @@ -40,6 +40,24 @@ The Markdown syntax is copyright (c) 2004, John Gruber. It is used under a BSD-s + + + + + + + + + + + + + + + + + + com.winterwell.markdown markdown.editor.parent - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT markdown.editor.feature eclipse-feature - Markdown Editor (feature) - Markdown Editor (feature) + Markdown Editor feature + Markdown Editor feature diff --git a/plugin/.classpath b/plugin/.classpath index cf4ff31..a24f30e 100644 --- a/plugin/.classpath +++ b/plugin/.classpath @@ -1,10 +1,14 @@ - + - - - - - + + + + + + + + + diff --git a/plugin/.gitignore b/plugin/.gitignore new file mode 100644 index 0000000..04aad86 --- /dev/null +++ b/plugin/.gitignore @@ -0,0 +1,2 @@ +/utils/ +/attic/ diff --git a/plugin/META-INF/MANIFEST.MF b/plugin/META-INF/MANIFEST.MF index 3c0bdf2..245fc60 100644 --- a/plugin/META-INF/MANIFEST.MF +++ b/plugin/META-INF/MANIFEST.MF @@ -2,40 +2,27 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Markdown Bundle-SymbolicName: winterwell.markdown;singleton:=true -Bundle-Version: 1.2.0.qualifier -Bundle-Activator: winterwell.markdown.Activator +Bundle-Version: 1.3.0.qualifier +Bundle-Activator: winterwell.markdown.MarkdownUI Bundle-Vendor: Winterwell Associates Ltd -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.ui.editors, - org.eclipse.jface.text, +Require-Bundle: org.eclipse.core.runtime, org.eclipse.core.resources, + org.eclipse.swt, + org.eclipse.ui.editors, org.eclipse.ui.views, + org.eclipse.core.commands, + org.eclipse.ui.workbench, org.eclipse.jface, - org.eclipse.swt, - org.eclipse.ui.workbench -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Import-Package: org.eclipse.core.internal.resources, org.eclipse.jface.text, - org.eclipse.ui.texteditor -Bundle-ClassPath: .,target/classes,lib/markdownj-1.0.2b4-0.3.0.jar, - lib/winterwell.utils.jar, - lib/net.sf.paperclips_1.0.1.jar -Export-Package: com.petebevin.markdown, - com.petebevin.markdown.test, - net.sf.paperclips, - net.sf.paperclips.decorator, - winterwell.markdown, - winterwell.markdown.editors, - winterwell.markdown.pagemodel, - winterwell.markdown.preferences, - winterwell.markdown.views, - winterwell.utils, - winterwell.utils.containers, - winterwell.utils.gui, - winterwell.utils.io, - winterwell.utils.reporting, - winterwell.utils.threads, - winterwell.utils.time, - winterwell.utils.web + org.eclipse.ui.workbench.texteditor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-ClassPath: ., + lib/markdownj-core.jar, + lib/commonmark.jar, + lib/txtmark.jar, + lib/pegdown.jar, + lib/parboiled-java.jar, + lib/parboiled-core.jar, + lib/asm-all.jar +Import-Package: org.eclipse.ui diff --git a/plugin/build.properties b/plugin/build.properties index ccfe656..21223ee 100644 --- a/plugin/build.properties +++ b/plugin/build.properties @@ -1,15 +1,18 @@ source.. = src/ +output.. = target/classes/ bin.includes = META-INF/,\ plugin.xml,\ icons/,\ .,\ - lib/ -src.includes = src/,\ - pom.xml,\ - plugin.xml,\ + resources/,\ + lib/markdownj-core.jar,\ + lib/commonmark.jar,\ + lib/pegdown.jar,\ + lib/txtmark.jar,\ + lib/parboiled-java.jar,\ + lib/parboiled-core.jar,\ + lib/asm-all.jar +src.includes = pom.xml,\ icons/,\ lib/,\ - .project,\ - .classpath,\ - META-INF/,\ - build.properties + resources/ diff --git a/plugin/icons/markdown.png b/plugin/icons/markdown.png new file mode 100644 index 0000000..d8e7317 Binary files /dev/null and b/plugin/icons/markdown.png differ diff --git a/plugin/lib/asm-all.jar b/plugin/lib/asm-all.jar new file mode 100644 index 0000000..117d001 Binary files /dev/null and b/plugin/lib/asm-all.jar differ diff --git a/plugin/lib/commonmark.jar b/plugin/lib/commonmark.jar new file mode 100644 index 0000000..8ee020f Binary files /dev/null and b/plugin/lib/commonmark.jar differ diff --git a/plugin/lib/markdownj-1.0.2b4-0.3.0.jar b/plugin/lib/markdownj-1.0.2b4-0.3.0.jar deleted file mode 100755 index 5e8f22b..0000000 Binary files a/plugin/lib/markdownj-1.0.2b4-0.3.0.jar and /dev/null differ diff --git a/plugin/lib/markdownj-core.jar b/plugin/lib/markdownj-core.jar new file mode 100644 index 0000000..fcf983b Binary files /dev/null and b/plugin/lib/markdownj-core.jar differ diff --git a/plugin/lib/net.sf.paperclips_1.0.1.jar b/plugin/lib/net.sf.paperclips_1.0.1.jar deleted file mode 100644 index 1853182..0000000 Binary files a/plugin/lib/net.sf.paperclips_1.0.1.jar and /dev/null differ diff --git a/plugin/lib/parboiled-core.jar b/plugin/lib/parboiled-core.jar new file mode 100644 index 0000000..ee49777 Binary files /dev/null and b/plugin/lib/parboiled-core.jar differ diff --git a/plugin/lib/parboiled-java.jar b/plugin/lib/parboiled-java.jar new file mode 100644 index 0000000..e3289cd Binary files /dev/null and b/plugin/lib/parboiled-java.jar differ diff --git a/plugin/lib/pegdown.jar b/plugin/lib/pegdown.jar new file mode 100644 index 0000000..9eb06e6 Binary files /dev/null and b/plugin/lib/pegdown.jar differ diff --git a/plugin/lib/txtmark.jar b/plugin/lib/txtmark.jar new file mode 100644 index 0000000..9772d47 Binary files /dev/null and b/plugin/lib/txtmark.jar differ diff --git a/plugin/lib/winterwell.utils.jar b/plugin/lib/winterwell.utils.jar deleted file mode 100644 index 1a5784a..0000000 Binary files a/plugin/lib/winterwell.utils.jar and /dev/null differ diff --git a/plugin/plugin.xml b/plugin/plugin.xml index 8d043e7..bf06744 100755 --- a/plugin/plugin.xml +++ b/plugin/plugin.xml @@ -1,31 +1,137 @@ + + + + + + + + + + + + + + + + + + + name="LitCoffee Markdown Editor"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + style="push" + tooltip="Use Github-flavored Markdown "> + style="push" + tooltip="Open Preferences"> + icon="icons/markdown.png" + style="push" + tooltip="Open Markdown Viewer"> @@ -58,64 +167,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - + point="org.eclipse.ui.keywords"> + + - diff --git a/plugin/pom.xml b/plugin/pom.xml index cb4e888..6bc3001 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -1,18 +1,151 @@ - + 4.0.0 - + com.winterwell.markdown markdown.editor.parent - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT - + winterwell.markdown eclipse-plugin - Markdown Editor (plugin) - Markdown Editor Plugin for Eclipse + Markdown Editor plugin + Markdown Editor plugin - + + + + org.markdownj + markdownj-core + 0.4 + + + + com.atlassian.commonmark + commonmark + 0.7.1 + + + + org.pegdown + pegdown + 1.6.0 + + + + org.parboiled + parboiled-core + 1.1.7 + + + + org.parboiled + parboiled-java + 1.1.7 + + + + org.ow2.asm + asm-all + 4.0 + + + + com.github.rjeschke + txtmark + 0.13 + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.8 + + + copy + process-resources + + copy + + + + + + org.markdownj + markdownj-core + 0.4 + jar + true + + + + org.pegdown + pegdown + 1.6.0 + jar + true + + + + org.parboiled + parboiled-core + 1.1.7 + jar + true + + + + org.parboiled + parboiled-java + 1.1.7 + jar + true + + + + org.ow2.asm + asm-all + 4.0 + jar + true + + + + com.atlassian.commonmark + commonmark + 0.7.1 + jar + true + + + + com.github.rjeschke + txtmark + 0.13 + jar + true + + + + lib + true + true + true + + + + + + + + + diff --git a/plugin/resources/amblin.css b/plugin/resources/amblin.css new file mode 100644 index 0000000..34df864 --- /dev/null +++ b/plugin/resources/amblin.css @@ -0,0 +1,493 @@ +/* +Copyright 2013 Brett Terpstra +This document has been created with Marked.app +Content is property of the document author +Please leave this notice in place, along with any additional credits below. +--------------------------------------------------------------- +Title: Amblin +Author: Erik Sagan (@kartooner) +Description: A clean theme with bold headlines and carefully crafted typography. +Non-standard fonts used: + * Rockwell (Installed by Microsoft Office) + * Rokkit (Fallback, available at ) +Download @font-face kits from . Woff versions included as data uris. +*/ +#wrapper div, #wrapper span, #wrapper object, #wrapper iframe, #wrapper h1, + #wrapper h2, #wrapper h3, #wrapper h4, #wrapper h5, #wrapper h6, + #wrapper p, #wrapper blockquote, #wrapper pre, #wrapper a, #wrapper abbr, + #wrapper acronym, #wrapper address, #wrapper code, #wrapper del, + #wrapper dfn, #wrapper em, #wrapper img, #wrapper q, #wrapper dl, + #wrapper dt, #wrapper dd, #wrapper ol, #wrapper ul, #wrapper li, + #wrapper fieldset, #wrapper form, #wrapper label, #wrapper legend, + #wrapper table, #wrapper caption, #wrapper tbody, #wrapper tfoot, + #wrapper thead, #wrapper tr, #wrapper th, #wrapper td { + margin: 0; + padding: 0; + border: 0; + font-weight: inherit; + font-style: inherit; + font-size: 100%; + font-family: inherit; + vertical-align: baseline; +} + +body { + background: #fff; + margin: 1.5em 0; +} + +body #wrapper { + line-height: 1.8; +} + +#wrapper table { + border-collapse: collapse; + border-spacing: 0; +} + +#wrapper caption, #wrapper th, #wrapper td { + text-align: left; + font-weight: 400; +} + +#wrapper blockquote:before, #wrapper blockquote:after, #wrapper q:before, + #wrapper q:after { + content: ""; +} + +#wrapper blockquote, #wrapper q { + quotes: "" ""; +} + +#wrapper a img { + border: none; +} + +#wrapper input, #wrapper textarea { + margin: 0; +} + +/* -------------------------------------------------------------- +Typography +--------------------------------------------------------------*/ +body { + -webkit-font-smoothing: antialiased; + font-size: 110%; + margin: 2em 0 0; +} + +/* Headings +--------------------------------------------------------------*/ +@font-face { + font-family: 'RokkittRegular'; + src: + url("data:font/woff;base64,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") + format("woff"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'LiberationMonoRegular'; + src: + url("data:font/woff;base64,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") + format("woff"); + font-weight: normal; + font-style: normal; +} + +#wrapper { + font-size: 13px; + /* Text elements + --------------------------------------------------------------*/ + padding: 20px; +} + +#wrapper p, #wrapper ul, #wrapper ol, #wrapper dl, #wrapper td, #wrapper input, + #wrapper textarea, #wrapper div { + color: #333; + font-family: Lucida Grande, Verdana, Helvetica, Arial, sans-serif; +} + +#wrapper h1, #wrapper h2, #wrapper h3, #wrapper h4, #wrapper h5, + #wrapper h6 { + color: #333; + font-family: Rockwell, 'Rokkitt', Georgia, serif; + letter-spacing: 0; +} + +#wrapper h1 { + font-size: 3em; + line-height: 1; + margin-bottom: 0.5em; +} + +#wrapper h2 { + font-size: 2em; + margin-bottom: 0.75em; +} + +#wrapper h3 { + font-size: 1.5em; + line-height: 1; + margin-bottom: 1em; +} + +#wrapper h4 { + font-size: 1.2em; + line-height: 1.25; + margin-bottom: 1.25em; +} + +#wrapper h5 { + font-size: 1em; + margin-bottom: 1.5em; +} + +#wrapper h6 { + font-size: 1em; +} + +#wrapper p { + margin: 0 0 1.5em; + font-size: 1.2em; +} + +#wrapper ul, #wrapper ol { + color: #444; + margin: 1em 2em; +} + +#wrapper ul { + list-style-type: circle; + font-size: 1.05em; +} + +#wrapper ol { + list-style-type: decimal; +} + +#wrapper li p { + font-size: 1em; +} + +#wrapper li>p:first-child { + margin: 0; +} + +#wrapper dl { + margin: 0 0 1.5em 0; +} + +#wrapper dl dt { + font-weight: bold; +} + +#wrapper dl dd { + margin-left: 1.5em; +} + +#wrapper abbr, #wrapper acronym { + border-bottom: 1px dotted #000; +} + +#wrapper address { + margin-top: 1.5em; + font-style: italic; +} + +#wrapper del { + color: #000; +} + +#wrapper a { + color: #D33637; + text-decoration: none; + border-bottom: 1px dotted #D33637; + -webkit-transition: color .2s ease-in-out; + -moz-transition: color .2s ease-in-out; + -o-transition: color .2s ease-in-out; + -ms-transition: color .2s ease-in-out; + transition: color .2s ease-in-out; +} + +#wrapper a:hover { + text-decoration: none; + color: #666; + border-bottom-color: #666; +} + +#wrapper a:active { + outline: 0; + position: relative; + top: 1px; +} + +#wrapper blockquote { + background: #e7e7e7; + border-top: 1px solid #ccc; + margin: 1.5em; + color: #444; + font-style: italic; +} + +#wrapper blockquote p { + padding: 1em; + border-top: 1px solid #fff; +} + +#wrapper strong { + font-weight: bold; +} + +#wrapper em { + font-style: italic; +} + +#wrapper dfn { + font-weight: bold; +} + +#wrapper pre, #wrapper code, #wrapper tt { + color: #8b8074; + font-family: 'LiberationMonoRegular', Menlo, Monaco, monospace; + font-size: 1em; + font-weight: bold; + text-align: left; + margin: 2em 0; +} + +#wrapper tt { + display: block; + margin: 1.5em 0; + line-height: 1.5; +} + +#wrapper span.amp { + font-family: Baskerville, Palatino, "Book Antiqua", serif; + font-style: italic; +} + +#wrapper img { + max-width: 100%; + height: auto; +} + +#wrapper .footnote { + font-size: .8em; + vertical-align: super; +} + +#wrapper caption, #wrapper col, #wrapper colgroup, #wrapper table, + #wrapper tbody, #wrapper td, #wrapper tfoot, #wrapper th, #wrapper thead, + #wrapper tr { + border-spacing: 0; +} + +#wrapper caption { + color: #666; +} + +#wrapper figure { + display: inline-block; + margin: .5em 0 1.8em; + position: relative; +} + +#wrapper .poetry pre { + display: block; + font-family: Georgia, Garamond, serif !important; + font-size: 110% !important; + font-style: italic; + line-height: 1.6em; + margin-left: 1em; +} + +#wrapper .poetry pre code { + font-family: Georgia, Garamond, serif !important; + word-break: break-all; + word-break: break-word; + /* Non standard for webkit */ + -webkit-hyphens: auto; + -moz-hyphens: auto; + hyphens: auto; + white-space: pre-wrap; +} + +#wrapper sup, #wrapper sub, #wrapper a.footnote { + font-size: 1.4ex; + height: 0; + line-height: 1; + position: relative; + vertical-align: super; +} + +#wrapper sub { + vertical-align: sub; + top: -1px; +} + +#wrapper table { + width: 100%; + margin-bottom: 2em; + padding: 0; + font-size: 13px; + border: 1px solid #ddd; + border-collapse: separate; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; +} + +#wrapper table th, #wrapper table td { + padding: 10px 10px 9px; + line-height: 18px; + text-align: left; +} + +#wrapper table th { + padding-top: 9px; + font-weight: bold; + vertical-align: middle; +} + +#wrapper table td { + vertical-align: top; + border-top: 1px solid #ddd; +} + +#wrapper table tbody th { + border-top: 1px solid #ddd; + vertical-align: top; +} + +#wrapper table th+th, #wrapper table td+td, #wrapper table th+td { + border-left: 1px solid #ddd; +} + +#wrapper table thead tr:first-child th:first-child, #wrapper table tbody tr:first-child td:first-child + { + border-radius: 4px 0 0 0; + -moz-border-radius: 4px 0 0 0; + -webkit-border-radius: 4px 0 0 0; +} + +#wrapper table thead tr:first-child th:last-child, #wrapper table tbody tr:first-child td:last-child + { + border-radius: 0 4px 0 0; + -moz-border-radius: 0 4px 0 0; + -webkit-border-radius: 0 4px 0 0; +} + +#wrapper table tbody tr:last-child td:first-child { + border-radius: 0 0 0 4px; + -moz-border-radius: 0 0 0 4px; + -webkit-border-radius: 0 0 0 4px; +} + +#wrapper table tbody tr:last-child td:last-child { + border-radius: 0 0 4px 0; + -moz-border-radius: 0 0 4px 0; + -webkit-border-radius: 0 0 4px 0; +} + +#wrapper table tbody tr:nth-child(odd) { + background-color: rgba(0, 0, 0, 0.03); +} + +@media print { + body { + overflow: auto; + } + img, pre, blockquote, table, figure { + page-break-inside: avoid; + } + #wrapper { + background: #fff; + color: #303030; + font-size: 85%; + padding: 10px; + position: relative; + text-indent: 0; + } +} + +@media screen { + #wrapper ::selection { + background: rgba(157, 193, 200, 0.5); + } + #wrapper h1::selection { + background-color: rgba(45, 156, 208, 0.3); + } + #wrapper h2::selection { + background-color: rgba(90, 182, 224, 0.3); + } + #wrapper h3::selection, #wrapper h4::selection, #wrapper h5::selection, + #wrapper h6::selection, #wrapper li::selection, #wrapper ol::selection + { + background-color: rgba(133, 201, 232, 0.3); + } + #wrapper code::selection { + background-color: rgba(0, 0, 0, 0.7); + color: #eee; + } + #wrapper code span::selection { + background-color: rgba(0, 0, 0, 0.7) !important; + color: #eee !important; + } + #wrapper a::selection { + background-color: rgba(255, 230, 102, 0.2); + } + #wrapper .inverted a::selection { + background-color: rgba(255, 230, 102, 0.6); + } + #wrapper td::selection, #wrapper th::selection, #wrapper caption::selection + { + background-color: rgba(180, 237, 95, 0.5); + } + .inverted { + background: #0b2531; + } + .inverted #wrapper { + padding: 20px; + background: #0b2531; + } + .inverted #wrapper p, .inverted #wrapper td, .inverted #wrapper li, + .inverted #wrapper h1, .inverted #wrapper h2, .inverted #wrapper h3, + .inverted #wrapper h4, .inverted #wrapper h5, .inverted #wrapper h6, + .inverted #wrapper pre, .inverted #wrapper code, .inverted #wrapper th, + .inverted #wrapper .math, .inverted #wrapper dd, .inverted #wrapper dt + { + color: #eee !important; + } + .inverted #wrapper a { + border-bottom: none; + color: #fff; + text-decoration: underline; + } + .inverted #wrapper blockquote { + background: rgba(255, 255, 255, 0.1); + border-top-color: rgba(255, 255, 255, 0.1); + } + .inverted #wrapper blockquote p { + border-top-color: rgba(255, 255, 255, 0.3); + } + .inverted #wrapper caption, .inverted #wrapper .footnote, .inverted #wrapper figcaption + { + color: rgba(255, 255, 255, 0.5); + } + .inverted #wrapper table { + border-color: rgba(255, 255, 255, 0.3); + } + .inverted #wrapper table td { + vertical-align: top; + border-top: none; + } + .inverted #wrapper table th+th, .inverted #wrapper table td+td, + .inverted #wrapper table th+td { + border-left: 1px solid rgba(255, 255, 255, 0.3); + } + .inverted #wrapper table tbody tr:nth-child(odd) { + background-color: rgba(255, 255, 255, 0.1); + } +} \ No newline at end of file diff --git a/plugin/resources/avenir-white.css b/plugin/resources/avenir-white.css new file mode 100644 index 0000000..baee59b --- /dev/null +++ b/plugin/resources/avenir-white.css @@ -0,0 +1,108 @@ +/* https://github.com/jasonm23/markdown-css-themes/blob/gh-pages/avenir-white.css */ + +body { + font-family: "Avenir Next", Helvetica, Arial, sans-serif; + padding:1em; + margin:auto; + max-width:42em; + background:#fefefe; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: bold; +} + +h1 { + color: #000000; + font-size: 28pt; +} + +h2 { + border-bottom: 1px solid #CCCCCC; + color: #000000; + font-size: 24px; +} + +h3 { + font-size: 18px; +} + +h4 { + font-size: 16px; +} + +h5 { + font-size: 14px; +} + +h6 { + color: #777777; + background-color: inherit; + font-size: 14px; +} + +hr { + height: 0.2em; + border: 0; + color: #CCCCCC; + background-color: #CCCCCC; +} + +p, blockquote, ul, ol, dl, li, table, pre { + margin: 15px 0; +} + +a, a:visited { + color: #4183C4; + background-color: inherit; + text-decoration: none; +} + +#message { + border-radius: 6px; + border: 1px solid #ccc; + display:block; + width:100%; + height:60px; + margin:6px 0px; +} + +button, #ws { + font-size: 10pt; + padding: 4px 6px; + border-radius: 5px; + border: 1px solid #bbb; + background-color: #eee; +} + +code, pre, #ws, #message { + font-family: Monaco; + font-size: 10pt; + border-radius: 3px; + background-color: #F8F8F8; + color: inherit; +} + +code { + border: 1px solid #EAEAEA; + margin: 0 2px; + padding: 0 5px; +} + +pre { + border: 1px solid #CCCCCC; + overflow: auto; + padding: 4px 8px; +} + +pre > code { + border: 0; + margin: 0; + padding: 0; +} + +#ws { background-color: #f8f8f8; } + +.send { color:#77bb77; } +.server { color:#7799bb; } +.error { color:#AA0000; } diff --git a/plugin/resources/github.css b/plugin/resources/github.css new file mode 100644 index 0000000..2e30dc6 --- /dev/null +++ b/plugin/resources/github.css @@ -0,0 +1,617 @@ +/* +This document has been created with Marked.app , Copyright 2013 Brett Terpstra +Content is property of the document author +Please leave this notice in place, along with any additional credits below. +--------------------------------------------------------------- +Title: GitHub +Author: Brett Terpstra +Description: Github README style. Includes theme for Pygmentized code blocks. +*/ +html, body { + color: black; +} + +/* +*:not('#mkdbuttons') { + margin: 0; + padding: 0; } +*/ +#wrapper { + font: 15px helvetica, arial, freesans, clean, sans-serif; + -webkit-font-smoothing: antialiased; + line-height: 1.7; + padding: 3px; + background: #fff; + border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; +} + +p { + margin: 1em 0; +} + +a { + color: #4183c4; + text-decoration: none; +} + +#wrapper { + background-color: #fff; + padding: 30px; + margin: 15px; + font-size: 15px; + line-height: 1.6; +} + +#wrapper>*:first-child { + margin-top: 0 !important; +} + +#wrapper>*:last-child { + margin-bottom: 0 !important; +} + +@media screen { + #wrapper { + box-shadow: 0 0 0 1px #cacaca, 0 0 0 4px #eee; + } +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.7; + cursor: text; + position: relative; + margin: 1em 0 15px; + padding: 0; +} + +h1 { + font-size: 2.5em; + border-bottom: 1px solid #ddd; +} + +h2 { + font-size: 2em; + border-bottom: 1px solid #eee; +} + +h3 { + font-size: 1.5em; +} + +h4 { + font-size: 1.2em; +} + +h5 { + font-size: 1em; +} + +h6 { + color: #777; + font-size: 1em; +} + +p, blockquote, table, pre { + margin: 15px 0; +} + +ul { + padding-left: 30px; +} + +ol { + padding-left: 30px; +} + +ol li ul:first-of-type { + margin-top: 0px; +} + +hr { + background: transparent + url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAYAAACtBE5DAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OENDRjNBN0E2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OENDRjNBN0I2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4Q0NGM0E3ODY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4Q0NGM0E3OTY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqqezsUAAAAfSURBVHjaYmRABcYwBiM2QSA4y4hNEKYDQxAEAAIMAHNGAzhkPOlYAAAAAElFTkSuQmCC) + repeat-x 0 0; + border: 0 none; + color: #ccc; + height: 4px; + margin: 15px 0; + padding: 0; +} + +#wrapper>h2:first-child { + margin-top: 0; + padding-top: 0; +} + +#wrapper>h1:first-child { + margin-top: 0; + padding-top: 0; +} + +#wrapper>h1:first-child+h2 { + margin-top: 0; + padding-top: 0; +} + +#wrapper>h3:first-child, #wrapper>h4:first-child, #wrapper>h5:first-child, + #wrapper>h6:first-child { + margin-top: 0; + padding-top: 0; +} + +a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, + a:first-child h5, a:first-child h6 { + margin-top: 0; + padding-top: 0; +} + +h1+p, h2+p, h3+p, h4+p, h5+p, h6+p, ul li>:first-child, ol li>:first-child + { + margin-top: 0; +} + +dl { + padding: 0; +} + +dl dt { + font-size: 14px; + font-weight: bold; + font-style: italic; + padding: 0; + margin: 15px 0 5px; +} + +dl dt:first-child { + padding: 0; +} + +dl dt>:first-child { + margin-top: 0; +} + +dl dt>:last-child { + margin-bottom: 0; +} + +dl dd { + margin: 0 0 15px; + padding: 0 15px; +} + +dl dd>:first-child { + margin-top: 0; +} + +dl dd>:last-child { + margin-bottom: 0; +} + +blockquote { + border-left: 4px solid #DDD; + padding: 0 15px; + color: #777; +} + +blockquote>:first-child { + margin-top: 0; +} + +blockquote>:last-child { + margin-bottom: 0; +} + +table { + border-collapse: collapse; + border-spacing: 0; + font-size: 100%; + font: inherit; +} + +table th { + font-weight: bold; + border: 1px solid #ccc; + padding: 6px 13px; +} + +table td { + border: 1px solid #ccc; + padding: 6px 13px; +} + +table tr { + border-top: 1px solid #ccc; + background-color: #fff; +} + +table tr:nth-child(2n) { + background-color: #f8f8f8; +} + +img { + max-width: 100%; +} + +code, tt { + margin: 0 2px; + padding: 0 5px; + white-space: nowrap; + border: 1px solid #eaeaea; + background-color: #f8f8f8; + border-radius: 3px; + font-family: Consolas, 'Liberation Mono', Courier, monospace; + font-size: 12px; + color: #333333; +} + +pre>code { + margin: 0; + padding: 0; + white-space: pre; + border: none; + background: transparent; +} + +.highlight pre { + background-color: #f8f8f8; + border: 1px solid #ccc; + font-size: 13px; + line-height: 19px; + overflow: auto; + padding: 6px 10px; + border-radius: 3px; +} + +pre { + background-color: #f8f8f8; + border: 1px solid #ccc; + font-size: 14px; + line-height: 19px; + overflow: auto; + padding: 6px 10px; + border-radius: 3px; + margin: 26px 0; +} + +pre code, pre tt { + background-color: transparent; + border: none; +} + +.poetry pre { + font-family: Georgia, Garamond, serif !important; + font-style: italic; + font-size: 110% !important; + line-height: 1.6em; + display: block; + margin-left: 1em; +} + +.poetry pre code { + font-family: Georgia, Garamond, serif !important; + word-break: break-all; + word-break: break-word; + /* Non standard for webkit */ + -webkit-hyphens: auto; + -moz-hyphens: auto; + hyphens: auto; + white-space: pre-wrap; +} + +sup, sub, a.footnote { + font-size: 1.4ex; + height: 0; + line-height: 1; + vertical-align: super; + position: relative; +} + +sub { + vertical-align: sub; + top: -1px; +} + +@media print { + body { + background: #fff; + } + img, pre, blockquote, table, figure { + page-break-inside: avoid; + } + #wrapper { + background: #fff; + border: none; + } + pre code { + overflow: visible; + } +} + +@media screen { + body.inverted { + color: #eee !important; + border-color: #555; + box-shadow: none; + } + .inverted #wrapper, .inverted hr, .inverted p, .inverted td, .inverted li, + .inverted h1, .inverted h2, .inverted h3, .inverted h4, .inverted h5, + .inverted h6, .inverted th, .inverted .math, .inverted caption, + .inverted dd, .inverted dt, .inverted blockquote { + color: #eee !important; + border-color: #555; + box-shadow: none; + } + .inverted td, .inverted th { + background: #333; + } + .inverted pre, .inverted code, .inverted tt { + background: #eeeeee !important; + color: #111; + } + .inverted h2 { + border-color: #555555; + } + .inverted hr { + border-color: #777; + border-width: 1px !important; + } + ::selection { + background: rgba(157, 193, 200, 0.5); + } + h1::selection { + background-color: rgba(45, 156, 208, 0.3); + } + h2::selection { + background-color: rgba(90, 182, 224, 0.3); + } + h3::selection, h4::selection, h5::selection, h6::selection, li::selection, + ol::selection { + background-color: rgba(133, 201, 232, 0.3); + } + code::selection { + background-color: rgba(0, 0, 0, 0.7); + color: #eeeeee; + } + code span::selection { + background-color: rgba(0, 0, 0, 0.7) !important; + color: #eeeeee !important; + } + a::selection { + background-color: rgba(255, 230, 102, 0.2); + } + .inverted a::selection { + background-color: rgba(255, 230, 102, 0.6); + } + td::selection, th::selection, caption::selection { + background-color: rgba(180, 237, 95, 0.5); + } + .inverted { + background: #0b2531; + background: #252a2a; + } + .inverted #wrapper { + background: #252a2a; + } + .inverted a { + color: #acd1d5; + } +} + +.highlight .c { + color: #998; + font-style: italic; +} + +.highlight .err { + color: #a61717; + background-color: #e3d2d2; +} + +.highlight .k, .highlight .o { + font-weight: bold; +} + +.highlight .cm { + color: #998; + font-style: italic; +} + +.highlight .cp { + color: #999; + font-weight: bold; +} + +.highlight .c1 { + color: #998; + font-style: italic; +} + +.highlight .cs { + color: #999; + font-weight: bold; + font-style: italic; +} + +.highlight .gd { + color: #000; + background-color: #fdd; +} + +.highlight .gd .x { + color: #000; + background-color: #faa; +} + +.highlight .ge { + font-style: italic; +} + +.highlight .gr { + color: #a00; +} + +.highlight .gh { + color: #999; +} + +.highlight .gi { + color: #000; + background-color: #dfd; +} + +.highlight .gi .x { + color: #000; + background-color: #afa; +} + +.highlight .go { + color: #888; +} + +.highlight .gp { + color: #555; +} + +.highlight .gs { + font-weight: bold; +} + +.highlight .gu { + color: #800080; + font-weight: bold; +} + +.highlight .gt { + color: #a00; +} + +.highlight .kc, .highlight .kd, .highlight .kn, .highlight .kp, + .highlight .kr { + font-weight: bold; +} + +.highlight .kt { + color: #458; + font-weight: bold; +} + +.highlight .m { + color: #099; +} + +.highlight .s { + color: #d14; +} + +.highlight .na { + color: #008080; +} + +.highlight .nb { + color: #0086B3; +} + +.highlight .nc { + color: #458; + font-weight: bold; +} + +.highlight .no { + color: #008080; +} + +.highlight .ni { + color: #800080; +} + +.highlight .ne, .highlight .nf { + color: #900; + font-weight: bold; +} + +.highlight .nn { + color: #555; +} + +.highlight .nt { + color: #000080; +} + +.highlight .nv { + color: #008080; +} + +.highlight .ow { + font-weight: bold; +} + +.highlight .w { + color: #bbb; +} + +.highlight .mf, .highlight .mh, .highlight .mi, .highlight .mo { + color: #099; +} + +.highlight .sb, .highlight .sc, .highlight .sd, .highlight .s2, + .highlight .se, .highlight .sh, .highlight .si, .highlight .sx { + color: #d14; +} + +.highlight .sr { + color: #009926; +} + +.highlight .s1 { + color: #d14; +} + +.highlight .ss { + color: #990073; +} + +.highlight .bp { + color: #999; +} + +.highlight .vc, .highlight .vg, .highlight .vi { + color: #008080; +} + +.highlight .il { + color: #099; +} + +.highlight .gc { + color: #999; + background-color: #EAF2F5; +} + +.type-csharp .highlight .k, .type-csharp .highlight .kt { + color: #00F; +} + +.type-csharp .highlight .nf { + color: #000; + font-weight: normal; +} + +.type-csharp .highlight .nc { + color: #2B91AF; +} + +.type-csharp .highlight .nn { + color: #000; +} + +.type-csharp .highlight .s, .type-csharp .highlight .sc { + color: #A31515; +} + +body.dark #wrapper { + background: transparent !important; + box-shadow: none !important; +} \ No newline at end of file diff --git a/plugin/resources/markdown.css b/plugin/resources/markdown.css new file mode 100644 index 0000000..9eab02e --- /dev/null +++ b/plugin/resources/markdown.css @@ -0,0 +1,266 @@ +/* https://github.com/simonlc/Markdown-CSS/blob/master/markdown.css */ +html { + font-size: 100%; + overflow-y: scroll; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + color: #444; + font-family: Georgia, Palatino, 'Palatino Linotype', Times, + 'Times New Roman', serif; + font-size: 12px; + line-height: 1.5em; + padding: 1em; + margin: auto; + max-width: 42em; + background: #fefefe; +} + +a { + color: #0645ad; + text-decoration: none; +} + +a:visited { + color: #0b0080; +} + +a:hover { + color: #06e; +} + +a:active { + color: #faa700; +} + +a:focus { + outline: thin dotted; +} + +a:hover, a:active { + outline: 0; +} + +::-moz-selection { + background: rgba(255, 255, 0, 0.3); + color: #000 +} + +::selection { + background: rgba(255, 255, 0, 0.3); + color: #000 +} + +a::-moz-selection { + background: rgba(255, 255, 0, 0.3); + color: #0645ad +} + +a::selection { + background: rgba(255, 255, 0, 0.3); + color: #0645ad +} + +p { + margin: 1em 0; +} + +img { + max-width: 100%; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: normal; + color: #111; + line-height: 1em; +} + +h4, h5, h6 { + font-weight: bold; +} + +h1 { + font-size: 2.5em; +} + +h2 { + font-size: 2em; +} + +h3 { + font-size: 1.5em; +} + +h4 { + font-size: 1.2em; +} + +h5 { + font-size: 1em; +} + +h6 { + font-size: 0.9em; +} + +blockquote { + color: #666666; + margin: 0; + padding-left: 3em; + border-left: 0.5em #EEE solid; +} + +hr { + display: block; + height: 2px; + border: 0; + border-top: 1px solid #aaa; + border-bottom: 1px solid #eee; + margin: 1em 0; + padding: 0; +} + +pre, code, kbd, samp { + color: #000; + font-family: monospace, monospace; + _font-family: 'courier new', monospace; + font-size: 0.98em; +} + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +b, strong { + font-weight: bold; +} + +dfn { + font-style: italic; +} + +ins { + background: #ff9; + color: #000; + text-decoration: none; +} + +mark { + background: #ff0; + color: #000; + font-style: italic; + font-weight: bold; +} + +sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +ul, ol { + margin: 1em 0; + padding: 0 0 0 2em; +} + +li p:last-child { + margin: 0 +} + +dd { + margin: 0 0 0 2em; +} + +img { + border: 0; + -ms-interpolation-mode: bicubic; + vertical-align: middle; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td { + vertical-align: top; +} + +@media only screen and (min-width: 480px) { + body { + font-size: 14px; + } +} + +@media only screen and (min-width: 768px) { + body { + font-size: 16px; + } +} + +@media print { + * { + background: transparent !important; + color: black !important; + filter: none !important; + -ms-filter: none !important; + } + body { + font-size: 12pt; + max-width: 100%; + } + a, a:visited { + text-decoration: underline; + } + hr { + height: 1px; + border: 0; + border-bottom: 1px solid black; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { + content: ""; + } + pre, blockquote { + border: 1px solid #999; + padding-right: 1em; + page-break-inside: avoid; + } + tr, img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + @page :left { + margin: 15mm 20mm 15mm 10mm; + } + @page :right { + margin: 15mm 10mm 15mm 20mm; + } + p, h2, h3 { + orphans: 3; + widows: 3; + } + h2, h3 { + page-break-after: avoid; + } +} \ No newline at end of file diff --git a/plugin/resources/modest.css b/plugin/resources/modest.css new file mode 100644 index 0000000..86e0448 --- /dev/null +++ b/plugin/resources/modest.css @@ -0,0 +1,169 @@ +/* https://github.com/markdowncss/modest */ +pre, code { + font-family: Menlo, Monaco, "Courier New", monospace; +} + +pre { + padding: .5rem; + line-height: 1.25; + overflow-x: scroll; +} + +a, a:visited { + color: #3498db; +} + +a:hover, a:focus, a:active { + color: #2980b9; +} + +.modest-no-decoration { + text-decoration: none; +} + +html { + font-size: 12px; +} + +@media screen and (min-width: 32rem) and (max-width: 48rem) { + html { + font-size: 15px; + } +} + +@media screen and (min-width: 48rem) { + html { + font-size: 16px; + } +} + +@media print { + *, *:before, *:after { + background: transparent !important; + color: #000 !important; + box-shadow: none !important; + text-shadow: none !important; + } + a, a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + a[href^="#"]:after, a[href^="javascript:"]:after { + content: ""; + } + pre, blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, h2, h3 { + orphans: 3; + widows: 3; + } + h2, h3 { + page-break-after: avoid; + } +} + +body { + line-height: 1.85; +} + +p, .modest-p { + font-size: 1rem; + margin-bottom: 1.3rem; +} + +h1, .modest-h1, h2, .modest-h2, h3, .modest-h3, h4, .modest-h4 { + margin: 1.414rem 0 .5rem; + font-weight: inherit; + line-height: 1.42; +} + +h1, .modest-h1 { + margin-top: 0; + font-size: 3.998rem; +} + +h2, .modest-h2 { + font-size: 2.827rem; +} + +h3, .modest-h3 { + font-size: 1.999rem; +} + +h4, .modest-h4 { + font-size: 1.414rem; +} + +h5, .modest-h5 { + font-size: 1.121rem; +} + +h6, .modest-h6 { + font-size: .88rem; +} + +small, .modest-small { + font-size: .707em; +} + +/* https://github.com/mrmrs/fluidity */ +img, canvas, iframe, video, svg, select, textarea { + max-width: 100%; +} + +@import + url(http://fonts.googleapis.com/css?family=Open+Sans+Condensed:300,300italic,700) + ; + +@import url(http://fonts.googleapis.com/css?family=Arimo:700,700italic); + +html { + font-size: 18px; + max-width: 100%; +} + +body { + color: #444; + font-family: 'Open Sans Condensed', sans-serif; + font-weight: 300; + margin: 0 auto; + max-width: 48rem; + line-height: 1.45; + padding: .25rem; +} + +h1, h2, h3, h4, h5, h6 { + font-family: Arimo, Helvetica, sans-serif; +} + +h1, h2, h3 { + border-bottom: 2px solid #fafafa; + margin-bottom: 1.15rem; + padding-bottom: .5rem; + text-align: center; +} + +blockquote { + border-left: 8px solid #fafafa; + padding: 1rem; +} + +pre, code { + background-color: #fafafa; +} \ No newline at end of file diff --git a/plugin/resources/screen.css b/plugin/resources/screen.css new file mode 100644 index 0000000..a98a70d --- /dev/null +++ b/plugin/resources/screen.css @@ -0,0 +1,175 @@ +/* https://github.com/jasonm23/markdown-css-themes/blob/gh-pages/screen.css */ +html { + font-size: 62.5%; +} + +html, body { + height: 100%; +} + +body { + font-family: Helvetica, Arial, sans-serif; + font-size: 150%; + line-height: 1.3; + color: #f6e6cc; + width: 700px; + margin: auto; + background: #27221a; + position: relative; + padding: 0 30px; +} + +p, ul, ol, dl, table, pre { + margin-bottom: 1em; +} + +ul { + margin-left: 20px; +} + +a { + text-decoration: none; + cursor: pointer; + color: #ba832c; + font-weight: bold; +} + +a:focus { + outline: 1px dotted; +} + +a:visited { + +} + +a:hover, a:focus { + color: #d3a459; + text-decoration: none; +} + +a *, button * { + cursor: pointer; +} + +hr { + display: none; +} + +small { + font-size: 90%; +} + +input, select, button, textarea, option { + font-family: Arial, "Lucida Grande", "Lucida Sans Unicode", Arial, + Verdana, sans-serif; + font-size: 100%; +} + +button, label, select, option, input[type=submit] { + cursor: pointer; +} + +.group:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + +.group { + display: inline-block; +} +/* Hides from IE-mac \*/ +* html .group { + height: 1%; +} + +.group { + display: block; +} /* End hide from IE-mac */ +sup { + font-size: 80%; + line-height: 1; + vertical-align: super; +} + +button::-moz-focus-inner { + border: 0; + padding: 1px; +} + +span.amp { + font-family: Baskerville, "Goudy Old Style", "Palatino", "Book Antiqua", + serif; + font-weight: normal; + font-style: italic; + font-size: 1.2em; + line-height: 0.8; +} + +h1, h2, h3, h4, h5, h6 { + line-height: 1.1; + font-family: Baskerville, "Goudy Old Style", "Palatino", "Book Antiqua", + serif; +} + +h2 { + font-size: 22pt; +} + +h3 { + font-size: 20pt; +} + +h4 { + font-size: 18pt; +} + +h5 { + font-size: 16pt; +} + +h6 { + font-size: 14pt; +} + +::selection { + background: #745626; +} + +::-moz-selection { + background: #745626; +} + +h1 { + font-size: 420%; + margin: 0 0 0.1em; + font-family: Baskerville, "Goudy Old Style", "Palatino", "Book Antiqua", + serif; +} + +h1 a, h1 a:hover { + color: #d7af72; + font-weight: normal; + text-decoration: none; +} + +pre { + background: rgba(0, 0, 0, 0.3); + color: #fff; + padding: 8px 10px; + border-radius: 0.4em; + -moz-border-radius: 0.4em; + -webkit-border-radius: 0.4em; + overflow-x: hidden; +} + +pre code { + font-size: 10pt; +} + +.thumb { + float: left; + margin: 10px; +} \ No newline at end of file diff --git a/plugin/resources/swiss.css b/plugin/resources/swiss.css new file mode 100644 index 0000000..123be1b --- /dev/null +++ b/plugin/resources/swiss.css @@ -0,0 +1,111 @@ +@charset "utf-8"; + +/** + * markdown.css + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see http://gnu.org/licenses/lgpl.txt. + * + * @project Weblog and Open Source Projects of Florian Wolters + * @version GIT: $Id$ + * @package xhtml-css + * @author Florian Wolters + * @copyright 2012 Florian Wolters + * @cssdoc version 1.0-pre + * @license http://gnu.org/licenses/lgpl.txt GNU Lesser General Public License + * @link http://github.com/FlorianWolters/jekyll-bootstrap-theme + * @media all + * @valid true + */ +body { + font-family: Helvetica, Arial, Freesans, clean, sans-serif; + padding: 1em; + margin: auto; + max-width: 42em; + background: #fefefe; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: bold; +} + +h1 { + color: #000000; + font-size: 28px; +} + +h2 { + border-bottom: 1px solid #CCCCCC; + color: #000000; + font-size: 24px; +} + +h3 { + font-size: 18px; +} + +h4 { + font-size: 16px; +} + +h5 { + font-size: 14px; +} + +h6 { + color: #777777; + background-color: inherit; + font-size: 14px; +} + +hr { + height: 0.2em; + border: 0; + color: #CCCCCC; + background-color: #CCCCCC; +} + +p, blockquote, ul, ol, dl, li, table, pre { + margin: 15px 0; +} + +code, pre { + border-radius: 3px; + background-color: #F8F8F8; + color: inherit; +} + +code { + border: 1px solid #EAEAEA; + margin: 0 2px; + padding: 0 5px; +} + +pre { + border: 1px solid #CCCCCC; + line-height: 1.25em; + overflow: auto; + padding: 6px 10px; +} + +pre>code { + border: 0; + margin: 0; + padding: 0; +} + +a, a:visited { + color: #4183C4; + background-color: inherit; + text-decoration: none; +} \ No newline at end of file diff --git a/plugin/src/winterwell/markdown/Activator.java b/plugin/src/winterwell/markdown/Activator.java deleted file mode 100755 index 3000144..0000000 --- a/plugin/src/winterwell/markdown/Activator.java +++ /dev/null @@ -1,60 +0,0 @@ -package winterwell.markdown; - -import org.eclipse.ui.plugin.AbstractUIPlugin; -import org.osgi.framework.BundleContext; - -import winterwell.markdown.preferences.MarkdownPreferencePage; - -/** - * The activator class controls the plug-in life cycle - */ -public class Activator extends AbstractUIPlugin { - - // The plug-in ID - public static final String PLUGIN_ID = "winterwell.markdown"; - - // The shared instance - private static Activator plugin; - - /** - * The constructor - */ - public Activator() { - } - - /* - * (non-Javadoc) - * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext) - */ - public void start(BundleContext context) throws Exception { - super.start(context); - plugin = this; - doInstall(); - MarkdownPreferencePage.setDefaultPreferences(getPreferenceStore()); - } - - // ?? Have this method called by start(), saving a reminder so it doesn't repeat itself?? - private void doInstall() { - // ??Try to make this the default for file types -- but is this possible?? - // c.f. http://stackoverflow.com/questions/15877123/eclipse-rcp-programmatically-associate-file-type-with-editor - } - - /* - * (non-Javadoc) - * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext) - */ - public void stop(BundleContext context) throws Exception { - plugin = null; - super.stop(context); - } - - /** - * Returns the shared instance - * - * @return the shared instance - */ - public static Activator getDefault() { - return plugin; - } - -} diff --git a/plugin/src/winterwell/markdown/Log.java b/plugin/src/winterwell/markdown/Log.java new file mode 100644 index 0000000..8d7848c --- /dev/null +++ b/plugin/src/winterwell/markdown/Log.java @@ -0,0 +1,46 @@ +package winterwell.markdown; + +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; + +/** + * Nodeclipse Log Util + * + * @author Lamb Gao, Paul Verest + */ +public class Log { + + public static IStatus createStatus(int severity, int code, String message, Throwable exception) { + return new Status(severity, MarkdownUI.PLUGIN_ID, code, message, exception); + } + + public static void info(String message) { + log(IStatus.INFO, IStatus.OK, message, null); + } + + public static void error(Throwable exception) { + error("Unexpected Exception", exception); + } + + public static void error(String message) { + error(message, null); + } + + public static void error(String message, Throwable exception) { + log(IStatus.ERROR, IStatus.ERROR, message, exception); + } + + public static void error(int code, String message, Throwable exception) { + log(createStatus(IStatus.ERROR, code, message, exception)); + } + + public static void log(int severity, int code, String message, Throwable exception) { + log(createStatus(severity, code, message, exception)); + } + + public static void log(IStatus status) { + ILog log = MarkdownUI.getDefault().getLog(); + log.log(status); + } +} diff --git a/plugin/src/winterwell/markdown/LogUtil.java b/plugin/src/winterwell/markdown/LogUtil.java deleted file mode 100644 index 384b951..0000000 --- a/plugin/src/winterwell/markdown/LogUtil.java +++ /dev/null @@ -1,41 +0,0 @@ -package winterwell.markdown; - -import org.eclipse.core.runtime.ILog; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; - -/** - * Nodeclipse Log Util - * @author Lamb Gao, Paul Verest - */ -public class LogUtil { - - public static void info(String message) { - log(IStatus.INFO, IStatus.OK, message, null); - } - - public static void error(Throwable exception) { - error("Unexpected Exception", exception); - } - - public static void error(String message) { - error(message, null); - } - - public static void error(String message, Throwable exception) { - log(IStatus.ERROR, IStatus.ERROR, message, exception); - } - - public static void log(int severity, int code, String message, Throwable exception) { - log(createStatus(severity, code, message, exception)); - } - - public static IStatus createStatus(int severity, int code, String message, Throwable exception) { - return new Status(severity, Activator.PLUGIN_ID, code, message, exception); - } - - public static void log(IStatus status) { - ILog log = Activator.getDefault().getLog(); - log.log(status); - } -} diff --git a/plugin/src/winterwell/markdown/MarkdownUI.java b/plugin/src/winterwell/markdown/MarkdownUI.java new file mode 100755 index 0000000..fff04cf --- /dev/null +++ b/plugin/src/winterwell/markdown/MarkdownUI.java @@ -0,0 +1,60 @@ +package winterwell.markdown; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.editors.text.EditorsUI; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +/** + * The activator class controls the plug-in life cycle + */ +public class MarkdownUI extends AbstractUIPlugin { + + // The plug-in ID + public static final String PLUGIN_ID = "winterwell.markdown"; + + // The shared instance + private static MarkdownUI plugin; + + private IPreferenceStore combinedStore; + + public MarkdownUI() { + super(); + } + + public void start(BundleContext context) throws Exception { + super.start(context); + plugin = this; + } + + public void stop(BundleContext context) throws Exception { + plugin = null; + super.stop(context); + } + + /** + * Returns the shared instance + */ + public static MarkdownUI getDefault() { + return plugin; + } + + /** + * Returns a chained preference store representing the combined values of the MarkdownUI, + * EditorsUI, and PlatformUI stores. + */ + public IPreferenceStore getCombinedPreferenceStore() { + if (combinedStore == null) { + List stores = new ArrayList<>(); + stores.add(getPreferenceStore()); // MarkdownUI store + stores.add(EditorsUI.getPreferenceStore()); + stores.add(PlatformUI.getPreferenceStore()); + combinedStore = new WritableChainedPreferenceStore(stores.toArray(new IPreferenceStore[stores.size()])); + } + return combinedStore; + } +} diff --git a/plugin/src/winterwell/markdown/StringMethods.java b/plugin/src/winterwell/markdown/StringMethods.java index 208e0ae..a56952d 100755 --- a/plugin/src/winterwell/markdown/StringMethods.java +++ b/plugin/src/winterwell/markdown/StringMethods.java @@ -1,557 +1,510 @@ -/** - * Basic String manipulation utilities. - * (c) Winterwell 2010 and ThinkTank Mathematics 2007 - */ -package winterwell.markdown; - -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -import winterwell.utils.Mutable; -import winterwell.utils.containers.Pair; - -/** - * A collection of general-purpose String handling methods. - * - * @author daniel.winterstein - */ -public final class StringMethods { - - /** - * Removes xml tags, comment blocks and script blocks. - * - * @param page - * @return the page with all xml tags removed. - */ - public static String stripTags(String page) { - // This code is rather ugly, but it does the job - StringBuilder stripped = new StringBuilder(page.length()); - boolean inTag = false; - // Comment blocks and script blocks are given special treatment - boolean inComment = false; - boolean inScript = false; - // Go through the text - for (int i = 0; i < page.length(); i++) { - char c = page.charAt(i); - // First check whether we are ignoring text - if (inTag) { - if (c == '>') - inTag = false; - } else if (inComment) { - if (c == '>' && page.charAt(i - 1) == '-' - && page.charAt(i - 1) == '-') { - inComment = false; - } - } else if (inScript) { - if (c == '>' && page.substring(i - 7, i).equals("/script")) { - inScript = false; - } - } else { - // Check for the start of a tag - looks for '<' followed by any - // non-whitespace character - if (c == '<' && !Character.isWhitespace(page.charAt(i + 1))) { - // Comment, script-block or tag? - if (page.charAt(i + 1) == '!' && page.charAt(i + 2) == '-' - && page.charAt(i + 3) == '-') { - inComment = true; - } else if (i + 8 < page.length() - && page.substring(i + 1, i + 7).equals("script")) { - inScript = true; - i += 7; - } else - inTag = true; // Normal tag by default - } else { - // Append all non-tag chars - stripped.append(c); - } - } // end if... - } - return stripped.toString(); - } - - /** - * The local line-end string. \n on unix, \r\n on windows, \r on mac. - */ - public static final String LINEEND = System.getProperty("line.separator"); - - /** - * @param s - * @return A version of s where the first letter is uppercase and all others - * are lowercase - */ - public static final String capitalise(final String s) { - return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); - } - - /** - * Convert all line breaks into the system line break. - */ - public static final String convertLineBreaks(String text) { - return convertLineBreaks(text, LINEEND); - } - - /** - * Convert all line breaks into the specified line break. - */ - public static final String convertLineBreaks(String text, String br) { - text = text.replaceAll("\r\n", br); - text = text.replaceAll("\r", br); - text = text.replaceAll("\n", br); - return text; - } - - /** - * @param string - * @param character - * @return the number of times character appears in the string - * @author Sam Halliday - */ - static public int countCharsInString(String string, char character) { - int count = 0; - for (char c : string.toCharArray()) { - if (c == character) { - count++; - } - } - return count; - } - - /** - * - * E.g. - * findEnclosingRegion("text with a [region] inside", 15, '[', ']') - * is (??,??) - * - * @param text - * @param offset - * @param start - * @param end - * @return the smallest enclosed region (including start and end chars, the - * 1st number is inclusive, the 2nd exclusive), or null if none. So - * text.subString(start,end) is the specified region - */ - public static Pair findEnclosingRegion(String text, int offset, - char startMarker, char endMarker) { - // Forward - int end = findEnclosingRegion2(text, offset, endMarker, 1); - if (end == -1) - return null; - end++; // end is exclusive - // Backward - int start = findEnclosingRegion2(text, offset, startMarker, -1); - if (start == -1) - return null; - // Sanity - assert text.substring(start, end).charAt(0) == startMarker; - assert text.substring(start, end).endsWith("" + endMarker); - // Done - return new Pair(start, end); - } - - private static int findEnclosingRegion2(String text, int offset, - char endMarker, int direction) { - while (offset > -1 && offset < text.length()) { - char c = text.charAt(offset); - if (c == endMarker) - return offset; - offset += direction; - } - return -1; - } - - /** - * A convenience wrapper for - * {@link #findEnclosingRegion(String, int, char, char)} E.g. - findEnclosingRegion("text with a [region] inside", 15, '[', ']') .equals("[region]"); - - * - * @param text - * @param offset - * @param start - * @param end - * @return the smallest enclosed region (including start and end chars), or - * null if none. - */ - public static String findEnclosingText(String text, int offset, - char startMarker, char endMarker) { - Pair region = findEnclosingRegion(text, offset, startMarker, - endMarker); - if (region == null) - return null; - String s = text.substring(region.first, region.second); - return s; - } - - /** - * Format a block of text to use the given line-width. I.e. adjust the line - * breaks. Also known as hard line-wrapping. Paragraphs are - * recognised by a line of blank space between them (e.g. two returns). - *

- * Note: a side-effect of this method is that it converts all line-breaks - * into the local system's line-breaks. E.g. on Windows, \n will become \r\n - * - * @param text - * The text to format - * @param lineWidth - * The number of columns in a line. Typically 78 or 80. - * @param respectLeadingCharacters - * Can be null. If set, the specified leading characters will be - * copied if the line is split. Use with " \t" to keep indented - * paragraphs properly indented. Use with "> \t" to also handle - * email-style quoting. Note that respected leading characters - * receive no special treatment when they are used inside a - * paragraph. - * @return A copy of text, formatted to the given line-width. - *

- * TODO: recognise paragraphs by changes in the respected leading - * characters - */ - public static String format(String text, int lineWidth, int tabWidth, - String respectLeadingCharacters) { - // Switch to Linux line breaks for easier internal workings - text = convertLineBreaks(text, "\n"); - // Find paragraphs - List paras = format2_splitParagraphs(text, - respectLeadingCharacters); - // Rebuild text - StringBuilder sb = new StringBuilder(text.length() + 10); - for (String p : paras) { - String fp = format3_oneParagraph(p, lineWidth, tabWidth, - respectLeadingCharacters); - sb.append(fp); - // Paragraphs end with a double line break - sb.append("\n\n"); - } - // Pop the last line breaks - sb.delete(sb.length() - 2, sb.length()); - // Convert line breaks to system ones - text = convertLineBreaks(sb.toString()); - // Done - return text; - } - - private static List format2_splitParagraphs(String text, - String respectLeadingCharacters) { - List paras = new ArrayList(); - Mutable.Int index = new Mutable.Int(0); - // TODO The characters prefacing this paragraph - String leadingChars = ""; - while (index.value < text.length()) { - // One paragraph - boolean inSpace = false; - int start = index.value; - while (index.value < text.length()) { - char c = text.charAt(index.value); - index.value++; - if (!Character.isWhitespace(c)) { - inSpace = false; - continue; - } - // Line end? - if (c == '\r' || c == '\n') { - // // Handle MS Windows 2 character \r\n line breaks - // if (index.value < text.length()) { - // char c2 = text.charAt(index.value); - // if (c=='\r' && c2=='\n') index.value++; // Push on past - // the 2nd line break char - // } - // Double line end - indicating a paragraph break - if (inSpace) - break; - inSpace = true; - } - // TODO Other paragraph markers, spotted by a change in - // leadingChars - } - String p = text.substring(start, index.value); - paras.add(p); - } - // Done - return paras; - } - - /** - * Format a block of text to fit the given line width - * - * @param p - * @param lineWidth - * @param tabWidth - * @param respectLeadingCharacters - * @return - */ - private static String format3_oneParagraph(String p, int lineWidth, - int tabWidth, String respectLeadingCharacters) { - // Collect the reformatted paragraph - StringBuilder sb = new StringBuilder(p.length() + 10); // Allow for - // some extra - // line-breaks - // Get respected leading chars - String leadingChars = format4_getLeadingChars(p, - respectLeadingCharacters); - // First Line - sb.append(leadingChars); - int lineLength = leadingChars.length(); - int index = leadingChars.length(); - // Loop - while (index < p.length()) { - // Get the next word - StringBuilder word = new StringBuilder(); - char c = p.charAt(index); - index++; - while (!Character.isWhitespace(c)) { - word.append(c); - if (index == p.length()) - break; - c = p.charAt(index); - index++; - } - // Break the line if the word will not fit - if (lineLength + word.length() > lineWidth && lineLength != 0) { - trimEnd(sb); - sb.append('\n'); // lineEnd(sb); - // New line - sb.append(leadingChars); - lineLength = leadingChars.length(); - } - // Add word - sb.append(word); - lineLength += word.length(); - // Add the whitespace - if (index != p.length() && lineLength < lineWidth) { - if (c == '\n') { - c = ' '; - } - sb.append(c); - lineLength += (c == '\t') ? tabWidth : 1; - } - } - // A final trim - trimEnd(sb); - // Done - return sb.toString(); - } - - /** - * - * @param text - * @param respectLeadingCharacters - * Can be null - * @return The characters at the beginning of text which are respected. E.g. - * ("> Hello", " \t>") --> "> " - */ - private static String format4_getLeadingChars(String text, - String respectLeadingCharacters) { - if (respectLeadingCharacters == null) - return ""; - // Line-breaks cannot be respected - assert respectLeadingCharacters.indexOf('\n') == -1; - // Look for the first non-respected char - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (respectLeadingCharacters.indexOf(c) == -1) { - // Return the previous chars - return text.substring(0, i); - } - } - // All chars are respected - return text; - } - - /** - * Ensure that line ends with the right line-end character(s) - */ - public static final String lineEnd(String line) { - // strip possibly inappropriate line-endings - if (line.endsWith("\n")) { - line = line.substring(0, line.length() - 1); - } - if (line.endsWith("\r\n")) { - line = line.substring(0, line.length() - 2); - } - if (line.endsWith("\r")) { - line = line.substring(0, line.length() - 1); - } - // add in proper line end - if (!line.endsWith(LINEEND)) { - line += LINEEND; - } - return line; - } - - /** - * Ensure that line ends with the right line-end character(s). This is more - * efficient than the version for Strings. - * - * @param line - */ - public static final void lineEnd(final StringBuilder line) { - if (line.length() == 0) { - line.append(LINEEND); - return; - } - // strip possibly inappropriate line-endings - final char last = line.charAt(line.length() - 1); - if (last == '\n') { - if ((line.length() > 1) && (line.charAt(line.length() - 2) == '\r')) { - // \r\n - line.replace(line.length() - 2, line.length(), LINEEND); - return; - } - line.replace(line.length() - 1, line.length(), LINEEND); - return; - } - if (last == '\r') { - line.replace(line.length() - 1, line.length(), LINEEND); - return; - } - line.append(LINEEND); - return; - } - - - - /** - * @param string - * @return the MD5 sum of the string using the default charset. Null if - * there was an error in calculating the hash. - * @author Sam Halliday - */ - public static String md5Hash(String string) { - MessageDigest md5 = null; - try { - md5 = MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException e) { - // ignore this exception, we know MD5 exists - } - md5.update(string.getBytes()); - BigInteger hash = new BigInteger(1, md5.digest()); - return hash.toString(16); - } - - /** - * Removes HTML-style tags from a string. - * - * @param s - * a String from which to remove tags - * @return a string with all instances of <.*> removed. - */ - public static String removeTags(String s) { - StringBuffer sb = new StringBuffer(); - boolean inTag = false; - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (c == '<') - inTag = true; - if (!inTag) - sb.append(c); - if (c == '>') - inTag = false; - } - return sb.toString(); - } - - /** - * Repeat a character. - * - * @param c - * @param i - * @return A String consisting of i x c. - * @example assert repeat('-', 5).equals("-----"); - */ - public static String repeat(Character c, int i) { - StringBuilder dashes = new StringBuilder(i); - for (int j = 0; j < i; j++) - dashes.append(c); - return dashes.toString(); - } - - /** - * Split a piece of text into separate lines. The line breaks are left at - * the end of each line. - * - * @param text - * @return The individual lines in the text. - */ - public static List splitLines(String text) { - List lines = new ArrayList(); - // Search for lines - int start = 0; - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (c == '\r' || c == '\n') { - // Handle MS Windows 2 character \r\n line breaks - if (i + 1 < text.length()) { - char c2 = text.charAt(i + 1); - if (c == '\r' && c2 == '\n') - i++; - } - // Get the line, with the line break - String line = text.substring(start, i + 1); - lines.add(line); - start = i + 1; - } - } - // Last one - if (start != text.length()) { - String line = text.substring(start); - lines.add(line); - } - return lines; - } - - /** - * Remove trailing whitespace. c.f. String#trim() which removes - * leading and trailing whitespace. - * - * @param sb - */ - private static void trimEnd(StringBuilder sb) { - while (true) { - // Get the last character - int i = sb.length() - 1; - if (i == -1) - return; // Quit if sb is empty - char c = sb.charAt(i); - if (!Character.isWhitespace(c)) - return; // Finish? - sb.deleteCharAt(i); // Remove and continue - } - } - - /** - * Returns true if the string is just whitespace, or empty, or null. - * - * @param s - */ - public static final boolean whitespace(final String s) { - if (s == null) { - return true; - } - for (int i = 0; i < s.length(); i++) { - final char c = s.charAt(i); - if (!Character.isWhitespace(c)) { - return false; - } - } - return true; - } - - /** - * @param text - * @return the number of words in text. Uses a crude whitespace - * measure. - */ - public static int wordCount(String text) { - String[] bits = text.split("\\W+"); - int wc = 0; - for (String string : bits) { - if (!whitespace(string)) wc++; - } - return wc; - } - -} +/** + * Basic String manipulation utilities. + * (c) Winterwell 2010 and ThinkTank Mathematics 2007 + */ +package winterwell.markdown; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; + +import winterwell.markdown.util.Mutable; +import winterwell.markdown.util.Pair; + +/** + * A collection of general-purpose String handling methods. + * + * @author daniel.winterstein + */ +public final class StringMethods { + + /** + * Removes xml tags, comment blocks and script blocks. + * + * @param page + * @return the page with all xml tags removed. + */ + public static String stripTags(String page) { + // This code is rather ugly, but it does the job + StringBuilder stripped = new StringBuilder(page.length()); + boolean inTag = false; + // Comment blocks and script blocks are given special treatment + boolean inComment = false; + boolean inScript = false; + // Go through the text + for (int i = 0; i < page.length(); i++) { + char c = page.charAt(i); + // First check whether we are ignoring text + if (inTag) { + if (c == '>') inTag = false; + } else if (inComment) { + if (c == '>' && page.charAt(i - 1) == '-' && page.charAt(i - 1) == '-') { + inComment = false; + } + } else if (inScript) { + if (c == '>' && page.substring(i - 7, i).equals("/script")) { + inScript = false; + } + } else { + // Check for the start of a tag - looks for '<' followed by any + // non-whitespace character + if (c == '<' && !Character.isWhitespace(page.charAt(i + 1))) { + // Comment, script-block or tag? + if (page.charAt(i + 1) == '!' && page.charAt(i + 2) == '-' && page.charAt(i + 3) == '-') { + inComment = true; + } else if (i + 8 < page.length() && page.substring(i + 1, i + 7).equals("script")) { + inScript = true; + i += 7; + } else + inTag = true; // Normal tag by default + } else { + // Append all non-tag chars + stripped.append(c); + } + } // end if... + } + return stripped.toString(); + } + + /** + * The local line-end string. \n on unix, \r\n on windows, \r on mac. + */ + public static final String LINEEND = System.getProperty("line.separator"); + + /** + * @param s + * @return A version of s where the first letter is uppercase and all others are lowercase + */ + public static final String capitalise(final String s) { + return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + } + + /** + * Convert all line breaks into the system line break. + */ + public static final String convertLineBreaks(String text) { + return convertLineBreaks(text, LINEEND); + } + + /** + * Convert all line breaks into the specified line break. + */ + public static final String convertLineBreaks(String text, String br) { + text = text.replaceAll("\r\n", br); + text = text.replaceAll("\r", br); + text = text.replaceAll("\n", br); + return text; + } + + /** + * @param string + * @param character + * @return the number of times character appears in the string + * @author Sam Halliday + */ + static public int countCharsInString(String string, char character) { + int count = 0; + for (char c : string.toCharArray()) { + if (c == character) { + count++; + } + } + return count; + } + + /** + * E.g. findEnclosingRegion("text with a [region] inside", 15, '[', ']') is (??,??) + * + * @param text + * @param offset + * @param start + * @param end + * @return the smallest enclosed region (including start and end chars, the 1st number is + * inclusive, the 2nd exclusive), or null if none. So text.subString(start,end) is the + * specified region + */ + public static Pair findEnclosingRegion(String text, int offset, char startMarker, char endMarker) { + // Forward + int end = findEnclosingRegion2(text, offset, endMarker, 1); + if (end == -1) return null; + end++; // end is exclusive + // Backward + int start = findEnclosingRegion2(text, offset, startMarker, -1); + if (start == -1) return null; + // Sanity + assert text.substring(start, end).charAt(0) == startMarker; + assert text.substring(start, end).endsWith("" + endMarker); + // Done + return new Pair(start, end); + } + + private static int findEnclosingRegion2(String text, int offset, char endMarker, int direction) { + while (offset > -1 && offset < text.length()) { + char c = text.charAt(offset); + if (c == endMarker) return offset; + offset += direction; + } + return -1; + } + + /** + * A convenience wrapper for {@link #findEnclosingRegion(String, int, char, char)} E.g. + findEnclosingRegion("text with a [region] inside", 15, '[', ']') .equals("[region]"); + + * + * @param text + * @param offset + * @param start + * @param end + * @return the smallest enclosed region (including start and end chars), or null if none. + */ + public static String findEnclosingText(String text, int offset, char startMarker, char endMarker) { + Pair region = findEnclosingRegion(text, offset, startMarker, endMarker); + if (region == null) return null; + String s = text.substring(region.first, region.second); + return s; + } + + /** + * Format a block of text to use the given line-width. I.e. adjust the line breaks. Also known + * as hard line-wrapping. Paragraphs are recognised by a line of blank space between them + * (e.g. two returns). + *

+ * Note: a side-effect of this method is that it converts all line-breaks into the local + * system's line-breaks. E.g. on Windows, \n will become \r\n + * + * @param text The text to format + * @param lineWidth The number of columns in a line. Typically 78 or 80. + * @param respectLeadingCharacters Can be null. If set, the specified leading characters will be + * copied if the line is split. Use with " \t" to keep indented paragraphs properly + * indented. Use with "> \t" to also handle email-style quoting. Note that respected + * leading characters receive no special treatment when they are used inside a + * paragraph. + * @return A copy of text, formatted to the given line-width. + *

+ * TODO: recognise paragraphs by changes in the respected leading characters + */ + public static String format(String text, int lineWidth, int tabWidth, String respectLeadingCharacters) { + // Switch to Linux line breaks for easier internal workings + text = convertLineBreaks(text, "\n"); + // Find paragraphs + List paras = format2_splitParagraphs(text, respectLeadingCharacters); + // Rebuild text + StringBuilder sb = new StringBuilder(text.length() + 10); + for (String p : paras) { + String fp = format3_oneParagraph(p, lineWidth, tabWidth, respectLeadingCharacters); + sb.append(fp); + // Paragraphs end with a double line break + sb.append("\n\n"); + } + // Pop the last line breaks + sb.delete(sb.length() - 2, sb.length()); + // Convert line breaks to system ones + text = convertLineBreaks(sb.toString()); + // Done + return text; + } + + private static List format2_splitParagraphs(String text, String respectLeadingCharacters) { + List paras = new ArrayList(); + Mutable.Int index = new Mutable.Int(0); + // TODO The characters prefacing this paragraph + // String leadingChars = ""; + while (index.value < text.length()) { + // One paragraph + boolean inSpace = false; + int start = index.value; + while (index.value < text.length()) { + char c = text.charAt(index.value); + index.value++; + if (!Character.isWhitespace(c)) { + inSpace = false; + continue; + } + // Line end? + if (c == '\r' || c == '\n') { + // // Handle MS Windows 2 character \r\n line breaks + // if (index.value < text.length()) { + // char c2 = text.charAt(index.value); + // if (c=='\r' && c2=='\n') index.value++; // Push on past + // the 2nd line break char + // } + // Double line end - indicating a paragraph break + if (inSpace) break; + inSpace = true; + } + // TODO Other paragraph markers, spotted by a change in + // leadingChars + } + String p = text.substring(start, index.value); + paras.add(p); + } + // Done + return paras; + } + + /** + * Format a block of text to fit the given line width + * + * @param p + * @param lineWidth + * @param tabWidth + * @param respectLeadingCharacters + * @return + */ + private static String format3_oneParagraph(String p, int lineWidth, int tabWidth, String respectLeadingCharacters) { + // Collect the reformatted paragraph + StringBuilder sb = new StringBuilder(p.length() + 10); // Allow for + // some extra + // line-breaks + // Get respected leading chars + String leadingChars = format4_getLeadingChars(p, respectLeadingCharacters); + // First Line + sb.append(leadingChars); + int lineLength = leadingChars.length(); + int index = leadingChars.length(); + // Loop + while (index < p.length()) { + // Get the next word + StringBuilder word = new StringBuilder(); + char c = p.charAt(index); + index++; + while (!Character.isWhitespace(c)) { + word.append(c); + if (index == p.length()) break; + c = p.charAt(index); + index++; + } + // Break the line if the word will not fit + if (lineLength + word.length() > lineWidth && lineLength != 0) { + trimEnd(sb); + sb.append('\n'); // lineEnd(sb); + // New line + sb.append(leadingChars); + lineLength = leadingChars.length(); + } + // Add word + sb.append(word); + lineLength += word.length(); + // Add the whitespace + if (index != p.length() && lineLength < lineWidth) { + if (c == '\n') { + c = ' '; + } + sb.append(c); + lineLength += (c == '\t') ? tabWidth : 1; + } + } + // A final trim + trimEnd(sb); + // Done + return sb.toString(); + } + + /** + * @param text + * @param respectLeadingCharacters Can be null + * @return The characters at the beginning of text which are respected. E.g. ("> Hello", " \t>") + * --> "> " + */ + private static String format4_getLeadingChars(String text, String respectLeadingCharacters) { + if (respectLeadingCharacters == null) return ""; + // Line-breaks cannot be respected + assert respectLeadingCharacters.indexOf('\n') == -1; + // Look for the first non-respected char + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (respectLeadingCharacters.indexOf(c) == -1) { + // Return the previous chars + return text.substring(0, i); + } + } + // All chars are respected + return text; + } + + /** + * Ensure that line ends with the right line-end character(s) + */ + public static final String lineEnd(String line) { + // strip possibly inappropriate line-endings + if (line.endsWith("\n")) { + line = line.substring(0, line.length() - 1); + } + if (line.endsWith("\r\n")) { + line = line.substring(0, line.length() - 2); + } + if (line.endsWith("\r")) { + line = line.substring(0, line.length() - 1); + } + // add in proper line end + if (!line.endsWith(LINEEND)) { + line += LINEEND; + } + return line; + } + + /** + * Ensure that line ends with the right line-end character(s). This is more efficient than the + * version for Strings. + * + * @param line + */ + public static final void lineEnd(final StringBuilder line) { + if (line.length() == 0) { + line.append(LINEEND); + return; + } + // strip possibly inappropriate line-endings + final char last = line.charAt(line.length() - 1); + if (last == '\n') { + if ((line.length() > 1) && (line.charAt(line.length() - 2) == '\r')) { + // \r\n + line.replace(line.length() - 2, line.length(), LINEEND); + return; + } + line.replace(line.length() - 1, line.length(), LINEEND); + return; + } + if (last == '\r') { + line.replace(line.length() - 1, line.length(), LINEEND); + return; + } + line.append(LINEEND); + return; + } + + /** + * @param string + * @return the MD5 sum of the string using the default charset. Null if there was an error in + * calculating the hash. + * @author Sam Halliday + */ + public static String md5Hash(String string) { + MessageDigest md5 = null; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + // ignore this exception, we know MD5 exists + } + md5.update(string.getBytes()); + BigInteger hash = new BigInteger(1, md5.digest()); + return hash.toString(16); + } + + /** + * Removes HTML-style tags from a string. + * + * @param s a String from which to remove tags + * @return a string with all instances of <.*> removed. + */ + public static String removeTags(String s) { + StringBuffer sb = new StringBuffer(); + boolean inTag = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '<') inTag = true; + if (!inTag) sb.append(c); + if (c == '>') inTag = false; + } + return sb.toString(); + } + + /** + * Repeat a character. + * + * @param c + * @param i + * @return A String consisting of i x c. + * @example assert repeat('-', 5).equals("-----"); + */ + public static String repeat(Character c, int i) { + StringBuilder dashes = new StringBuilder(i); + for (int j = 0; j < i; j++) + dashes.append(c); + return dashes.toString(); + } + + /** + * Split a piece of text into separate lines. The line breaks are left at the end of each line. + * + * @param text + * @return The individual lines in the text. + */ + public static List splitLines(String text) { + List lines = new ArrayList(); + // Search for lines + int start = 0; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '\r' || c == '\n') { + // Handle MS Windows 2 character \r\n line breaks + if (i + 1 < text.length()) { + char c2 = text.charAt(i + 1); + if (c == '\r' && c2 == '\n') i++; + } + // Get the line, with the line break + String line = text.substring(start, i + 1); + lines.add(line); + start = i + 1; + } + } + // Last one + if (start != text.length()) { + String line = text.substring(start); + lines.add(line); + } + return lines; + } + + /** + * Remove trailing whitespace. c.f. String#trim() which removes leading and trailing + * whitespace. + * + * @param sb + */ + private static void trimEnd(StringBuilder sb) { + while (true) { + // Get the last character + int i = sb.length() - 1; + if (i == -1) return; // Quit if sb is empty + char c = sb.charAt(i); + if (!Character.isWhitespace(c)) return; // Finish? + sb.deleteCharAt(i); // Remove and continue + } + } + + /** + * Returns true if the string is just whitespace, or empty, or null. + * + * @param s + */ + public static final boolean whitespace(final String s) { + if (s == null) { + return true; + } + for (int i = 0; i < s.length(); i++) { + final char c = s.charAt(i); + if (!Character.isWhitespace(c)) { + return false; + } + } + return true; + } + + /** + * @param text + * @return the number of words in text. Uses a crude whitespace measure. + */ + public static int wordCount(String text) { + String[] bits = text.split("\\W+"); + int wc = 0; + for (String string : bits) { + if (!whitespace(string)) wc++; + } + return wc; + } + +} diff --git a/plugin/src/winterwell/markdown/WritableChainedPreferenceStore.java b/plugin/src/winterwell/markdown/WritableChainedPreferenceStore.java new file mode 100644 index 0000000..328b455 --- /dev/null +++ b/plugin/src/winterwell/markdown/WritableChainedPreferenceStore.java @@ -0,0 +1,88 @@ +package winterwell.markdown; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.ui.texteditor.ChainedPreferenceStore; + +/** + * Provides a chained preference store where the first of the chained stores is available for writes. + */ +public class WritableChainedPreferenceStore extends ChainedPreferenceStore { + + private IPreferenceStore writeStore; + + public WritableChainedPreferenceStore(IPreferenceStore[] preferenceStores) { + super(preferenceStores); + this.writeStore = preferenceStores[0]; + } + + @Override + public boolean needsSaving() { + return writeStore.needsSaving(); + } + + @Override + public void setValue(String name, double value) { + writeStore.setValue(name, value); + } + + @Override + public void setValue(String name, float value) { + writeStore.setValue(name, value); + } + + @Override + public void setValue(String name, int value) { + writeStore.setValue(name, value); + } + + @Override + public void setValue(String name, long value) { + writeStore.setValue(name, value); + } + + @Override + public void setValue(String name, String value) { + writeStore.setValue(name, value); + } + + @Override + public void setValue(String name, boolean value) { + writeStore.setValue(name, value); + } + + @Override + public void setToDefault(String name) { + writeStore.setToDefault(name); + } + + @Override + public void setDefault(String name, boolean value) { + + writeStore.setDefault(name, value); + } + + @Override + public void setDefault(String name, double value) { + writeStore.setDefault(name, value); + } + + @Override + public void setDefault(String name, float value) { + writeStore.setDefault(name, value); + } + + @Override + public void setDefault(String name, int value) { + writeStore.setDefault(name, value); + } + + @Override + public void setDefault(String name, long value) { + writeStore.setDefault(name, value); + } + + @Override + public void setDefault(String name, String defaultObject) { + writeStore.setDefault(name, defaultObject); + } +} diff --git a/plugin/src/winterwell/markdown/editors/ExportHTMLAction.java b/plugin/src/winterwell/markdown/commands/ExportHTML.java similarity index 76% rename from plugin/src/winterwell/markdown/editors/ExportHTMLAction.java rename to plugin/src/winterwell/markdown/commands/ExportHTML.java index 3c9d26e..7d57df2 100755 --- a/plugin/src/winterwell/markdown/editors/ExportHTMLAction.java +++ b/plugin/src/winterwell/markdown/commands/ExportHTML.java @@ -1,4 +1,4 @@ -package winterwell.markdown.editors; +package winterwell.markdown.commands; import java.io.File; @@ -8,13 +8,16 @@ import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IPathEditorInput; -import winterwell.utils.io.FileUtils; +import winterwell.markdown.editors.ActionBarContributor; +import winterwell.markdown.editors.MarkdownEditor; +import winterwell.markdown.util.FileUtils; +public class ExportHTML extends Action { -public class ExportHTMLAction extends Action { - public ExportHTMLAction() { + public ExportHTML() { super("Export to HTML"); } + @Override public void run() { IEditorPart ed = ActionBarContributor.getActiveEditor(); @@ -33,5 +36,4 @@ public void run() { FileUtils.write(file, html); } } - } diff --git a/plugin/src/winterwell/markdown/editors/FormatAction.java b/plugin/src/winterwell/markdown/commands/FormatParagraph.java similarity index 74% rename from plugin/src/winterwell/markdown/editors/FormatAction.java rename to plugin/src/winterwell/markdown/commands/FormatParagraph.java index cd16589..ba2ffb8 100755 --- a/plugin/src/winterwell/markdown/editors/FormatAction.java +++ b/plugin/src/winterwell/markdown/commands/FormatParagraph.java @@ -1,4 +1,4 @@ -package winterwell.markdown.editors; +package winterwell.markdown.commands; import java.util.List; @@ -14,25 +14,27 @@ import org.eclipse.jface.text.Region; import org.eclipse.jface.text.source.ISourceViewer; +import winterwell.markdown.Log; +import winterwell.markdown.editors.ActionBarContributor; +import winterwell.markdown.editors.MarkdownEditor; import winterwell.markdown.pagemodel.MarkdownFormatter; import winterwell.markdown.pagemodel.MarkdownPage; import winterwell.markdown.pagemodel.MarkdownPage.KLineType; -import winterwell.utils.containers.IntRange; +import winterwell.markdown.util.IntRange; /** * TODO An action for formatting text (via hard wrapping, i.e. inserting returns). * - * * @author daniel */ -public class FormatAction extends Action implements IHandler { +public class FormatParagraph extends Action implements IHandler { - public FormatAction() { + public FormatParagraph() { super("&Format paragraph"); setActionDefinitionId("winterwell.markdown.formatParagraphCommand"); setToolTipText("Format the paragraph under the caret by inserting/removing line-breaks"); } - + @Override public void run() { try { @@ -52,9 +54,9 @@ public void run() { // Get a paragraph region MarkdownPage page = ed.getMarkdownPage(); IRegion pRegion = getParagraph(page, lineNum, ed.getDocument()); - if (pRegion==null) { + if (pRegion == null) { // Not in a paragraph - so give up - // TODO tell the user why we've given up + // TODO tell the user why we've given up return; } String paragraph = ed.getDocument().get(pRegion.getOffset(), pRegion.getLength()); @@ -65,91 +67,86 @@ public void run() { ed.getDocument().replace(pRegion.getOffset(), pRegion.getLength(), formatted); // Done } catch (Exception ex) { - System.out.println(ex); + Log.error(ex); } } - private void formatSelectedRegion(MarkdownEditor ed, ITextSelection s, int cols) - throws BadLocationException { + private void formatSelectedRegion(MarkdownEditor ed, ITextSelection s, int cols) throws BadLocationException { int start = s.getStartLine(); int end = s.getEndLine(); IDocument doc = ed.getDocument(); int soff = doc.getLineOffset(start); - int eoff = lineEndOffset(end, doc); - IntRange editedRegion = new IntRange(soff, eoff); + int eoff = lineEndOffset(end, doc); + IntRange editedRegion = new IntRange(soff, eoff); MarkdownPage page = ed.getMarkdownPage(); StringBuilder sb = new StringBuilder(s.getLength()); - for(int i=start; i<=end; i++) { + for (int i = start; i <= end; i++) { IRegion para = getParagraph(page, i, ed.getDocument()); - if (para==null) { + if (para == null) { sb.append(page.getText().get(i)); continue; } String paragraph = ed.getDocument().get(para.getOffset(), para.getLength()); -// int lines = StrUtils.splitLines(paragraph).length; + // int lines = Strings.splitLines(paragraph).length; String formatted = MarkdownFormatter.format(paragraph, cols); // append formatted and move forward sb.append(formatted); CharSequence le = lineEnd(i, doc); sb.append(le); - int pEnd = doc.getLineOfOffset(para.getOffset()+para.getLength()); + int pEnd = doc.getLineOfOffset(para.getOffset() + para.getLength()); i = pEnd; // Adjust edited region? - IntRange pr = new IntRange(para.getOffset(), - para.getOffset()+para.getLength()+le.length()); - editedRegion = new IntRange(Math.min(pr.low, editedRegion.low), - Math.max(pr.high, editedRegion.high)); - } + IntRange pr = new IntRange(para.getOffset(), para.getOffset() + para.getLength() + le.length()); + editedRegion = new IntRange(Math.min(pr.low, editedRegion.low), Math.max(pr.high, editedRegion.high)); + } // Replace the unformatted region with the new formatted one String old = doc.get(editedRegion.low, editedRegion.size()); String newText = sb.toString(); if (old.equals(newText)) return; - ed.getDocument().replace(editedRegion.low, editedRegion.size(), newText); + ed.getDocument().replace(editedRegion.low, editedRegion.size(), newText); } private CharSequence lineEnd(int line, IDocument doc) throws BadLocationException { - int eoff = doc.getLineOffset(line) + doc.getLineInformation(line).getLength(); + int eoff = doc.getLineOffset(line) + doc.getLineInformation(line).getLength(); char c = doc.getChar(eoff); - if (c=='\r' && doc.getLength() > eoff+1 - && doc.getChar(eoff+1) =='\n') return "\r\n"; - return ""+c; + if (c == '\r' && doc.getLength() > eoff + 1 && doc.getChar(eoff + 1) == '\n') return "\r\n"; + return "" + c; } - private int lineEndOffset(int end, IDocument doc) - throws BadLocationException { + private int lineEndOffset(int end, IDocument doc) throws BadLocationException { int eoff = doc.getLineOffset(end) + doc.getLineInformation(end).getLength(); // Include line end char c = doc.getChar(eoff); - if (c=='\r' && doc.getLength() > eoff+1 - && doc.getChar(eoff+1) =='\n') eoff += 2; - else eoff += 1; + if (c == '\r' && doc.getLength() > eoff + 1 && doc.getChar(eoff + 1) == '\n') + eoff += 2; + else + eoff += 1; return eoff; } /** - * * @param page * @param lineNum * @param doc * @return region of paragraph containing this line, or null * @throws BadLocationException */ - private IRegion getParagraph(MarkdownPage page, int lineNum, IDocument doc) - throws BadLocationException { + private IRegion getParagraph(MarkdownPage page, int lineNum, IDocument doc) throws BadLocationException { // Get doc info List lines = page.getText(); List lineInfo = page.getLineTypes(); // Check we are in a paragraph or list KLineType pType = lineInfo.get(lineNum); - switch(pType) { - case NORMAL: break; - default: // Not in a paragraph, so we cannot format. - return null; + switch (pType) { + case NORMAL: + break; + default: // Not in a paragraph, so we cannot format. + return null; } // Work out the paragraph // Beginning int start; - for(start=lineNum; start>-1; start--) { + for (start = lineNum; start > -1; start--) { if (lineInfo.get(start) != pType) { start++; break; @@ -157,7 +154,7 @@ private IRegion getParagraph(MarkdownPage page, int lineNum, IDocument doc) } // End int end; - for(end=lineNum; end fColorTable = new HashMap(10); public void dispose() { - Iterator e = fColorTable.values().iterator(); + Iterator e = fColorTable.values().iterator(); while (e.hasNext()) - ((Color) e.next()).dispose(); + e.next().dispose(); } + public Color getColor(RGB rgb) { - Color color = (Color) fColorTable.get(rgb); + Color color = fColorTable.get(rgb); if (color == null) { color = new Color(Display.getCurrent(), rgb); fColorTable.put(rgb, color); diff --git a/plugin/src/winterwell/markdown/editors/EmphasisRule.java b/plugin/src/winterwell/markdown/editors/EmphasisRule.java index 06f1420..7224acf 100755 --- a/plugin/src/winterwell/markdown/editors/EmphasisRule.java +++ b/plugin/src/winterwell/markdown/editors/EmphasisRule.java @@ -1,112 +1,111 @@ -/** - * Copyright winterwell Mathematics Ltd. - * @author Daniel Winterstein - * 11 Jan 2007 - */ -package winterwell.markdown.editors; - -import org.eclipse.core.runtime.Assert; -import org.eclipse.jface.text.rules.ICharacterScanner; -import org.eclipse.jface.text.rules.IRule; -import org.eclipse.jface.text.rules.IToken; -import org.eclipse.jface.text.rules.MultiLineRule; -import org.eclipse.jface.text.rules.Token; - -/** - * - * - * @author Daniel Winterstein - */ -public class EmphasisRule implements IRule { - private static char[][] fDelimiters = null; - private char[] fSequence; - protected IToken fToken; - - - public EmphasisRule(String marker, IToken token) { - assert marker.equals("*") || marker.equals("_") || marker.equals("**") - || marker.equals("***") || marker.equals("`") || marker.equals("``"); - Assert.isNotNull(token); - fSequence = marker.toCharArray(); - fToken = token; - } - - // Copied from org.eclipse.jface.text.rules.PatternRule - protected boolean sequenceDetected(ICharacterScanner scanner, char[] sequence, boolean eofAllowed) { - for (int i = 1; i < sequence.length; i++) { - int c = scanner.read(); - if (c == ICharacterScanner.EOF && eofAllowed) { - return true; - } else if (c != sequence[i]) { - // Non-matching character detected, rewind the scanner back to - // the start. - // Do not unread the first character. - for (int j = i; j > 0; j--) - scanner.unread(); - return false; - } - } - return true; - } - - /* - * @see IRule#evaluate(ICharacterScanner) - * - * @since 2.0 - */ - public IToken evaluate(ICharacterScanner scanner) { - // Should be connected only on the right side - scanner.unread(); - boolean sawSpaceBefore = Character.isWhitespace(scanner.read()); - if (!sawSpaceBefore && scanner.getColumn() != 0) { - return Token.UNDEFINED; - } - - int c = scanner.read(); - // Should be connected only on right side - if (c != fSequence[0] || !sequenceDetected(scanner, fSequence, false)) { - scanner.unread(); - return Token.UNDEFINED; - } - int readCount = fSequence.length; - if (fDelimiters == null) { - fDelimiters = scanner.getLegalLineDelimiters(); - } - // Start sequence detected - int delimiterFound = 0; - // Is it a list item marker, or just a floating *? - if (sawSpaceBefore) { - boolean after = Character.isWhitespace(scanner.read()); - scanner.unread(); - if (after) - delimiterFound = 2; - } - - while (delimiterFound < 2 - && (c = scanner.read()) != ICharacterScanner.EOF) { - readCount++; - - if (!sawSpaceBefore && c == fSequence[0] - && sequenceDetected(scanner, fSequence, false)) { - return fToken; - } - - int i; - for (i = 0; i < fDelimiters.length; i++) { - if (c == fDelimiters[i][0] - && sequenceDetected(scanner, fDelimiters[i], true)) { - delimiterFound++; - break; - } - } - if (i == fDelimiters.length) - delimiterFound = 0; - sawSpaceBefore = Character.isWhitespace(c); - } - // Reached ICharacterScanner.EOF - for (; readCount > 0; readCount--) - scanner.unread(); - return Token.UNDEFINED; - } - -} +/** + * Copyright winterwell Mathematics Ltd. + * @author Daniel Winterstein + * 11 Jan 2007 + */ +package winterwell.markdown.editors; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.jface.text.rules.ICharacterScanner; +import org.eclipse.jface.text.rules.IRule; +import org.eclipse.jface.text.rules.IToken; +import org.eclipse.jface.text.rules.Token; + +/** + * + * + * @author Daniel Winterstein + */ +public class EmphasisRule implements IRule { + private static char[][] fDelimiters = null; + private char[] fSequence; + protected IToken fToken; + + + public EmphasisRule(String marker, IToken token) { + assert marker.equals("*") || marker.equals("_") || marker.equals("**") + || marker.equals("***") || marker.equals("`") || marker.equals("``"); + Assert.isNotNull(token); + fSequence = marker.toCharArray(); + fToken = token; + } + + // Copied from org.eclipse.jface.text.rules.PatternRule + protected boolean sequenceDetected(ICharacterScanner scanner, char[] sequence, boolean eofAllowed) { + for (int i = 1; i < sequence.length; i++) { + int c = scanner.read(); + if (c == ICharacterScanner.EOF && eofAllowed) { + return true; + } else if (c != sequence[i]) { + // Non-matching character detected, rewind the scanner back to + // the start. + // Do not unread the first character. + for (int j = i; j > 0; j--) + scanner.unread(); + return false; + } + } + return true; + } + + /* + * @see IRule#evaluate(ICharacterScanner) + * + * @since 2.0 + */ + public IToken evaluate(ICharacterScanner scanner) { + // Should be connected only on the right side + scanner.unread(); + boolean sawSpaceBefore = Character.isWhitespace(scanner.read()); + if (!sawSpaceBefore && scanner.getColumn() != 0) { + return Token.UNDEFINED; + } + + int c = scanner.read(); + // Should be connected only on right side + if (c != fSequence[0] || !sequenceDetected(scanner, fSequence, false)) { + scanner.unread(); + return Token.UNDEFINED; + } + int readCount = fSequence.length; + if (fDelimiters == null) { + fDelimiters = scanner.getLegalLineDelimiters(); + } + // Start sequence detected + int delimiterFound = 0; + // Is it a list item marker, or just a floating *? + if (sawSpaceBefore) { + boolean after = Character.isWhitespace(scanner.read()); + scanner.unread(); + if (after) + delimiterFound = 2; + } + + while (delimiterFound < 2 + && (c = scanner.read()) != ICharacterScanner.EOF) { + readCount++; + + if (!sawSpaceBefore && c == fSequence[0] + && sequenceDetected(scanner, fSequence, false)) { + return fToken; + } + + int i; + for (i = 0; i < fDelimiters.length; i++) { + if (c == fDelimiters[i][0] + && sequenceDetected(scanner, fDelimiters[i], true)) { + delimiterFound++; + break; + } + } + if (i == fDelimiters.length) + delimiterFound = 0; + sawSpaceBefore = Character.isWhitespace(c); + } + // Reached ICharacterScanner.EOF + for (; readCount > 0; readCount--) + scanner.unread(); + return Token.UNDEFINED; + } + +} diff --git a/plugin/src/winterwell/markdown/editors/LinkRule.java b/plugin/src/winterwell/markdown/editors/LinkRule.java index 4beeb68..f91702c 100644 --- a/plugin/src/winterwell/markdown/editors/LinkRule.java +++ b/plugin/src/winterwell/markdown/editors/LinkRule.java @@ -5,11 +5,11 @@ */ package winterwell.markdown.editors; +import org.eclipse.core.runtime.Assert; import org.eclipse.jface.text.rules.ICharacterScanner; +import org.eclipse.jface.text.rules.IRule; import org.eclipse.jface.text.rules.IToken; import org.eclipse.jface.text.rules.Token; -import org.eclipse.jface.text.rules.IRule; -import org.eclipse.core.runtime.Assert; /** * diff --git a/plugin/src/winterwell/markdown/editors/ListRule.java b/plugin/src/winterwell/markdown/editors/ListRule.java index 11ff3f0..78c4f07 100644 --- a/plugin/src/winterwell/markdown/editors/ListRule.java +++ b/plugin/src/winterwell/markdown/editors/ListRule.java @@ -1,77 +1,70 @@ -/** - * Copyright winterwell Mathematics Ltd. - * @author Daniel Winterstein - * 11 Jan 2007 - */ -package winterwell.markdown.editors; - -import java.util.ArrayList; -import java.util.List; -import java.util.Arrays; - -import org.eclipse.core.runtime.Assert; -import org.eclipse.jface.text.rules.ICharacterScanner; -import org.eclipse.jface.text.rules.IRule; -import org.eclipse.jface.text.rules.IToken; -import org.eclipse.jface.text.rules.Token; - -/** - * - * - * @author Daniel Winterstein - */ -public class ListRule implements IRule { - private ArrayList markerList; - protected IToken fToken; - - public ListRule(IToken token) { - Assert.isNotNull(token); - fToken= token; - } - - - /* - * @see IRule#evaluate(ICharacterScanner) - * @since 2.0 - */ - public IToken evaluate(ICharacterScanner scanner) { - if (scanner.getColumn() != 0) { - return Token.UNDEFINED; - } -// // Fast mode -// if (scanner.read() != '-') { -// scanner.unread(); -// return Token.UNDEFINED; -// } -// if (Character.isWhitespace(scanner.read())) { -// return fToken; -// } -// scanner.unread(); -// scanner.unread(); -// return Token.UNDEFINED; -// // Fast mode - int readCount = 0; - int c; - while ((c = scanner.read()) != ICharacterScanner.EOF) { - readCount++; - if( !Character.isWhitespace( c ) ) { - int after = scanner.read(); -// readCount++; - scanner.unread(); -// if ( markerList.contains(c) && Character.isWhitespace( after ) ) { - if ( (c == '-' || c == '+' || c == '*') - && Character.isWhitespace( after ) ) { - return fToken; - } else { - for (; readCount > 0; readCount--) - scanner.unread(); - return Token.UNDEFINED; - } - } - } - // Reached ICharacterScanner.EOF - for (; readCount > 0; readCount--) - scanner.unread(); - return Token.UNDEFINED; - } -} +/** + * Copyright winterwell Mathematics Ltd. + * @author Daniel Winterstein + * 11 Jan 2007 + */ +package winterwell.markdown.editors; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.jface.text.rules.ICharacterScanner; +import org.eclipse.jface.text.rules.IRule; +import org.eclipse.jface.text.rules.IToken; +import org.eclipse.jface.text.rules.Token; + +/** + * @author Daniel Winterstein + */ +public class ListRule implements IRule { + + // private ArrayList markerList; + protected IToken fToken; + + public ListRule(IToken token) { + Assert.isNotNull(token); + fToken = token; + } + + /* + * @see IRule#evaluate(ICharacterScanner) + * @since 2.0 + */ + public IToken evaluate(ICharacterScanner scanner) { + if (scanner.getColumn() != 0) { + return Token.UNDEFINED; + } + // // Fast mode + // if (scanner.read() != '-') { + // scanner.unread(); + // return Token.UNDEFINED; + // } + // if (Character.isWhitespace(scanner.read())) { + // return fToken; + // } + // scanner.unread(); + // scanner.unread(); + // return Token.UNDEFINED; + // // Fast mode + int readCount = 0; + int c; + while ((c = scanner.read()) != ICharacterScanner.EOF) { + readCount++; + if (!Character.isWhitespace(c)) { + int after = scanner.read(); + // readCount++; + scanner.unread(); + // if ( markerList.contains(c) && Character.isWhitespace( after ) ) { + if ((c == '-' || c == '+' || c == '*') && Character.isWhitespace(after)) { + return fToken; + } else { + for (; readCount > 0; readCount--) + scanner.unread(); + return Token.UNDEFINED; + } + } + } + // Reached ICharacterScanner.EOF + for (; readCount > 0; readCount--) + scanner.unread(); + return Token.UNDEFINED; + } +} diff --git a/plugin/src/winterwell/markdown/editors/MDConfiguration.java b/plugin/src/winterwell/markdown/editors/MDConfiguration.java index f20a642..63fe0b7 100755 --- a/plugin/src/winterwell/markdown/editors/MDConfiguration.java +++ b/plugin/src/winterwell/markdown/editors/MDConfiguration.java @@ -1,81 +1,66 @@ package winterwell.markdown.editors; -import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.IDocument; -import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextHover; import org.eclipse.jface.text.presentation.IPresentationReconciler; import org.eclipse.jface.text.presentation.PresentationReconciler; -import org.eclipse.jface.text.reconciler.DirtyRegion; import org.eclipse.jface.text.reconciler.IReconciler; import org.eclipse.jface.text.reconciler.IReconcilingStrategy; import org.eclipse.jface.text.reconciler.MonoReconciler; import org.eclipse.jface.text.rules.DefaultDamagerRepairer; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.ui.editors.text.TextSourceViewerConfiguration; +import org.eclipse.ui.texteditor.spelling.SpellingReconcileStrategy; +import org.eclipse.ui.texteditor.spelling.SpellingService; + +import winterwell.markdown.preferences.Prefs; public class MDConfiguration extends TextSourceViewerConfiguration { + private ColorManager colorManager; - public MDConfiguration(ColorManager colorManager, IPreferenceStore prefStore) { - super(prefStore); + public MDConfiguration(ColorManager colorManager, IPreferenceStore store) { + super(store); this.colorManager = colorManager; } @Override public IPresentationReconciler getPresentationReconciler(ISourceViewer sourceViewer) { MDScanner scanner = new MDScanner(colorManager); - PresentationReconciler pr = (PresentationReconciler) super.getPresentationReconciler(sourceViewer); // FIXME + PresentationReconciler pr = (PresentationReconciler) super.getPresentationReconciler(sourceViewer); DefaultDamagerRepairer ddr = new DefaultDamagerRepairer(scanner); pr.setRepairer(ddr, IDocument.DEFAULT_CONTENT_TYPE); pr.setDamager(ddr, IDocument.DEFAULT_CONTENT_TYPE); return pr; } - @Override - public IReconciler getReconciler(ISourceViewer sourceViewer) { - // This awful mess adds in update support - // Get super strategy - IReconciler rs = super.getReconciler(sourceViewer); - if (true) return rs; // Seems to work fine?! - final IReconcilingStrategy fsuperStrategy = rs==null? null : rs.getReconcilingStrategy("text"); - // Add our own - IReconcilingStrategy strategy = new IReconcilingStrategy() { - private IDocument doc; - public void reconcile(IRegion partition) { - MarkdownEditor ed = MarkdownEditor.getEditor(doc); - if (ed != null) ed.updatePage(partition); - if (fsuperStrategy!=null) fsuperStrategy.reconcile(partition); - } - public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) { - MarkdownEditor ed = MarkdownEditor.getEditor(doc); - if (ed != null) ed.updatePage(subRegion); - if (fsuperStrategy!=null) fsuperStrategy.reconcile(dirtyRegion, subRegion); + public IReconciler getReconciler(ISourceViewer viewer) { + boolean local = fPreferenceStore.getBoolean(Prefs.PREF_SPELLING_ENABLED); + if (local) { + + // use the combined preference store + SpellingService service = new SpellingService(fPreferenceStore); + if (service.getActiveSpellingEngineDescriptor(fPreferenceStore) == null) { + return super.getReconciler(viewer); // bail } - public void setDocument(IDocument document) { - this.doc = document; - if (fsuperStrategy!=null) fsuperStrategy.setDocument(document); - } - }; - // Make a reconciler - MonoReconciler m2 = new MonoReconciler(strategy, true); - m2.setIsIncrementalReconciler(true); - m2.setProgressMonitor(new NullProgressMonitor()); - m2.setDelay(500); - // Done - return m2; + + IReconcilingStrategy strategy = new SpellingReconcileStrategy(viewer, service); + MonoReconciler reconciler = new MonoReconciler(strategy, false); + reconciler.setDelay(500); + return reconciler; + } + + // default; uses just the PlatformUI store + return super.getReconciler(viewer); } - + @SuppressWarnings("unused") @Override - public ITextHover getTextHover(ISourceViewer sourceViewer, - String contentType) { + public ITextHover getTextHover(ISourceViewer sourceViewer, String contentType) { if (true) return super.getTextHover(sourceViewer, contentType); // Add hover support for images return new MDTextHover(); } } - - diff --git a/plugin/src/winterwell/markdown/editors/MDScanner.java b/plugin/src/winterwell/markdown/editors/MDScanner.java index 60a307b..5cd27c1 100755 --- a/plugin/src/winterwell/markdown/editors/MDScanner.java +++ b/plugin/src/winterwell/markdown/editors/MDScanner.java @@ -1,61 +1,59 @@ -/** - * Copyright winterwell Mathematics Ltd. - * @author Daniel Winterstein - * 13 Jan 2007 - */ -package winterwell.markdown.editors; - -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.jface.preference.PreferenceConverter; -import org.eclipse.jface.text.TextAttribute; -import org.eclipse.jface.text.rules.IRule; -import org.eclipse.jface.text.rules.IWhitespaceDetector; -import org.eclipse.jface.text.rules.MultiLineRule; -import org.eclipse.jface.text.rules.RuleBasedScanner; -import org.eclipse.jface.text.rules.Token; -import org.eclipse.jface.text.rules.WhitespaceRule; -import org.eclipse.swt.SWT; - -import winterwell.markdown.Activator; -import winterwell.markdown.preferences.MarkdownPreferencePage; - -/** - * - * - * @author Daniel Winterstein - */ -public class MDScanner extends RuleBasedScanner { - ColorManager cm; - public MDScanner(ColorManager cm) { - this.cm = cm; - IPreferenceStore pStore = Activator.getDefault().getPreferenceStore(); - Token heading = new Token(new TextAttribute(cm.getColor(PreferenceConverter.getColor(pStore, MarkdownPreferencePage.PREF_HEADER)), null, SWT.BOLD)); - Token comment = new Token(new TextAttribute(cm.getColor(PreferenceConverter.getColor(pStore, MarkdownPreferencePage.PREF_COMMENT)))); - Token emphasis = new Token(new TextAttribute(cm.getColor(PreferenceConverter.getColor(pStore, MarkdownPreferencePage.PREF_DEFUALT)), null, SWT.ITALIC)); - Token list = new Token(new TextAttribute(cm.getColor(PreferenceConverter.getColor(pStore, MarkdownPreferencePage.PREF_HEADER)), null, SWT.BOLD)); - Token link = new Token(new TextAttribute(cm.getColor(PreferenceConverter.getColor(pStore, MarkdownPreferencePage.PREF_LINK)), null, TextAttribute.UNDERLINE)); - Token code = new Token(new TextAttribute( - cm.getColor(PreferenceConverter.getColor(pStore, MarkdownPreferencePage.PREF_CODE)), - cm.getColor(PreferenceConverter.getColor(pStore, MarkdownPreferencePage.PREF_CODE_BG)), - SWT.NORMAL)); - setRules(new IRule[] { - new LinkRule(link), - new HeaderRule(heading), - new HeaderWithUnderlineRule(heading), - new ListRule(list), - new EmphasisRule("_", emphasis), - new EmphasisRule("***", emphasis), - new EmphasisRule("**", emphasis), - new EmphasisRule("*", emphasis), - new EmphasisRule("``", code), - new EmphasisRule("`", code), - new MultiLineRule("", comment), - // WhitespaceRule messes up with the rest of rules -// new WhitespaceRule(new IWhitespaceDetector() { -// public boolean isWhitespace(char c) { -// return Character.isWhitespace(c); -// } -// }), - }); - } -} +/** + * Copyright winterwell Mathematics Ltd. + * @author Daniel Winterstein + * 13 Jan 2007 + */ +package winterwell.markdown.editors; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferenceConverter; +import org.eclipse.jface.text.TextAttribute; +import org.eclipse.jface.text.rules.IRule; +import org.eclipse.jface.text.rules.MultiLineRule; +import org.eclipse.jface.text.rules.RuleBasedScanner; +import org.eclipse.jface.text.rules.Token; +import org.eclipse.swt.SWT; + +import winterwell.markdown.MarkdownUI; +import winterwell.markdown.preferences.PrefPageGeneral; + +/** + * + * + * @author Daniel Winterstein + */ +public class MDScanner extends RuleBasedScanner { + ColorManager cm; + public MDScanner(ColorManager cm) { + this.cm = cm; + IPreferenceStore pStore = MarkdownUI.getDefault().getPreferenceStore(); + Token heading = new Token(new TextAttribute(cm.getColor(PreferenceConverter.getColor(pStore, PrefPageGeneral.PREF_HEADER)), null, SWT.BOLD)); + Token comment = new Token(new TextAttribute(cm.getColor(PreferenceConverter.getColor(pStore, PrefPageGeneral.PREF_COMMENT)))); + Token emphasis = new Token(new TextAttribute(cm.getColor(PreferenceConverter.getColor(pStore, PrefPageGeneral.PREF_DEFAULT)), null, SWT.ITALIC)); + Token list = new Token(new TextAttribute(cm.getColor(PreferenceConverter.getColor(pStore, PrefPageGeneral.PREF_HEADER)), null, SWT.BOLD)); + Token link = new Token(new TextAttribute(cm.getColor(PreferenceConverter.getColor(pStore, PrefPageGeneral.PREF_LINK)), null, TextAttribute.UNDERLINE)); + Token code = new Token(new TextAttribute( + cm.getColor(PreferenceConverter.getColor(pStore, PrefPageGeneral.PREF_CODE)), + cm.getColor(PreferenceConverter.getColor(pStore, PrefPageGeneral.PREF_CODE_BG)), + SWT.NORMAL)); + setRules(new IRule[] { + new LinkRule(link), + new HeaderRule(heading), + new HeaderWithUnderlineRule(heading), + new ListRule(list), + new EmphasisRule("_", emphasis), + new EmphasisRule("***", emphasis), + new EmphasisRule("**", emphasis), + new EmphasisRule("*", emphasis), + new EmphasisRule("``", code), + new EmphasisRule("`", code), + new MultiLineRule("", comment), + // WhitespaceRule messes up with the rest of rules +// new WhitespaceRule(new IWhitespaceDetector() { +// public boolean isWhitespace(char c) { +// return Character.isWhitespace(c); +// } +// }), + }); + } +} diff --git a/plugin/src/winterwell/markdown/editors/MDTextHover.java b/plugin/src/winterwell/markdown/editors/MDTextHover.java index 04377a6..92f9940 100755 --- a/plugin/src/winterwell/markdown/editors/MDTextHover.java +++ b/plugin/src/winterwell/markdown/editors/MDTextHover.java @@ -10,32 +10,23 @@ import org.eclipse.jface.text.Region; import winterwell.markdown.StringMethods; -import winterwell.utils.containers.Pair; +import winterwell.markdown.util.Pair; /** - * - * * @author daniel */ -public class MDTextHover implements ITextHover //, ITextHoverExtension -{ +public class MDTextHover implements ITextHover { - /* (non-Javadoc) - * @see org.eclipse.jface.text.ITextHover#getHoverInfo(org.eclipse.jface.text.ITextViewer, org.eclipse.jface.text.IRegion) - */ public String getHoverInfo(ITextViewer textViewer, IRegion region) { try { IDocument doc = textViewer.getDocument(); String text = doc.get(region.getOffset(), region.getLength()); - return ""+text+""; + return "" + text + ""; } catch (Exception e) { return null; - } + } } - /* (non-Javadoc) - * @see org.eclipse.jface.text.ITextHover#getHoverRegion(org.eclipse.jface.text.ITextViewer, int) - */ public IRegion getHoverRegion(ITextViewer textViewer, int offset) { try { IDocument doc = textViewer.getDocument(); @@ -45,37 +36,36 @@ public IRegion getHoverRegion(ITextViewer textViewer, int offset) { String text = doc.get(lineOffset, lineLength); // Look for image tags Pair altRegion; - Pair urlRegion = - StringMethods.findEnclosingRegion(text, offset-lineOffset, '(', ')'); - if (urlRegion==null) { - altRegion = StringMethods.findEnclosingRegion(text, offset-lineOffset, '[', ']'); + Pair urlRegion = StringMethods.findEnclosingRegion(text, offset - lineOffset, '(', ')'); + if (urlRegion == null) { + altRegion = StringMethods.findEnclosingRegion(text, offset - lineOffset, '[', ']'); if (altRegion == null) return null; urlRegion = StringMethods.findEnclosingRegion(text, altRegion.second, '(', ')'); } else { - altRegion = StringMethods.findEnclosingRegion(text, urlRegion.first-1, '[', ']'); + altRegion = StringMethods.findEnclosingRegion(text, urlRegion.first - 1, '[', ']'); } - if (urlRegion==null || altRegion==null) return null; + if (urlRegion == null || altRegion == null) return null; // Is it an image link? - if (text.charAt(altRegion.first-1) != '!') return null; - Region r = new Region(urlRegion.first+1+lineOffset, urlRegion.second-urlRegion.first-2); + if (text.charAt(altRegion.first - 1) != '!') return null; + Region r = new Region(urlRegion.first + 1 + lineOffset, urlRegion.second - urlRegion.first - 2); return r; } catch (Exception ex) { return null; } } -// public IInformationControlCreator getHoverControlCreator() { -// return new IInformationControlCreator() { -// public IInformationControl createInformationControl(Shell parent) { -// int style= fIsFocusable ? SWT.V_SCROLL | SWT.H_SCROLL : SWT.NONE; -// -// if (BrowserInformationControl.isAvailable(parent)) { -// final int shellStyle= SWT.TOOL | (fIsFocusable ? SWT.RESIZE : SWT.NO_TRIM); -// return new BrowserInformationControl(parent, shellStyle, style, null); -// } -// return new DefaultInformationControl(parent, style, new HTMLTextPresenter()); -// } -// }; -// } + // public IInformationControlCreator getHoverControlCreator() { + // return new IInformationControlCreator() { + // public IInformationControl createInformationControl(Shell parent) { + // int style= fIsFocusable ? SWT.V_SCROLL | SWT.H_SCROLL : SWT.NONE; + // + // if (BrowserInformationControl.isAvailable(parent)) { + // final int shellStyle= SWT.TOOL | (fIsFocusable ? SWT.RESIZE : SWT.NO_TRIM); + // return new BrowserInformationControl(parent, shellStyle, style, null); + // } + // return new DefaultInformationControl(parent, style, new HTMLTextPresenter()); + // } + // }; + // } } diff --git a/plugin/src/winterwell/markdown/editors/MarkdownEditor.java b/plugin/src/winterwell/markdown/editors/MarkdownEditor.java index 86699c4..05b96ec 100755 --- a/plugin/src/winterwell/markdown/editors/MarkdownEditor.java +++ b/plugin/src/winterwell/markdown/editors/MarkdownEditor.java @@ -30,167 +30,173 @@ import org.eclipse.jface.text.source.projection.ProjectionViewer; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; -import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.widgets.Composite; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IPathEditorInput; import org.eclipse.ui.editors.text.TextEditor; import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; import org.eclipse.ui.texteditor.IDocumentProvider; -import org.eclipse.ui.texteditor.SourceViewerDecorationSupport; import org.eclipse.ui.views.contentoutline.IContentOutlinePage; -import winterwell.markdown.Activator; +import winterwell.markdown.Log; +import winterwell.markdown.MarkdownUI; import winterwell.markdown.pagemodel.MarkdownPage; import winterwell.markdown.pagemodel.MarkdownPage.Header; -import winterwell.markdown.preferences.MarkdownPreferencePage; -import winterwell.markdown.views.MarkdownPreview; - +import winterwell.markdown.preferences.PrefPageGeneral; +import winterwell.markdown.preferences.Prefs; /** * Text editor with markdown support. + * * @author Daniel Winterstein */ -public class MarkdownEditor extends TextEditor implements IDocumentListener -{ +public class MarkdownEditor extends TextEditor { - /** - * Maximum length for a task tag message - */ + public static final String ID = "winterwell.markdown.editors.MarkdownEditor"; + public static final String ID2 = "org.nodeclipse.ui.editors.LitCoffeeEditor"; + + /** Maximum length for a task tag message */ private static final int MAX_TASK_MSG_LENGTH = 80; + private static final Annotation[] ANNOTATION_ARRAY = new Annotation[0]; + private static final Position[] POSITION_ARRAY = new Position[0]; + + private MarkdownOutlinePage fOutlinePage; private ColorManager colorManager; - private MarkdownContentOutlinePage fOutlinePage = null; - - IDocument oldDoc = null; - private MarkdownPage page; - - private boolean pageDirty = true; - + + private boolean haveRunFolding = false; private ProjectionSupport projectionSupport; - private final IPreferenceStore pStore; - private IPropertyChangeListener prefChangeListener; - + private Map oldAnnotations = new HashMap(0); + + private final IDocumentListener docListener = new IDocumentListener() { + + @Override + public void documentChanged(DocumentEvent event) { + pageDirty = true; + } + + @Override + public void documentAboutToBeChanged(DocumentEvent event) {} + }; + + private final IPropertyChangeListener prefChangeListener = new IPropertyChangeListener() { + + @Override + public void propertyChange(PropertyChangeEvent event) { + if (event.getProperty().equals(PrefPageGeneral.PREF_WORD_WRAP)) { + getViewer().getTextWidget().setWordWrap(isWordWrap()); + } + } + }; public MarkdownEditor() { super(); - pStore = Activator.getDefault().getPreferenceStore(); + initCombinedPreferenceStore(); colorManager = new ColorManager(); setSourceViewerConfiguration(new MDConfiguration(colorManager, getPreferenceStore())); } - @Override public void createPartControl(Composite parent) { - // Over-ride to add code-folding support + // add code-folding support super.createPartControl(parent); if (getSourceViewer() instanceof ProjectionViewer) { - ProjectionViewer viewer =(ProjectionViewer)getSourceViewer(); - projectionSupport = new ProjectionSupport(viewer,getAnnotationAccess(),getSharedColors()); - projectionSupport.install(); - //turn projection mode on - viewer.doOperation(ProjectionViewer.TOGGLE); + ProjectionViewer viewer = (ProjectionViewer) getSourceViewer(); + projectionSupport = new ProjectionSupport(viewer, getAnnotationAccess(), getSharedColors()); + projectionSupport.install(); + + // turn projection mode on + viewer.doOperation(ProjectionViewer.TOGGLE); } } - + /** - * Returns the editor's source viewer. May return null before the editor's part has been created and after disposal. + * Returns the editor's source viewer. May return null before the editor's part has been created + * and after disposal. */ public ISourceViewer getViewer() { return getSourceViewer(); } - + @Override - protected ISourceViewer createSourceViewer(Composite parent, - IVerticalRuler ruler, int styles) { -// if (true) return super.createSourceViewer(parent, ruler, styles); + protected ISourceViewer createSourceViewer(Composite parent, IVerticalRuler ruler, int styles) { // Create with code-folding - ISourceViewer viewer = new ProjectionViewer(parent, ruler, - getOverviewRuler(), isOverviewRulerVisible(), styles); + ISourceViewer viewer = new ProjectionViewer(parent, ruler, getOverviewRuler(), isOverviewRulerVisible(), + styles); + // ensure decoration support has been created and configured. - SourceViewerDecorationSupport decSupport = getSourceViewerDecorationSupport(viewer); -// SourceViewer viewer = (SourceViewer) super.createSourceViewer(parent, ruler, styles); - // Setup word-wrapping - final StyledText widget = viewer.getTextWidget(); - // Listen to pref changes - prefChangeListener = new IPropertyChangeListener() { - public void propertyChange(PropertyChangeEvent event) { - if (event.getProperty().equals(MarkdownPreferencePage.PREF_WORD_WRAP)) { - widget.setWordWrap(MarkdownPreferencePage.wordWrap()); - } - } - }; - pStore.addPropertyChangeListener(prefChangeListener); - // Switch on word-wrapping - if (MarkdownPreferencePage.wordWrap()) { - widget.setWordWrap(true); - } - return viewer; + getSourceViewerDecorationSupport(viewer); + + // initialize word-wrapping + viewer.getTextWidget().setWordWrap(isWordWrap()); + + return viewer; + } + + private boolean isWordWrap() { + return getPreferenceStore().getBoolean(Prefs.PREF_WORD_WRAP); } - + public void dispose() { - if (pStore != null) { - pStore.removePropertyChangeListener(prefChangeListener); - } + removePreferenceStoreListener(); colorManager.dispose(); - super.dispose(); - } - public void documentAboutToBeChanged(DocumentEvent event) { + colorManager = null; + super.dispose(); } - public void documentChanged(DocumentEvent event) { - pageDirty = true; - } - @Override protected void doSetInput(IEditorInput input) throws CoreException { - // Detach from old - if (oldDoc!= null) { - oldDoc.removeDocumentListener(this); - if (doc2editor.get(oldDoc) == this) doc2editor.remove(oldDoc); - } - // Set - super.doSetInput(input); - // Attach as a listener to new doc - IDocument doc = getDocument(); - oldDoc = doc; - if (doc==null) return; - doc.addDocumentListener(this); - doc2editor.put(doc, this); - // Initialise code folding + + // Remove old doc listener + if (getDocument() != null) getDocument().removeDocumentListener(docListener); + + super.doSetInput(input); + + // Attach listener to new doc + getDocument().addDocumentListener(docListener); + + // Initialize code folding haveRunFolding = false; updateSectionFoldingAnnotations(null); } - @Override - protected void editorSaved() { - if (MarkdownPreview.preview != null) { - // Update the preview when the file is saved - MarkdownPreview.preview.update(); + /** + * Initializes the preference store for this editor. The constucted store represents the + * combined values of the MarkdownUI, EditorsUI, and PlatformUI stores. + */ + private void initCombinedPreferenceStore() { + IPreferenceStore store = MarkdownUI.getDefault().getCombinedPreferenceStore(); + store.addPropertyChangeListener(prefChangeListener); + setPreferenceStore(store); + } + + private void removePreferenceStoreListener() { + if (getPreferenceStore() != null) { + getPreferenceStore().removePropertyChangeListener(prefChangeListener); } } + @SuppressWarnings({ "unchecked", "rawtypes" }) public Object getAdapter(Class required) { if (IContentOutlinePage.class.equals(required)) { if (fOutlinePage == null) { - fOutlinePage= new MarkdownContentOutlinePage(getDocumentProvider(), this); - if (getEditorInput() != null) - fOutlinePage.setInput(getEditorInput()); + fOutlinePage = new MarkdownOutlinePage(getDocumentProvider(), this); + if (getEditorInput() != null) fOutlinePage.setInput(getEditorInput()); } return fOutlinePage; } return super.getAdapter(required); } + public IDocument getDocument() { IEditorInput input = getEditorInput(); - IDocumentProvider docProvider = getDocumentProvider(); - return docProvider==null? null : docProvider.getDocument(input); + IDocumentProvider docProvider = getDocumentProvider(); + return docProvider == null ? null : docProvider.getDocument(input); } + /** - * - * @return The {@link MarkdownPage} for the document being edited, or null - * if unavailable. + * @return The {@link MarkdownPage} for the document being edited, or null if unavailable. */ public MarkdownPage getMarkdownPage() { if (pageDirty) updateMarkdownPage(); @@ -198,147 +204,109 @@ public MarkdownPage getMarkdownPage() { } public int getPrintColumns() { - return getPreferenceStore().getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLUMN); + return getPreferenceStore().getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLUMN); } /** - * @return The text of the editor's document, or null if unavailable. + * Gets the text of the editor's current document, or null if unavailable. */ public String getText() { IDocument doc = getDocument(); - return doc==null? null : doc.get(); + return doc == null ? null : doc.get(); } private void updateMarkdownPage() { String text = getText(); - if (text==null) text=""; + if (text == null) text = ""; page = new MarkdownPage(text); pageDirty = false; } void updateTaskTags(IRegion region) { - try { - boolean useTags = pStore.getBoolean(MarkdownPreferencePage.PREF_TASK_TAGS); - if (!useTags) return; - // Get task tags -// IPreferenceStore peuistore = EditorsUI.getPreferenceStore(); -//// IPreferenceStore pStore_jdt = org.eclipse.jdt.core.compiler.getDefault().getPreferenceStore(); -// String tagString = peuistore.getString("org.eclipse.jdt.core.compiler.taskTags"); - String tagString = pStore.getString(MarkdownPreferencePage.PREF_TASK_TAGS_DEFINED); - List tags = Arrays.asList(tagString.split(",")); - // Get resource for editor - IFile docFile = getResource(this); - // Get existing tasks - IMarker[] taskMarkers = docFile.findMarkers(IMarker.TASK, true, IResource.DEPTH_INFINITE); - List markers = new ArrayList(Arrays.asList(taskMarkers)); -// Collections.sort(markers, c) sort for efficiency - // Find tags in doc - List text = getMarkdownPage().getText(); - for(int i=1; i<=text.size(); i++) { - String line = text.get(i-1); // wierd off-by-one bug - for (String tag : tags) { - tag = tag.trim(); - int tagIndex = line.indexOf(tag); - if (tagIndex == -1) continue; - IMarker exists = updateTaskTags2_checkExisting(i, tagIndex, line, markers); - if (exists!=null) { - markers.remove(exists); - continue; + try { + boolean useTags = getPreferenceStore().getBoolean(PrefPageGeneral.PREF_TASK_TAGS); + if (!useTags) return; + // Get task tags + String tagString = getPreferenceStore().getString(PrefPageGeneral.PREF_TASK_TAGS_DEFINED); + List tags = Arrays.asList(tagString.split(",")); + // Get resource for editor + IFile docFile = getResource(this); + // Get existing tasks + IMarker[] taskMarkers = docFile.findMarkers(IMarker.TASK, true, IResource.DEPTH_INFINITE); + List markers = new ArrayList(Arrays.asList(taskMarkers)); + // Find tags in doc + List text = getMarkdownPage().getText(); + for (int i = 1; i <= text.size(); i++) { + String line = text.get(i - 1); // wierd off-by-one bug + for (String tag : tags) { + tag = tag.trim(); + int tagIndex = line.indexOf(tag); + if (tagIndex == -1) continue; + IMarker exists = updateTaskTags2_checkExisting(i, tagIndex, line, markers); + if (exists != null) { + markers.remove(exists); + continue; + } + IMarker marker = docFile.createMarker(IMarker.TASK); + // Once we have a marker object, we can set its attributes + marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_NORMAL); + String msg = line.substring(line.indexOf(tag), + Math.min(tagIndex + MAX_TASK_MSG_LENGTH, line.length() - 1)); + marker.setAttribute(IMarker.MESSAGE, msg); + marker.setAttribute(IMarker.LINE_NUMBER, i); } - IMarker marker = docFile.createMarker(IMarker.TASK); - //Once we have a marker object, we can set its attributes - marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_NORMAL); - String msg = line.substring(line.indexOf(tag), Math.min(tagIndex+MAX_TASK_MSG_LENGTH, line.length()-1)); - marker.setAttribute(IMarker.MESSAGE, msg); - marker.setAttribute(IMarker.LINE_NUMBER, i); } - } - // Remove old markers - for (IMarker m : markers) { - try { - m.delete(); - } catch (Exception ex) { - // - } - } - } catch (Exception ex) { - // - } + // Remove old markers + for (IMarker m : markers) { + try { + m.delete(); + } catch (Exception e) {} + } + } catch (Exception e) {} } /** * Find an existing marker, if there is one. - * @param i - * @param tagIndex - * @param line - * @param markers - * @return */ - private IMarker updateTaskTags2_checkExisting(int i, int tagIndex, - String line, List markers) { + private IMarker updateTaskTags2_checkExisting(int i, int tagIndex, String line, List markers) { String tagMessage = line.substring(tagIndex).trim(); for (IMarker marker : markers) { try { - Integer lineNum = (Integer) marker.getAttribute(IMarker.LINE_NUMBER); - if (i != lineNum) continue; - String txt = ((String) marker.getAttribute(IMarker.MESSAGE)).trim(); - if (tagMessage.equals(txt)) return marker; - } catch (Exception ex) { - // Ignore - } + Integer lineNum = (Integer) marker.getAttribute(IMarker.LINE_NUMBER); + if (i != lineNum) continue; + String txt = ((String) marker.getAttribute(IMarker.MESSAGE)).trim(); + if (tagMessage.equals(txt)) return marker; + } catch (Exception ex) {} } return null; } - private IFile getResource(MarkdownEditor markdownEditor) { IPathEditorInput input = (IPathEditorInput) getEditorInput(); - IPath path = input.getPath(); + IPath path = input.getPath(); IWorkspace workspace = ResourcesPlugin.getWorkspace(); IWorkspaceRoot root = workspace.getRoot(); - IFile[] files = root.findFilesForLocation(path); + IFile[] files = root.findFilesForLocationURI(path.toFile().toURI()); if (files.length != 1) return null; - IFile docFile = files[0]; + IFile docFile = files[0]; return docFile; } - - /** - * @param doc - * @return - */ - public static MarkdownEditor getEditor(IDocument doc) { - return doc2editor.get(doc); - } - - private static final Map doc2editor = new HashMap(); - - /** - * @param region - * + * @param region */ public void updatePage(IRegion region) { -// if (!pageDirty) return; updateTaskTags(region); updateSectionFoldingAnnotations(region); } - - - private static final Annotation[] ANNOTATION_ARRAY = new Annotation[0]; - - private static final Position[] POSITION_ARRAY = new Position[0]; - - private boolean haveRunFolding = false; - private Map oldAnnotations = new HashMap(0); /** * @param region can be null */ private void updateSectionFoldingAnnotations(IRegion region) { if (!haveRunFolding) region = null; // Do the whole doc - if ( ! (getSourceViewer() instanceof ProjectionViewer)) return; - ProjectionViewer viewer = ((ProjectionViewer)getSourceViewer()); + if (!(getSourceViewer() instanceof ProjectionViewer)) return; + ProjectionViewer viewer = ((ProjectionViewer) getSourceViewer()); MarkdownPage mPage = getMarkdownPage(); List

headers = mPage.getHeadings(null); // this will hold the new annotations along @@ -347,9 +315,9 @@ private void updateSectionFoldingAnnotations(IRegion region) { IDocument doc = getDocument(); updateSectionFoldingAnnotations2(doc, headers, annotations, doc.getLength()); // Filter existing ones - Position[] newValues = annotations.values().toArray(POSITION_ARRAY); + Position[] newValues = annotations.values().toArray(POSITION_ARRAY); List deletedAnnotations = new ArrayList(); - for(Entry ae : oldAnnotations.entrySet()) { + for (Entry ae : oldAnnotations.entrySet()) { Position oldp = ae.getValue(); boolean stillExists = false; for (Position newp : newValues) { @@ -364,54 +332,52 @@ private void updateSectionFoldingAnnotations(IRegion region) { } } // Filter out-of-region ones - for(Annotation a : annotations.keySet().toArray(ANNOTATION_ARRAY)) { + for (Annotation a : annotations.keySet().toArray(ANNOTATION_ARRAY)) { Position p = annotations.get(a); - if (!intersectsRegion(p , region)) annotations.remove(a); + if (!intersectsRegion(p, region)) annotations.remove(a); } // Adjust the page - ProjectionAnnotationModel annotationModel = viewer.getProjectionAnnotationModel(); - if (annotationModel==null) return; + ProjectionAnnotationModel annotationModel = viewer.getProjectionAnnotationModel(); + if (annotationModel == null) return; annotationModel.modifyAnnotations(deletedAnnotations.toArray(ANNOTATION_ARRAY), annotations, null); // Remember old values oldAnnotations.putAll(annotations); for (Annotation a : deletedAnnotations) { - oldAnnotations.remove(a); - } + oldAnnotations.remove(a); + } haveRunFolding = true; } - /** * @param p * @param region * @return true if p overlaps with region, or if region is null */ private boolean intersectsRegion(Position p, IRegion region) { - if (region==null) return true; - if (p.offset > region.getOffset()+region.getLength()) return false; - if (p.offset+p.length < region.getOffset()) return false; + if (region == null) return true; + if (p.offset > region.getOffset() + region.getLength()) return false; + if (p.offset + p.length < region.getOffset()) return false; return true; } - /** * Calculate where to fold, sticking the info into newAnnotations - * @param doc + * + * @param doc * @param headers * @param newAnnotations * @param endParent */ private void updateSectionFoldingAnnotations2(IDocument doc, List
headers, Map newAnnotations, int endParent) { - for (int i=0; i subHeaders = header.getSubHeaders(); @@ -419,37 +385,8 @@ private void updateSectionFoldingAnnotations2(IDocument doc, List
header updateSectionFoldingAnnotations2(doc, subHeaders, newAnnotations, end); } } catch (Exception ex) { - System.out.println(ex); - } - } + Log.error(ex); + } + } } - - } - - - -/* - - -- - - - - IEditorPart editor = null; - IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); - if (window != null) { - IWorkbenchPage activePage = window.getActivePage(); - if (activePage != null) editor = activePage.getActiveEditor(); - } - if (editor != null) { - // todo: Add operations for active editor - } - - - - - - - -*/ \ No newline at end of file diff --git a/plugin/src/winterwell/markdown/editors/MarkdownContentOutlinePage.java b/plugin/src/winterwell/markdown/editors/MarkdownOutlinePage.java similarity index 67% rename from plugin/src/winterwell/markdown/editors/MarkdownContentOutlinePage.java rename to plugin/src/winterwell/markdown/editors/MarkdownOutlinePage.java index 445a322..6716b74 100755 --- a/plugin/src/winterwell/markdown/editors/MarkdownContentOutlinePage.java +++ b/plugin/src/winterwell/markdown/editors/MarkdownOutlinePage.java @@ -1,538 +1,503 @@ -/** - * Copyright winterwell Mathematics Ltd. - * @author Daniel Winterstein - * 11 Jan 2007 - */ -package winterwell.markdown.editors; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; - -import org.eclipse.jface.action.Action; -import org.eclipse.jface.action.IAction; -import org.eclipse.jface.action.IToolBarManager; -import org.eclipse.jface.resource.ImageDescriptor; -import org.eclipse.jface.text.BadLocationException; -import org.eclipse.jface.text.DocumentEvent; -import org.eclipse.jface.text.IDocument; -import org.eclipse.jface.text.IDocumentListener; -import org.eclipse.jface.text.IRegion; -import org.eclipse.jface.text.Region; -import org.eclipse.jface.viewers.ISelection; -import org.eclipse.jface.viewers.IStructuredSelection; -import org.eclipse.jface.viewers.ITreeContentProvider; -import org.eclipse.jface.viewers.LabelProvider; -import org.eclipse.jface.viewers.SelectionChangedEvent; -import org.eclipse.jface.viewers.StructuredSelection; -import org.eclipse.jface.viewers.TreeViewer; -import org.eclipse.jface.viewers.Viewer; -import org.eclipse.swt.SWT; -import org.eclipse.swt.events.KeyEvent; -import org.eclipse.swt.events.KeyListener; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Control; -import org.eclipse.ui.IActionBars; -import org.eclipse.ui.part.IPageSite; -import org.eclipse.ui.texteditor.IDocumentProvider; -import org.eclipse.ui.views.contentoutline.ContentOutlinePage; - -import winterwell.markdown.pagemodel.MarkdownPage; -import winterwell.markdown.pagemodel.MarkdownPage.Header; -import winterwell.markdown.pagemodel.MarkdownPage.KLineType; -import winterwell.utils.StrUtils; -import winterwell.utils.Utils; -import winterwell.utils.web.WebUtils; - -/** - * - * - * @author Daniel Winterstein - */ -public final class MarkdownContentOutlinePage extends ContentOutlinePage { - - /** - * - * - * @author Daniel Winterstein - */ - public final class ContentProvider implements ITreeContentProvider, - IDocumentListener { - - // protected final static String SEGMENTS= "__md_segments"; - // //$NON-NLS-1$ - // protected IPositionUpdater fPositionUpdater= new - // DefaultPositionUpdater(SEGMENTS); - private MarkdownPage fContent; - // protected List fContent= new ArrayList(10); - private MarkdownEditor fTextEditor; - - private void parse() { - fContent = fTextEditor.getMarkdownPage(); - } - - /* - * @see IContentProvider#inputChanged(Viewer, Object, Object) - */ - public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { - // Detach from old - if (oldInput != null) { - IDocument document = fDocumentProvider.getDocument(oldInput); - if (document != null) { - document.removeDocumentListener(this); - } - } - fContent = null; - // Attach to new - if (newInput == null) - return; - IDocument document = fDocumentProvider.getDocument(newInput); - if (document == null) - return; - fTextEditor = MarkdownEditor.getEditor(document); - document.addDocumentListener(this); - parse(); - } - - /* - * @see IContentProvider#dispose - */ - public void dispose() { - fContent = null; - } - - /* - * @see IContentProvider#isDeleted(Object) - */ - public boolean isDeleted(Object element) { - return false; - } - - /* - * @see IStructuredContentProvider#getElements(Object) - */ - public Object[] getElements(Object element) { - return fContent.getHeadings(null).toArray(); - } - - /* - * @see ITreeContentProvider#hasChildren(Object) - */ - public boolean hasChildren(Object element) { - if (element == fInput) { - return true; - } - if (element instanceof MarkdownPage.Header) { - MarkdownPage.Header header = (MarkdownPage.Header) element; - return header.getSubHeaders().size() > 0; - } - ; - return false; - } - - /* - * @see ITreeContentProvider#getParent(Object) - */ - public Object getParent(Object element) { - if (!(element instanceof MarkdownPage.Header)) - return null; - return ((MarkdownPage.Header) element).getParent(); - } - - /* - * @see ITreeContentProvider#getChildren(Object) - */ - public Object[] getChildren(Object element) { - if (element == fInput) { - return fContent.getHeadings(null).toArray(); - } - if (!(element instanceof MarkdownPage.Header)) - return null; - return ((MarkdownPage.Header) element).getSubHeaders().toArray(); - } - - public void documentAboutToBeChanged(DocumentEvent event) { - // nothing - } - - public void documentChanged(DocumentEvent event) { - parse(); - update(); - } - } - - private Object fInput = null; - private final IDocumentProvider fDocumentProvider; - private final MarkdownEditor fTextEditor; - protected boolean showWordCounts; - private List
selectedHeaders; - - /** - * @param documentProvider - * @param mdEditor - */ - public MarkdownContentOutlinePage(IDocumentProvider documentProvider, - MarkdownEditor mdEditor) { - fDocumentProvider = documentProvider; - fTextEditor = mdEditor; - } - - /* - * (non-Javadoc) Method declared on ContentOutlinePage - */ - @Override - public void createControl(Composite parent) { - super.createControl(parent); - TreeViewer viewer = getTreeViewer(); - viewer.setContentProvider(new ContentProvider()); - // Add word count annotations - viewer.setLabelProvider(new LabelProvider() { - @Override - public String getText(Object element) { - if (!(element instanceof MarkdownPage.Header)) - return super.getText(element); - Header header = ((MarkdownPage.Header) element); - String hText = header.toString(); - if (!showWordCounts) - return hText; - IRegion region = getRegion(header); - String text; - try { - text = fTextEditor.getDocument().get(region.getOffset(), - region.getLength()); - text = WebUtils.stripTags(text); - text = text.replaceAll("#", "").trim(); - assert text.startsWith(hText); - text = text.substring(hText.length()); - int wc = StrUtils.wordCount(text); - return hText + " (" + wc + ":" + text.length() + ")"; - } catch (BadLocationException e) { - return hText; - } - } - }); - viewer.addSelectionChangedListener(this); - - if (fInput != null) - viewer.setInput(fInput); - - // Buttons - IPageSite site = getSite(); - IActionBars bars = site.getActionBars(); - IToolBarManager toolbar = bars.getToolBarManager(); - // Word count action - Action action = new Action("123", IAction.AS_CHECK_BOX) { - @Override - public void run() { - showWordCounts = isChecked(); - update(); - } - }; - action.setToolTipText("Show/hide section word:character counts"); - toolbar.add(action); - // +/- actions - action = new Action("<") { - @Override - public void run() { - doPromoteDemote(-1); - } - }; - action.setToolTipText("Promote the selected section\n -- move it up a level."); - toolbar.add(action); - // - action = new Action(">") { - @Override - public void run() { - doPromoteDemote(1); - } - }; - action.setToolTipText("Demote the selected section\n -- move it down a level."); - toolbar.add(action); - // up/down actions - action = new Action("/\\") { - @Override - public void run() { - try { - doMove(-1); - } catch (BadLocationException e) { - throw Utils.runtime(e); - } - } - }; - action.setToolTipText("Move the selected section earlier"); - toolbar.add(action); - // - action = new Action("\\/") { - @Override - public void run() { - try { - doMove(1); - } catch (BadLocationException e) { - throw Utils.runtime(e); - } - } - }; - action.setToolTipText("Move the selected section later"); - toolbar.add(action); - // Collapse - ImageDescriptor id = ImageDescriptor.createFromFile(getClass(), "collapseall.gif"); - action = new Action("collapse", id) { - @Override - public void run() { - doCollapseAll(); - } - }; - action.setImageDescriptor(id); - action.setToolTipText("Collapse outline tree"); - toolbar.add(action); - // Sync - id = ImageDescriptor.createFromFile(getClass(), "synced.gif"); - action = new Action("sync") { - @Override - public void run() { - try { - doSyncToEditor(); - } catch (BadLocationException e) { - throw Utils.runtime(e); - } - } - }; - action.setImageDescriptor(id); - action.setToolTipText("Link with editor"); - toolbar.add(action); - // Add edit ability - viewer.getControl().addKeyListener(new KeyListener() { - public void keyPressed(KeyEvent e) { - if (e.keyCode==SWT.F2) { - doEditHeader(); - } - } - public void keyReleased(KeyEvent e) { - // - } - }); - } - - /** - * @throws BadLocationException - * - */ - protected void doSyncToEditor() throws BadLocationException { - TreeViewer viewer = getTreeViewer(); - if (viewer == null) return; - // Get header - MarkdownPage page = fTextEditor.getMarkdownPage(); - int caretOffset = fTextEditor.getViewer().getTextWidget().getCaretOffset(); - IDocument doc = fTextEditor.getDocument(); - int line = doc.getLineOfOffset(caretOffset); - List lineTypes = page.getLineTypes(); - for(; line>-1; line--) { - KLineType lt = lineTypes.get(line); - if (lt.toString().startsWith("H")) break; - } - if (line<0) return; - Header header = (Header) page.getPageObject(line); - // Set - IStructuredSelection selection = new StructuredSelection(header); - viewer.setSelection(selection , true); - } - - void doEditHeader() { - TreeViewer viewer = getTreeViewer(); - viewer.editElement(selectedHeaders.get(0), 0); - } - - protected void doCollapseAll() { - TreeViewer viewer = getTreeViewer(); - if (viewer == null) return; -// Control control = viewer.getControl(); -// if (control != null && !control.isDisposed()) { -// control.setRedraw(false); - viewer.collapseAll(); -// control.setRedraw(true); -// } - } - - /** - * Move the selected sections up/down - * @param i 1 or -1. 1==move later, -1=earlier - * @throws BadLocationException - */ - protected void doMove(int i) throws BadLocationException { - assert i==1 || i==-1; - if (selectedHeaders == null || selectedHeaders.size() == 0) - return; - // Get text region to move - MarkdownPage.Header first = selectedHeaders.get(0); - MarkdownPage.Header last = selectedHeaders.get(selectedHeaders.size()-1); - int start = fTextEditor.getDocument().getLineOffset( - first.getLineNumber()); - IRegion r = getRegion(last); - int end = r.getOffset() + r.getLength(); - int length = end - start; - // Get new insertion point - int insert; - if (i==1) { - Header nextSection = last.getNext(); - if (nextSection==null) return; - IRegion nr = getRegion(nextSection); - insert = nr.getOffset()+nr.getLength(); - } else { - Header prevSection = first.getPrevious(); - if (prevSection==null) return; - IRegion nr = getRegion(prevSection); - insert = nr.getOffset(); - } - // Get text - String text = fTextEditor.getDocument().get(); - // Move text - String section = text.substring(start, end); - String pre, post; - if (i==1) { - pre = text.substring(0, start) + text.substring(end, insert); - post = text.substring(insert); - } else { - pre = text.substring(0, insert); - post = text.substring(insert,start)+text.substring(end); - } - text = pre + section + post; - assert text.length() == fTextEditor.getDocument().get().length() : - text.length()-fTextEditor.getDocument().get().length()+" chars gained/lost"; - // Update doc - fTextEditor.getDocument().set(text); - } - - /** - * Does not support -------- / ========= underlining, only # headers - * @param upDown 1 for demote (e.g. h2 -> h3), -1 for promote (e.g. h2 -> h1) - */ - protected void doPromoteDemote(int upDown) { - assert upDown==1 || upDown==-1; - if (selectedHeaders == null || selectedHeaders.size() == 0) - return; - HashSet
toAdjust = new HashSet
(selectedHeaders); - HashSet
adjusted = new HashSet
(); - // Adjust - MarkdownPage mdPage = fTextEditor.getMarkdownPage(); - List lines = new ArrayList(mdPage.getText()); - while(toAdjust.size() != 0) { - Header h = toAdjust.iterator().next(); - toAdjust.remove(h); - adjusted.add(h); - String line = lines.get(h.getLineNumber()); - if (upDown==-1) { - if (h.getLevel() == 1) return; // Level 1; can't promote - if (line.startsWith("##")) line = line.substring(1); - else { - return; // TODO support for ------ / ======== - } - } else line = "#" + line; - int ln = h.getLineNumber(); - lines.set(ln, line); - // kids - ArrayList
kids = new ArrayList
(h.getSubHeaders()); - for (Header header : kids) { - if ( ! adjusted.contains(header)) toAdjust.add(header); - } - } - // Set - StringBuilder sb = new StringBuilder(); - for (String line : lines) { - sb.append(line); - } - fTextEditor.getDocument().set(sb.toString()); - } - - /** - * The region of text for this header. This includes the header itself. - * @param header - * @return - * @throws BadLocationException - */ - protected IRegion getRegion(Header header) { - try { - IDocument doc = fTextEditor.getDocument(); - // Line numbers - int start = header.getLineNumber(); - Header next = header.getNext(); - int end; - if (next != null) { - end = next.getLineNumber() - 1; - } else { - end = doc.getNumberOfLines() - 1; - } - int offset = doc.getLineOffset(start); - IRegion ei = doc.getLineInformation(end); - int length = ei.getOffset() + ei.getLength() - offset; - return new Region(offset, length); - } catch (BadLocationException ex) { - throw Utils.runtime(ex); - } - } - - /* - * (non-Javadoc) Method declared on ContentOutlinePage - */ - @Override - public void selectionChanged(SelectionChangedEvent event) { - super.selectionChanged(event); - selectedHeaders = null; - ISelection selection = event.getSelection(); - if (selection.isEmpty()) - return; - if (!(selection instanceof IStructuredSelection)) - return; - try { - IStructuredSelection strucSel = (IStructuredSelection) selection; - Object[] sections = strucSel.toArray(); - selectedHeaders = (List) Arrays.asList(sections); - MarkdownPage.Header first = (Header) sections[0]; - MarkdownPage.Header last = (Header) sections[sections.length - 1]; - int start = fTextEditor.getDocument().getLineOffset( - first.getLineNumber()); - int length; - if (first == last) { - length = fTextEditor.getDocument().getLineLength( - first.getLineNumber()); - } else { - IRegion r = getRegion(last); - int end = r.getOffset() + r.getLength(); - length = end - start; - } - fTextEditor.setHighlightRange(start, length, true); - } catch (Exception x) { - System.out.println(x.getStackTrace()); - fTextEditor.resetHighlightRange(); - } - } - - /** - * Sets the input of the outline page - * - * @param input - * the input of this outline page - */ - public void setInput(Object input) { - fInput = input; - update(); - } - - /** - * Updates the outline page. - */ - public void update() { - TreeViewer viewer = getTreeViewer(); - - if (viewer != null) { - Control control = viewer.getControl(); - if (control != null && !control.isDisposed()) { - control.setRedraw(false); - viewer.setInput(fInput); - viewer.expandAll(); - control.setRedraw(true); - } - } - } - -} +/** + * Copyright winterwell Mathematics Ltd. + * @author Daniel Winterstein + * 11 Jan 2007 + */ +package winterwell.markdown.editors; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentListener; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITextListener; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.TextEvent; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.part.IPageSite; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.eclipse.ui.views.contentoutline.ContentOutlinePage; + +import winterwell.markdown.Log; +import winterwell.markdown.pagemodel.MarkdownPage; +import winterwell.markdown.pagemodel.MarkdownPage.Header; +import winterwell.markdown.pagemodel.MarkdownPage.KLineType; +import winterwell.markdown.util.Strings; + +/** + * @author Daniel Winterstein + */ +public final class MarkdownOutlinePage extends ContentOutlinePage { + + public final class ContentProvider implements ITreeContentProvider, IDocumentListener { + + private MarkdownPage page; + + private void parse() { + this.page = editor.getMarkdownPage(); + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + + if (oldInput != null) { + IDocument document = fDocumentProvider.getDocument(oldInput); + if (document != null) { + document.removeDocumentListener(this); + } + } + + page = null; + if (newInput != null) { + IDocument document = fDocumentProvider.getDocument(newInput); + if (document != null) { + document.addDocumentListener(this); + parse(); + } + } + } + + public void dispose() { + page = null; + } + + public boolean isDeleted(Object element) { + return false; + } + + public Object[] getElements(Object element) { + return page.getHeadings(null).toArray(); + } + + public boolean hasChildren(Object element) { + if (element == fInput) { + return true; + } + if (element instanceof MarkdownPage.Header) { + MarkdownPage.Header header = (MarkdownPage.Header) element; + return header.getSubHeaders().size() > 0; + } ; + return false; + } + + public Object getParent(Object element) { + if (!(element instanceof MarkdownPage.Header)) return null; + return ((MarkdownPage.Header) element).getParent(); + } + + public Object[] getChildren(Object element) { + if (element == fInput) { + return page.getHeadings(null).toArray(); + } + if (!(element instanceof MarkdownPage.Header)) return null; + return ((MarkdownPage.Header) element).getSubHeaders().toArray(); + } + + public void documentAboutToBeChanged(DocumentEvent event) {} + + public void documentChanged(DocumentEvent event) { + parse(); + update(); + } + } + + private Object fInput = null; + private final IDocumentProvider fDocumentProvider; + private final MarkdownEditor editor; + protected boolean showWordCounts; + private List
selectedHeaders; + + /** + * @param documentProvider + * @param editor + */ + public MarkdownOutlinePage(IDocumentProvider documentProvider, MarkdownEditor editor) { + this.fDocumentProvider = documentProvider; + this.editor = editor; + + editor.getViewer().addTextListener(new ITextListener() { + + @Override + public void textChanged(TextEvent event) {} + }); + } + + @Override + public void createControl(Composite parent) { + super.createControl(parent); + TreeViewer viewer = getTreeViewer(); + viewer.setContentProvider(new ContentProvider()); + // Add word count annotations + viewer.setLabelProvider(new LabelProvider() { + + @Override + public String getText(Object element) { + if (!(element instanceof MarkdownPage.Header)) return super.getText(element); + Header header = ((MarkdownPage.Header) element); + String hText = header.toString(); + if (!showWordCounts) return hText; + IRegion region = getRegion(header); + String text; + try { + text = editor.getDocument().get(region.getOffset(), region.getLength()); + text = Strings.stripTags(text); + text = text.replaceAll("#", "").trim(); + assert text.startsWith(hText); + text = text.substring(hText.length()); + int wc = Strings.wordCount(text); + return hText + " (" + wc + ":" + text.length() + ")"; + } catch (BadLocationException e) { + return hText; + } + } + }); + viewer.addSelectionChangedListener(this); + + if (fInput != null) viewer.setInput(fInput); + + // Buttons + IPageSite site = getSite(); + IActionBars bars = site.getActionBars(); + IToolBarManager toolbar = bars.getToolBarManager(); + // Word count action + Action action = new Action("123", IAction.AS_CHECK_BOX) { + + @Override + public void run() { + showWordCounts = isChecked(); + update(); + } + }; + action.setToolTipText("Show/hide section word:character counts"); + toolbar.add(action); + // +/- actions + action = new Action("<") { + + @Override + public void run() { + doPromoteDemote(-1); + } + }; + action.setToolTipText("Promote the selected section\n -- move it up a level."); + toolbar.add(action); + // + action = new Action(">") { + + @Override + public void run() { + doPromoteDemote(1); + } + }; + action.setToolTipText("Demote the selected section\n -- move it down a level."); + toolbar.add(action); + // up/down actions + action = new Action("/\\") { + + @Override + public void run() { + try { + doMove(-1); + } catch (BadLocationException e) { + throw new RuntimeException(e); + } + } + }; + action.setToolTipText("Move the selected section earlier"); + toolbar.add(action); + // + action = new Action("\\/") { + + @Override + public void run() { + try { + doMove(1); + } catch (BadLocationException e) { + throw new RuntimeException(e); + } + } + }; + action.setToolTipText("Move the selected section later"); + toolbar.add(action); + // Collapse + ImageDescriptor id = ImageDescriptor.createFromFile(getClass(), "collapseall.gif"); + action = new Action("collapse", id) { + + @Override + public void run() { + doCollapseAll(); + } + }; + action.setImageDescriptor(id); + action.setToolTipText("Collapse outline tree"); + toolbar.add(action); + // Sync + id = ImageDescriptor.createFromFile(getClass(), "synced.gif"); + action = new Action("sync") { + + @Override + public void run() { + try { + doSyncToEditor(); + } catch (BadLocationException e) { + throw new RuntimeException(e); + } + } + }; + action.setImageDescriptor(id); + action.setToolTipText("Link with editor"); + toolbar.add(action); + // Add edit ability + viewer.getControl().addKeyListener(new KeyListener() { + + public void keyPressed(KeyEvent e) { + if (e.keyCode == SWT.F2) { + doEditHeader(); + } + } + + public void keyReleased(KeyEvent e) { + // + } + }); + } + + /** + * @throws BadLocationException + */ + protected void doSyncToEditor() throws BadLocationException { + TreeViewer viewer = getTreeViewer(); + if (viewer == null) return; + // Get header + MarkdownPage page = editor.getMarkdownPage(); + int caretOffset = editor.getViewer().getTextWidget().getCaretOffset(); + IDocument doc = editor.getDocument(); + int line = doc.getLineOfOffset(caretOffset); + List lineTypes = page.getLineTypes(); + for (; line > -1; line--) { + KLineType lt = lineTypes.get(line); + if (lt.toString().startsWith("H")) break; + } + if (line < 0) return; + Header header = (Header) page.getPageObject(line); + // Set + IStructuredSelection selection = new StructuredSelection(header); + viewer.setSelection(selection, true); + } + + void doEditHeader() { + TreeViewer viewer = getTreeViewer(); + viewer.editElement(selectedHeaders.get(0), 0); + } + + protected void doCollapseAll() { + TreeViewer viewer = getTreeViewer(); + if (viewer == null) return; + // Control control = viewer.getControl(); + // if (control != null && !control.isDisposed()) { + // control.setRedraw(false); + viewer.collapseAll(); + // control.setRedraw(true); + // } + } + + /** + * Move the selected sections up/down + * + * @param i 1 or -1. 1==move later, -1=earlier + * @throws BadLocationException + */ + protected void doMove(int i) throws BadLocationException { + assert i == 1 || i == -1; + if (selectedHeaders == null || selectedHeaders.size() == 0) return; + // Get text region to move + MarkdownPage.Header first = selectedHeaders.get(0); + MarkdownPage.Header last = selectedHeaders.get(selectedHeaders.size() - 1); + int start = editor.getDocument().getLineOffset(first.getLineNumber()); + IRegion r = getRegion(last); + int end = r.getOffset() + r.getLength(); + // int length = end - start; + // Get new insertion point + int insert; + if (i == 1) { + Header nextSection = last.getNext(); + if (nextSection == null) return; + IRegion nr = getRegion(nextSection); + insert = nr.getOffset() + nr.getLength(); + } else { + Header prevSection = first.getPrevious(); + if (prevSection == null) return; + IRegion nr = getRegion(prevSection); + insert = nr.getOffset(); + } + // Get text + String text = editor.getDocument().get(); + // Move text + String section = text.substring(start, end); + String pre, post; + if (i == 1) { + pre = text.substring(0, start) + text.substring(end, insert); + post = text.substring(insert); + } else { + pre = text.substring(0, insert); + post = text.substring(insert, start) + text.substring(end); + } + text = pre + section + post; + assert text.length() == editor.getDocument().get().length() : text.length() + - editor.getDocument().get().length() + " chars gained/lost"; + // Update doc + editor.getDocument().set(text); + } + + /** + * Does not support -------- / ========= underlining, only # headers + * + * @param upDown 1 for demote (e.g. h2 -> h3), -1 for promote (e.g. h2 -> h1) + */ + protected void doPromoteDemote(int upDown) { + assert upDown == 1 || upDown == -1; + if (selectedHeaders == null || selectedHeaders.size() == 0) return; + HashSet
toAdjust = new HashSet
(selectedHeaders); + HashSet
adjusted = new HashSet
(); + // Adjust + MarkdownPage mdPage = editor.getMarkdownPage(); + List lines = new ArrayList(mdPage.getText()); + while (toAdjust.size() != 0) { + Header h = toAdjust.iterator().next(); + toAdjust.remove(h); + adjusted.add(h); + String line = lines.get(h.getLineNumber()); + if (upDown == -1) { + if (h.getLevel() == 1) return; // Level 1; can't promote + if (line.startsWith("##")) + line = line.substring(1); + else { + return; // TODO support for ------ / ======== + } + } else + line = "#" + line; + int ln = h.getLineNumber(); + lines.set(ln, line); + // kids + ArrayList
kids = new ArrayList
(h.getSubHeaders()); + for (Header header : kids) { + if (!adjusted.contains(header)) toAdjust.add(header); + } + } + // Set + StringBuilder sb = new StringBuilder(); + for (String line : lines) { + sb.append(line); + } + editor.getDocument().set(sb.toString()); + } + + /** + * The region of text for this header. This includes the header itself. + * + * @param header + * @return + * @throws BadLocationException + */ + protected IRegion getRegion(Header header) { + try { + IDocument doc = editor.getDocument(); + // Line numbers + int start = header.getLineNumber(); + Header next = header.getNext(); + int end; + if (next != null) { + end = next.getLineNumber() - 1; + } else { + end = doc.getNumberOfLines() - 1; + } + int offset = doc.getLineOffset(start); + IRegion ei = doc.getLineInformation(end); + int length = ei.getOffset() + ei.getLength() - offset; + return new Region(offset, length); + } catch (BadLocationException ex) { + throw new RuntimeException(ex); + } + } + + /* + * (non-Javadoc) Method declared on ContentOutlinePage + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public void selectionChanged(SelectionChangedEvent event) { + super.selectionChanged(event); + selectedHeaders = null; + ISelection selection = event.getSelection(); + if (selection.isEmpty()) return; + if (!(selection instanceof IStructuredSelection)) return; + try { + IStructuredSelection strucSel = (IStructuredSelection) selection; + Object[] sections = strucSel.toArray(); + selectedHeaders = (List) Arrays.asList(sections); + MarkdownPage.Header first = (Header) sections[0]; + MarkdownPage.Header last = (Header) sections[sections.length - 1]; + int start = editor.getDocument().getLineOffset(first.getLineNumber()); + int length; + if (first == last) { + length = editor.getDocument().getLineLength(first.getLineNumber()); + } else { + IRegion r = getRegion(last); + int end = r.getOffset() + r.getLength(); + length = end - start; + } + editor.setHighlightRange(start, length, true); + } catch (Exception e) { + Log.error(e); + editor.resetHighlightRange(); + } + } + + /** + * Sets the input of the outline page + * + * @param input the input of this outline page + */ + public void setInput(Object input) { + fInput = input; + update(); + } + + /** + * Updates the outline page. + */ + public void update() { + TreeViewer viewer = getTreeViewer(); + + if (viewer != null) { + Control control = viewer.getControl(); + if (control != null && !control.isDisposed()) { + control.setRedraw(false); + viewer.setInput(fInput); + viewer.expandAll(); + control.setRedraw(true); + } + } + } + +} diff --git a/plugin/src/winterwell/markdown/pagemodel/MarkdownFormatter.java b/plugin/src/winterwell/markdown/pagemodel/MarkdownFormatter.java index 4c38f19..4277a64 100755 --- a/plugin/src/winterwell/markdown/pagemodel/MarkdownFormatter.java +++ b/plugin/src/winterwell/markdown/pagemodel/MarkdownFormatter.java @@ -1,351 +1,316 @@ - package winterwell.markdown.pagemodel; import java.util.List; -import winterwell.utils.StrUtils; +import winterwell.markdown.util.Strings; /** - * Formats a string that is compatible with the Markdown syntax. - * Strings must not include headers. + * Formats a string that is compatible with the Markdown syntax. Strings must not include headers. * * @author Howard Abrams */ -public class MarkdownFormatter -{ - // Expect everyone to simply use the public static methods... - private MarkdownFormatter () - { - } - - /** - * Formats a collection of lines to a particular width and honors typical - * Markdown syntax and formatting. - * - * The method assumes that if the first line ends with a line - * termination character, all the other lines will as well. - * - * @param lines A list of strings that should be formatted and wrapped. - * @param lineWidth The width of the page - * @return A string containing each - */ - public static String format (List lines, int lineWidth) - { - if (lines == null) - return null; // Should we return an empty string? - - final String lineEndings; - if ( lines.get(0).endsWith ("\r\n") ) - lineEndings = "\r\n"; - else if ( lines.get(0).endsWith ("\r") ) - lineEndings = "\r"; - else - lineEndings = StrUtils.LINEEND; - - final StringBuilder buf = new StringBuilder(); - for (String line : lines) { - buf.append (line); - buf.append (' '); // We can add extra spaces with impunity, and this - // makes sure our lines don't run together. - } - return format ( buf.toString(), lineWidth, lineEndings ); - } - - - /** - * Formats a string of text. The formatting does line wrapping at the - * lineWidth boundary, but it also honors the formatting - * of initial paragraph lines, allowing indentation of the entire - * paragraph. - * - * @param text The line of text to format - * @param lineWidth The width of the lines - * @return A string containing the formatted text. - */ - public static String format ( final String text, final int lineWidth) - { - return format(text, lineWidth, StrUtils.LINEEND); - } - - /** - * Formats a string of text. The formatting does line wrapping at the - * lineWidth boundary, but it also honors the formatting - * of initial paragraph lines, allowing indentation of the entire - * paragraph. - * - * @param text The line of text to format - * @param lineWidth The width of the lines - * @param lineEnding The line ending that overrides the default System value - * @return A string containing the formatted text. - */ - public static String format (final String text, final int lineWidth, final String lineEnding) - { - return new String( format(text.toCharArray (), lineWidth, lineEnding)); - } - - /** - * The available cursor position states as it sits in the buffer. - */ - private enum StatePosition { - /** The beginning of a paragraph ... the start of the buffer */ - BEGIN_FIRST_LINE, - - /** The beginning of the next line, which may be completely ignored. */ - BEGIN_OTHER_LINE, - - /** The beginning of a new line that will not be ignored, but appended. */ - BEGIN_NEW_LINE, - - /** The middle of a line. */ - MIDDLE_OF_LINE - } - - /** - * The method that does the work of formatting a string of text. The text, - * however, is a character array, which is more efficient to work with. - * - * TODO: Should we make the format(char[]) method public? - * - * @param text The line of text to format - * @param lineWidth The width of the lines - * @param lineEnding The line ending that overrides the default System value - * @return A string containing the formatted text. - */ - static char[] format ( final char[] text, final int lineWidth, final String lineEnding ) - { - final StringBuilder word = new StringBuilder(); - final StringBuilder indent = new StringBuilder(); - final StringBuilder buffer = new StringBuilder(text.length + 10); - - StatePosition state = StatePosition.BEGIN_FIRST_LINE; - int lineLength = 0; - - // There are times when we will run across a character(s) that will - // cause us to stop doing word wrap until we get to the - // "end of non-wordwrap" character(s). - // - // If this string is set to null, it tells us to "do" word-wrapping. - char endWordwrap1 = 0; - char endWordwrap2 = 0; - - // We loop one character past the end of the loop, and when we get to - // this position, we assign 'c' to be 0 ... as a marker for the end of - // the string... - - for (int i = 0; i <= text.length; i++) - { - final char c; - if (i < text.length) - c = text[i]; - else - c = 0; - - final char nextChar; - if (i+1 < text.length) - nextChar = text[i+1]; - else - nextChar = 0; - - // Are we actually word-wrapping? - if (endWordwrap1 != 0) { - // Did we get the ending sequence of the non-word-wrap? - if ( ( endWordwrap2 == 0 && c == endWordwrap1 ) || - ( c == endWordwrap1 && nextChar == endWordwrap2 ) ) - endWordwrap1 = 0; - buffer.append (c); - lineLength++; - - if (endWordwrap1 == 0 && endWordwrap2 != 0) { - buffer.append (nextChar); - lineLength++; - i++; - } - continue; - } - - // Check to see if we got one of our special non-word-wrapping - // character sequences ... - - if ( c == '[' ) { // [Hyperlink] - endWordwrap1 = ']'; - } - else if ( c == '*' && nextChar == '*' ) { // **Bold** - endWordwrap1 = '*'; - endWordwrap2 = '*'; - } // *Italics* - else if ( c == '*' && state == StatePosition.MIDDLE_OF_LINE ) { - endWordwrap1 = '*'; - } - else if ( c == '`' ) { // `code` - endWordwrap1 = '`'; - } - else if ( c == '(' && nextChar == '(' ) { // ((Footnote)) - endWordwrap1 = ')'; - endWordwrap2 = ')'; - } - else if ( c == '!' && nextChar == '[' ) { // ![Image] - endWordwrap1 = ')'; - } - - // We are no longer doing word-wrapping, so tidy the situation up... - if (endWordwrap1 != 0) { - if (word.length() > 0) - lineLength = addWordToBuffer (lineWidth, lineEnding, word, indent, buffer, lineLength); - else if (buffer.length() > 0 && buffer.charAt (buffer.length()-1) != ']' ) - buffer.append(' '); - // We are adding an extra space for most situations, unless we get a - // [link][ref] where we want them to be together without a space. - - buffer.append (c); - lineLength++; - continue; - } - - // Normal word-wrapping processing continues ... - - if (state == StatePosition.BEGIN_FIRST_LINE) - { - if ( c == '\n' || c == '\r' ) { // Keep, but ignore initial line feeds - buffer.append (c); - lineLength = 0; - continue; - } - - if (Character.isWhitespace (c)) - indent.append (c); - else if ( (c == '*' || c == '-' || c == '.' ) && - Character.isWhitespace (nextChar) ) - indent.append (' '); - else if ( Character.isDigit (c) && nextChar == '.' && - Character.isWhitespace (text[i+2])) - indent.append (' '); - else if ( c == '>' ) - indent.append ('>'); - else - state = StatePosition.MIDDLE_OF_LINE; - - // If we are still in the initial state, then put 'er in... - if (state == StatePosition.BEGIN_FIRST_LINE) { - buffer.append (c); - lineLength++; - } - } - - // While it would be more accurate to explicitely state the range of - // possibilities, with something like: - // EnumSet.range (StatePosition.BEGIN_OTHER_LINE, StatePosition.MIDDLE_OF_LINE ).contains (state) - // We know that what is left is just the BEGIN_FIRST_LINE ... - - if ( state != StatePosition.BEGIN_FIRST_LINE ) - { - // If not the middle of the line, then it must be at the first of a line - // Either BEGIN_OTHER_LINE or BEGIN_NEW_LINE - if (state != StatePosition.MIDDLE_OF_LINE) - { - if ( Character.isWhitespace(c) || c == '>' || c == '.' ) - word.append (c); - else if ( ( ( c == '*' || c == '-' ) && Character.isWhitespace (nextChar) ) || - ( Character.isDigit(c) && nextChar == '.' && Character.isWhitespace( text[i+2] ) ) ) { - word.append (c); - state = StatePosition.BEGIN_NEW_LINE; - } - else { - if (state == StatePosition.BEGIN_NEW_LINE) { - buffer.append (word); - lineLength = word.substring ( word.indexOf("\n")+1 ).length(); - } - word.setLength (0); - state = StatePosition.MIDDLE_OF_LINE; - } - } - - if (state == StatePosition.MIDDLE_OF_LINE) - { - // Are we at the end of a word? Then we need to calculate whether - // to wrap the line or not. - // - // This condition does double duty, in that is also serves to - // ignore multiple spaces and special characters that may be at - // the beginning of the line. - if ( Character.isWhitespace(c) || c == 0 ) - { - if ( word.length() > 0) { - lineLength = addWordToBuffer (lineWidth, lineEnding, word, indent, buffer, lineLength); - } - // Do we we two spaces at the end of the line? Honor this... - else if ( c == ' ' && ( nextChar == '\r' || nextChar == '\n' ) && - state != StatePosition.BEGIN_OTHER_LINE ) { - buffer.append (" "); - buffer.append (lineEnding); - lineLength = 0; - } - - if ( c == '\r' || c == '\n' ) { - state = StatePosition.BEGIN_OTHER_LINE; - word.append(c); - } - - // Linefeeds are completely ignored and just treated as whitespace, - // unless, of course, there are two of 'em... and of course, end of - // lines are simply evil on Windows machines. - - if ( (c == '\n' && nextChar == '\n') || // Unix-style line-ends - ( c == '\r' && nextChar == '\n' && // Windows-style line-ends - text[i+2] == '\r' && text[i+3] == '\n' ) ) - { - state = StatePosition.BEGIN_FIRST_LINE; - word.setLength(0); - indent.setLength (0); - lineLength = 0; - - if (c == '\r') { // If we are dealing with Windows-style line-ends, - i++; // we need to skip past the next character... - buffer.append("\r\n"); - } else - buffer.append(c); - } - - } else { - word.append (c); - state = StatePosition.MIDDLE_OF_LINE; - } - } - } - } - - return buffer.toString().toCharArray(); - } - - /** - * Adds a word to the buffer, performing word wrap if necessary. - * @param lineWidth The current width of the line - * @param lineEnding The line ending to append, if necessary - * @param word The word to append - * @param indent The indentation string to insert, if necesary - * @param buffer The buffer to perform all this stuff to - * @param lineLength The current length of the current line - * @return The new length of the current line - */ - private static int addWordToBuffer (final int lineWidth, final String lineEnding, - final StringBuilder word, - final StringBuilder indent, - final StringBuilder buffer, int lineLength) - { - if ( word.length() + lineLength + 1 > lineWidth ) - { - buffer.append (lineEnding); - buffer.append (indent); - buffer.append (word); - - lineLength = indent.length() + word.length(); - } - else { - if ( lineLength > indent.length() ) - buffer.append (' '); - buffer.append (word); - lineLength += word.length() + 1; - } - word.setLength (0); - return lineLength; - } +public class MarkdownFormatter { + + // Expect everyone to simply use the public static methods... + private MarkdownFormatter() {} + + /** + * Formats a collection of lines to a particular width and honors typical Markdown syntax and + * formatting. The method assumes that if the first line ends with a line termination + * character, all the other lines will as well. + * + * @param lines A list of strings that should be formatted and wrapped. + * @param lineWidth The width of the page + * @return A string containing each + */ + public static String format(List lines, int lineWidth) { + if (lines == null) return null; // Should we return an empty string? + + final String lineEndings; + if (lines.get(0).endsWith("\r\n")) + lineEndings = "\r\n"; + else if (lines.get(0).endsWith("\r")) + lineEndings = "\r"; + else + lineEndings = Strings.EOL; + + final StringBuilder buf = new StringBuilder(); + for (String line : lines) { + buf.append(line); + buf.append(' '); // We can add extra spaces with impunity, and this + // makes sure our lines don't run together. + } + return format(buf.toString(), lineWidth, lineEndings); + } + + /** + * Formats a string of text. The formatting does line wrapping at the lineWidth + * boundary, but it also honors the formatting of initial paragraph lines, allowing indentation + * of the entire paragraph. + * + * @param text The line of text to format + * @param lineWidth The width of the lines + * @return A string containing the formatted text. + */ + public static String format(final String text, final int lineWidth) { + return format(text, lineWidth, Strings.EOL); + } + + /** + * Formats a string of text. The formatting does line wrapping at the lineWidth + * boundary, but it also honors the formatting of initial paragraph lines, allowing indentation + * of the entire paragraph. + * + * @param text The line of text to format + * @param lineWidth The width of the lines + * @param lineEnding The line ending that overrides the default System value + * @return A string containing the formatted text. + */ + public static String format(final String text, final int lineWidth, final String lineEnding) { + return new String(format(text.toCharArray(), lineWidth, lineEnding)); + } + + /** + * The available cursor position states as it sits in the buffer. + */ + private enum StatePosition { + /** The beginning of a paragraph ... the start of the buffer */ + BEGIN_FIRST_LINE, + + /** The beginning of the next line, which may be completely ignored. */ + BEGIN_OTHER_LINE, + + /** The beginning of a new line that will not be ignored, but appended. */ + BEGIN_NEW_LINE, + + /** The middle of a line. */ + MIDDLE_OF_LINE + } + + /** + * The method that does the work of formatting a string of text. The text, however, is a + * character array, which is more efficient to work with. TODO: Should we make the + * format(char[]) method public? + * + * @param text The line of text to format + * @param lineWidth The width of the lines + * @param lineEnding The line ending that overrides the default System value + * @return A string containing the formatted text. + */ + static char[] format(final char[] text, final int lineWidth, final String lineEnding) { + final StringBuilder word = new StringBuilder(); + final StringBuilder indent = new StringBuilder(); + final StringBuilder buffer = new StringBuilder(text.length + 10); + + StatePosition state = StatePosition.BEGIN_FIRST_LINE; + int lineLength = 0; + + // There are times when we will run across a character(s) that will + // cause us to stop doing word wrap until we get to the + // "end of non-wordwrap" character(s). + // + // If this string is set to null, it tells us to "do" word-wrapping. + char endWordwrap1 = 0; + char endWordwrap2 = 0; + + // We loop one character past the end of the loop, and when we get to + // this position, we assign 'c' to be 0 ... as a marker for the end of + // the string... + + for (int i = 0; i <= text.length; i++) { + final char c; + if (i < text.length) + c = text[i]; + else + c = 0; + + final char nextChar; + if (i + 1 < text.length) + nextChar = text[i + 1]; + else + nextChar = 0; + + // Are we actually word-wrapping? + if (endWordwrap1 != 0) { + // Did we get the ending sequence of the non-word-wrap? + if ((endWordwrap2 == 0 && c == endWordwrap1) || (c == endWordwrap1 && nextChar == endWordwrap2)) + endWordwrap1 = 0; + buffer.append(c); + lineLength++; + + if (endWordwrap1 == 0 && endWordwrap2 != 0) { + buffer.append(nextChar); + lineLength++; + i++; + } + continue; + } + + // Check to see if we got one of our special non-word-wrapping + // character sequences ... + + if (c == '[') { // [Hyperlink] + endWordwrap1 = ']'; + } else if (c == '*' && nextChar == '*') { // **Bold** + endWordwrap1 = '*'; + endWordwrap2 = '*'; + } // *Italics* + else if (c == '*' && state == StatePosition.MIDDLE_OF_LINE) { + endWordwrap1 = '*'; + } else if (c == '`') { // `code` + endWordwrap1 = '`'; + } else if (c == '(' && nextChar == '(') { // ((Footnote)) + endWordwrap1 = ')'; + endWordwrap2 = ')'; + } else if (c == '!' && nextChar == '[') { // ![Image] + endWordwrap1 = ')'; + } + + // We are no longer doing word-wrapping, so tidy the situation up... + if (endWordwrap1 != 0) { + if (word.length() > 0) + lineLength = addWordToBuffer(lineWidth, lineEnding, word, indent, buffer, lineLength); + else if (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) != ']') buffer.append(' '); + // We are adding an extra space for most situations, unless we get a + // [link][ref] where we want them to be together without a space. + + buffer.append(c); + lineLength++; + continue; + } + + // Normal word-wrapping processing continues ... + + if (state == StatePosition.BEGIN_FIRST_LINE) { + if (c == '\n' || c == '\r') { // Keep, but ignore initial line feeds + buffer.append(c); + lineLength = 0; + continue; + } + + if (Character.isWhitespace(c)) + indent.append(c); + else if ((c == '*' || c == '-' || c == '.') && Character.isWhitespace(nextChar)) + indent.append(' '); + else if (Character.isDigit(c) && nextChar == '.' && Character.isWhitespace(text[i + 2])) + indent.append(' '); + else if (c == '>') + indent.append('>'); + else + state = StatePosition.MIDDLE_OF_LINE; + + // If we are still in the initial state, then put 'er in... + if (state == StatePosition.BEGIN_FIRST_LINE) { + buffer.append(c); + lineLength++; + } + } + + // While it would be more accurate to explicitely state the range of + // possibilities, with something like: + // EnumSet.range (StatePosition.BEGIN_OTHER_LINE, StatePosition.MIDDLE_OF_LINE + // ).contains (state) + // We know that what is left is just the BEGIN_FIRST_LINE ... + + if (state != StatePosition.BEGIN_FIRST_LINE) { + // If not the middle of the line, then it must be at the first of a line + // Either BEGIN_OTHER_LINE or BEGIN_NEW_LINE + if (state != StatePosition.MIDDLE_OF_LINE) { + if (Character.isWhitespace(c) || c == '>' || c == '.') + word.append(c); + else if (((c == '*' || c == '-') && Character.isWhitespace(nextChar)) + || (Character.isDigit(c) && nextChar == '.' && Character.isWhitespace(text[i + 2]))) { + word.append(c); + state = StatePosition.BEGIN_NEW_LINE; + } else { + if (state == StatePosition.BEGIN_NEW_LINE) { + buffer.append(word); + lineLength = word.substring(word.indexOf("\n") + 1).length(); + } + word.setLength(0); + state = StatePosition.MIDDLE_OF_LINE; + } + } + + if (state == StatePosition.MIDDLE_OF_LINE) { + // Are we at the end of a word? Then we need to calculate whether + // to wrap the line or not. + // + // This condition does double duty, in that is also serves to + // ignore multiple spaces and special characters that may be at + // the beginning of the line. + if (Character.isWhitespace(c) || c == 0) { + if (word.length() > 0) { + lineLength = addWordToBuffer(lineWidth, lineEnding, word, indent, buffer, lineLength); + } + // Do we we two spaces at the end of the line? Honor this... + else if (c == ' ' && (nextChar == '\r' || nextChar == '\n') + && state != StatePosition.BEGIN_OTHER_LINE) { + buffer.append(" "); + buffer.append(lineEnding); + lineLength = 0; + } + + if (c == '\r' || c == '\n') { + state = StatePosition.BEGIN_OTHER_LINE; + word.append(c); + } + + // Linefeeds are completely ignored and just treated as whitespace, + // unless, of course, there are two of 'em... and of course, end of + // lines are simply evil on Windows machines. + + if ((c == '\n' && nextChar == '\n') || // Unix-style line-ends + (c == '\r' && nextChar == '\n' && // Windows-style line-ends + text[i + 2] == '\r' && text[i + 3] == '\n')) { + state = StatePosition.BEGIN_FIRST_LINE; + word.setLength(0); + indent.setLength(0); + lineLength = 0; + + if (c == '\r') { // If we are dealing with Windows-style line-ends, + i++; // we need to skip past the next character... + buffer.append("\r\n"); + } else + buffer.append(c); + } + + } else { + word.append(c); + state = StatePosition.MIDDLE_OF_LINE; + } + } + } + } + + return buffer.toString().toCharArray(); + } + + /** + * Adds a word to the buffer, performing word wrap if necessary. + * + * @param lineWidth The current width of the line + * @param lineEnding The line ending to append, if necessary + * @param word The word to append + * @param indent The indentation string to insert, if necesary + * @param buffer The buffer to perform all this stuff to + * @param lineLength The current length of the current line + * @return The new length of the current line + */ + private static int addWordToBuffer(final int lineWidth, final String lineEnding, final StringBuilder word, + final StringBuilder indent, final StringBuilder buffer, int lineLength) { + if (word.length() + lineLength + 1 > lineWidth) { + buffer.append(lineEnding); + buffer.append(indent); + buffer.append(word); + + lineLength = indent.length() + word.length(); + } else { + if (lineLength > indent.length()) buffer.append(' '); + buffer.append(word); + lineLength += word.length() + 1; + } + word.setLength(0); + return lineLength; + } } diff --git a/plugin/src/winterwell/markdown/pagemodel/MarkdownPage.java b/plugin/src/winterwell/markdown/pagemodel/MarkdownPage.java index 7390452..68e1098 100755 --- a/plugin/src/winterwell/markdown/pagemodel/MarkdownPage.java +++ b/plugin/src/winterwell/markdown/pagemodel/MarkdownPage.java @@ -1,626 +1,568 @@ -/** - * Copyright winterwell Mathematics Ltd. - * @author Daniel Winterstein - * 11 Jan 2007 - */ -package winterwell.markdown.pagemodel; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.eclipse.jface.preference.IPreferenceStore; - -import winterwell.markdown.Activator; -import winterwell.markdown.StringMethods; -import winterwell.markdown.preferences.MarkdownPreferencePage; -import winterwell.utils.FailureException; -import winterwell.utils.Process; -import winterwell.utils.StrUtils; -import winterwell.utils.Utils; -import winterwell.utils.io.FileUtils; - -import com.petebevin.markdown.MarkdownProcessor; - -/** - * Understands Markdown syntax. - * - * @author Daniel Winterstein - */ -public class MarkdownPage { - - /** - * Strip leading and trailing #s and whitespace - * - * @param line - * @return cleaned up line - */ - private String cleanHeader(String line) { - for (int j = 0; j < line.length(); j++) { - char c = line.charAt(j); - if (c != '#' && !Character.isWhitespace(c)) { - line = line.substring(j); - break; - } - } - for (int j = line.length() - 1; j > 0; j--) { - char c = line.charAt(j); - if (c != '#' && !Character.isWhitespace(c)) { - line = line.substring(0, j + 1); - break; - } - } - return line; - } - - /** - * Represents information about a section header. E.g. ## Misc Warblings - * - * @author daniel - */ - public class Header { - /** - * 1 = top-level (i.e. #), 2= 2nd-level (i.e. ##), etc. - */ - final int level; - /** - * The text of the Header - */ - final String heading; - /** - * Sub-sections, if any - */ - final List
subHeaders = new ArrayList
(); - /** - * The line on which this header occurs. - */ - final int lineNumber; - - public int getLineNumber() { - return lineNumber; - } - - /** - * - * @return the next section (at this depth if possible), null if none - */ - public Header getNext() { - if (parent == null) { - int ti = level1Headers.indexOf(this); - if (ti == -1 || ti == level1Headers.size() - 1) - return null; - return level1Headers.get(ti + 1); - } - int i = parent.subHeaders.indexOf(this); - assert i != -1 : this; - if (i == parent.subHeaders.size() - 1) - return parent.getNext(); - return parent.subHeaders.get(i + 1); - } - /** - * - * @return the next section (at this depth if possible), null if none - */ - public Header getPrevious() { - if (parent == null) { - int ti = level1Headers.indexOf(this); - if (ti == -1 || ti == 0) - return null; - return level1Headers.get(ti - 1); - } - int i = parent.subHeaders.indexOf(this); - assert i != -1 : this; - if (i == 0) - return parent.getPrevious(); - return parent.subHeaders.get(i - 1); - } - - - /** - * The parent section. Can be null. - */ - private Header parent; - - /** - * Create a marker for a section Header - * - * @param level - * 1 = top-level (i.e. #), 2= 2nd-level (i.e. ##), etc. - * @param lineNumber - * The line on which this header occurs - * @param heading - * The text of the Header, trimmed of #s - * @param currentHeader - * The previous Header. This is used to find the parent - * section if there is one. Can be null. - */ - Header(int level, int lineNumber, String heading, Header currentHeader) { - this.lineNumber = lineNumber; - this.level = level; - this.heading = cleanHeader(heading); - // Heading Tree - setParent(currentHeader); - } - - private void setParent(Header currentHeader) { - if (currentHeader == null) { - parent = null; - return; - } - if (currentHeader.level < level) { - parent = currentHeader; - parent.subHeaders.add(this); - return; - } - setParent(currentHeader.parent); - } - - public Header getParent() { - return parent; - } - - /** - * Sub-sections. May be zero-length, never null. - */ - public List
getSubHeaders() { - return subHeaders; - } - - @Override - public String toString() { - return heading; - } - - public int getLevel() { - return level; - } - } - - /** - * The raw text, broken up into individual lines. - */ - private List lines; - - /** - * The raw text, broken up into individual lines. - */ - public List getText() { - return Collections.unmodifiableList(lines); - } - - public enum KLineType { - NORMAL, H1, H2, H3, H4, H5, H6, BLANK, - // TODO LIST, BLOCKQUOTE, - /** A line marking Markdown info about the preceding line, e.g. ====== */ - MARKER, - /** A line containing meta-data, e.g. title: My Page */ - META - } - - /** - * Information about each line. - */ - private List lineTypes; - private Map pageObjects = new HashMap(); - - // TODO meta-data, footnotes, tables, link & image attributes - private static Pattern multiMarkdownTag = Pattern.compile("^([\\w].*):(.*)"); - private Map multiMarkdownTags = new HashMap(); - - // Regular expression for Github support - private static Pattern githubURLDetection = Pattern.compile("((https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|])"); - - /** - * The top-level headers. FIXME handle documents which have a 2nd level - * header before any 1st level ones - */ - private final List
level1Headers = new ArrayList
(); - private final IPreferenceStore pStore; - - /** - * Create a page. - * - * @param text - */ - public MarkdownPage(String text) { - pStore = Activator.getDefault().getPreferenceStore(); - setText(text); - } - - /** - * Reset the text for this page. - * - * @param text - */ - private void setText(String text) { - // Get lines - lines = StringMethods.splitLines(text); - // Clean out old - level1Headers.clear(); - lineTypes = new ArrayList(lines.size()); - pageObjects.clear(); - // Dummy level-1 header in case there are none - Header dummyTopHeader = new Header(1, 0, "", null); - level1Headers.add(dummyTopHeader); - Header currentHeader = dummyTopHeader; - // Identify line types - int lineNum = 0; - - // Check if we should support the Multi-Markdown Metadata - boolean multiMarkdownMetadataSupport = - pStore.getBoolean(MarkdownPreferencePage.PREF_MULTIMARKDOWN_METADATA); - - // Multi-markdown header - if (multiMarkdownMetadataSupport) { - // The key is the text before the colon, and the data is the text - // after the - // colon. In the above example, notice that there are two lines of - // information - // for the Author key. If you end a line with “space-space-newline”, - // the newline - // will be included when converted to other formats. - // - // There must not be any whitespace above the metadata, and the - // metadata block - // ends with the first whitespace only line. The metadata is - // stripped from the - // document before it is passed on to the syntax parser. - - // - // Check if the Metdatas are valid - // - boolean validMetadata = true; - for (lineNum = 0; lineNum < lines.size(); lineNum++) { - String line = lines.get(lineNum); - if (Utils.isBlank(line)) { - break; - } - Matcher m = multiMarkdownTag.matcher(line); - if (!m.find()) { - if (lineNum == 0) { - // No MultiMarkdown metadata - validMetadata = false; - break; - } else if (!line.matches("^\\s.*\n")) { - // The next line was not intended (ie. it does not start - // with a whitespace) - validMetadata = false; - break; - } - } - } - - // Valid Metadatas have been found. We need to retrieve these keys/values. - if (validMetadata) { - String data = ""; - String tag = ""; - for (lineNum = 0; lineNum < lines.size(); lineNum++) { - String line = lines.get(lineNum); - if (Utils.isBlank(line)) { - break; - } - Matcher m = multiMarkdownTag.matcher(line); - if (!m.find()) { - if (lineNum == 0) { - break; - } - // Multi-line tag - lineTypes.add(KLineType.META); - data += StrUtils.LINEEND + line.trim(); - multiMarkdownTags.put(tag, data); - } else { - lineTypes.add(KLineType.META); - tag = m.group(0); - data = m.group(1).trim(); - if (m.group(1).endsWith(line)) - multiMarkdownTags.put(tag, data); - } - } - } else { - lineNum = 0; - } - } - - boolean githubSyntaxSupport = - pStore.getBoolean(MarkdownPreferencePage.PREF_GITHUB_SYNTAX); - - boolean inCodeBlock = false; - - for (; lineNum < lines.size(); lineNum++) { - String line = lines.get(lineNum); - // Code blocks - if (githubSyntaxSupport && line.startsWith("```")) { - inCodeBlock = !inCodeBlock; - } - if (!inCodeBlock) { - // Headings - int h = numHash(line); - String hLine = line; - int hLineNum = lineNum; - int underline = -1; - if (lineNum != 0) { - underline = just(line, '=') ? 1 : just(line, '-') ? 2 : -1; - } - if (underline != -1) { - h = underline; - hLineNum = lineNum - 1; - hLine = lines.get(lineNum - 1); - lineTypes.set(hLineNum, KLineType.values()[h]); - lineTypes.add(KLineType.MARKER); - } - // Create a Header object - if (h > 0) { - if (underline == -1) - lineTypes.add(KLineType.values()[h]); - Header header = new Header(h, hLineNum, hLine, currentHeader); - if (h == 1) { - level1Headers.add(header); - } - pageObjects.put(hLineNum, header); - currentHeader = header; - continue; - } - } - // TODO List - // TODO Block quote - // Blank line - if (Utils.isBlank(line)) { - lineTypes.add(KLineType.BLANK); - continue; - } - // Normal - lineTypes.add(KLineType.NORMAL); - } // end line-loop - // Remove dummy header? - if (dummyTopHeader.getSubHeaders().size() == 0) { - level1Headers.remove(dummyTopHeader); - } - if (githubSyntaxSupport) { - /* - * Support Code block - */ - inCodeBlock = false; - for (lineNum = 0; lineNum < lines.size(); lineNum++) { - String line = lines.get(lineNum); - // Found the start or end of a code block - if (line.matches("^```.*\n")) { - // We reverse the boolean value - inCodeBlock = !inCodeBlock; - - // We force the line to be blank. But we mark it as normal - // to prevent to be stripped - lines.set(lineNum, "\n"); - lineTypes.set(lineNum, KLineType.NORMAL); - continue; - } - if (inCodeBlock) { - lines.set(lineNum, " " + line); - } - } - - /* - * Support for URL Detection - * We search for links that are not captured by Markdown syntax - */ - for (lineNum = 0; lineNum < lines.size(); lineNum++) { - String line = lines.get(lineNum); - // When a link has been replaced we need to scan again the string - // as the offsets have changed (we add '<' and '>' to the link to - // be interpreted by the markdown library) - boolean urlReplaced; - - do { - urlReplaced = false; - Matcher m = githubURLDetection.matcher(line); - while (m.find()) { - // Ignore the URL following the format - if ((m.start() - 1 >= 0) && (m.end() < line.length()) && - (line.charAt(m.start() - 1) == '<') && - (line.charAt(m.end()) == '>')) - { - continue; - } - - // Ignore the URL following the format [description](link) - if ((m.start() - 2 >= 0) && (m.end() < line.length()) && - (line.charAt(m.start() - 2) == ']') && - (line.charAt(m.start() - 1) == '(') && - (line.charAt(m.end()) == ')')) - { - continue; - } - - // Ignore the URL following the format [description](link "title") - if ((m.start() - 2 >= 0) && (m.end() + 1 < line.length()) && - (line.charAt(m.start() - 2) == ']') && - (line.charAt(m.start() - 1) == '(') && - (line.charAt(m.end()) == ' ') && - (line.charAt(m.end() + 1) == '"')) - { - continue; - } - - if (m.start() - 1 >= 0) { - // Case when the link is at the beginning of the string - line = line.substring(0, m.start()) + "<" + m.group(0) + ">" + line.substring(m.end()); - } else { - line = "<" + m.group(0) + ">" + line.substring(m.end()); - } - - // We replaced the string in the array - lines.set(lineNum, line); - urlReplaced = true; - break; - } - } while (urlReplaced); - } - } - } - - /** - * @param line - * @param c - * @return true if line is just cs (and whitespace at the start/end) - */ - boolean just(String line, char c) { - return line.matches("\\s*"+c+"+\\s*"); - } - - /** - * @param line - * @return The number of # symbols prepending the line. - */ - private int numHash(String line) { - for (int i = 0; i < line.length(); i++) { - if (line.charAt(i) != '#') - return i; - } - return line.length(); - } - - /** - * - * @param parent - * Can be null for top-level - * @return List of sub-headers. Never null. FIXME handle documents which - * have a 2nd level header before any 1st level ones - */ - public List
getHeadings(Header parent) { - if (parent == null) { - return Collections.unmodifiableList(level1Headers); - } - return Collections.unmodifiableList(parent.subHeaders); - } - - // public WebPage getWebPage() { - // WebPage page = new WebPage(); - // // Add the lines, one by one - // boolean inParagraph = false; - // for (int i=0; i"); - // line = cleanHeader(line); - // page.addText("<"+type+">"+line+""); - // continue; - // case MARKER: // Ignore - // continue; - // // TODO List? - // // TODO Block quote? - // } - // // Paragraph end? - // if (Utils.isBlank(line)) { - // if (inParagraph) page.addText("

"); - // continue; - // } - // // Paragraph start? - // if (!inParagraph) { - // page.addText("

"); - // inParagraph = true; - // } - // // Plain text - // page.addText(line); - // } - // return page; - // } - - /** - * Get the HTML for this page. Uses the MarkdownJ project. - */ - public String html() { - // Section numbers?? - boolean sectionNumbers = pStore - .getBoolean(MarkdownPreferencePage.PREF_SECTION_NUMBERS); - // Chop out multi-markdown header - StringBuilder sb = new StringBuilder(); - assert lines.size() == lineTypes.size(); - for (int i = 0, n = lines.size(); i < n; i++) { - KLineType type = lineTypes.get(i); - if (type == KLineType.META) - continue; - String line = lines.get(i); - if (sectionNumbers && isHeader(type) && line.contains("$section")) { - // TODO Header section = headers.get(i); - // String secNum = section.getSectionNumber(); - // line.replace("$section", secNum); - } - sb.append(line); - } - String text = sb.toString(); - // Use external converter? - final String cmd = pStore - .getString(MarkdownPreferencePage.PREF_MARKDOWN_COMMAND); - if (Utils.isBlank(cmd) - || (cmd.startsWith("(") && cmd.contains("MarkdownJ"))) { - // Use MarkdownJ - MarkdownProcessor markdown = new MarkdownProcessor(); - // MarkdownJ doesn't convert £s for some reason - text = text.replace("£", "£"); - String html = markdown.markdown(text); - return html; - } - // Attempt to run external command - try { - final File md = File.createTempFile("tmp", ".md"); - FileUtils.write(md, text); - Process process = new Process(cmd+" "+md.getAbsolutePath()); - process.run(); - int ok = process.waitFor(10000); - if (ok != 0) throw new FailureException(cmd+" failed:\n"+process.getError()); - String html = process.getOutput(); - FileUtils.delete(md); - return html; - } catch (Exception e) { - throw Utils.runtime(e); - } - } - - /** - * @param type - * @return - */ - private boolean isHeader(KLineType type) { - return type == KLineType.H1 || type == KLineType.H2 - || type == KLineType.H3 || type == KLineType.H4 - || type == KLineType.H5 || type == KLineType.H6; - } - - /** - * Return the raw text of this page. - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - for (String line : lines) { - sb.append(line); - } - return sb.toString(); - } - - /** - * Line type information for the raw text. - * - * @return - */ - public List getLineTypes() { - return Collections.unmodifiableList(lineTypes); - } - - /** - * @param line - * @return - */ - public Object getPageObject(int line) { - return pageObjects.get(line); - } - -} +/** + * Copyright winterwell Mathematics Ltd. + * @author Daniel Winterstein + * 11 Jan 2007 + */ +package winterwell.markdown.pagemodel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jface.preference.IPreferenceStore; + +import winterwell.markdown.MarkdownUI; +import winterwell.markdown.StringMethods; +import winterwell.markdown.preferences.PrefPageGeneral; +import winterwell.markdown.util.Strings; + +/** + * Understands Markdown syntax. + * + * @author Daniel Winterstein + */ +public class MarkdownPage { + + /** + * Strip leading and trailing #s and whitespace + * + * @param line + * @return cleaned up line + */ + private String cleanHeader(String line) { + for (int j = 0; j < line.length(); j++) { + char c = line.charAt(j); + if (c != '#' && !Character.isWhitespace(c)) { + line = line.substring(j); + break; + } + } + for (int j = line.length() - 1; j > 0; j--) { + char c = line.charAt(j); + if (c != '#' && !Character.isWhitespace(c)) { + line = line.substring(0, j + 1); + break; + } + } + return line; + } + + /** + * Represents information about a section header. E.g. ## Misc Warblings + * + * @author daniel + */ + public class Header { + + /** + * 1 = top-level (i.e. #), 2= 2nd-level (i.e. ##), etc. + */ + final int level; + /** + * The text of the Header + */ + final String heading; + /** + * Sub-sections, if any + */ + final List

subHeaders = new ArrayList
(); + /** + * The line on which this header occurs. + */ + final int lineNumber; + + public int getLineNumber() { + return lineNumber; + } + + /** + * @return the next section (at this depth if possible), null if none + */ + public Header getNext() { + if (parent == null) { + int ti = level1Headers.indexOf(this); + if (ti == -1 || ti == level1Headers.size() - 1) return null; + return level1Headers.get(ti + 1); + } + int i = parent.subHeaders.indexOf(this); + assert i != -1 : this; + if (i == parent.subHeaders.size() - 1) return parent.getNext(); + return parent.subHeaders.get(i + 1); + } + + /** + * @return the next section (at this depth if possible), null if none + */ + public Header getPrevious() { + if (parent == null) { + int ti = level1Headers.indexOf(this); + if (ti == -1 || ti == 0) return null; + return level1Headers.get(ti - 1); + } + int i = parent.subHeaders.indexOf(this); + assert i != -1 : this; + if (i == 0) return parent.getPrevious(); + return parent.subHeaders.get(i - 1); + } + + /** + * The parent section. Can be null. + */ + private Header parent; + + /** + * Create a marker for a section Header + * + * @param level 1 = top-level (i.e. #), 2= 2nd-level (i.e. ##), etc. + * @param lineNumber The line on which this header occurs + * @param heading The text of the Header, trimmed of #s + * @param currentHeader The previous Header. This is used to find the parent section if + * there is one. Can be null. + */ + Header(int level, int lineNumber, String heading, Header currentHeader) { + this.lineNumber = lineNumber; + this.level = level; + this.heading = cleanHeader(heading); + // Heading Tree + setParent(currentHeader); + } + + private void setParent(Header currentHeader) { + if (currentHeader == null) { + parent = null; + return; + } + if (currentHeader.level < level) { + parent = currentHeader; + parent.subHeaders.add(this); + return; + } + setParent(currentHeader.parent); + } + + public Header getParent() { + return parent; + } + + /** + * Sub-sections. May be zero-length, never null. + */ + public List
getSubHeaders() { + return subHeaders; + } + + @Override + public String toString() { + return heading; + } + + public int getLevel() { + return level; + } + } + + /** + * The raw text, broken up into individual lines. + */ + private List lines; + + /** + * The raw text, broken up into individual lines. + */ + public List getText() { + return Collections.unmodifiableList(lines); + } + + public enum KLineType { + NORMAL, + H1, + H2, + H3, + H4, + H5, + H6, + BLANK, + // TODO LIST, BLOCKQUOTE, + /** A line marking Markdown info about the preceding line, e.g. ====== */ + MARKER, + /** A line containing meta-data, e.g. title: My Page */ + META + } + + /** + * Information about each line. + */ + private List lineTypes; + private Map pageObjects = new HashMap(); + + // TODO meta-data, footnotes, tables, link & image attributes + private static Pattern multiMarkdownTag = Pattern.compile("^([\\w].*):(.*)"); + private Map multiMarkdownTags = new HashMap(); + + // Regular expression for Github support + private static Pattern githubURLDetection = Pattern + .compile("((https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|])"); + + /** + * The top-level headers. FIXME handle documents which have a 2nd level header before any 1st + * level ones + */ + private final List
level1Headers = new ArrayList
(); + private final IPreferenceStore pStore; + + /** + * Create a page. + * + * @param text + */ + public MarkdownPage(String text) { + pStore = MarkdownUI.getDefault().getPreferenceStore(); + setText(text); + } + + /** + * Reset the text for this page. + * + * @param text + */ + private void setText(String text) { + // Get lines + lines = StringMethods.splitLines(text); + // Clean out old + level1Headers.clear(); + lineTypes = new ArrayList(lines.size()); + pageObjects.clear(); + // Dummy level-1 header in case there are none + Header dummyTopHeader = new Header(1, 0, "", null); + level1Headers.add(dummyTopHeader); + Header currentHeader = dummyTopHeader; + // Identify line types + int lineNum = 0; + + // Check if we should support the Multi-Markdown Metadata + boolean multiMarkdownMetadataSupport = pStore.getBoolean(PrefPageGeneral.PREF_MULTIMARKDOWN_METADATA); + + // Multi-markdown header + if (multiMarkdownMetadataSupport) { + // The key is the text before the colon, and the data is the text + // after the + // colon. In the above example, notice that there are two lines of + // information + // for the Author key. If you end a line with “space-space-newline”, + // the newline + // will be included when converted to other formats. + // + // There must not be any whitespace above the metadata, and the + // metadata block + // ends with the first whitespace only line. The metadata is + // stripped from the + // document before it is passed on to the syntax parser. + + // + // Check if the Metdatas are valid + // + boolean validMetadata = true; + for (lineNum = 0; lineNum < lines.size(); lineNum++) { + String line = lines.get(lineNum); + if (Strings.isBlank(line)) { + break; + } + Matcher m = multiMarkdownTag.matcher(line); + if (!m.find()) { + if (lineNum == 0) { + // No MultiMarkdown metadata + validMetadata = false; + break; + } else if (!line.matches("^\\s.*\n")) { + // The next line was not intended (ie. it does not start + // with a whitespace) + validMetadata = false; + break; + } + } + } + + // Valid Metadatas have been found. We need to retrieve these keys/values. + if (validMetadata) { + String data = ""; + String tag = ""; + for (lineNum = 0; lineNum < lines.size(); lineNum++) { + String line = lines.get(lineNum); + if (Strings.isBlank(line)) { + break; + } + Matcher m = multiMarkdownTag.matcher(line); + if (!m.find()) { + if (lineNum == 0) { + break; + } + // Multi-line tag + lineTypes.add(KLineType.META); + data += Strings.EOL + line.trim(); + multiMarkdownTags.put(tag, data); + } else { + lineTypes.add(KLineType.META); + tag = m.group(0); + data = m.group(1).trim(); + if (m.group(1).endsWith(line)) multiMarkdownTags.put(tag, data); + } + } + } else { + lineNum = 0; + } + } + + boolean githubSyntaxSupport = pStore.getBoolean(PrefPageGeneral.PREF_GITHUB_SYNTAX); + + boolean inCodeBlock = false; + + for (; lineNum < lines.size(); lineNum++) { + String line = lines.get(lineNum); + // Code blocks + if (githubSyntaxSupport && line.startsWith("```")) { + inCodeBlock = !inCodeBlock; + } + if (!inCodeBlock) { + // Headings + int h = numHash(line); + String hLine = line; + int hLineNum = lineNum; + int underline = -1; + if (lineNum != 0) { + underline = just(line, '=') ? 1 : just(line, '-') ? 2 : -1; + } + if (underline != -1) { + h = underline; + hLineNum = lineNum - 1; + hLine = lines.get(lineNum - 1); + lineTypes.set(hLineNum, KLineType.values()[h]); + lineTypes.add(KLineType.MARKER); + } + // Create a Header object + if (h > 0) { + if (underline == -1) lineTypes.add(KLineType.values()[h]); + Header header = new Header(h, hLineNum, hLine, currentHeader); + if (h == 1) { + level1Headers.add(header); + } + pageObjects.put(hLineNum, header); + currentHeader = header; + continue; + } + } + // TODO List + // TODO Block quote + // Blank line + if (Strings.isBlank(line)) { + lineTypes.add(KLineType.BLANK); + continue; + } + // Normal + lineTypes.add(KLineType.NORMAL); + } // end line-loop + // Remove dummy header? + if (dummyTopHeader.getSubHeaders().size() == 0) { + level1Headers.remove(dummyTopHeader); + } + if (githubSyntaxSupport) { + /* + * Support Code block + */ + inCodeBlock = false; + for (lineNum = 0; lineNum < lines.size(); lineNum++) { + String line = lines.get(lineNum); + // Found the start or end of a code block + if (line.matches("^```.*\n")) { + // We reverse the boolean value + inCodeBlock = !inCodeBlock; + + // We force the line to be blank. But we mark it as normal + // to prevent to be stripped + lines.set(lineNum, "\n"); + lineTypes.set(lineNum, KLineType.NORMAL); + continue; + } + if (inCodeBlock) { + lines.set(lineNum, " " + line); + } + } + + /* + * Support for URL Detection We search for links that are not captured by Markdown + * syntax + */ + for (lineNum = 0; lineNum < lines.size(); lineNum++) { + String line = lines.get(lineNum); + // When a link has been replaced we need to scan again the string + // as the offsets have changed (we add '<' and '>' to the link to + // be interpreted by the markdown library) + boolean urlReplaced; + + do { + urlReplaced = false; + Matcher m = githubURLDetection.matcher(line); + while (m.find()) { + // Ignore the URL following the format + if ((m.start() - 1 >= 0) && (m.end() < line.length()) && (line.charAt(m.start() - 1) == '<') + && (line.charAt(m.end()) == '>')) { + continue; + } + + // Ignore the URL following the format [description](link) + if ((m.start() - 2 >= 0) && (m.end() < line.length()) && (line.charAt(m.start() - 2) == ']') + && (line.charAt(m.start() - 1) == '(') && (line.charAt(m.end()) == ')')) { + continue; + } + + // Ignore the URL following the format [description](link "title") + if ((m.start() - 2 >= 0) && (m.end() + 1 < line.length()) && (line.charAt(m.start() - 2) == ']') + && (line.charAt(m.start() - 1) == '(') && (line.charAt(m.end()) == ' ') + && (line.charAt(m.end() + 1) == '"')) { + continue; + } + + if (m.start() - 1 >= 0) { + // Case when the link is at the beginning of the string + line = line.substring(0, m.start()) + "<" + m.group(0) + ">" + line.substring(m.end()); + } else { + line = "<" + m.group(0) + ">" + line.substring(m.end()); + } + + // We replaced the string in the array + lines.set(lineNum, line); + urlReplaced = true; + break; + } + } while (urlReplaced); + } + } + } + + /** + * @param line + * @param c + * @return true if line is just cs (and whitespace at the start/end) + */ + boolean just(String line, char c) { + return line.matches("\\s*" + c + "+\\s*"); + } + + /** + * @param line + * @return The number of # symbols prepending the line. + */ + private int numHash(String line) { + for (int i = 0; i < line.length(); i++) { + if (line.charAt(i) != '#') return i; + } + return line.length(); + } + + /** + * @param parent Can be null for top-level + * @return List of sub-headers. Never null. FIXME handle documents which have a 2nd level header + * before any 1st level ones + */ + public List
getHeadings(Header parent) { + if (parent == null) { + return Collections.unmodifiableList(level1Headers); + } + return Collections.unmodifiableList(parent.subHeaders); + } + + // public WebPage getWebPage() { + // WebPage page = new WebPage(); + // // Add the lines, one by one + // boolean inParagraph = false; + // for (int i=0; i"); + // line = cleanHeader(line); + // page.addText("<"+type+">"+line+""); + // continue; + // case MARKER: // Ignore + // continue; + // // TODO List? + // // TODO Block quote? + // } + // // Paragraph end? + // if (Utils.isBlank(line)) { + // if (inParagraph) page.addText("

"); + // continue; + // } + // // Paragraph start? + // if (!inParagraph) { + // page.addText("

"); + // inParagraph = true; + // } + // // Plain text + // page.addText(line); + // } + // return page; + // } + + /** + * Get the HTML for this page. + */ + public String html() { + // Section numbers?? + boolean sectionNumbers = pStore.getBoolean(PrefPageGeneral.PREF_SECTION_NUMBERS); + // Chop out multi-markdown header + StringBuilder sb = new StringBuilder(); + assert lines.size() == lineTypes.size(); + for (int i = 0, n = lines.size(); i < n; i++) { + KLineType type = lineTypes.get(i); + if (type == KLineType.META) continue; + String line = lines.get(i); + if (sectionNumbers && isHeader(type) && line.contains("$section")) { + // TODO Header section = headers.get(i); + // String secNum = section.getSectionNumber(); + // line.replace("$section", secNum); + } + sb.append(line); + } + String text = sb.toString(); + + MdConverter converter = new MdConverter(); + return converter.convert(text); + } + + private boolean isHeader(KLineType type) { + return type == KLineType.H1 || type == KLineType.H2 || type == KLineType.H3 || type == KLineType.H4 + || type == KLineType.H5 || type == KLineType.H6; + } + + /** + * Return the raw text of this page. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (String line : lines) { + sb.append(line); + } + return sb.toString(); + } + + /** + * Line type information for the raw text. + * + * @return + */ + public List getLineTypes() { + return Collections.unmodifiableList(lineTypes); + } + + public Object getPageObject(int line) { + return pageObjects.get(line); + } +} diff --git a/plugin/src/winterwell/markdown/pagemodel/MarkdownPageTest.java b/plugin/src/winterwell/markdown/pagemodel/MarkdownPageTest.java index 244d437..bcc2b72 100755 --- a/plugin/src/winterwell/markdown/pagemodel/MarkdownPageTest.java +++ b/plugin/src/winterwell/markdown/pagemodel/MarkdownPageTest.java @@ -4,22 +4,18 @@ import java.util.List; import winterwell.markdown.pagemodel.MarkdownPage.Header; -import winterwell.utils.io.FileUtils; +import winterwell.markdown.util.FileUtils; - - -public class MarkdownPageTest //extends TestCase -{ +public class MarkdownPageTest /* extends TestCase */ { public static void main(String[] args) { MarkdownPageTest mpt = new MarkdownPageTest(); mpt.testGetHeadings(); } - + public void testGetHeadings() { // problem caused by a line beginning --, now fixed - String txt = FileUtils.read(new File( - "/home/daniel/winterwell/companies/DTC/projects/DTC-bayes/report1.txt")); + String txt = FileUtils.read(new File("/home/daniel/winterwell/companies/DTC/projects/DTC-bayes/report1.txt")); MarkdownPage p = new MarkdownPage(txt); List

h1s = p.getHeadings(null); Header h1 = h1s.get(0); diff --git a/plugin/src/winterwell/markdown/pagemodel/MdConverter.java b/plugin/src/winterwell/markdown/pagemodel/MdConverter.java new file mode 100644 index 0000000..ae3c8c1 --- /dev/null +++ b/plugin/src/winterwell/markdown/pagemodel/MdConverter.java @@ -0,0 +1,101 @@ +package winterwell.markdown.pagemodel; + +import java.io.File; + +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; +import org.eclipse.jface.preference.IPreferenceStore; +import org.markdownj.MarkdownProcessor; +import org.pegdown.PegDownProcessor; + +import com.github.rjeschke.txtmark.Configuration; +import com.github.rjeschke.txtmark.Configuration.Builder; +import com.github.rjeschke.txtmark.Processor; + +import winterwell.markdown.MarkdownUI; +import winterwell.markdown.preferences.PrefPageGeneral; +import winterwell.markdown.preferences.Prefs; +import winterwell.markdown.util.FailureException; +import winterwell.markdown.util.FileUtils; +import winterwell.markdown.util.Process; +import winterwell.markdown.util.Strings; + +public class MdConverter { + + private IPreferenceStore store; + + public MdConverter() { + super(); + store = MarkdownUI.getDefault().getPreferenceStore(); + } + + public String convert(String text) { + switch (store.getString(Prefs.PREF_MD_CONVERTER)) { + case Prefs.KEY_MARDOWNJ: + return useMarkDownJ(text); + case Prefs.KEY_PEGDOWN: + return usePegDown(text); + case Prefs.KEY_COMMONMARK: + return useCommonMark(text); + case Prefs.KEY_TXTMARK: + return useTxtMark(text); + case Prefs.PREF_EXTERNAL_COMMAND: + return useExternalCli(text); + } + return ""; + } + + // Use MarkdownJ + private String useMarkDownJ(String text) { + MarkdownProcessor markdown = new MarkdownProcessor(); + return markdown.markdown(text); + } + + // Use PegDown + private String usePegDown(String text) { + PegDownProcessor pegdown = new PegDownProcessor(); + return pegdown.markdownToHtml(text); + } + + // Use CommonMark + private String useCommonMark(String text) { + Parser parser = Parser.builder().build(); + Node document = parser.parse(text); + HtmlRenderer renderer = HtmlRenderer.builder().build(); + return renderer.render(document); + } + + // Use TxtMark + private String useTxtMark(String text) { + boolean safeMode = store.getBoolean(Prefs.PREF_TXTMARK_SAFEMODE); + boolean extended = store.getBoolean(Prefs.PREF_TXTMARK_EXTENDED); + + Builder builder = Configuration.builder(); + if (safeMode) builder.enableSafeMode(); + if (extended) builder.forceExtentedProfile(); + Configuration config = builder.build(); + return Processor.process(text, config); + } + + // Run external command + private String useExternalCli(String text) { + String cmd = store.getString(PrefPageGeneral.PREF_EXTERNAL_COMMAND); + if (Strings.isBlank(cmd) || (cmd.startsWith("(") && cmd.contains("MarkdownJ"))) { + return "No external markdown converter specified; update preferences."; + } + + try { + final File md = File.createTempFile("tmp", ".md"); + FileUtils.write(md, text); + Process process = new Process(cmd + " " + md.getAbsolutePath()); + process.run(); + int ok = process.waitFor(10000); + if (ok != 0) throw new FailureException(cmd + " failed:\n" + process.getError()); + String html = process.getOutput(); + FileUtils.delete(md); + return html; + } catch (Exception e) {} + return "External markdown convertion failed; update preferences."; + } +} diff --git a/plugin/src/winterwell/markdown/preferences/MarkdownPreferencePage.java b/plugin/src/winterwell/markdown/preferences/MarkdownPreferencePage.java deleted file mode 100755 index 36dadbc..0000000 --- a/plugin/src/winterwell/markdown/preferences/MarkdownPreferencePage.java +++ /dev/null @@ -1,214 +0,0 @@ -package winterwell.markdown.preferences; - -import org.eclipse.jface.preference.BooleanFieldEditor; -import org.eclipse.jface.preference.ColorFieldEditor; -import org.eclipse.jface.preference.FieldEditor; -import org.eclipse.jface.preference.FieldEditorPreferencePage; -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.jface.preference.PreferenceConverter; -import org.eclipse.jface.preference.StringFieldEditor; -import org.eclipse.swt.graphics.RGB; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.ui.IWorkbench; -import org.eclipse.ui.IWorkbenchPreferencePage; - -import winterwell.markdown.Activator; - -/** - * This class represents a preference page that - * is contributed to the Preferences dialog. By - * subclassing FieldEditorPreferencePage, we - * can use the field support built into JFace that allows - * us to create a page that is small and knows how to - * save, restore and apply itself. - *

- * This page is used to modify preferences only. They - * are stored in the preference store that belongs to - * the main plug-in class. That way, preferences can - * be accessed directly via the preference store. - */ - -public class MarkdownPreferencePage - extends FieldEditorPreferencePage - implements IWorkbenchPreferencePage { - - public static final String PREF_FOLDING = "Pref_Folding"; - public static final String PREF_WORD_WRAP = "Pref_WordWrap"; - public static final String PREF_TASK_TAGS = "Pref_TaskTagsOn"; - public static final String PREF_TASK_TAGS_DEFINED = "Pref_TaskTags"; - public static final String PREF_SECTION_NUMBERS = "Pref_SectionNumbers"; - - public static final String PREF_MARKDOWN_COMMAND = "Pref_Markdown_Command"; - private static final String MARKDOWNJ = "(use built-in MarkdownJ converter)"; - - - public static final String PREF_DEFUALT = "Pref_Default"; - public static final String PREF_COMMENT = "Pref_Comment"; - public static final String PREF_HEADER = "Pref_Header"; - public static final String PREF_LINK = "Pref_Link"; - public static final String PREF_CODE = "Pref_Code"; - public static final String PREF_CODE_BG = "Pref_Code_Background"; - - public static final String PREF_GITHUB_SYNTAX = "Pref_Github_Syntax"; - public static final String PREF_MULTIMARKDOWN_METADATA = "Pref_MultiMarkdown_Metadata"; - - private static final RGB DEF_DEFAULT = new RGB(0, 0, 0); - private static final RGB DEF_COMMENT = new RGB(128, 0, 0); - private static final RGB DEF_HEADER = new RGB(0, 128, 0); - private static final RGB DEF_LINK = new RGB(106, 131, 199); - private static final RGB DEF_CODE = new RGB(0, 0, 0); - private static final RGB DEF_CODE_BG = new RGB(244,244,244); - - public MarkdownPreferencePage() { - super(GRID); - IPreferenceStore pStore = Activator.getDefault().getPreferenceStore(); - setDefaultPreferences(pStore); - setPreferenceStore(pStore); - setDescription("Settings for the Markdown text editor. See also the general text editor preferences."); - } - - public static void setDefaultPreferences(IPreferenceStore pStore) { - pStore.setDefault(PREF_WORD_WRAP, false); - pStore.setDefault(PREF_FOLDING, true); - pStore.setDefault(PREF_TASK_TAGS, true); - pStore.setDefault(PREF_TASK_TAGS_DEFINED, "TODO,FIXME,??"); - pStore.setDefault(PREF_MARKDOWN_COMMAND, MARKDOWNJ); - pStore.setDefault(PREF_SECTION_NUMBERS, true); - pStore.setDefault(PREF_GITHUB_SYNTAX, true); - pStore.setDefault(PREF_MULTIMARKDOWN_METADATA, false); - - PreferenceConverter.setDefault(pStore, PREF_DEFUALT, DEF_DEFAULT); - PreferenceConverter.setDefault(pStore, PREF_COMMENT, DEF_COMMENT); - PreferenceConverter.setDefault(pStore, PREF_HEADER, DEF_HEADER); - PreferenceConverter.setDefault(pStore, PREF_LINK, DEF_LINK); - PreferenceConverter.setDefault(pStore, PREF_CODE, DEF_CODE); - PreferenceConverter.setDefault(pStore, PREF_CODE_BG, DEF_CODE_BG); - } - - /** - * Creates the field editors. Field editors are abstractions of - * the common GUI blocks needed to manipulate various types - * of preferences. Each field editor knows how to save and - * restore itself. - */ - @Override - public void createFieldEditors() { - // Word wrap - BooleanFieldEditor fd = new BooleanFieldEditor(PREF_WORD_WRAP, - "Soft word wrapping \r\n" -+"Note: may cause line numbers and related \r\n" + - "functionality to act a bit strangely", - getFieldEditorParent()); - addField(fd); - // Task tags - fd = new BooleanFieldEditor(PREF_TASK_TAGS, - "Manage tasks using task tags \r\n" + - "If true, this will add and delete tags in sync with edits.", - getFieldEditorParent()); - addField(fd); - StringFieldEditor tags = new StringFieldEditor(PREF_TASK_TAGS_DEFINED, - "Task tags\nComma separated list of recognised task tags.", getFieldEditorParent()); - addField(tags); - // Code folding - fd = new BooleanFieldEditor(PREF_FOLDING, - "Document folding, a.k.a. outline support", - getFieldEditorParent()); - addField(fd); - // Command line -// addField(new DummyField() { -// protected void makeComponent(Composite parent) { -// Label label = new Label(parent, 0); -// label.setText("Hello!"); -// GridData gd = new GridData(100, 20); -// label.setLayoutData(gd); -// } -// }); - StringFieldEditor cmd = new StringFieldEditor(PREF_MARKDOWN_COMMAND, - "UNSTABLE: Command-line to run Markdown.\r\n" + - "This should take in a file and output to std-out.\n" + - "Leave blank to use the built-in Java converter.", getFieldEditorParent()); - addField(cmd); - - ColorFieldEditor def = new ColorFieldEditor(PREF_DEFUALT, "Default text", getFieldEditorParent()); - addField(def); - - ColorFieldEditor com = new ColorFieldEditor(PREF_COMMENT, "Comment", getFieldEditorParent()); - addField(com); - - ColorFieldEditor link = new ColorFieldEditor(PREF_LINK, "Link", getFieldEditorParent()); - addField(link); - - ColorFieldEditor head = new ColorFieldEditor(PREF_HEADER, "Header and List indicator", getFieldEditorParent()); - addField(head); - - ColorFieldEditor code = new ColorFieldEditor(PREF_CODE, "Code", getFieldEditorParent()); - addField(code); - - ColorFieldEditor codeBg = new ColorFieldEditor(PREF_CODE_BG, "Code Background", getFieldEditorParent()); - addField(codeBg); - - /* - * Fields for the preview window - */ - - // Github Syntax support - fd = new BooleanFieldEditor(PREF_GITHUB_SYNTAX, - "Support Github Syntax", - getFieldEditorParent()); - addField(fd); - - // Multi-Markdown support - fd = new BooleanFieldEditor(PREF_MULTIMARKDOWN_METADATA, - "Support Multi-Markdown Metadata", - getFieldEditorParent()); - addField(fd); - } - - /* (non-Javadoc) - * @see org.eclipse.ui.IWorkbenchPreferencePage#init(org.eclipse.ui.IWorkbench) - */ - public void init(IWorkbench workbench) { - - } - - public static boolean wordWrap() { - IPreferenceStore pStore = Activator.getDefault().getPreferenceStore(); - if (! pStore.contains(MarkdownPreferencePage.PREF_WORD_WRAP)) { - return false; - } - return pStore.getBoolean(MarkdownPreferencePage.PREF_WORD_WRAP); - } - -} - -abstract class DummyField extends FieldEditor { - @Override - protected void adjustForNumColumns(int numColumns) { - // do nothing - } - @Override - protected void doFillIntoGrid(Composite parent, int numColumns) { - makeComponent(parent); - } - abstract protected void makeComponent(Composite parent); - - @Override - protected void doLoad() { - // - } - @Override - protected void doLoadDefault() { - // - } - - @Override - protected void doStore() { - // - } - - @Override - public int getNumberOfControls() { - return 1; - } - -} \ No newline at end of file diff --git a/plugin/src/winterwell/markdown/preferences/PrefPageColoring.java b/plugin/src/winterwell/markdown/preferences/PrefPageColoring.java new file mode 100644 index 0000000..1ff1eed --- /dev/null +++ b/plugin/src/winterwell/markdown/preferences/PrefPageColoring.java @@ -0,0 +1,47 @@ +package winterwell.markdown.preferences; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.preference.ColorFieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +import winterwell.markdown.MarkdownUI; + +public class PrefPageColoring extends FieldEditorPreferencePage implements IWorkbenchPreferencePage, Prefs { + + public PrefPageColoring() { + super(GRID); + setDescription(""); + } + + public void init(IWorkbench workbench) { + setPreferenceStore(MarkdownUI.getDefault().getPreferenceStore()); + } + + /** Creates the field editors. */ + @Override + public void createFieldEditors() { + Composite parent = getFieldEditorParent(); + + Group frame = new Group(parent, SWT.NONE); + GridDataFactory.fillDefaults().indent(0, 6).grab(true, false).span(2, 1).applyTo(frame); + GridLayoutFactory.fillDefaults().margins(6, 6).applyTo(frame); + frame.setText("Highlight Elements"); + + Composite internal = new Composite(frame, SWT.NONE); + GridDataFactory.fillDefaults().indent(0, 4).grab(true, false).applyTo(internal); + GridLayoutFactory.fillDefaults().applyTo(internal); + + addField(new ColorFieldEditor(PREF_DEFAULT, "Default text", internal)); + addField(new ColorFieldEditor(PREF_COMMENT, "Comment", internal)); + addField(new ColorFieldEditor(PREF_LINK, "Link", internal)); + addField(new ColorFieldEditor(PREF_HEADER, "Header and List indicator", internal)); + addField(new ColorFieldEditor(PREF_CODE, "Code", internal)); + addField(new ColorFieldEditor(PREF_CODE_BG, "Code Background", internal)); + } +} diff --git a/plugin/src/winterwell/markdown/preferences/PrefPageGeneral.java b/plugin/src/winterwell/markdown/preferences/PrefPageGeneral.java new file mode 100755 index 0000000..d75d9fa --- /dev/null +++ b/plugin/src/winterwell/markdown/preferences/PrefPageGeneral.java @@ -0,0 +1,136 @@ +package winterwell.markdown.preferences; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.preference.BooleanFieldEditor; +import org.eclipse.jface.preference.ComboFieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.StringFieldEditor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +import winterwell.markdown.MarkdownUI; + +public class PrefPageGeneral extends FieldEditorPreferencePage implements IWorkbenchPreferencePage, Prefs { + + private String[][] converters; + private Group txtGroup; + private Group extGroup; + + private BooleanFieldEditor safeMode; + private BooleanFieldEditor extended; + private StringFieldEditor extField; + + private class ComboFieldEditor2 extends ComboFieldEditor { + + public ComboFieldEditor2(String name, String labelText, String[][] entryNamesAndValues, Composite parent) { + super(name, labelText, entryNamesAndValues, parent); + } + + @Override + protected void fireValueChanged(String property, Object oldValue, Object newValue) { + super.fireValueChanged(property, oldValue, newValue); + update((String) newValue); + } + } + + public PrefPageGeneral() { + super(GRID); + setDescription(""); + } + + public void init(IWorkbench workbench) { + setPreferenceStore(MarkdownUI.getDefault().getPreferenceStore()); + } + + /** Create fields controlling general editor behavior */ + @Override + public void createFieldEditors() { + Composite parent = getFieldEditorParent(); + + Group frame = new Group(parent, SWT.NONE); + GridDataFactory.fillDefaults().indent(0, 6).grab(true, false).applyTo(frame); + GridLayoutFactory.fillDefaults().margins(6, 6).applyTo(frame); + frame.setText("General"); + + Composite internal = new Composite(frame, SWT.NONE); + GridDataFactory.fillDefaults().indent(0, 4).grab(true, false).applyTo(internal); + + // Word wrap + addField(new BooleanFieldEditor(PREF_WORD_WRAP, "Soft word wrapping", internal)); + + // Code folding + addField(new BooleanFieldEditor(PREF_FOLDING, "Document folding", internal)); + + // Task tags + addField(new BooleanFieldEditor(PREF_TASK_TAGS, "Use task tags ", internal)); + addField(new StringFieldEditor(PREF_TASK_TAGS_DEFINED, "Task tags defined:", internal)); + + // Converter selection + addField(new ComboFieldEditor2(PREF_MD_CONVERTER, "Markdown Converter:", converters(), internal)); + + // Converter related options + txtGroup = new Group(internal, SWT.NONE); + txtGroup.setText("TxtMark Options"); + + GridDataFactory.fillDefaults().indent(2, 6).grab(true, false).span(2, 1).applyTo(txtGroup); + GridLayoutFactory.fillDefaults().margins(6, 6).applyTo(txtGroup); + + safeMode = new BooleanFieldEditor(PREF_TXTMARK_SAFEMODE, "Use safe mode", txtGroup); + addField(safeMode); + extended = new BooleanFieldEditor(PREF_TXTMARK_EXTENDED, "Use extended profile", txtGroup); + addField(extended); + + // External cli + extGroup = new Group(internal, SWT.NONE); + extGroup.setText("External Run Command"); + + GridDataFactory.fillDefaults().indent(2, 6).grab(true, false).span(2, 1).applyTo(extGroup); + GridLayoutFactory.fillDefaults().margins(6, 6).applyTo(extGroup); + + extField = new StringFieldEditor(PREF_EXTERNAL_COMMAND, "", extGroup); + + ((GridLayout) internal.getLayout()).numColumns = 2; + update(getPreferenceStore().getString(PREF_MD_CONVERTER)); // init visibility + } + + private void update(String value) { + switch (value) { + case KEY_TXTMARK: + safeMode.setEnabled(true, txtGroup); + extended.setEnabled(true, txtGroup); + extField.setEnabled(false, extGroup); + break; + case KEY_USE_EXTERNAL: + safeMode.setEnabled(false, txtGroup); + extended.setEnabled(false, txtGroup); + extField.setEnabled(true, extGroup); + break; + default: + safeMode.setEnabled(false, txtGroup); + extended.setEnabled(false, txtGroup); + extField.setEnabled(false, extGroup); + } + } + + private String[][] converters() { + if (converters == null) { + converters = new String[5][2]; + converters[0][0] = "MarkdownJ"; + converters[0][1] = KEY_MARDOWNJ; + converters[1][0] = "Commonmark"; + converters[1][1] = KEY_COMMONMARK; + converters[2][0] = "PegDown"; + converters[2][1] = KEY_PEGDOWN; + converters[3][0] = "TxtMark"; + converters[3][1] = KEY_TXTMARK; + converters[4][0] = "External converter"; + converters[4][1] = KEY_USE_EXTERNAL; + } + return converters; + } +} diff --git a/plugin/src/winterwell/markdown/preferences/PrefPageSpeller.java b/plugin/src/winterwell/markdown/preferences/PrefPageSpeller.java new file mode 100644 index 0000000..35e6ce3 --- /dev/null +++ b/plugin/src/winterwell/markdown/preferences/PrefPageSpeller.java @@ -0,0 +1,59 @@ +package winterwell.markdown.preferences; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.ui.editors.text.ITextEditorHelpContextIds; +import org.eclipse.ui.texteditor.spelling.IPreferenceStatusMonitor; + +import winterwell.markdown.MarkdownUI; +import winterwell.markdown.spelling.AbstractConfigurationBlockPreferencePage; +import winterwell.markdown.spelling.IPreferenceConfigurationBlock; +import winterwell.markdown.spelling.OverlayPreferenceStore; +import winterwell.markdown.spelling.SpellingConfigurationBlock; +import winterwell.markdown.spelling.StatusUtil; + +/** + * Spelling preference page for options specific to Markdown. + */ +public class PrefPageSpeller extends AbstractConfigurationBlockPreferencePage { + + /** Status monitor */ + private class StatusMonitor implements IPreferenceStatusMonitor { + + @Override + public void statusChanged(IStatus status) { + handleStatusChanged(status); + } + } + + public PrefPageSpeller() { + super(); + } + + @Override + protected IPreferenceConfigurationBlock createConfigurationBlock(OverlayPreferenceStore overlayPreferenceStore) { + return new SpellingConfigurationBlock(overlayPreferenceStore, new StatusMonitor()); + } + + /** + * Handles status changes. + * + * @param status the new status + */ + protected void handleStatusChanged(IStatus status) { + setValid(!status.matches(IStatus.ERROR)); + StatusUtil.applyToStatusLine(this, status); + } + + @Override + protected void setDescription() {} + + @Override + protected void setPreferenceStore() { + setPreferenceStore(MarkdownUI.getDefault().getCombinedPreferenceStore()); + } + + @Override + protected String getHelpId() { + return ITextEditorHelpContextIds.SPELLING_PREFERENCE_PAGE; + } +} diff --git a/plugin/src/winterwell/markdown/preferences/PrefPageStyles.java b/plugin/src/winterwell/markdown/preferences/PrefPageStyles.java new file mode 100644 index 0000000..bd20700 --- /dev/null +++ b/plugin/src/winterwell/markdown/preferences/PrefPageStyles.java @@ -0,0 +1,99 @@ +package winterwell.markdown.preferences; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.preference.BooleanFieldEditor; +import org.eclipse.jface.preference.ComboFieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.FileFieldEditor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; +import org.osgi.framework.Bundle; + +import winterwell.markdown.MarkdownUI; + +public class PrefPageStyles extends FieldEditorPreferencePage implements IWorkbenchPreferencePage, Prefs { + + public PrefPageStyles() { + super(GRID); + setDescription(""); + } + + public void init(IWorkbench workbench) { + setPreferenceStore(MarkdownUI.getDefault().getPreferenceStore()); + } + + /** Creates the field editors. */ + @Override + public void createFieldEditors() { + Composite parent = getFieldEditorParent(); + + Group frame = new Group(parent, SWT.NONE); + GridDataFactory.fillDefaults().indent(0, 6).grab(true, false).span(2, 1).applyTo(frame); + GridLayoutFactory.fillDefaults().numColumns(3).margins(6, 6).applyTo(frame); + frame.setText("Stylesheets"); + + Composite internal = new Composite(frame, SWT.NONE); + GridDataFactory.fillDefaults().indent(0, 4).grab(true, false).applyTo(internal); + GridLayoutFactory.fillDefaults().numColumns(3).applyTo(internal); + + // Github Syntax support + addField(new BooleanFieldEditor(PREF_GITHUB_SYNTAX, "Support Github Syntax", internal)); + + // Multi-Markdown support + addField(new BooleanFieldEditor(PREF_MULTIMARKDOWN_METADATA, "Support Multi-Markdown Metadata", internal)); + + // Browser CSS + addField(new ComboFieldEditor(PREF_CSS_DEFAULT, "Default Stylesheet", builtins(), internal)); + addField(new FileFieldEditor(PREF_CSS_CUSTOM, "Custom Stylesheet", internal)); + } + + // build list of builtin stylesheets + // key=name, value=bundle cache URL as string + private String[][] builtins() { + Bundle bundle = Platform.getBundle(MarkdownUI.PLUGIN_ID); + URL url = bundle.getEntry("resources/"); + File dir = null; + try { + url = FileLocator.toFileURL(url); // extracts to bundle cache + dir = new File(url.toURI()); + } catch (IOException | URISyntaxException e) { + String[][] values = new String[1][2]; + values[0][0] = ""; + values[0][1] = ""; + return values; + } + List cssNames = new ArrayList<>(); + if (dir.isDirectory()) { + for (String name : dir.list()) { + if (name.endsWith("." + CSS)) { + cssNames.add(name); + } + } + } + + String[][] values = new String[cssNames.size()][2]; + for (int idx = 0; idx < cssNames.size(); idx++) { + String cssName = cssNames.get(idx); + values[idx][0] = cssName; + try { + values[idx][1] = url.toURI().resolve(cssName).toString(); + } catch (URISyntaxException e) { + values[idx][0] = cssName + " "; + } + } + return values; + } +} diff --git a/plugin/src/winterwell/markdown/preferences/Prefs.java b/plugin/src/winterwell/markdown/preferences/Prefs.java new file mode 100644 index 0000000..c6fa5e9 --- /dev/null +++ b/plugin/src/winterwell/markdown/preferences/Prefs.java @@ -0,0 +1,46 @@ +package winterwell.markdown.preferences; + +public interface Prefs { + + // preference related values + public static final String DEF_MDCSS = "markdown.css"; + public static final String CSS = "css"; + + // preference store keys + public static final String PREF_FOLDING = "Pref_Folding"; + public static final String PREF_WORD_WRAP = "Pref_WordWrap"; + public static final String PREF_TASK_TAGS = "Pref_TaskTagsOn"; + public static final String PREF_TASK_TAGS_DEFINED = "Pref_TaskTags"; + public static final String PREF_SECTION_NUMBERS = "Pref_SectionNumbers"; + + public static final String PREF_MD_CONVERTER = "Pref_Converter_Selection"; + + public static final String KEY_MARDOWNJ = "Pref_MarkdownJ"; + public static final String KEY_COMMONMARK = "Pref_Commonmark"; + public static final String KEY_PEGDOWN = "Pref_PegDown"; + public static final String KEY_TXTMARK = "Pref_TxtMark"; + public static final String KEY_USE_EXTERNAL = "Pref_ExtMark"; + + public static final String PREF_TXTMARK_SAFEMODE = "Pref_TxtMark_SafeMode"; + public static final String PREF_TXTMARK_EXTENDED = "Pref_TxtMark_ExtendedMode"; + + public static final String PREF_EXTERNAL_COMMAND = "Pref_Markdown_Command"; + + public static final String PREF_DEFAULT = "Pref_Default"; + public static final String PREF_COMMENT = "Pref_Comment"; + public static final String PREF_HEADER = "Pref_Header"; + public static final String PREF_LINK = "Pref_Link"; + public static final String PREF_CODE = "Pref_Code"; + public static final String PREF_CODE_BG = "Pref_Code_Background"; + + public static final String PREF_GITHUB_SYNTAX = "Pref_Github_Syntax"; + public static final String PREF_MULTIMARKDOWN_METADATA = "Pref_MultiMarkdown_Metadata"; + + public static final String PREF_CSS_DEFAULT = "Pref_Markdown_Css"; + public static final String PREF_CSS_CUSTOM = ""; + + public static final String PREF_SPELLING_ENABLED = "Pref_Spelling_Enabled"; + + public static final String PREF_UPDATE_DELAY = "Pref_Update_Delay"; + +} diff --git a/plugin/src/winterwell/markdown/preferences/PrefsInit.java b/plugin/src/winterwell/markdown/preferences/PrefsInit.java new file mode 100644 index 0000000..17720ce --- /dev/null +++ b/plugin/src/winterwell/markdown/preferences/PrefsInit.java @@ -0,0 +1,73 @@ +package winterwell.markdown.preferences; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferenceConverter; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.ui.texteditor.spelling.SpellingService; +import org.osgi.framework.Bundle; + +import winterwell.markdown.MarkdownUI; + +/** + * Initialize default preference values + */ +public class PrefsInit extends AbstractPreferenceInitializer implements Prefs { + + private static final RGB DEF_DEFAULT = new RGB(0, 0, 0); + private static final RGB DEF_COMMENT = new RGB(128, 0, 0); + private static final RGB DEF_HEADER = new RGB(0, 128, 0); + private static final RGB DEF_LINK = new RGB(106, 131, 199); + private static final RGB DEF_CODE = new RGB(0, 0, 0); + private static final RGB DEF_CODE_BG = new RGB(244, 244, 244); + + public void initializeDefaultPreferences() { + IPreferenceStore store = MarkdownUI.getDefault().getPreferenceStore(); + + store.setDefault(PREF_WORD_WRAP, false); + store.setDefault(PREF_FOLDING, true); + store.setDefault(PREF_TASK_TAGS, true); + store.setDefault(PREF_TASK_TAGS_DEFINED, "TODO,FIXME,??"); + + store.setDefault(PREF_MD_CONVERTER, KEY_MARDOWNJ); + store.setDefault(PREF_TXTMARK_SAFEMODE, false); + store.setDefault(PREF_TXTMARK_EXTENDED, true); + + store.setDefault(PREF_EXTERNAL_COMMAND, ""); + store.setDefault(PREF_SECTION_NUMBERS, true); + + store.setDefault(PREF_CSS_DEFAULT, cssDefault()); + store.setDefault(PREF_CSS_CUSTOM, ""); + store.setDefault(PREF_GITHUB_SYNTAX, true); + store.setDefault(PREF_MULTIMARKDOWN_METADATA, false); + + // hides the corresponding PlatformUI preference value + store.setDefault(SpellingService.PREFERENCE_SPELLING_ENABLED, true); + store.setDefault(PREF_SPELLING_ENABLED, true); + + PreferenceConverter.setDefault(store, PREF_DEFAULT, DEF_DEFAULT); + PreferenceConverter.setDefault(store, PREF_COMMENT, DEF_COMMENT); + PreferenceConverter.setDefault(store, PREF_HEADER, DEF_HEADER); + PreferenceConverter.setDefault(store, PREF_LINK, DEF_LINK); + PreferenceConverter.setDefault(store, PREF_CODE, DEF_CODE); + PreferenceConverter.setDefault(store, PREF_CODE_BG, DEF_CODE_BG); + } + + // create bundle cache URL for the default stylesheet + private String cssDefault() { + Bundle bundle = Platform.getBundle(MarkdownUI.PLUGIN_ID); + URL url = FileLocator.find(bundle, new Path("resources/" + DEF_MDCSS), null); + try { + url = FileLocator.toFileURL(url); + return url.toURI().toString(); + } catch (IOException | URISyntaxException e) {} + return DEF_MDCSS; // really an error + } +} diff --git a/plugin/src/winterwell/markdown/spelling/AbstractConfigurationBlockPreferencePage.java b/plugin/src/winterwell/markdown/spelling/AbstractConfigurationBlockPreferencePage.java new file mode 100644 index 0000000..1a2b01d --- /dev/null +++ b/plugin/src/winterwell/markdown/spelling/AbstractConfigurationBlockPreferencePage.java @@ -0,0 +1,96 @@ +package winterwell.markdown.spelling; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.preference.PreferencePage; + +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; +import org.eclipse.ui.PlatformUI; + +/** + * Abstract preference page which is used to wrap a + * {@link org.eclipse.ui.internal.editors.text.IPreferenceConfigurationBlock}. + * + * @since 3.0 + */ +public abstract class AbstractConfigurationBlockPreferencePage extends PreferencePage + implements IWorkbenchPreferencePage { + + private IPreferenceConfigurationBlock fConfigurationBlock; + private OverlayPreferenceStore fOverlayStore; + + /** + * Creates a new preference page. + */ + public AbstractConfigurationBlockPreferencePage() { + setDescription(); + setPreferenceStore(); + fOverlayStore = new OverlayPreferenceStore(getPreferenceStore(), new OverlayPreferenceStore.OverlayKey[] {}); + fConfigurationBlock = createConfigurationBlock(fOverlayStore); + } + + protected abstract IPreferenceConfigurationBlock createConfigurationBlock( + OverlayPreferenceStore overlayPreferenceStore); + + protected abstract String getHelpId(); + + protected abstract void setDescription(); + + protected abstract void setPreferenceStore(); + + @Override + public void init(IWorkbench workbench) {} + + @Override + public void createControl(Composite parent) { + super.createControl(parent); + PlatformUI.getWorkbench().getHelpSystem().setHelp(getControl(), getHelpId()); + } + + @Override + protected Control createContents(Composite parent) { + fOverlayStore.load(); + fOverlayStore.start(); + Control content = fConfigurationBlock.createControl(parent); + initialize(); + Dialog.applyDialogFont(content); + return content; + } + + private void initialize() { + fConfigurationBlock.initialize(); + } + + @Override + public boolean performOk() { + if (!fConfigurationBlock.canPerformOk()) return false; + fConfigurationBlock.performOk(); + fOverlayStore.propagate(); // TODO: chained store cannot propogate!!! + return true; + } + + @Override + public void performDefaults() { + fOverlayStore.loadDefaults(); + fConfigurationBlock.performDefaults(); + super.performDefaults(); + } + + @Override + public void dispose() { + fConfigurationBlock.dispose(); + if (fOverlayStore != null) { + fOverlayStore.stop(); + fOverlayStore = null; + } + super.dispose(); + } + + @Override + public void applyData(Object data) { + fConfigurationBlock.applyData(data); + } +} diff --git a/plugin/src/winterwell/markdown/spelling/IEditorsStatusConstants.java b/plugin/src/winterwell/markdown/spelling/IEditorsStatusConstants.java new file mode 100644 index 0000000..cfdf018 --- /dev/null +++ b/plugin/src/winterwell/markdown/spelling/IEditorsStatusConstants.java @@ -0,0 +1,18 @@ +package winterwell.markdown.spelling; + +/** + * Defines plug-in-specific status codes. + * + * @see org.eclipse.core.runtime.IStatus#getCode() + * @see org.eclipse.core.runtime.Status#Status(int, java.lang.String, int, java.lang.String, + * java.lang.Throwable) + * @since 2.1 + */ +public interface IEditorsStatusConstants { + + /** + * Status constant indicating that an internal error occurred. Value: 1001 + */ + public static final int INTERNAL_ERROR = 10001; + +} diff --git a/plugin/src/winterwell/markdown/spelling/IPreferenceConfigurationBlock.java b/plugin/src/winterwell/markdown/spelling/IPreferenceConfigurationBlock.java new file mode 100644 index 0000000..61e2c9e --- /dev/null +++ b/plugin/src/winterwell/markdown/spelling/IPreferenceConfigurationBlock.java @@ -0,0 +1,74 @@ +package winterwell.markdown.spelling; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +import org.eclipse.jface.preference.PreferencePage; + +/** + * Interface for preference configuration blocks which can either be wrapped by a + * {@link org.eclipse.ui.internal.editors.text.AbstractConfigurationBlockPreferencePage} or be + * included some preference page. + *

+ * Clients may implement this interface. + *

+ * + * @since 3.0 + */ +public interface IPreferenceConfigurationBlock { + + /** + * Creates the preference control. + * + * @param parent the parent composite to which to add the preferences control + * @return the control that was added to parent + */ + Control createControl(Composite parent); + + /** + * Called after creating the control. Implementations should load the preferences values and + * update the controls accordingly. + */ + void initialize(); + + /** + * Called when the OK button is pressed on the preference page. Implementations + * should commit the configured preference settings into their form of preference storage. + */ + void performOk(); + + /** + * Called when the OK button is pressed on the preference page. Implementations can + * abort the 'OK' operation by returning false. + * + * @return true iff the 'OK' operation can be performed + * @since 3.1 + */ + boolean canPerformOk(); + + /** + * Called when the Defaults button is pressed on the preference page. + * Implementation should reset any preference settings to their default values and adjust the + * controls accordingly. + */ + void performDefaults(); + + /** + * Called when the preference page is being disposed. Implementations should free any resources + * they are holding on to. + */ + void dispose(); + + /** + * Applies the given data. + *

+ * It is up to the implementor to define whether it supports this and which kind of data it + * accepts. + *

+ * + * @param data the data which is specified by each configuration block + * @see PreferencePage#applyData(Object) + * @since 3.4 + */ + void applyData(Object data); +} diff --git a/plugin/src/winterwell/markdown/spelling/OverlayPreferenceStore.java b/plugin/src/winterwell/markdown/spelling/OverlayPreferenceStore.java new file mode 100644 index 0000000..2f3b336 --- /dev/null +++ b/plugin/src/winterwell/markdown/spelling/OverlayPreferenceStore.java @@ -0,0 +1,438 @@ +package winterwell.markdown.spelling; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferenceStore; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; + +/** + * An overlaying preference store. + */ +public class OverlayPreferenceStore implements IPreferenceStore { + + /** + * Descriptor used to denote data types. + */ + public static final class TypeDescriptor { + + private TypeDescriptor() {} + } + + public static final TypeDescriptor BOOLEAN = new TypeDescriptor(); + public static final TypeDescriptor DOUBLE = new TypeDescriptor(); + public static final TypeDescriptor FLOAT = new TypeDescriptor(); + public static final TypeDescriptor INT = new TypeDescriptor(); + public static final TypeDescriptor LONG = new TypeDescriptor(); + public static final TypeDescriptor STRING = new TypeDescriptor(); + + /** + * Data structure for the overlay key. + */ + public static class OverlayKey { + + TypeDescriptor fDescriptor; + String fKey; + + public OverlayKey(TypeDescriptor descriptor, String key) { + fDescriptor = descriptor; + fKey = key; + } + } + + /* + * @see IPropertyChangeListener + */ + private class PropertyListener implements IPropertyChangeListener { + + @Override + public void propertyChange(PropertyChangeEvent event) { + OverlayKey key = findOverlayKey(event.getProperty()); + if (key != null) propagateProperty(fParent, key, fStore); + } + } + + /** The parent preference store. */ + private IPreferenceStore fParent; + /** This store. */ + private IPreferenceStore fStore; + /** The keys of this store. */ + private OverlayKey[] fOverlayKeys; + /** The property listener. */ + private PropertyListener fPropertyListener; + private boolean fLoaded; + + /** + * Creates and returns a new overlay preference store. + * + * @param parent the parent preference store + * @param overlayKeys the overlay keys + */ + public OverlayPreferenceStore(IPreferenceStore parent, OverlayKey[] overlayKeys) { + fParent = parent; + fOverlayKeys = overlayKeys; + fStore = new PreferenceStore(); + } + + /** + * Tries to find and return the overlay key for the given preference key string. + * + * @param key the preference key string + * @return the overlay key or null if none can be found + */ + private OverlayKey findOverlayKey(String key) { + for (OverlayKey fOverlayKey : fOverlayKeys) { + if (fOverlayKey.fKey.equals(key)) return fOverlayKey; + } + return null; + } + + /** + * Tells whether the given preference key string is covered by this overlay store. + * + * @param key the preference key string + * @return true if this overlay store covers the given key + */ + private boolean covers(String key) { + return (findOverlayKey(key) != null); + } + + /** + * Propagates the given overlay key from the origin to the target preference store. + * + * @param origin the source preference store + * @param key the overlay key + * @param target the preference store to which the key is propagated + */ + private void propagateProperty(IPreferenceStore origin, OverlayKey key, IPreferenceStore target) { + + if (origin.isDefault(key.fKey)) { + if (!target.isDefault(key.fKey)) target.setToDefault(key.fKey); + return; + } + + TypeDescriptor d = key.fDescriptor; + if (BOOLEAN == d) { + + boolean originValue = origin.getBoolean(key.fKey); + boolean targetValue = target.getBoolean(key.fKey); + if (targetValue != originValue) target.setValue(key.fKey, originValue); + + } else if (DOUBLE == d) { + + double originValue = origin.getDouble(key.fKey); + double targetValue = target.getDouble(key.fKey); + if (targetValue != originValue) target.setValue(key.fKey, originValue); + + } else if (FLOAT == d) { + + float originValue = origin.getFloat(key.fKey); + float targetValue = target.getFloat(key.fKey); + if (targetValue != originValue) target.setValue(key.fKey, originValue); + + } else if (INT == d) { + + int originValue = origin.getInt(key.fKey); + int targetValue = target.getInt(key.fKey); + if (targetValue != originValue) target.setValue(key.fKey, originValue); + + } else if (LONG == d) { + + long originValue = origin.getLong(key.fKey); + long targetValue = target.getLong(key.fKey); + if (targetValue != originValue) target.setValue(key.fKey, originValue); + + } else if (STRING == d) { + + String originValue = origin.getString(key.fKey); + String targetValue = target.getString(key.fKey); + if (targetValue != null && originValue != null && !targetValue.equals(originValue)) + target.setValue(key.fKey, originValue); + + } + } + + /** + * Propagates all overlay keys from this store to the parent store. + */ + public void propagate() { + for (OverlayKey fOverlayKey : fOverlayKeys) + propagateProperty(fStore, fOverlayKey, fParent); + } + + /** + * Loads the given key from the origin into the target. + * + * @param origin the source preference store + * @param key the overlay key + * @param target the preference store to which the key is propagated + * @param forceInitialization if true the value in the target gets initialized + * before loading + */ + private void loadProperty(IPreferenceStore origin, OverlayKey key, IPreferenceStore target, + boolean forceInitialization) { + TypeDescriptor d = key.fDescriptor; + if (BOOLEAN == d) { + + if (forceInitialization) target.setValue(key.fKey, true); + target.setValue(key.fKey, origin.getBoolean(key.fKey)); + target.setDefault(key.fKey, origin.getDefaultBoolean(key.fKey)); + + } else if (DOUBLE == d) { + + if (forceInitialization) target.setValue(key.fKey, 1.0D); + target.setValue(key.fKey, origin.getDouble(key.fKey)); + target.setDefault(key.fKey, origin.getDefaultDouble(key.fKey)); + + } else if (FLOAT == d) { + + if (forceInitialization) target.setValue(key.fKey, 1.0F); + target.setValue(key.fKey, origin.getFloat(key.fKey)); + target.setDefault(key.fKey, origin.getDefaultFloat(key.fKey)); + + } else if (INT == d) { + + if (forceInitialization) target.setValue(key.fKey, 1); + target.setValue(key.fKey, origin.getInt(key.fKey)); + target.setDefault(key.fKey, origin.getDefaultInt(key.fKey)); + + } else if (LONG == d) { + + if (forceInitialization) target.setValue(key.fKey, 1L); + target.setValue(key.fKey, origin.getLong(key.fKey)); + target.setDefault(key.fKey, origin.getDefaultLong(key.fKey)); + + } else if (STRING == d) { + + if (forceInitialization) target.setValue(key.fKey, "1"); //$NON-NLS-1$ + target.setValue(key.fKey, origin.getString(key.fKey)); + target.setDefault(key.fKey, origin.getDefaultString(key.fKey)); + + } + } + + /** + * Loads the values from the parent into this store. + */ + public void load() { + for (OverlayKey fOverlayKey : fOverlayKeys) + loadProperty(fParent, fOverlayKey, fStore, true); + + fLoaded = true; + } + + /** + * Loads the default values. + */ + public void loadDefaults() { + for (OverlayKey fOverlayKey : fOverlayKeys) + setToDefault(fOverlayKey.fKey); + } + + /** + * Starts to listen for changes. + */ + public void start() { + if (fPropertyListener == null) { + fPropertyListener = new PropertyListener(); + fParent.addPropertyChangeListener(fPropertyListener); + } + } + + /** + * Stops to listen for changes. + */ + public void stop() { + if (fPropertyListener != null) { + fParent.removePropertyChangeListener(fPropertyListener); + fPropertyListener = null; + } + } + + @Override + public void addPropertyChangeListener(IPropertyChangeListener listener) { + fStore.addPropertyChangeListener(listener); + } + + @Override + public void removePropertyChangeListener(IPropertyChangeListener listener) { + fStore.removePropertyChangeListener(listener); + } + + @Override + public void firePropertyChangeEvent(String name, Object oldValue, Object newValue) { + fStore.firePropertyChangeEvent(name, oldValue, newValue); + } + + @Override + public boolean contains(String name) { + return fStore.contains(name); + } + + @Override + public boolean getBoolean(String name) { + return fStore.getBoolean(name); + } + + @Override + public boolean getDefaultBoolean(String name) { + return fStore.getDefaultBoolean(name); + } + + @Override + public double getDefaultDouble(String name) { + return fStore.getDefaultDouble(name); + } + + @Override + public float getDefaultFloat(String name) { + return fStore.getDefaultFloat(name); + } + + @Override + public int getDefaultInt(String name) { + return fStore.getDefaultInt(name); + } + + @Override + public long getDefaultLong(String name) { + return fStore.getDefaultLong(name); + } + + @Override + public String getDefaultString(String name) { + return fStore.getDefaultString(name); + } + + @Override + public double getDouble(String name) { + return fStore.getDouble(name); + } + + @Override + public float getFloat(String name) { + return fStore.getFloat(name); + } + + @Override + public int getInt(String name) { + return fStore.getInt(name); + } + + @Override + public long getLong(String name) { + return fStore.getLong(name); + } + + @Override + public String getString(String name) { + return fStore.getString(name); + } + + @Override + public boolean isDefault(String name) { + return fStore.isDefault(name); + } + + @Override + public boolean needsSaving() { + return fStore.needsSaving(); + } + + @Override + public void putValue(String name, String value) { + if (covers(name)) fStore.putValue(name, value); + } + + @Override + public void setDefault(String name, double value) { + if (covers(name)) fStore.setDefault(name, value); + } + + @Override + public void setDefault(String name, float value) { + if (covers(name)) fStore.setDefault(name, value); + } + + @Override + public void setDefault(String name, int value) { + if (covers(name)) fStore.setDefault(name, value); + } + + @Override + public void setDefault(String name, long value) { + if (covers(name)) fStore.setDefault(name, value); + } + + @Override + public void setDefault(String name, String value) { + if (covers(name)) fStore.setDefault(name, value); + } + + @Override + public void setDefault(String name, boolean value) { + if (covers(name)) fStore.setDefault(name, value); + } + + @Override + public void setToDefault(String name) { + fStore.setToDefault(name); + } + + @Override + public void setValue(String name, double value) { + if (covers(name)) fStore.setValue(name, value); + } + + @Override + public void setValue(String name, float value) { + if (covers(name)) fStore.setValue(name, value); + } + + @Override + public void setValue(String name, int value) { + if (covers(name)) fStore.setValue(name, value); + } + + @Override + public void setValue(String name, long value) { + if (covers(name)) fStore.setValue(name, value); + } + + @Override + public void setValue(String name, String value) { + if (covers(name)) fStore.setValue(name, value); + } + + @Override + public void setValue(String name, boolean value) { + if (covers(name)) fStore.setValue(name, value); + } + + /** + * The keys to add to the list of overlay keys. + *

+ * Note: This method must be called before {@link #load()} is called. + *

+ * + * @param keys an array with overlay keys + * @since 3.0 + */ + public void addKeys(OverlayKey[] keys) { + Assert.isTrue(!fLoaded); + Assert.isNotNull(keys); + + int overlayKeysLength = fOverlayKeys.length; + OverlayKey[] result = new OverlayKey[keys.length + overlayKeysLength]; + + for (int i = 0, length = overlayKeysLength; i < length; i++) + result[i] = fOverlayKeys[i]; + + for (int i = 0, length = keys.length; i < length; i++) + result[overlayKeysLength + i] = keys[i]; + + fOverlayKeys = result; + + if (fLoaded) load(); + } +} diff --git a/plugin/src/winterwell/markdown/spelling/SpellingConfigurationBlock.java b/plugin/src/winterwell/markdown/spelling/SpellingConfigurationBlock.java new file mode 100644 index 0000000..555d110 --- /dev/null +++ b/plugin/src/winterwell/markdown/spelling/SpellingConfigurationBlock.java @@ -0,0 +1,567 @@ +package winterwell.markdown.spelling; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.ISafeRunnable; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.SafeRunner; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.layout.PixelConverter; +import org.eclipse.jface.viewers.ComboViewer; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.editors.text.EditorsUI; +import org.eclipse.ui.texteditor.spelling.IPreferenceStatusMonitor; +import org.eclipse.ui.texteditor.spelling.ISpellingPreferenceBlock; +import org.eclipse.ui.texteditor.spelling.SpellingEngineDescriptor; +import org.eclipse.ui.texteditor.spelling.SpellingService; + +import winterwell.markdown.Log; +import winterwell.markdown.spelling.OverlayPreferenceStore.OverlayKey; + +/** + * Configures spelling preferences specific to Markdown. + */ +public class SpellingConfigurationBlock implements IPreferenceConfigurationBlock { + + /** Error preferences block. */ + private static class ErrorPreferences implements ISpellingPreferenceBlock { + + /** Error message */ + private String fMessage; + + /** Error label */ + private Label fLabel; + + /** + * Initialize with the given error message. + * + * @param message the error message + */ + protected ErrorPreferences(String message) { + fMessage = message; + } + + @Override + public Control createControl(Composite composite) { + Composite inner = new Composite(composite, SWT.NONE); + inner.setLayout(new FillLayout(SWT.VERTICAL)); + + fLabel = new Label(inner, SWT.CENTER); + fLabel.setText(fMessage); + + return inner; + } + + @Override + public void initialize(IPreferenceStatusMonitor statusMonitor) {} + + @Override + public boolean canPerformOk() { + return true; + } + + @Override + public void performOk() {} + + @Override + public void performDefaults() {} + + @Override + public void performRevert() {} + + @Override + public void dispose() {} + + @Override + public void setEnabled(boolean enabled) { + fLabel.setEnabled(enabled); + } + } + + /** + * Forwarding status monitor for accessing the current status. + */ + private static class ForwardingStatusMonitor implements IPreferenceStatusMonitor { + + /** Status monitor to which status changes are forwarded */ + private IPreferenceStatusMonitor fForwardedMonitor; + + /** Latest reported status */ + private IStatus fStatus; + + /** + * Initializes with the given status monitor to which status changes are forwarded. + * + * @param forwardedMonitor the status monitor to which changes are forwarded + */ + public ForwardingStatusMonitor(IPreferenceStatusMonitor forwardedMonitor) { + fForwardedMonitor = forwardedMonitor; + } + + @Override + public void statusChanged(IStatus status) { + fStatus = status; + fForwardedMonitor.statusChanged(status); + } + + /** + * Returns the latest reported status. + * + * @return the latest reported status, can be null + */ + public IStatus getStatus() { + return fStatus; + } + } + + /** The overlay preference store. */ + private final OverlayPreferenceStore fStore; + + /* The controls */ + private Combo fProviderCombo; + private Button fEnablementCheckbox; + private ComboViewer fProviderViewer; + private Composite fComboGroup; + private Composite fGroup; + private StackLayout fStackLayout; + + /* the model */ + private final Map fProviderDescriptors; + private final Map fProviderPreferences; + private final Map fProviderControls; + + private ForwardingStatusMonitor fStatusMonitor; + + private ISpellingPreferenceBlock fCurrentBlock; + + public SpellingConfigurationBlock(OverlayPreferenceStore store, IPreferenceStatusMonitor statusMonitor) { + Assert.isNotNull(store); + fStore = store; + fStore.addKeys(createOverlayStoreKeys()); + fStatusMonitor = new ForwardingStatusMonitor(statusMonitor); + fProviderDescriptors = createListModel(); + fProviderPreferences = new HashMap<>(); + fProviderControls = new HashMap<>(); + } + + private Map createListModel() { + SpellingEngineDescriptor[] descs = EditorsUI.getSpellingService().getSpellingEngineDescriptors(); + Map map = new HashMap<>(); + for (SpellingEngineDescriptor desc : descs) { + map.put(desc.getId(), desc); + } + return map; + } + + private OverlayPreferenceStore.OverlayKey[] createOverlayStoreKeys() { + + ArrayList overlayKeys = new ArrayList<>(); + + overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, + SpellingService.PREFERENCE_SPELLING_ENABLED)); + overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.STRING, + SpellingService.PREFERENCE_SPELLING_ENGINE)); + + OverlayPreferenceStore.OverlayKey[] keys = new OverlayPreferenceStore.OverlayKey[overlayKeys.size()]; + overlayKeys.toArray(keys); + return keys; + } + + /** + * Creates page for spelling preferences. + * + * @param parent the parent composite + * @return the control for the preference page + */ + @Override + public Control createControl(Composite parent) { + + Composite composite = new Composite(parent, SWT.NULL); + // assume parent page uses grid-data + GridData gd = new GridData(GridData.HORIZONTAL_ALIGN_CENTER | GridData.VERTICAL_ALIGN_FILL); + composite.setLayoutData(gd); + GridLayout layout = new GridLayout(); + layout.numColumns = 2; + layout.marginHeight = 0; + layout.marginWidth = 0; + + PixelConverter pc = new PixelConverter(composite); + layout.verticalSpacing = pc.convertHeightInCharsToPixels(1) / 2; + composite.setLayout(layout); + + if (EditorsUI.getSpellingService().getSpellingEngineDescriptors().length == 0) { + Label label = new Label(composite, SWT.NONE); + label.setText(TextEditorMessages.SpellingConfigurationBlock_error_not_installed); + return composite; + } + + /* check box for new editors */ + fEnablementCheckbox = new Button(composite, SWT.CHECK); + fEnablementCheckbox.setText(TextEditorMessages.SpellingConfigurationBlock_enable); + gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_BEGINNING); + fEnablementCheckbox.setLayoutData(gd); + fEnablementCheckbox.addSelectionListener(new SelectionListener() { + + @Override + public void widgetSelected(SelectionEvent e) { + boolean enabled = fEnablementCheckbox.getSelection(); + fStore.setValue(SpellingService.PREFERENCE_SPELLING_ENABLED, enabled); + updateCheckboxDependencies(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) {} + }); + + Label label = new Label(composite, SWT.CENTER); + gd = new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_BEGINNING); + label.setLayoutData(gd); + + if (fProviderDescriptors.size() > 1) { + fComboGroup = new Composite(composite, SWT.NONE); + gd = new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_BEGINNING); + gd.horizontalIndent = 10; + fComboGroup.setLayoutData(gd); + GridLayout gridLayout = new GridLayout(2, false); + gridLayout.marginWidth = 0; + fComboGroup.setLayout(gridLayout); + + Label comboLabel = new Label(fComboGroup, SWT.CENTER); + gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_CENTER); + comboLabel.setLayoutData(gd); + comboLabel.setText(TextEditorMessages.SpellingConfigurationBlock_combo_caption); + + label = new Label(composite, SWT.CENTER); + gd = new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_BEGINNING); + label.setLayoutData(gd); + + fProviderCombo = new Combo(fComboGroup, SWT.READ_ONLY | SWT.DROP_DOWN); + gd = new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_CENTER); + fProviderCombo.setLayoutData(gd); + + fProviderViewer = createProviderViewer(); + } + + Composite groupComp = new Composite(composite, SWT.NONE); + gd = new GridData(GridData.FILL_BOTH); + gd.horizontalSpan = 2; + groupComp.setLayoutData(gd); + GridLayout gridLayout = new GridLayout(1, false); + gridLayout.marginWidth = 0; + groupComp.setLayout(gridLayout); + + /* contributed provider preferences. */ + fGroup = new Composite(groupComp, SWT.NONE); + gd = new GridData(SWT.FILL, SWT.FILL, true, true); + gd.horizontalIndent = 10; + fGroup.setLayoutData(gd); + fStackLayout = new StackLayout(); + fGroup.setLayout(fStackLayout); + + return composite; + } + + @Override + public void applyData(Object data) {} + + private ComboViewer createProviderViewer() { + /* list viewer */ + final ComboViewer viewer = new ComboViewer(fProviderCombo); + viewer.setContentProvider(new IStructuredContentProvider() { + + @Override + public void dispose() {} + + @Override + public void inputChanged(Viewer v, Object oldInput, Object newInput) {} + + @Override + public Object[] getElements(Object inputElement) { + return fProviderDescriptors.values().toArray(); + } + }); + viewer.setLabelProvider(new LabelProvider() { + + @Override + public Image getImage(Object element) { + return null; + } + + @Override + public String getText(Object element) { + return ((SpellingEngineDescriptor) element).getLabel(); + } + }); + viewer.addSelectionChangedListener(new ISelectionChangedListener() { + + @Override + public void selectionChanged(SelectionChangedEvent event) { + IStructuredSelection sel = (IStructuredSelection) event.getSelection(); + if (sel.isEmpty()) return; + if (fCurrentBlock != null && fStatusMonitor.getStatus() != null + && fStatusMonitor.getStatus().matches(IStatus.ERROR)) + if (isPerformRevert()) { + ISafeRunnable runnable = new ISafeRunnable() { + + @Override + public void run() throws Exception { + fCurrentBlock.performRevert(); + } + + @Override + public void handleException(Throwable x) {} + }; + SafeRunner.run(runnable); + } else { + revertSelection(); + return; + } + fStore.setValue(SpellingService.PREFERENCE_SPELLING_ENGINE, + ((SpellingEngineDescriptor) sel.getFirstElement()).getId()); + updateListDependencies(); + } + + private boolean isPerformRevert() { + Shell shell = viewer.getControl().getShell(); + MessageDialog dialog = new MessageDialog(shell, + TextEditorMessages.SpellingConfigurationBlock_error_title, null, + TextEditorMessages.SpellingConfigurationBlock_error_message, MessageDialog.QUESTION, + new String[] { IDialogConstants.YES_LABEL, IDialogConstants.NO_LABEL }, 1); + return dialog.open() == 0; + } + + private void revertSelection() { + try { + viewer.removeSelectionChangedListener(this); + SpellingEngineDescriptor desc = EditorsUI.getSpellingService() + .getActiveSpellingEngineDescriptor(fStore); + if (desc != null) viewer.setSelection(new StructuredSelection(desc), true); + } finally { + viewer.addSelectionChangedListener(this); + } + } + }); + viewer.setInput(fProviderDescriptors); + viewer.refresh(); + + return viewer; + } + + private void updateCheckboxDependencies() { + final boolean enabled = fEnablementCheckbox.getSelection(); + if (fComboGroup != null) setEnabled(fComboGroup, enabled); + SpellingEngineDescriptor desc = EditorsUI.getSpellingService().getActiveSpellingEngineDescriptor(fStore); + String id = desc != null ? desc.getId() : ""; //$NON-NLS-1$ + final ISpellingPreferenceBlock preferenceBlock = fProviderPreferences.get(id); + if (preferenceBlock != null) { + ISafeRunnable runnable = new ISafeRunnable() { + + @Override + public void run() throws Exception { + preferenceBlock.setEnabled(enabled); + } + + @Override + public void handleException(Throwable x) {} + }; + SafeRunner.run(runnable); + } + } + + private void setEnabled(Control control, boolean enabled) { + if (control instanceof Composite) { + Control[] children = ((Composite) control).getChildren(); + for (Control child : children) + setEnabled(child, enabled); + } + control.setEnabled(enabled); + } + + void updateListDependencies() { + SpellingEngineDescriptor desc = EditorsUI.getSpellingService().getActiveSpellingEngineDescriptor(fStore); + String id; + if (desc == null) { + // safety in case there is no such descriptor + id = ""; //$NON-NLS-1$ + String message = TextEditorMessages.SpellingConfigurationBlock_error_not_exist; + Log.log(new Status(IStatus.WARNING, EditorsUI.PLUGIN_ID, IStatus.OK, message, null)); + fCurrentBlock = new ErrorPreferences(message); + } else { + id = desc.getId(); + fCurrentBlock = fProviderPreferences.get(id); + if (fCurrentBlock == null) { + try { + fCurrentBlock = desc.createPreferences(); + fProviderPreferences.put(id, fCurrentBlock); + } catch (CoreException e) { + Log.error(e); + fCurrentBlock = new ErrorPreferences(e.getLocalizedMessage()); + } + } + } + + Control control = fProviderControls.get(id); + if (control == null) { + final Control[] result = new Control[1]; + ISafeRunnable runnable = new ISafeRunnable() { + + @Override + public void run() throws Exception { + result[0] = fCurrentBlock.createControl(fGroup); + } + + @Override + public void handleException(Throwable x) {} + }; + SafeRunner.run(runnable); + control = result[0]; + if (control == null) { + String message = TextEditorMessages.SpellingConfigurationBlock_info_no_preferences; + Log.log(IStatus.WARNING, IStatus.OK, message, null); + control = new ErrorPreferences(message).createControl(fGroup); + } else { + fProviderControls.put(id, control); + } + } + Dialog.applyDialogFont(control); + fStackLayout.topControl = control; + control.pack(); + fGroup.layout(); + fGroup.getParent().layout(); + + fStatusMonitor.statusChanged(new StatusInfo()); + ISafeRunnable runnable = new ISafeRunnable() { + + @Override + public void run() throws Exception { + fCurrentBlock.initialize(fStatusMonitor); + } + + @Override + public void handleException(Throwable x) {} + }; + SafeRunner.run(runnable); + } + + @Override + public void initialize() { + restoreFromPreferences(); + } + + @Override + public boolean canPerformOk() { + SpellingEngineDescriptor desc = EditorsUI.getSpellingService().getActiveSpellingEngineDescriptor(fStore); + String id = desc != null ? desc.getId() : ""; //$NON-NLS-1$ + final ISpellingPreferenceBlock block = fProviderPreferences.get(id); + if (block == null) return true; + + final Boolean[] result = new Boolean[] { Boolean.TRUE }; + ISafeRunnable runnable = new ISafeRunnable() { + + @Override + public void run() throws Exception { + result[0] = Boolean.valueOf(block.canPerformOk()); + } + + @Override + public void handleException(Throwable x) {} + }; + SafeRunner.run(runnable); + return result[0].booleanValue(); + } + + @Override + public void performOk() { + for (ISpellingPreferenceBlock block : fProviderPreferences.values()) { + ISafeRunnable runnable = new ISafeRunnable() { + + @Override + public void run() throws Exception { + block.performOk(); + } + + @Override + public void handleException(Throwable x) {} + }; + SafeRunner.run(runnable); + } + } + + @Override + public void performDefaults() { + restoreFromPreferences(); + for (ISpellingPreferenceBlock block : fProviderPreferences.values()) { + ISafeRunnable runnable = new ISafeRunnable() { + + @Override + public void run() throws Exception { + block.performDefaults(); + } + + @Override + public void handleException(Throwable x) {} + }; + SafeRunner.run(runnable); + } + } + + @Override + public void dispose() { + for (ISpellingPreferenceBlock block : fProviderPreferences.values()) { + ISafeRunnable runnable = new ISafeRunnable() { + + @Override + public void run() throws Exception { + block.dispose(); + } + + @Override + public void handleException(Throwable x) {} + }; + SafeRunner.run(runnable); + } + } + + private void restoreFromPreferences() { + if (fEnablementCheckbox == null) return; + + boolean enabled = fStore.getBoolean(SpellingService.PREFERENCE_SPELLING_ENABLED); + fEnablementCheckbox.setSelection(enabled); + + if (fProviderViewer == null) + updateListDependencies(); + else { + SpellingEngineDescriptor desc = EditorsUI.getSpellingService().getActiveSpellingEngineDescriptor(fStore); + if (desc != null) fProviderViewer.setSelection(new StructuredSelection(desc), true); + } + + updateCheckboxDependencies(); + } +} diff --git a/plugin/src/winterwell/markdown/spelling/StatusInfo.java b/plugin/src/winterwell/markdown/spelling/StatusInfo.java new file mode 100644 index 0000000..d576c33 --- /dev/null +++ b/plugin/src/winterwell/markdown/spelling/StatusInfo.java @@ -0,0 +1,177 @@ +package winterwell.markdown.spelling; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IStatus; + +import org.eclipse.ui.editors.text.EditorsUI; + +/** + * A settable IStatus. + * Can be an error, warning, info or ok. For error, info and warning states, + * a message describes the problem. + * + * @since 2.1 + */ +class StatusInfo implements IStatus { + + /** The message of this status. */ + private String fStatusMessage; + /** The severity of this status. */ + private int fSeverity; + + /** + * Creates a status set to OK (no message). + */ + public StatusInfo() { + this(OK, null); + } + + /** + * Creates a status with the given severity and message. + * + * @param severity the severity of this status: ERROR, WARNING, INFO and OK. + * @param message the message of this status. Applies only for ERROR, + * WARNING and INFO. + */ + public StatusInfo(int severity, String message) { + fStatusMessage= message; + fSeverity= severity; + } + + @Override + public boolean isOK() { + return fSeverity == IStatus.OK; + } + + /** + * Returns whether this status indicates a warning. + * + * @return true if this status has severity + * {@link IStatus#WARNING} and false otherwise + */ + public boolean isWarning() { + return fSeverity == IStatus.WARNING; + } + + /** + * Returns whether this status indicates an info. + * + * @return true if this status has severity + * {@link IStatus#INFO} and false otherwise + */ + public boolean isInfo() { + return fSeverity == IStatus.INFO; + } + + /** + * Returns whether this status indicates an error. + * + * @return true if this status has severity + * {@link IStatus#ERROR} and false otherwise + */ + public boolean isError() { + return fSeverity == IStatus.ERROR; + } + + @Override + public String getMessage() { + return fStatusMessage; + } + + /** + * Sets the status to ERROR. + * + * @param errorMessage the error message which can be an empty string, but not null + */ + public void setError(String errorMessage) { + Assert.isNotNull(errorMessage); + fStatusMessage= errorMessage; + fSeverity= IStatus.ERROR; + } + + /** + * Sets the status to WARNING. + * + * @param warningMessage the warning message which can be an empty string, but not null + */ + public void setWarning(String warningMessage) { + Assert.isNotNull(warningMessage); + fStatusMessage= warningMessage; + fSeverity= IStatus.WARNING; + } + + /** + * Sets the status to INFO. + * + * @param infoMessage the info message which can be an empty string, but not null + */ + public void setInfo(String infoMessage) { + Assert.isNotNull(infoMessage); + fStatusMessage= infoMessage; + fSeverity= IStatus.INFO; + } + + /** + * Sets the status to OK. + */ + public void setOK() { + fStatusMessage= null; + fSeverity= IStatus.OK; + } + + @Override + public boolean matches(int severityMask) { + return (fSeverity & severityMask) != 0; + } + + /** + * Returns always false. + * + * @see IStatus#isMultiStatus() + */ + @Override + public boolean isMultiStatus() { + return false; + } + + @Override + public int getSeverity() { + return fSeverity; + } + + @Override + public String getPlugin() { + return EditorsUI.PLUGIN_ID; + } + + /** + * Returns always null. + * + * @see IStatus#getException() + */ + @Override + public Throwable getException() { + return null; + } + + /** + * Returns always the error severity. + * + * @see IStatus#getCode() + */ + @Override + public int getCode() { + return fSeverity; + } + + /** + * Returns always null. + * + * @see IStatus#getChildren() + */ + @Override + public IStatus[] getChildren() { + return new IStatus[0]; + } + +} diff --git a/plugin/src/winterwell/markdown/spelling/StatusUtil.java b/plugin/src/winterwell/markdown/spelling/StatusUtil.java new file mode 100644 index 0000000..e4c6bed --- /dev/null +++ b/plugin/src/winterwell/markdown/spelling/StatusUtil.java @@ -0,0 +1,81 @@ +package winterwell.markdown.spelling; + +import org.eclipse.core.runtime.IStatus; + +import org.eclipse.jface.dialogs.DialogPage; +import org.eclipse.jface.dialogs.IMessageProvider; + +/** + * A utility class to work with IStatus. + * + * @since 3.1 + */ +public class StatusUtil { + + /** + * Compares two instances of {@link IStatus}. The more severe is returned: An error is more + * severe than a warning, and a warning is more severe than OK. If the two statuses have the + * same severity, the second is returned. + * + * @param s1 a status object + * @param s2 a status object + * @return the more severe status + */ + public static IStatus getMoreSevere(IStatus s1, IStatus s2) { + if (s1.getSeverity() > s2.getSeverity()) return s1; + + return s2; + } + + /** + * Finds the most severe status from a array of statuses. An error is more severe than a + * warning, and a warning is more severe than OK. + * + * @param status an array with status objects + * @return the most severe status object + */ + public static IStatus getMostSevere(IStatus[] status) { + IStatus max = null; + for (int i = 0; i < status.length; i++) { + IStatus curr = status[i]; + if (curr.matches(IStatus.ERROR)) { + return curr; + } + if (max == null || curr.getSeverity() > max.getSeverity()) { + max = curr; + } + } + return max; + } + + /** + * Applies the status to the status line of a dialog page. + * + * @param page the dialog page + * @param status the status + */ + public static void applyToStatusLine(DialogPage page, IStatus status) { + String message = status.getMessage(); + switch (status.getSeverity()) { + case IStatus.OK: + page.setMessage(message, IMessageProvider.NONE); + page.setErrorMessage(null); + break; + case IStatus.WARNING: + page.setMessage(message, IMessageProvider.WARNING); + page.setErrorMessage(null); + break; + case IStatus.INFO: + page.setMessage(message, IMessageProvider.INFORMATION); + page.setErrorMessage(null); + break; + default: + if (message.length() == 0) { + message = null; + } + page.setMessage(null); + page.setErrorMessage(message); + break; + } + } +} diff --git a/plugin/src/winterwell/markdown/spelling/TextEditorMessages.java b/plugin/src/winterwell/markdown/spelling/TextEditorMessages.java new file mode 100644 index 0000000..92500f0 --- /dev/null +++ b/plugin/src/winterwell/markdown/spelling/TextEditorMessages.java @@ -0,0 +1,153 @@ +package winterwell.markdown.spelling; + +import org.eclipse.osgi.util.NLS; + +/** + * Helper class to get NLSed messages. + * + * @since 2.1 + */ +public final class TextEditorMessages extends NLS { + + private static final String BUNDLE_NAME = TextEditorMessages.class.getName(); + + private TextEditorMessages() { + // Do not instantiate + } + + public static String AnnotationsConfigurationBlock_DASHED_BOX; + public static String EditorsPlugin_additionalInfo_affordance; + public static String EditorsPlugin_internal_error; + public static String LinkedModeConfigurationBlock_DASHED_BOX; + public static String TextEditorPreferencePage_displayedTabWidth; + public static String TextEditorPreferencePage_enableWordWrap; + public static String TextEditorPreferencePage_convertTabsToSpaces; + public static String TextEditorPreferencePage_undoHistorySize; + public static String TextEditorPreferencePage_printMarginColumn; + public static String TextEditorPreferencePage_showLineNumbers; + public static String TextEditorPreferencePage_highlightCurrentLine; + public static String TextEditorPreferencePage_showPrintMargin; + public static String TextEditorPreferencePage_color; + public static String TextEditorPreferencePage_appearanceOptions; + public static String TextEditorPreferencePage_lineNumberForegroundColor; + public static String TextEditorPreferencePage_currentLineHighlighColor; + public static String TextEditorPreferencePage_printMarginColor; + public static String TextEditorPreferencePage_foregroundColor; + public static String TextEditorPreferencePage_backgroundColor; + public static String TextEditorPreferencePage_findScopeColor; + public static String TextEditorPreferencePage_accessibility_disableCustomCarets; + public static String TextEditorPreferencePage_accessibility_wideCaret; + public static String TextEditorPreferencePage_accessibility_useSaturatedColorsInOverviewRuler; + public static String TextEditorPreferencePage_showAffordance; + public static String TextEditorPreferencePage_selectionForegroundColor; + public static String TextEditorPreferencePage_selectionBackgroundColor; + public static String TextEditorPreferencePage_systemDefault; + public static String TextEditorPreferencePage_invalidInput; + public static String TextEditorPreferencePage_invalidRange; + public static String TextEditorPreferencePage_emptyInput; + public static String TextEditorPreferencePage_colorsAndFonts_link; + public static String TextEditorPreferencePage_Font_link; + public static String QuickDiffConfigurationBlock_description; + public static String QuickDiffConfigurationBlock_referenceProviderTitle; + public static String QuickDiffConfigurationBlock_referenceProviderNoteMessage; + public static String QuickDiffConfigurationBlock_referenceProviderNoteTitle; + public static String QuickDiffConfigurationBlock_characterMode; + public static String QuickDiffConfigurationBlock_showForNewEditors; + public static String QuickDiffConfigurationBlock_showInOverviewRuler; + public static String QuickDiffConfigurationBlock_colorTitle; + public static String QuickDiffConfigurationBlock_changeColor; + public static String QuickDiffConfigurationBlock_additionColor; + public static String QuickDiffConfigurationBlock_deletionColor; + public static String NewTextEditorAction_namePrefix; + public static String AnnotationsConfigurationBlock_description; + public static String AnnotationsConfigurationBlock_showInText; + public static String AnnotationsConfigurationBlock_showInOverviewRuler; + public static String AnnotationsConfigurationBlock_showInVerticalRuler; + public static String AnnotationsConfigurationBlock_isNavigationTarget; + public static String AnnotationsConfigurationBlock_annotationPresentationOptions; + public static String AnnotationsConfigurationBlock_SQUIGGLES; + public static String AnnotationsConfigurationBlock_PROBLEM_UNDERLINE; + public static String AnnotationsConfigurationBlock_UNDERLINE; + public static String AnnotationsConfigurationBlock_BOX; + public static String AnnotationsConfigurationBlock_IBEAM; + public static String AnnotationsConfigurationBlock_HIGHLIGHT; + public static String AnnotationsConfigurationBlock_labels_showIn; + public static String AnnotationsConfigurationBlock_color; + public static String HyperlinkDetectorsConfigurationBlock_description; + public static String HyperlinkDetectorTable_nameColumn; + public static String HyperlinkDetectorTable_modifierKeysColumn; + public static String HyperlinkDetectorTable_targetNameColumn; + public static String SelectResourcesDialog_filterSelection; + public static String SelectResourcesDialog_deselectAll; + public static String SelectResourcesDialog_selectAll; + public static String SelectResourcesDialog_noFilesSelected; + public static String SelectResourcesDialog_oneFileSelected; + public static String SelectResourcesDialog_nFilesSelected; + public static String ConvertLineDelimitersAction_convert_all; + public static String ConvertLineDelimitersAction_convert_text; + public static String ConvertLineDelimitersAction_default_label; + public static String ConvertLineDelimitersAction_dialog_title; + public static String ConvertLineDelimitersToWindows_label; + public static String ConvertLineDelimitersToUnix_label; + public static String ConvertLineDelimitersAction_dialog_description; + public static String ConvertLineDelimitersAction_nontext_selection; + public static String ConvertLineDelimitersAction_show_only_text_files; + public static String RemoveTrailingWhitespaceHandler_dialog_title; + public static String RemoveTrailingWhitespaceHandler_dialog_description; + public static String HyperlinksEnabled_label; + public static String HyperlinkColor_label; + public static String HyperlinkKeyModifier_label; + public static String HyperlinkDefaultKeyModifier_label; + public static String HyperlinkKeyModifier_error_modifierIsNotValid; + public static String HyperlinkKeyModifier_error_shiftIsDisabled; + public static String HyperlinkKeyModifier_delimiter; + public static String HyperlinkKeyModifier_concatModifierStrings; + public static String HyperlinkKeyModifier_insertDelimiterAndModifier; + public static String HyperlinkKeyModifier_insertDelimiterAndModifierAndDelimiter; + public static String HyperlinkKeyModifier_insertModifierAndDelimiter; + public static String AccessibilityPreferencePage_accessibility_title; + public static String SpellingConfigurationBlock_enable; + public static String SpellingConfigurationBlock_combo_caption; + public static String SpellingConfigurationBlock_info_no_preferences; + public static String SpellingConfigurationBlock_error_not_installed; + public static String SpellingConfigurationBlock_error_not_exist; + public static String SpellingConfigurationBlock_error_title; + public static String SpellingConfigurationBlock_error_message; + + static { + NLS.initializeMessages(BUNDLE_NAME, TextEditorMessages.class); + } + + public static String TextEditorDefaultsPreferencePage_carriageReturn; + public static String TextEditorDefaultsPreferencePage_transparencyLevel; + public static String TextEditorDefaultsPreferencePage_configureWhitespaceCharacterPainterProperties; + public static String TextEditorDefaultsPreferencePage_enclosed; + public static String TextEditorDefaultsPreferencePage_enrichHoverMode; + public static String TextEditorDefaultsPreferencePage_enrichHover_immediately; + public static String TextEditorDefaultsPreferencePage_enrichHover_afterDelay; + public static String TextEditorDefaultsPreferencePage_enrichHover_disabled; + public static String TextEditorDefaultsPreferencePage_enrichHover_onClick; + public static String TextEditorDefaultsPreferencePage_ideographicSpace; + public static String TextEditorDefaultsPreferencePage_leading; + public static String TextEditorDefaultsPreferencePage_lineFeed; + public static String TextEditorDefaultsPreferencePage_range_indicator; + public static String TextEditorDefaultsPreferencePage_smartHomeEnd; + public static String TextEditorDefaultsPreferencePage_warn_if_derived; + public static String TextEditorDefaultsPreferencePage_showWhitespaceCharacters; + public static String TextEditorDefaultsPreferencePage_showWhitespaceCharactersLinkText; + public static String TextEditorDefaultsPreferencePage_showWhitespaceCharactersDialogInvalidInput; + public static String TextEditorDefaultsPreferencePage_showWhitespaceCharactersDialogTitle; + public static String TextEditorDefaultsPreferencePage_space; + public static String TextEditorDefaultsPreferencePage_tab; + public static String TextEditorDefaultsPreferencePage_textDragAndDrop; + public static String TextEditorDefaultsPreferencePage_trailing; + public static String LinkedModeConfigurationBlock_annotationPresentationOptions; + public static String LinkedModeConfigurationBlock_SQUIGGLES; + public static String LinkedModeConfigurationBlock_UNDERLINE; + public static String LinkedModeConfigurationBlock_BOX; + public static String LinkedModeConfigurationBlock_IBEAM; + public static String LinkedModeConfigurationBlock_HIGHLIGHT; + public static String LinkedModeConfigurationBlock_labels_showIn; + public static String LinkedModeConfigurationBlock_color; + public static String LinkedModeConfigurationBlock_linking_title; +} diff --git a/plugin/src/winterwell/markdown/spelling/TextEditorMessages.properties b/plugin/src/winterwell/markdown/spelling/TextEditorMessages.properties new file mode 100644 index 0000000..053cf9f --- /dev/null +++ b/plugin/src/winterwell/markdown/spelling/TextEditorMessages.properties @@ -0,0 +1,160 @@ +############################################################################### +# Copyright (c) 2000, 2015 IBM Corporation and others. +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# Contributors: +# IBM Corporation - initial API and implementation +############################################################################### + +EditorsPlugin_additionalInfo_affordance=Press 'Tab' from proposal table or click for focus +EditorsPlugin_internal_error=Internal Error + +TextEditorPreferencePage_displayedTabWidth=Displayed &tab width: +TextEditorPreferencePage_enableWordWrap=&Enable word wrap when opening an editor +TextEditorPreferencePage_convertTabsToSpaces=&Insert spaces for tabs +TextEditorPreferencePage_undoHistorySize=&Undo history size: +TextEditorPreferencePage_printMarginColumn=Print margin colu&mn: +TextEditorPreferencePage_showLineNumbers=Show line num&bers +TextEditorPreferencePage_highlightCurrentLine=Hi&ghlight current line +TextEditorPreferencePage_showPrintMargin=Sho&w print margin +TextEditorPreferencePage_color=&Color: +TextEditorPreferencePage_appearanceOptions=Appearance co&lor options: +TextEditorPreferencePage_lineNumberForegroundColor=Line number foreground +TextEditorPreferencePage_currentLineHighlighColor=Current line highlight +TextEditorPreferencePage_printMarginColor=Print margin +TextEditorPreferencePage_foregroundColor=Foreground color +TextEditorPreferencePage_backgroundColor=Background color +TextEditorPreferencePage_findScopeColor=Find scope +TextEditorPreferencePage_accessibility_disableCustomCarets= Use &custom caret +TextEditorPreferencePage_accessibility_wideCaret= &Enable thick caret +TextEditorPreferencePage_accessibility_useSaturatedColorsInOverviewRuler=U&se saturated colors in overview ruler +TextEditorDefaultsPreferencePage_carriageReturn=Carriage Return ( \u00a4 ) +TextEditorDefaultsPreferencePage_transparencyLevel=&Transparency level (0 is transparent and 255 is opaque): +TextEditorDefaultsPreferencePage_configureWhitespaceCharacterPainterProperties=Configure visibility of whitespace characters in different regions of a line of text: +TextEditorDefaultsPreferencePage_enclosed=Enclosed +TextEditorDefaultsPreferencePage_enrichHoverMode=When mouse mo&ved into hover: +TextEditorDefaultsPreferencePage_enrichHover_afterDelay=Enrich after delay +TextEditorDefaultsPreferencePage_enrichHover_disabled=Close hover +TextEditorDefaultsPreferencePage_enrichHover_immediately=Enrich immediately +TextEditorDefaultsPreferencePage_enrichHover_onClick=Enrich on click +TextEditorDefaultsPreferencePage_ideographicSpace=Ideographic space ( \u00b0 ) +TextEditorDefaultsPreferencePage_leading=Leading +TextEditorDefaultsPreferencePage_lineFeed=Line Feed ( \u00b6 ) +TextEditorDefaultsPreferencePage_range_indicator=Show &range indicator +TextEditorDefaultsPreferencePage_warn_if_derived= War&n before editing a derived file +TextEditorDefaultsPreferencePage_smartHomeEnd= &Smart caret positioning at line start and end +TextEditorDefaultsPreferencePage_showWhitespaceCharacters= Sh&ow whitespace characters +TextEditorDefaultsPreferencePage_showWhitespaceCharactersLinkText= (configure visibility) +TextEditorDefaultsPreferencePage_showWhitespaceCharactersDialogInvalidInput=''{0}'' is not a valid input. +TextEditorDefaultsPreferencePage_showWhitespaceCharactersDialogTitle=Show Whitespace Characters +TextEditorDefaultsPreferencePage_space=Space ( \u00b7 ) +TextEditorDefaultsPreferencePage_tab=Tab ( \u00bb ) +TextEditorDefaultsPreferencePage_textDragAndDrop= Enable drag and dro&p of text +TextEditorDefaultsPreferencePage_trailing=Trailing +TextEditorPreferencePage_colorsAndFonts_link= More colors can be configured on the 'Colors and Fonts' preference page. +TextEditorPreferencePage_Font_link= Some editors may not honor all of these settings.\n\ +\n\ +See 'Colors and Fonts' to configure the font. + +TextEditorPreferencePage_selectionForegroundColor= Selection foreground color +TextEditorPreferencePage_selectionBackgroundColor= Selection background color +TextEditorPreferencePage_systemDefault= System De&fault + +TextEditorPreferencePage_invalidInput= ''{0}'' is not a valid input. +TextEditorPreferencePage_invalidRange= Value must be in between ''{0}'' and ''{1}''. +TextEditorPreferencePage_emptyInput= Empty input. + +TextEditorPreferencePage_showAffordance= S&how affordance in hover on how to make it sticky + + +QuickDiffConfigurationBlock_description= General Quick Diff settings. +QuickDiffConfigurationBlock_referenceProviderTitle= &Use this reference source: +QuickDiffConfigurationBlock_referenceProviderNoteMessage= Changing the reference source does not update open editors. +QuickDiffConfigurationBlock_referenceProviderNoteTitle= Note: +QuickDiffConfigurationBlock_characterMode= &Use characters to show changes in vertical ruler +QuickDiffConfigurationBlock_showForNewEditors= &Enable quick diff +QuickDiffConfigurationBlock_showInOverviewRuler= Show differences in &overview ruler +QuickDiffConfigurationBlock_colorTitle= Colo&rs +QuickDiffConfigurationBlock_changeColor= C&hanges: +QuickDiffConfigurationBlock_additionColor= Addi&tions: +QuickDiffConfigurationBlock_deletionColor= De&letions: + +NewTextEditorAction_namePrefix=Untitled + +AnnotationsConfigurationBlock_description= General annotation settings. +AnnotationsConfigurationBlock_showInText=&Text as +AnnotationsConfigurationBlock_DASHED_BOX=Dashed Box +AnnotationsConfigurationBlock_showInOverviewRuler=&Overview ruler +AnnotationsConfigurationBlock_showInVerticalRuler=&Vertical ruler +AnnotationsConfigurationBlock_isNavigationTarget=&Include in next/previous navigation +AnnotationsConfigurationBlock_annotationPresentationOptions=Annotation ty&pes: +AnnotationsConfigurationBlock_SQUIGGLES=Squiggly Line +AnnotationsConfigurationBlock_PROBLEM_UNDERLINE=Native Problem Underline +AnnotationsConfigurationBlock_UNDERLINE=Underlined +AnnotationsConfigurationBlock_BOX=Box +AnnotationsConfigurationBlock_IBEAM=Vertical Bar +AnnotationsConfigurationBlock_HIGHLIGHT=Highlighted +AnnotationsConfigurationBlock_labels_showIn=Show in +AnnotationsConfigurationBlock_color=&Color: + +HyperlinkDetectorsConfigurationBlock_description= On demand hyperlinks are shown when moving the mouse in the editor while the specified modifier is pressed. The hyperlinks appear on mouse move when no modifier is specified.\n +HyperlinksEnabled_label= &Enable on demand hyperlink style navigation +HyperlinkDetectorTable_nameColumn= Link Kind +HyperlinkDetectorTable_modifierKeysColumn= Modifier Keys +HyperlinkDetectorTable_targetNameColumn= Available In +HyperlinkColor_label=Hyperlink +HyperlinkDefaultKeyModifier_label= De&fault modifier key: +HyperlinkKeyModifier_label= &Modifier keys for selected detector: +HyperlinkKeyModifier_error_modifierIsNotValid= Modifier ''{0}'' is not valid. +HyperlinkKeyModifier_error_shiftIsDisabled= The modifier 'Shift' is not allowed because 'Shift' + click sets a new selection. +HyperlinkKeyModifier_delimiter= + +HyperlinkKeyModifier_concatModifierStrings= {0} + {1} +HyperlinkKeyModifier_insertDelimiterAndModifier= \ + {0} +# The following two property values need to end with a space +HyperlinkKeyModifier_insertDelimiterAndModifierAndDelimiter= \ + {0} +\ +HyperlinkKeyModifier_insertModifierAndDelimiter= \ {0} +\ + +SelectResourcesDialog_filterSelection= &Filter Selection... +SelectResourcesDialog_deselectAll= &Deselect All +SelectResourcesDialog_selectAll= &Select All +SelectResourcesDialog_noFilesSelected= No file selected. +SelectResourcesDialog_oneFileSelected= 1 file selected. +SelectResourcesDialog_nFilesSelected= {0} files selected. + +ConvertLineDelimitersAction_convert_all=Convert &All +ConvertLineDelimitersAction_convert_text=Convert &Text Files +ConvertLineDelimitersAction_default_label=\ [default] +ConvertLineDelimitersAction_dialog_title=Convert Line Delimiters to {0} +ConvertLineDelimitersToWindows_label=&Windows (CRLF, \\r\\n, 0D0A, \u00A4\u00B6) +ConvertLineDelimitersToUnix_label=&Unix (LF, \\n, 0A, \u00B6) +ConvertLineDelimitersAction_dialog_description=Select files to convert: +ConvertLineDelimitersAction_nontext_selection=The selection contains files that could have binary content. Do you want to convert all files or only text files? +ConvertLineDelimitersAction_show_only_text_files=Show only &text files +RemoveTrailingWhitespaceHandler_dialog_title=Remove Trailing Whitespace +RemoveTrailingWhitespaceHandler_dialog_description=Select files: + + +AccessibilityPreferencePage_accessibility_title=Accessibility + +SpellingConfigurationBlock_enable= &Enable spell checking +SpellingConfigurationBlock_combo_caption= Select spelling engine to &use: +SpellingConfigurationBlock_info_no_preferences= The selected spelling engine did not provide a preference control +SpellingConfigurationBlock_error_not_installed= The spelling service is not installed. +SpellingConfigurationBlock_error_not_exist= The selected spelling engine does not exist +SpellingConfigurationBlock_error_title=Dismiss Changes +SpellingConfigurationBlock_error_message=The currently displayed spelling engine preferences contain invalid values. Dismiss changes? + +# linked mode +LinkedModeConfigurationBlock_annotationPresentationOptions= &Ranges: +LinkedModeConfigurationBlock_SQUIGGLES=Squiggles +LinkedModeConfigurationBlock_UNDERLINE=Underlined +LinkedModeConfigurationBlock_BOX=Box +LinkedModeConfigurationBlock_IBEAM=Vertical Bar +LinkedModeConfigurationBlock_HIGHLIGHT=Highlighted +LinkedModeConfigurationBlock_DASHED_BOX=Dashed Box +LinkedModeConfigurationBlock_labels_showIn=&Show in text as: +LinkedModeConfigurationBlock_color= C&olor: +LinkedModeConfigurationBlock_linking_title=Lin&ked Mode diff --git a/plugin/src/winterwell/markdown/util/Environment.java b/plugin/src/winterwell/markdown/util/Environment.java new file mode 100644 index 0000000..ffdd1f2 --- /dev/null +++ b/plugin/src/winterwell/markdown/util/Environment.java @@ -0,0 +1,114 @@ +package winterwell.markdown.util; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Stack; + +public final class Environment implements IProperties { + + private static final Map defaultProperties = new HashMap(); + private static final Environment dflt = new Environment(); + private final ThreadLocal>> localVars = new ThreadLocal>>() { + + protected Map> initialValue() { + return new HashMap<>(); + } + }; + + public Environment() {} + + public static Environment get() { + return dflt; + } + + public static void putDefault(Key key, Object value) { + if (value == null) { + defaultProperties.remove(key); + } else { + defaultProperties.put(key, value); + } + } + + public boolean containsKey(Key key) { + Object result = get(key); + return result != null; + } + + public Object get(Key key) { + if (key == null) { + throw new AssertionError(); + } + Map> properties = localVars.get(); + Object v = properties.get(key); + if (v == null) v = defaultProperties.get(key); + return v; + } + + public Collection getKeys() { + Map> properties = localVars.get(); + HashSet keys = new HashSet(); + keys.addAll(properties.keySet()); + keys.addAll(defaultProperties.keySet()); + return keys; + } + + public boolean isTrue(Key key) { + Boolean v = (Boolean) get(key); + return v != null && v.booleanValue(); + } + + public Object pop(Key key) { + if (key == null) { + throw new AssertionError(); + } + Map> properties = localVars.get(); + Key stackKey = new StackKey(key); + Stack stack = (Stack) properties.get(stackKey); + if (stack == null) { + throw new AssertionError(); + } else { + Object oldValue = stack.pop(); + Object newValue = stack.peek(); + put(key, newValue); + return oldValue; + } + } + + public void push(Key key, Object value) { + if ((key == null || value == null)) throw new AssertionError(); + Map> properties = localVars.get(); + put(key, value); + Key stackKey = new StackKey(key); + Stack stack = properties.get(stackKey); + if (stack == null) { + stack = new Stack(); + properties.put(stackKey, stack); + } + stack.push(value); + } + + @SuppressWarnings("unchecked") + public Object put(Key key, Object value) { + if (key == null) throw new AssertionError(); + Map> properties = localVars.get(); + if (value == null) return properties.remove(key); + return properties.put(key, (Stack) value); + } + + public void reset() { + localVars.remove(); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + Key k; + for (Iterator iterator = getKeys().iterator(); iterator.hasNext(); sb + .append((new StringBuilder()).append(k).append(": ").append(get(k)).toString())) { + k = iterator.next(); + } + return sb.toString(); + } +} diff --git a/plugin/src/winterwell/markdown/util/FailureException.java b/plugin/src/winterwell/markdown/util/FailureException.java new file mode 100644 index 0000000..b9aefd0 --- /dev/null +++ b/plugin/src/winterwell/markdown/util/FailureException.java @@ -0,0 +1,55 @@ +package winterwell.markdown.util; + +import java.util.Arrays; +import java.util.List; + +public class FailureException extends RuntimeException { + + private String source; + + public FailureException() { + super(); + } + + public FailureException(Exception e) { + super(e); + } + + public FailureException(String msg) { + super((new StringBuilder(String.valueOf(msg))).append(" @").append(getCaller(new String[0])).toString()); + } + + public FailureException(String msg, Exception cause) { + super((new StringBuilder(String.valueOf(msg))).append(" @").append(getCaller(new String[0])).toString(), cause); + } + + public FailureException(String source, String msg) { + super((new StringBuilder(String.valueOf(source))).append(": ").append(msg).append(" @") + .append(getCaller(new String[0])).toString()); + this.source = source; + } + + public String getSource() { + return source; + } + + public static StackTraceElement getCaller(String ignore[]) { + List ignoreNames = Arrays.asList(ignore); + StackTraceElement trace[]; + int i; + try { + throw new Exception(); + } catch (Exception e) { + trace = e.getStackTrace(); + i = 2; + } + for (; i < trace.length; i++) { + String clazz = trace[2].getClassName(); + String method = trace[2].getMethodName(); + if (!ignoreNames.contains(clazz) && !ignoreNames.contains(method)) { + return trace[2]; + } + } + return new StackTraceElement("filtered", "?", null, -1); + } +} diff --git a/plugin/src/winterwell/markdown/util/FileUtils.java b/plugin/src/winterwell/markdown/util/FileUtils.java new file mode 100644 index 0000000..a1966e1 --- /dev/null +++ b/plugin/src/winterwell/markdown/util/FileUtils.java @@ -0,0 +1,1373 @@ +package winterwell.markdown.util; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.UnsupportedEncodingException; + +public final class FileUtils { + + public static final String UTF8 = "UTF8"; + public static final char UTF8_BOM = 65279; + + public FileUtils() {} + + // public static void append(String string, File file) { + // try { + // BufferedWriter w = getWriter(new FileOutputStream(file, true)); + // w.write(string); + // close(w); + // } catch (IOException e) { + // throw new RuntimeException(e); + // } + // } + // + // public static File changeType(File file, String type) { + // String fName = file.getName(); + // int i = fName.lastIndexOf('.'); + // if (type.length() == 0) { + // if (i == -1) { + // return file; + // } else { + // fName = fName.substring(0, i); + // return new File(file.getParentFile(), fName); + // } + // } + // if (type.charAt(0) == '.') { + // type = type.substring(1); + // } + // if (type.length() <= 0) { + // throw new AssertionError(); + // } + // if (i == -1) { + // fName = (new StringBuilder(String.valueOf(fName))).append(".").append(type).toString(); + // } else { + // fName = (new StringBuilder(String.valueOf(fName.substring(0, i + + // 1)))).append(type).toString(); + // } + // return new File(file.getParentFile(), fName); + // } + + public static void close(Closeable io) { + if (io == null) { + return; + } + try { + io.close(); + } catch (IOException e) { + if (e.getMessage() != null && e.getMessage().contains("Closed")) { + return; + } + e.printStackTrace(); + } + } + + // public static File copy(File in, File out) { + // return copy(in, out, true); + // } + // + // public static File copy(File in, File out, boolean overwrite) + // throws RuntimeException + // { + // if( !in.exists()) + // { + // throw new AssertionError((new StringBuilder("File does not exist: + // ")).append(in.getAbsolutePath()).toString()); + // } + // if( in.equals(out)) + // { + // throw new AssertionError((new StringBuilder()).append(in).append(" = ").append(out).append(" + // can cause a delete!").toString()); + // } + // if(in.isDirectory()) + // { + // ArrayList failed = new ArrayList(); + // copyDir(in, out, overwrite, failed); + // if(failed.size() != 0) + // { + // throw new RuntimeException((new StringBuilder("Could not copy files: + // ")).append(Printer.toString(failed)).toString()); + // } else + // { + // return out; + // } + // } + // if(out.isDirectory()) + // { + // out = new File(out, in.getName()); + // } + // if(out.exists() && !overwrite) + // { + // throw new RuntimeException((new StringBuilder("Copy failed: ")).append(out).append(" already + // exists.").toString()); + // } + // copy(((InputStream) (new FileInputStream(in))), out); + // return out; + // IOException e; + // e; + // throw new RuntimeException((new StringBuilder(String.valueOf(e.getMessage()))).append(" + // copying ").append(in.getAbsolutePath()).append(" to + // ").append(out.getAbsolutePath()).toString()); + // } + // + // public static void copy(InputStream in, File out) { + // if ((in == null || out == null)) { + // throw new AssertionError(); + // } + // if (!out.getParentFile().isDirectory()) { + // throw new RuntimeException( + // (new StringBuilder("Directory does not exist: ")).append(out.getParentFile()).toString()); + // } + // try { + // FileOutputStream outStream = new FileOutputStream(out); + // copy(in, ((OutputStream) (outStream))); + // } catch (IOException e) { + // throw new RuntimeException(e); + // } + // } + // + // public static void copy(InputStream in, OutputStream out) + // { + // try + // { + // byte bytes[] = new byte[20480]; + // do + // { + // int len = in.read(bytes); + // if(len == -1) + // { + // break; + // } + // out.write(bytes, 0, len); + // } while(true); + // } + // catch(IOException e) + // { + // throw new RuntimeException(e); + // } + // break MISSING_BLOCK_LABEL_53; + // Exception exception; + // exception; + // close(in); + // close(out); + // throw exception; + // close(in); + // close(out); + // return; + // } + // + // private static void copyDir(File in, File out, boolean overwrite, List failed) { + // if (!in.isDirectory()) { + // throw new AssertionError(in); + // } + // if (!out.exists()) { + // boolean ok = out.mkdir(); + // if (!ok) { + // failed.add(in); + // return; + // } + // } + // if (!out.isDirectory()) { + // throw new AssertionError(out); + // } + // File afile[]; + // int j = (afile = in.listFiles()).length; + // for (int i = 0; i < j; i++) { + // File f = afile[i]; + // if (f.isDirectory()) { + // File subOut = new File(out, f.getName()); + // copyDir(f, subOut, overwrite, failed); + // } else { + // try { + // copy(f, out, overwrite); + // } catch (RuntimeException e) { + // failed.add(f); + // } + // } + // } + // + // } + // + // public static File createTempDir() + // { + // File f; + // f = File.createTempFile("tmp", "dir"); + // if(f.exists()) + // { + // delete(f); + // } + // f.mkdirs(); + // return f; + // Exception e; + // e; + // throw Utils.runtime(e); + // } + // + // public static File createTempFile(String prefix, String suffix) + // { + // return File.createTempFile(prefix, suffix); + // IOException e; + // e; + // throw new RuntimeException(e); + // } + + public static void delete(File file) { + if (!file.exists()) return; + if (file.delete()) return; + System.gc(); + if (file.delete()) return; + + try { + Thread.sleep(50L); + } catch (InterruptedException interruptedexception) {} + file.delete(); + if (!file.exists()) return; + + // if (file.isDirectory() && isSymLink(file) + // && (Utils.getOperatingSystem().contains("linux") || + // Utils.getOperatingSystem().contains("unix"))) { + // String path = file.getAbsolutePath(); + // Process p = new Process((new StringBuilder("rm -f ")).append(path).toString()); + // p.run(); + // p.waitFor(1000L); + // if (!file.exists()) { + // return; + // } else { + // throw new RuntimeException(new IOException((new StringBuilder("Could not delete + // file")).append(file) + // .append("; ").append(p.getError()).toString())); + // } + // } else { + // throw new RuntimeException( + // new IOException((new StringBuilder("Could not delete file ")).append(file).toString())); + // } + throw new RuntimeException( + new IOException((new StringBuilder("Could not delete file ")).append(file).toString())); + } + + // public static void deleteDir(File file) { + // if (!file.isDirectory()) { + // throw new RuntimeException((new StringBuilder()).append(file).append(" is not a + // directory").toString()); + // } + // if (isSymLink(file)) { + // delete(file); + // return; + // } + // File afile[]; + // int j = (afile = file.listFiles()).length; + // for (int i = 0; i < j; i++) { + // File f = afile[i]; + // if (f.isDirectory()) { + // deleteDir(f); + // } else { + // delete(f); + // } + // } + // + // delete(file); + // } + // + // private static void deleteNative(File out) { + // if (!Utils.OSisUnix()) { + // throw new TodoException((new StringBuilder()).append(out).toString()); + // } + // Process p = new Process((new StringBuilder("rm -f + // ")).append(out.getAbsolutePath()).toString()); + // p.run(); + // int ok = p.waitFor(); + // if (ok != 0) { + // throw new RuntimeException(p.getError()); + // } else { + // return; + // } + // } + // + // public static String filenameDecode(String name) { + // name = name.replace("//", ""); + // name = name.replace("_", "%"); + // name = name.replace("%%", "_"); + // String original = WebUtils.urlDecode(name); + // original = original.replace("%2E", "."); + // original = original.replace("%3B", ";"); + // original = original.replace("%2F", "/"); + // return original; + // } + // + // public static String filenameEncode(String name) { + // String url = WebUtils.urlEncode(name); + // url = url.replace("..", ".%2E"); + // url = url.replace(";", "%3B"); + // url = url.replace("%2F", "/"); + // url = url.replace("//", "/%2F"); + // url = url.replace("_", "__"); + // url = url.replace("%", "_"); + // String bits[] = url.split("/"); + // StringBuilder path = new StringBuilder(url.length()); + // boolean dbl = false; + // String as[]; + // int k = (as = bits).length; + // for (int j = 0; j < k; j++) { + // String bit = as[j]; + // if (bit.length() == 0) { + // System.out.println(path); + // } + // for (int i = 0; i < bit.length(); i += 240) { + // int e = Math.min(bit.length(), i + 240); + // path.append(bit.substring(i, e)); + // if (e == bit.length()) { + // path.append("/"); + // dbl = false; + // } else { + // path.append("//"); + // dbl = true; + // } + // } + // + // } + // + // StrUtils.pop(path, dbl ? 2 : 1); + // if (url.endsWith("/")) { + // path.append('/'); + // } + // return path.toString(); + // } + // + // public static List find(File baseDir, FileFilter filter) { + // return find(baseDir, filter, true); + // } + // + // public static List find(File baseDir, FileFilter filter, boolean includeHiddenFiles) { + // if (!baseDir.isDirectory()) { + // throw new IllegalArgumentException((new + // StringBuilder(String.valueOf(baseDir.getAbsolutePath()))) + // .append(" is not a directory").toString()); + // } else { + // List files = new ArrayList(); + // find2(baseDir, filter, files, includeHiddenFiles); + // return files; + // } + // } + // + // public static List find(File baseDir, String regex) { + // return find(baseDir, ((FileFilter) (new RegexFileFilter(regex)))); + // } + // + // private static void find2(File baseDir, FileFilter filter, List files, boolean + // includeHiddenFiles) { + // if ((baseDir == null || filter == null || files == null)) { + // throw new AssertionError(); + // } + // File afile[]; + // int j = (afile = baseDir.listFiles()).length; + // for (int i = 0; i < j; i++) { + // File f = afile[i]; + // if (!f.equals(baseDir) && (includeHiddenFiles || !f.isHidden())) { + // if (!includeHiddenFiles && f.getName().startsWith(".")) { + // throw new AssertionError(f); + // } + // if (filter.accept(f)) { + // files.add(f); + // } + // if (f.isDirectory()) { + // find2(f, filter, files, includeHiddenFiles); + // } + // } + // } + // + // } + // + // private static List getAllClasses(File root) throws IOException { + // if (root == null) { + // throw new AssertionError("Root cannot be null"); + // } else { + // List classNames = new ArrayList(); + // String path = root.getCanonicalPath(); + // getAllClasses(root, path.length() + 1, classNames); + // return classNames; + // } + // } + // + // private static void getAllClasses(File root, int prefixLength, List result) throws + // IOException { + // if (root == null) { + // throw new AssertionError("Root cannot be null"); + // } + // if (prefixLength < 0) { + // throw new AssertionError("Illegal index specifier"); + // } + // if (result == null) { + // throw new AssertionError("Missing return array"); + // } + // File afile[]; + // int j = (afile = root.listFiles()).length; + // for (int i = 0; i < j; i++) { + // File entry = afile[i]; + // if (entry.isDirectory()) { + // if (entry.canRead()) { + // getAllClasses(entry, prefixLength, result); + // } + // } else { + // String path = entry.getPath(); + // boolean isClass = path.endsWith(".class") && path.indexOf("$") < 0; + // if (isClass) { + // String name = entry.getCanonicalPath().substring(prefixLength); + // String className = name.replace(File.separatorChar, '.').substring(0, name.length() - 6); + // result.add(className); + // } + // } + // } + // + // } + // + // public static String getBasename(File file) { + // return getBasename(file.getName()); + // } + // + // public static String getBasename(String filename) { + // int i = filename.lastIndexOf('.'); + // if (i == -1) { + // return filename; + // } else { + // return filename.substring(0, i); + // } + // } + // + // public static String getBasenameCautious(String filename) { + // int i = filename.lastIndexOf('.'); + // if (i == -1) { + // return filename; + // } + // if (filename.length() - i > 5) { + // return filename; + // } else { + // return filename.substring(0, i); + // } + // } + // + // /** + // * @deprecated Method getDataFile is deprecated + // */ + // + // public static File getDataFile(String relativePath) { + // File f = new File(dataDir, relativePath); + // if (!f.getParentFile().exists()) { + // f.getParentFile().mkdirs(); + // } + // return f; + // } + // + // public static String getExtension(File f) { + // String filename = f.getName(); + // int i = filename.indexOf('.'); + // if (i == -1) { + // return ""; + // } else { + // return filename.substring(i).toLowerCase(); + // } + // } + // + // public static String getExtension(String filename) { + // return getExtension(new File(filename)); + // } + // + // public static File getNewFile(File file) { + // if (!file.exists()) { + // return file; + // } + // String path = file.getParent(); + // String name = file.getName(); + // int dotI = name.lastIndexOf('.'); + // String dotType = ""; + // String preType; + // if (dotI == -1) { + // preType = name; + // } else { + // preType = name.substring(0, dotI); + // dotType = name.substring(dotI); + // } + // for (int i = 2; i < 10000; i++) { + // File f = new File(path, (new + // StringBuilder(String.valueOf(preType))).append(i).append(dotType).toString()); + // if (!f.exists()) { + // return f; + // } + // } + // + // throw new RuntimeException( + // (new StringBuilder("Could not find a non-existing file name for ")).append(file).toString()); + // } + + public static BufferedReader getReader(File file) { + try { + return getReader(((InputStream) (new FileInputStream(file)))); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + public static BufferedReader getReader(InputStream in) { + InputStreamReader reader; + try { + reader = new InputStreamReader(in, "UTF8"); + return new BufferedReader(reader); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + // public static FileFilter getRegexFilter(String regex) { + // return new RegexFileFilter(regex); + // } + // + // public static String getRelativePath(File f, File base) throws IllegalArgumentException { + // String fp = resolveDotDot(f.getAbsolutePath()); + // String bp = resolveDotDot(base.getAbsolutePath()); + // if (!fp.startsWith(bp)) { + // if (f.equals(base)) { + // return ""; + // } else { + // throw new IllegalArgumentException((new StringBuilder()).append(f).append("=").append(fp) + // .append(" is not a sub-file of ").append(base).append("=").append(bp).toString()); + // } + // } + // String rp = fp.substring(bp.length()); + // char ec = rp.charAt(0); + // if (ec == '\\' || ec == '/') { + // rp = rp.substring(1); + // } + // return rp; + // } + // + // public static String getType(File f) { + // String fs = f.toString(); + // return getType(fs); + // } + // + // public static String getType(String filename) { + // int i = filename.lastIndexOf("."); + // if (i == -1 || i == filename.length() - 1) { + // return ""; + // } else { + // return filename.substring(i + 1).toLowerCase(); + // } + // } + // + // public static File getWinterwellDir() { + // File f; + // String dd = System.getenv("WINTERWELL_HOME"); + // if (Utils.isBlank(dd)) { + // break MISSING_BLOCK_LABEL_110; + // } + // if (dd.startsWith("~")) { + // String home = System.getProperty("user.home"); + // if (home != null) { + // dd = (new + // StringBuilder(String.valueOf(home))).append("/").append(dd.substring(1)).toString(); + // } + // } + // f = (new File(dd)).getCanonicalFile(); + // if (!f.exists()) { + // throw new FailureException( + // (new StringBuilder("Path does not exist: WINTERWELL_HOME = ")).append(f).toString()); + // } + // return f; + // File ddf; + // String home = System.getProperty("user.home"); + // if (Utils.isBlank(home)) { + // home = "/home"; + // } + // ddf = (new File(home, "winterwell")).getCanonicalFile(); + // if (ddf.exists() && ddf.isDirectory()) { + // return ddf; + // } + // try { + // throw new FailureException("Could not find directory - environment variable WINTERWELL_HOME + // is not set."); + // } catch (IOException e) { + // throw Utils.runtime(e); + // } + // } + // + // public static File getWorkingDirectory() + // { + // return (new File(".")).getCanonicalFile(); + // IOException e; + // e; + // throw new RuntimeException(e); + // } + + // public static BufferedWriter getWriter(OutputStream out) + // { + // OutputStreamWriter writer = new OutputStreamWriter(out, "UTF8"); + // return new BufferedWriter(writer); + // UnsupportedEncodingException e; + // e; + // throw new RuntimeException(e); + // } + // + // public static BufferedReader getZippedReader(File file) + // { + // GZIPInputStream zos; + // FileInputStream fos = new FileInputStream(file); + // zos = new GZIPInputStream(fos); + // return getReader(zos); + // IOException ex; + // ex; + // throw Utils.runtime(ex); + // } + // + // public static BufferedWriter getZippedWriter(File file, boolean append) + // { + // GZIPOutputStream zos; + // FileOutputStream fos = new FileOutputStream(file, append); + // zos = new GZIPOutputStream(fos); + // return getWriter(zos); + // IOException ex; + // ex; + // throw Utils.runtime(ex); + // } + // + // public static Iterable grep(File baseDir, String regex, String fileNameRegex) { + // List files = find(baseDir, fileNameRegex); + // Pattern p = Pattern.compile(regex); + // List found = new ArrayList(); + // for (Iterator iterator = files.iterator(); iterator.hasNext();) { + // File file = (File) iterator.next(); + // String lines[] = StrUtils.splitLines(read(file)); + // String as[]; + // int j = (as = lines).length; + // for (int i = 0; i < j; i++) { + // String line = as[i]; + // if (p.matcher(line).find()) { + // found.add(new Pair2(line, file)); + // } + // } + // + // } + // + // return found; + // } + // + // public static boolean isSafe(String filename) { + // if (Utils.isBlank(filename)) { + // return false; + // } + // if (filename.contains("..")) { + // return false; + // } + // if (filename.contains(";")) { + // return false; + // } + // if (filename.contains("|")) { + // return false; + // } + // if (filename.contains(">")) { + // return false; + // } + // return !filename.contains("<"); + // } + // + // public static boolean isSymLink(File f) + // { + // File canon = f.getCanonicalFile(); + // if(!canon.getName().equals(f.getName())) + // { + // return true; + // } + // File parent; + // parent = f.getParentFile(); + // if(parent == null) + // { + // parent = f.getAbsoluteFile().getParentFile(); + // } + // if(parent == null) + // { + // return false; + // } + // File canonParent; + // parent = parent.getCanonicalFile(); + // canonParent = canon.getParentFile(); + // return !parent.equals(canonParent); + // IOException e; + // e; + // throw Utils.runtime(e); + // } + // + // public static Object load(File file) { + // BufferedReader reader = getReader(file); + // return XStreamUtils.serialiseFromXml(reader); + // } + // + // public static Properties loadProperties(File propsFile) { + // Exception exception; + // InputStream stream = null; + // Properties properties; + // try { + // stream = new FileInputStream(propsFile); + // Properties props = new Properties(); + // props.load(stream); + // properties = props; + // } catch (IOException e) { + // throw Utils.runtime(e); + // } finally { + // close(stream); + // } + // close(stream); + // return properties; + // throw exception; + // } + // + // public static File[] ls(File dir, String fileNameRegex) { + // if (!dir.isDirectory()) { + // throw new RuntimeException( + // (new StringBuilder()).append(dir).append(" is not a valid directory").toString()); + // } else { + // return dir.listFiles(getRegexFilter((new + // StringBuilder(".*")).append(fileNameRegex).toString())); + // } + // } + // + // public static void makeSymLink(File original, File out) { + // makeSymLink(original, out, true); + // } + // + // public static void makeSymLink(File original, File out, boolean overwrite) { + // if (!Utils.getOperatingSystem().contains("linux")) { + // throw new TodoException(); + // } + // if (original.getAbsolutePath().equals(out.getAbsolutePath())) { + // throw new IllegalArgumentException((new StringBuilder("Cannot sym-link to self: + // ")).append(original) + // .append(" = ").append(out).toString()); + // } + // if (!original.exists()) { + // throw new winterwell.utils.RuntimeException.FileNotFoundException(original); + // } + // if (!original.isDirectory() && !original.isFile()) { + // throw new RuntimeException((new StringBuilder("Weird: ")).append(original).toString()); + // } + // if (out.exists()) { + // if (overwrite) { + // delete(out); + // } else { + // throw new RuntimeException((new StringBuilder("Creating symlink failed: ")).append(out) + // .append(" already exists.").toString()); + // } + // } + // String err; + // original = original.getCanonicalFile(); + // ShellScript ss = new ShellScript( + // (new StringBuilder("ln -s ")).append(original).append(" ").append(out).toString()); + // ss.run(); + // ss.waitFor(); + // err = ss.getError(); + // if (Utils.isBlank(err)) { + // break MISSING_BLOCK_LABEL_276; + // } + // if (overwrite && err.contains("File exists")) { + // deleteNative(out); + // makeSymLink(original, out, overwrite); + // return; + // } + // try { + // throw new RuntimeException(err); + // } catch (Exception e) { + // throw Utils.runtime(e); + // } + // } + // + // public static File move(File src, File dest) throws RuntimeException { + // if (!src.exists()) { + // throw new AssertionError(); + // } + // File src2 = new File(src.getPath()); + // if (!src2.equals(src)) { + // throw new AssertionError(); + // } + // boolean ok = src2.renameTo(dest); + // if (ok) { + // return dest; + // } else { + // copy(src, dest); + // delete(src); + // return dest; + // } + // } + // + // public static int numLines(File file) { + // int cnt = 0; + // BufferedReader r = getReader(file); + // try { + // do { + // String line = r.readLine(); + // if (line == null) break; + // cnt++; + // } while (true); + // } catch (IOException e) { + // throw new RuntimeException(e); + // } + // return cnt; + // } + // + // public static void prepend(File file, String string) { + // if ((file.isDirectory() || string == null)) { + // throw new AssertionError(); + // } + // if (!file.exists() || file.length() == 0L) { + // write(file, string); + // return; + // } + // try { + // File temp = File.createTempFile("prepend", ""); + // write(temp, string); + // FileInputStream in = new FileInputStream(file); + // FileOutputStream out = new FileOutputStream(temp, true); + // copy(in, out); + // move(temp, file); + // } catch (IOException e) { + // throw new RuntimeException(e); + // } + // } + + public static String read(File file) throws RuntimeException { + try { + return read(((InputStream) (new FileInputStream(file)))); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + public static String read(InputStream in) { + return read(((Reader) (getReader(in)))); + } + + public static String read(Reader r) { + try (BufferedReader reader = (r instanceof BufferedReader) ? (BufferedReader) r : new BufferedReader(r);) { + int bufSize = 8192; + StringBuilder sb = new StringBuilder(bufSize); + char cbuf[] = new char[bufSize]; + do { + int chars = reader.read(cbuf); + if (chars == -1) break; + if (sb.length() == 0 && cbuf[0] == '\uFEFF') { + sb.append(cbuf, 1, chars - 1); + } else { + sb.append(cbuf, 0, chars); + } + } while (true); + return sb.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // public static byte[] readRaw(InputStream raw) + // { + // byte trimmed[]; + // byte all[] = new byte[10240]; + // int offset = 0; + // do + // { + // int space = all.length - offset; + // if(space < all.length / 8) + // { + // all = Arrays.copyOf(all, all.length * 2); + // space = all.length - offset; + // } + // int r = raw.read(all, offset, space); + // if(r == -1) + // { + // break; + // } + // offset += r; + // } while(true); + // trimmed = Arrays.copyOf(all, offset); + // return trimmed; + // Exception e; + // e; + // throw new RuntimeException(e); + // } + // + // public static String resolveDotDot(String absolutePath) + // { + // return (new File(absolutePath)).getCanonicalPath(); + // IOException e; + // e; + // throw Utils.runtime(e); + // } + // + // public static String safeFilename(String name) { + // return safeFilename(name, true); + // } + // + // public static String safeFilename(String name, boolean allowSubDirs) { + // if (name == null) { + // return "null"; + // } + // name = name.trim(); + // if (name.equals("")) { + // name = "empty"; + // } + // if (name.length() > 5000) { + // throw new IllegalArgumentException((new StringBuilder("Name is too long: + // ")).append(name).toString()); + // } + // name = name.replace("_", "__"); + // name = name.replace("..", "_."); + // name = name.replaceAll("[^ a-zA-Z0-9-_.~/\\\\]", ""); + // name = name.trim(); + // name = name.replaceAll("\\s+", "_"); + // if (!allowSubDirs) { + // name = name.replace("/", "_"); + // name = name.replace("\\", "_"); + // } + // for (; "./-\\".indexOf(name.charAt(name.length() - 1)) != -1; name = name.substring(0, + // name.length() - 1)) {} + // if (name.length() > 50) { + // name = (new StringBuilder(String.valueOf(name.substring(0, 10)))).append(name.hashCode()) + // .append(name.substring(name.length() - 10)).toString(); + // } + // return name; + // } + // + // public static void save(Object obj, File file) { + // if (file.getParentFile() != null) { + // file.getParentFile().mkdirs(); + // } + // write(file, XStreamUtils.serialiseToXml(obj)); + // } + // + // private static File setDataDir() + // { + // String dd = System.getenv("WINTERWELL_DATA"); + // if(!Utils.isBlank(dd)) + // { + // return (new File(dd)).getAbsoluteFile(); + // } + // File ddf; + // String home = System.getProperty("user.home"); + // ddf = new File(home, ".winterwell/data"); + // ddf.mkdirs(); + // if(ddf.exists() && ddf.isDirectory()) + // { + // return ddf; + // } + // File f; + // dd = "data"; + // f = (new File(dd)).getAbsoluteFile(); + // Log.report((new StringBuilder("Using fallback data directory ")).append(f).toString(), + // Level.WARNING); + // return f; + // Exception e; + // e; + // return null; + // } + + public static void write(File out, CharSequence page) { + try (BufferedWriter writer = getWriter(new FileOutputStream(out))) { + writer.append(page); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static BufferedWriter getWriter(File file) { + try { + return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF8")); + } catch (UnsupportedEncodingException | FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + public static BufferedWriter getWriter(OutputStream out) { + OutputStreamWriter writer; + try { + writer = new OutputStreamWriter(out, "UTF8"); + return new BufferedWriter(writer); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + // public static BufferedReader getZippedReader(File file) + // { + // GZIPInputStream zos; + // FileInputStream fos = new FileInputStream(file); + // zos = new GZIPInputStream(fos); + // return getReader(zos); + // IOException ex; + // ex; + // throw Utils.runtime(ex); + // } + // + // public static BufferedWriter getZippedWriter(File file, boolean append) + // { + // GZIPOutputStream zos; + // FileOutputStream fos = new FileOutputStream(file, append); + // zos = new GZIPOutputStream(fos); + // return getWriter(zos); + // IOException ex; + // ex; + // throw Utils.runtime(ex); + // } + // + // public static Iterable grep(File baseDir, String regex, String fileNameRegex) { + // List files = find(baseDir, fileNameRegex); + // Pattern p = Pattern.compile(regex); + // List found = new ArrayList(); + // for (Iterator iterator = files.iterator(); iterator.hasNext();) { + // File file = (File) iterator.next(); + // String lines[] = StrUtils.splitLines(read(file)); + // String as[]; + // int j = (as = lines).length; + // for (int i = 0; i < j; i++) { + // String line = as[i]; + // if (p.matcher(line).find()) { + // found.add(new Pair2(line, file)); + // } + // } + // + // } + // + // return found; + // } + // + // public static boolean isSafe(String filename) { + // if (Utils.isBlank(filename)) { + // return false; + // } + // if (filename.contains("..")) { + // return false; + // } + // if (filename.contains(";")) { + // return false; + // } + // if (filename.contains("|")) { + // return false; + // } + // if (filename.contains(">")) { + // return false; + // } + // return !filename.contains("<"); + // } + // + // public static boolean isSymLink(File f) + // { + // File canon = f.getCanonicalFile(); + // if(!canon.getName().equals(f.getName())) + // { + // return true; + // } + // File parent; + // parent = f.getParentFile(); + // if(parent == null) + // { + // parent = f.getAbsoluteFile().getParentFile(); + // } + // if(parent == null) + // { + // return false; + // } + // File canonParent; + // parent = parent.getCanonicalFile(); + // canonParent = canon.getParentFile(); + // return !parent.equals(canonParent); + // IOException e; + // e; + // throw Utils.runtime(e); + // } + // + // public static Object load(File file) { + // BufferedReader reader = getReader(file); + // return XStreamUtils.serialiseFromXml(reader); + // } + // + // public static Properties loadProperties(File propsFile) { + // Exception exception; + // InputStream stream = null; + // Properties properties; + // try { + // stream = new FileInputStream(propsFile); + // Properties props = new Properties(); + // props.load(stream); + // properties = props; + // } catch (IOException e) { + // throw Utils.runtime(e); + // } finally { + // close(stream); + // } + // close(stream); + // return properties; + // throw exception; + // } + // + // public static File[] ls(File dir, String fileNameRegex) { + // if (!dir.isDirectory()) { + // throw new RuntimeException( + // (new StringBuilder()).append(dir).append(" is not a valid directory").toString()); + // } else { + // return dir.listFiles(getRegexFilter((new + // StringBuilder(".*")).append(fileNameRegex).toString())); + // } + // } + // + // public static void makeSymLink(File original, File out) { + // makeSymLink(original, out, true); + // } + // + // public static void makeSymLink(File original, File out, boolean overwrite) { + // if (!Utils.getOperatingSystem().contains("linux")) { + // throw new TodoException(); + // } + // if (original.getAbsolutePath().equals(out.getAbsolutePath())) { + // throw new IllegalArgumentException((new StringBuilder("Cannot sym-link to self: + // ")).append(original) + // .append(" = ").append(out).toString()); + // } + // if (!original.exists()) { + // throw new winterwell.utils.RuntimeException.FileNotFoundException(original); + // } + // if (!original.isDirectory() && !original.isFile()) { + // throw new RuntimeException((new StringBuilder("Weird: ")).append(original).toString()); + // } + // if (out.exists()) { + // if (overwrite) { + // delete(out); + // } else { + // throw new RuntimeException((new StringBuilder("Creating symlink failed: ")).append(out) + // .append(" already exists.").toString()); + // } + // } + // String err; + // original = original.getCanonicalFile(); + // ShellScript ss = new ShellScript( + // (new StringBuilder("ln -s ")).append(original).append(" ").append(out).toString()); + // ss.run(); + // ss.waitFor(); + // err = ss.getError(); + // if (Utils.isBlank(err)) { + // break MISSING_BLOCK_LABEL_276; + // } + // if (overwrite && err.contains("File exists")) { + // deleteNative(out); + // makeSymLink(original, out, overwrite); + // return; + // } + // try { + // throw new RuntimeException(err); + // } catch (Exception e) { + // throw Utils.runtime(e); + // } + // } + // + // public static File move(File src, File dest) throws RuntimeException { + // if (!src.exists()) { + // throw new AssertionError(); + // } + // File src2 = new File(src.getPath()); + // if (!src2.equals(src)) { + // throw new AssertionError(); + // } + // boolean ok = src2.renameTo(dest); + // if (ok) { + // return dest; + // } else { + // copy(src, dest); + // delete(src); + // return dest; + // } + // } + // + // public static int numLines(File file) { + // int cnt = 0; + // BufferedReader r = getReader(file); + // try { + // do { + // String line = r.readLine(); + // if (line == null) break; + // cnt++; + // } while (true); + // } catch (IOException e) { + // throw new RuntimeException(e); + // } + // return cnt; + // } + // + // public static void prepend(File file, String string) { + // if ((file.isDirectory() || string == null)) { + // throw new AssertionError(); + // } + // if (!file.exists() || file.length() == 0L) { + // write(file, string); + // return; + // } + // try { + // File temp = File.createTempFile("prepend", ""); + // write(temp, string); + // FileInputStream in = new FileInputStream(file); + // FileOutputStream out = new FileOutputStream(temp, true); + // copy(in, out); + // move(temp, file); + // } catch (IOException e) { + // throw new RuntimeException(e); + // } + // } + // + // public static String read(File file) + // throws RuntimeException + // { + // return read(((InputStream) (new FileInputStream(file)))); + // IOException e; + // e; + // throw Utils.runtime(e); + // } + // + // public static String read(InputStream in) { + // return read(((Reader) (getReader(in)))); + // } + // + // public static String read(Reader r) { + // Exception exception; + // String s; + // try { + // BufferedReader reader = (r instanceof BufferedReader) ? (BufferedReader) r : new + // BufferedReader(r); + // int bufSize = 8192; + // StringBuilder sb = new StringBuilder(8192); + // char cbuf[] = new char[8192]; + // do { + // int chars = reader.read(cbuf); + // if (chars == -1) { + // break; + // } + // if (sb.length() == 0 && cbuf[0] == '\uFEFF') { + // sb.append(cbuf, 1, chars - 1); + // } else { + // sb.append(cbuf, 0, chars); + // } + // } while (true); + // s = sb.toString(); + // } catch (IOException e) { + // throw new RuntimeException(e); + // } finally { + // close(r); + // } + // close(r); + // return s; + // throw exception; + // } + // + // public static byte[] readRaw(InputStream raw) + // { + // byte trimmed[]; + // byte all[] = new byte[10240]; + // int offset = 0; + // do + // { + // int space = all.length - offset; + // if(space < all.length / 8) + // { + // all = Arrays.copyOf(all, all.length * 2); + // space = all.length - offset; + // } + // int r = raw.read(all, offset, space); + // if(r == -1) + // { + // break; + // } + // offset += r; + // } while(true); + // trimmed = Arrays.copyOf(all, offset); + // return trimmed; + // Exception e; + // e; + // throw new RuntimeException(e); + // } + // + // public static String resolveDotDot(String absolutePath) + // { + // return (new File(absolutePath)).getCanonicalPath(); + // IOException e; + // e; + // throw Utils.runtime(e); + // } + // + // public static String safeFilename(String name) { + // return safeFilename(name, true); + // } + // + // public static String safeFilename(String name, boolean allowSubDirs) { + // if (name == null) { + // return "null"; + // } + // name = name.trim(); + // if (name.equals("")) { + // name = "empty"; + // } + // if (name.length() > 5000) { + // throw new IllegalArgumentException((new StringBuilder("Name is too long: + // ")).append(name).toString()); + // } + // name = name.replace("_", "__"); + // name = name.replace("..", "_."); + // name = name.replaceAll("[^ a-zA-Z0-9-_.~/\\\\]", ""); + // name = name.trim(); + // name = name.replaceAll("\\s+", "_"); + // if (!allowSubDirs) { + // name = name.replace("/", "_"); + // name = name.replace("\\", "_"); + // } + // for (; "./-\\".indexOf(name.charAt(name.length() - 1)) != -1; name = name.substring(0, + // name.length() - 1)) {} + // if (name.length() > 50) { + // name = (new StringBuilder(String.valueOf(name.substring(0, 10)))).append(name.hashCode()) + // .append(name.substring(name.length() - 10)).toString(); + // } + // return name; + // } + // + // public static void save(Object obj, File file) { + // if (file.getParentFile() != null) { + // file.getParentFile().mkdirs(); + // } + // write(file, XStreamUtils.serialiseToXml(obj)); + // } + // + // private static File setDataDir() + // { + // String dd = System.getenv("WINTERWELL_DATA"); + // if(!Utils.isBlank(dd)) + // { + // return (new File(dd)).getAbsoluteFile(); + // } + // File ddf; + // String home = System.getProperty("user.home"); + // ddf = new File(home, ".winterwell/data"); + // ddf.mkdirs(); + // if(ddf.exists() && ddf.isDirectory()) + // { + // return ddf; + // } + // File f; + // dd = "data"; + // f = (new File(dd)).getAbsoluteFile(); + // Log.report((new StringBuilder("Using fallback data directory ")).append(f).toString(), + // Level.WARNING); + // return f; + // Exception e; + // e; + // return null; + // } + +} diff --git a/plugin/src/winterwell/markdown/util/IProperties.java b/plugin/src/winterwell/markdown/util/IProperties.java new file mode 100644 index 0000000..bbba574 --- /dev/null +++ b/plugin/src/winterwell/markdown/util/IProperties.java @@ -0,0 +1,16 @@ +package winterwell.markdown.util; + +import java.util.Collection; + +public interface IProperties { + + public abstract boolean containsKey(Key key); + + public abstract Object get(Key key); + + public abstract Collection getKeys(); + + public abstract boolean isTrue(Key key); + + public abstract Object put(Key key, Object obj); +} diff --git a/plugin/src/winterwell/markdown/util/IReplace.java b/plugin/src/winterwell/markdown/util/IReplace.java new file mode 100644 index 0000000..1da7fef --- /dev/null +++ b/plugin/src/winterwell/markdown/util/IReplace.java @@ -0,0 +1,8 @@ +package winterwell.markdown.util; + +import java.util.regex.Matcher; + +public interface IReplace { + + public abstract void appendReplacementTo(StringBuilder stringbuilder, Matcher matcher); +} diff --git a/plugin/src/winterwell/markdown/util/IntRange.java b/plugin/src/winterwell/markdown/util/IntRange.java new file mode 100644 index 0000000..686e987 --- /dev/null +++ b/plugin/src/winterwell/markdown/util/IntRange.java @@ -0,0 +1,36 @@ +package winterwell.markdown.util; + +import java.util.AbstractList; + +public final class IntRange extends AbstractList { + + public final int high; + public final int low; + + public IntRange(int a, int b) { + if (a < b) { + low = a; + high = b; + } else { + low = b; + high = a; + } + } + + public boolean contains(int x) { + return x >= low && x <= high; + } + + public Integer get(int index) { + if (!contains(index + low)) throw new AssertionError(); + return Integer.valueOf(index + low); + } + + public int size() { + return (high - low) + 1; + } + + public String toString() { + return (new StringBuilder("[")).append(low).append(", ").append(high).append("]").toString(); + } +} diff --git a/plugin/src/winterwell/markdown/util/Key.java b/plugin/src/winterwell/markdown/util/Key.java new file mode 100644 index 0000000..47ac85d --- /dev/null +++ b/plugin/src/winterwell/markdown/util/Key.java @@ -0,0 +1,70 @@ +package winterwell.markdown.util; + +import java.io.Serializable; +import java.util.Map; + +public class Key implements Comparable, Serializable { + + public static class RichKey extends Key { + + public final String description; + public final Class valueClass; + + public RichKey(String name, Class valueClass, String description) { + super(name); + if (valueClass == null) { + throw new AssertionError(); + } else { + this.valueClass = valueClass; + this.description = description; + return; + } + } + } + + private final String name; + + public Key(String name) { + if (name == null) { + throw new AssertionError(); + } else { + this.name = name; + return; + } + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof Key)) return false; + return name.equals(((Key) obj).name); + } + + public Object getFromMap(Map map) { + return map.get(name); + } + + public final String getName() { + return name; + } + + @Override + public final int hashCode() { + return 31 + name.hashCode(); + } + + @Override + public String toString() { + return name; + } + + public int compareTo(Key key) { + return name.compareTo(key.name); + } + + @Override + public int compareTo(String o) { + return name.compareTo(o); + } +} diff --git a/plugin/src/winterwell/markdown/util/Mutable.java b/plugin/src/winterwell/markdown/util/Mutable.java new file mode 100644 index 0000000..8b41f1c --- /dev/null +++ b/plugin/src/winterwell/markdown/util/Mutable.java @@ -0,0 +1,72 @@ +package winterwell.markdown.util; + +public final class Mutable { + + public static final class Dble { + + public String toString() { + return (new StringBuilder()).append(value).toString(); + } + + public double value; + + public Dble() { + this(0.0D); + } + + public Dble(double v) { + value = v; + } + } + + public static final class Int { + + public String toString() { + return (new StringBuilder()).append(value).toString(); + } + + public int value; + + public Int() { + this(0); + } + + public Int(int v) { + value = v; + } + } + + public static final class Ref { + + public String toString() { + return Printer.toString(value); + } + + public Object value; + + public Ref() {} + + public Ref(Object value) { + this.value = value; + } + } + + public static final class Strng { + + public String toString() { + return value; + } + + public String value; + + public Strng() { + this(""); + } + + public Strng(String v) { + value = v; + } + } + + public Mutable() {} +} diff --git a/plugin/src/winterwell/markdown/util/Pair.java b/plugin/src/winterwell/markdown/util/Pair.java new file mode 100644 index 0000000..f701d59 --- /dev/null +++ b/plugin/src/winterwell/markdown/util/Pair.java @@ -0,0 +1,60 @@ +package winterwell.markdown.util; + +public class Pair { + + public final T first; + public final T second; + + public Pair(T first, T second) { + this.first = first; + this.second = second; + } + + /** + * Return the first item in the pair + * + * @return the first item in the pair + */ + public T getFirst() { + return first; + } + + /** + * Return the second item in the pair + * + * @return the second item in the pair + */ + public T getSecond() { + return second; + } + + @Override + public String toString() { + return "Pair [first=" + first + ", second=" + second + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((first == null) ? 0 : first.hashCode()); + result = prime * result + ((second == null) ? 0 : second.hashCode()); + return result; + } + + @SuppressWarnings("rawtypes") + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + Pair other = (Pair) obj; + if (first == null) { + if (other.first != null) return false; + } else if (!first.equals(other.first)) return false; + if (second == null) { + if (other.second != null) return false; + } else if (!second.equals(other.second)) return false; + return true; + } +} diff --git a/plugin/src/winterwell/markdown/util/PartListener.java b/plugin/src/winterwell/markdown/util/PartListener.java new file mode 100644 index 0000000..f6a9da5 --- /dev/null +++ b/plugin/src/winterwell/markdown/util/PartListener.java @@ -0,0 +1,23 @@ +package winterwell.markdown.util; + +import org.eclipse.ui.IPartListener; +import org.eclipse.ui.IWorkbenchPart; + +public class PartListener implements IPartListener { + + @Override + public void partActivated(IWorkbenchPart part) {} + + @Override + public void partBroughtToTop(IWorkbenchPart part) {} + + @Override + public void partClosed(IWorkbenchPart part) {} + + @Override + public void partDeactivated(IWorkbenchPart part) {} + + @Override + public void partOpened(IWorkbenchPart part) {} + +} diff --git a/plugin/src/winterwell/markdown/util/Printer.java b/plugin/src/winterwell/markdown/util/Printer.java new file mode 100644 index 0000000..0ba058a --- /dev/null +++ b/plugin/src/winterwell/markdown/util/Printer.java @@ -0,0 +1,297 @@ +package winterwell.markdown.util; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.w3c.dom.Node; + +public class Printer { + + private static boolean useListMarkers; + // static final Map useMe = Collections.synchronizedMap(new HashMap()); + private static final Class ArraysListType = Arrays.asList(new Object[0]).getClass(); + private static final DecimalFormat df = new DecimalFormat("#.##"); + public static final Key INDENT; + // private static final int MAX_ITEMS = 12; + static final Pattern n = Pattern.compile("[1-9]"); + // private static final IPrinter PLAIN_TO_STRING = new IPrinter() { + // + // public void append(Object obj, StringBuilder sb) { + // sb.append(obj); + // } + // + // public String toString() { + // return "PLAIN_TO_STRING"; + // } + // + // }; + + static { + INDENT = new Key("Printer.indent"); + Environment.putDefault(INDENT, ""); + } + + public static interface IPrinter { + + public abstract void append(Object obj, StringBuilder stringbuilder); + } + + public Printer() {} + + public static void addIndent(String indent) { + Environment env = Environment.get(); + String oldindent = (String) env.get(INDENT); + String newIndent = (new StringBuilder(String.valueOf(oldindent))).append(indent).toString(); + env.put(INDENT, newIndent); + } + + private static void append(Object x, StringBuilder sb) { + if (x == null) { + return; + } + if (x instanceof CharSequence) { + sb.append((CharSequence) x); + return; + } + if (x.getClass().isArray()) { + for (Object y : (Object[]) x) { + sb.append(y); + } + return; + } + if (x instanceof Number) { + sb.append(toStringNumber((Number) x)); + return; + } + if ((x instanceof ArrayList) || (x instanceof HashSet) || x.getClass() == ArraysListType) { + append(sb, (Collection) x, ", "); + return; + } + if (x instanceof HashMap) { + append(sb, (Map) x, (new StringBuilder(String.valueOf(Strings.EOL))) + .append((String) Environment.get().get(INDENT)).toString(), ": ", "{}"); + return; + } + // if (ReflectionUtils.hasMethod(x.getClass(), "toString")) { + // useMe.put(x.getClass(), PLAIN_TO_STRING); + // sb.append(x.toString()); + // return; + // } + if (x instanceof Iterable) { + List target = new ArrayList<>(); + for (Object y : (Iterable) x) { + target.add(y); + } + x = target; + } + if (x instanceof Enumeration) { + x = Collections.list((Enumeration) x); + } + if (x instanceof Collection) { + append(sb, (Collection) x, ", "); + return; + } + if (x instanceof Map) { + append(sb, (Map) x, (new StringBuilder(String.valueOf(Strings.EOL))) + .append((String) Environment.get().get(INDENT)).toString(), ": ", "{}"); + return; + } + if (x instanceof Exception) { + sb.append(toString((Exception) x, true)); + return; + } + if (x instanceof Node) { + Node node = (Node) x; + sb.append((new StringBuilder("<")).append(node.getNodeName()).append(">").append(node.getTextContent()) + .append("").toString()); + return; + } else { + sb.append(x); + return; + } + } + + public static StringBuilder append(StringBuilder sb, Collection list, String separator) { + boolean added = false; + if (useListMarkers) { + sb.append('['); + } + for (Iterator iterator = list.iterator(); iterator.hasNext();) { + Object y = iterator.next(); + if (y != null) { + added = true; + if (y == list) { + sb.append("(this Collection)"); + } else { + append(y, sb); + sb.append(separator); + } + } + } + + if (added) { + Strings.pop(sb, separator.length()); + } + if (useListMarkers) { + sb.append(']'); + } + return sb; + } + + public static void append(StringBuilder sb, Map x, String entrySeparator, String keyValueSeparator, + String startEnd) { + if (startEnd != null && startEnd.length() != 0 && startEnd.length() != 2) { + throw new AssertionError(); + } + List keys = new ArrayList<>(x.keySet()); + if (keys.size() > 12) { + keys = keys.subList(0, 12); + } + if (startEnd != null && startEnd.length() > 0) { + sb.append(startEnd.charAt(0)); + } + for (Iterator iterator = keys.iterator(); iterator.hasNext(); sb.append(entrySeparator)) { + Object k = iterator.next(); + sb.append(toString(k)); + sb.append(keyValueSeparator); + sb.append(toString(x.get(k))); + } + + if (keys.size() > 1) { + Strings.pop(sb, entrySeparator.length()); + } + if (startEnd != null && startEnd.length() > 1) { + sb.append(startEnd.charAt(1)); + } + } + + public static void appendFormat(StringBuilder result, String message, Object args[]) { + for (int i = 0; i < args.length; i++) { + message = message.replace((new StringBuilder("{")).append(i).append("}").toString(), toString(args[i])); + } + + result.append(message); + } + + public static String format(String message, Object args[]) { + for (int i = 0; i < args.length; i++) { + message = message.replace((new StringBuilder("{")).append(i).append("}").toString(), toString(args[i])); + } + + return message; + } + + public static void formatOut(String message, Object args[]) { + String fm = format(message, args); + System.out.println(fm); + } + + public static void out(Object x[]) { + System.out.println(toString(((Object) (x)))); + } + + public static String prettyNumber(double x) { + return prettyNumber(x, 3); + } + + public static String prettyNumber(double x, int sigFigs) { + if (x >= 1000000D) { + return (new StringBuilder(String.valueOf(Strings.toNSigFigs(x / 1000000D, sigFigs)))).append(" million") + .toString(); + } + if (x >= 1000D) { + String s = Strings.toNSigFigs(x, sigFigs); + x = Double.valueOf(s).doubleValue(); + DecimalFormat f = new DecimalFormat("###,###"); + return f.format(x); + } else { + return Strings.toNSigFigs(x, sigFigs); + } + } + + public static void removeIndent(String indent) { + Environment env = Environment.get(); + String oldindent = (String) env.get(INDENT); + if (!oldindent.endsWith(indent)) { + throw new AssertionError(); + } else { + String newIndent = oldindent.substring(0, oldindent.length() - indent.length()); + env.put(INDENT, newIndent); + return; + } + } + + // public static void setUseListMarkers(boolean useListMarkers) { + // useListMarkers = useListMarkers; + // } + + public static String toString(Collection list, String separator) { + StringBuilder sb = new StringBuilder(); + append(sb, list, separator); + return sb.toString(); + } + + public static String toString(Map x, String entrySeparator, String keyValueSeparator) { + StringBuilder sb = new StringBuilder(); + append(sb, x, entrySeparator, keyValueSeparator, "{}"); + return sb.toString(); + } + + public static String toString(Object x) { + if (x == null) { + return ""; + } else { + StringBuilder sb = new StringBuilder(); + append(x, sb); + return sb.toString(); + } + } + + public static String toString(Throwable x, boolean stacktrace) { + if (!stacktrace) { + return x.getMessage() != null ? (new StringBuilder(String.valueOf(x.getClass().getSimpleName()))) + .append(": ").append(x.getMessage()).toString() : x.getClass().getSimpleName(); + } else { + StringWriter w = new StringWriter(); + w.append((new StringBuilder()).append(x.getClass()).append(": ").append(x.getMessage()).append(Strings.EOL) + .append((String) Environment.get().get(INDENT)).append("\t").toString()); + try (PrintWriter pw = new PrintWriter(w);) { + x.printStackTrace(pw); + pw.flush(); + } + return w.toString(); + } + } + + public static String toStringNumber(Number x) { + float f = x.floatValue(); + if (f == (float) Math.round(f)) { + return Integer.toString((int) f); + } + if (Math.abs(f) >= 1.0F) { + return df.format(f); + } + String fs = Float.toString(f); + if (fs.contains("E")) { + return fs; + } + String fss = Strings.substring(fs, 0, 5); + if (n.matcher(fss).find()) { + return fss; + } else { + return fs; + } + } +} diff --git a/plugin/src/winterwell/markdown/util/Process.java b/plugin/src/winterwell/markdown/util/Process.java new file mode 100644 index 0000000..0166236 --- /dev/null +++ b/plugin/src/winterwell/markdown/util/Process.java @@ -0,0 +1,196 @@ +package winterwell.markdown.util; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class Process { + + boolean closed; + private String command; + private boolean echo; + private StreamGobbler err; + private StreamGobbler out; + private final ProcessBuilder pb; + private java.lang.Process process; + + public Process(List command) { + pb = new ProcessBuilder(command); + } + + public Process(String command) { + this.command = command; + pb = new ProcessBuilder(parse(command)); + } + + private void closeStreams() { + if (process == null) return; + if (err != null) err.pleaseStop(); + if (out != null) out.pleaseStop(); + + FileUtils.close(process.getInputStream()); + FileUtils.close(process.getOutputStream()); + FileUtils.close(process.getErrorStream()); + } + + public void destroy() { + if (closed) { + return; + } else { + closed = true; + process.destroy(); + closeStreams(); + return; + } + } + + protected void finalize() throws Throwable { + super.finalize(); + closeStreams(); + } + + public String getCommand() { + return command != null ? command : Printer.toString(pb.command(), " "); + } + + public Map getEnvironment() { + return pb.environment(); + } + + public String getError() { + try { + return err != null ? err.getString() : ""; + } catch (IOException e) { + return ""; + } + } + + public String getOutput() { + try { + return out.getString(); + } catch (IOException e) { + return ""; + } + } + + List parse(String command) { + List bits = new ArrayList(); + StringBuilder bit = new StringBuilder(); + boolean inQuotes = false; + char ac[]; + int j = (ac = command.toCharArray()).length; + for (int i = 0; i < j; i++) { + char c = ac[i]; + if (inQuotes) { + bit.append(c); + if (c == '"') { + inQuotes = false; + } + } else if (Character.isWhitespace(c)) { + if (bit.length() != 0) { + bits.add(bit.toString()); + bit = new StringBuilder(); + } + } else { + bit.append(c); + if (c == '"') { + inQuotes = true; + } + } + } + + if (bit.length() != 0) { + bits.add(bit.toString()); + } + String osName = System.getProperty("os.name"); + if (osName.equals("Windows 95")) { + ArrayList wbits = new ArrayList(bits.size() + 2); + wbits.add("command.com"); + wbits.add("/C"); + wbits.addAll(bits); + bits = wbits; + } else if (osName.startsWith("Windows")) { + ArrayList wbits = new ArrayList(bits.size() + 2); + wbits.add("cmd.exe"); + wbits.add("/C"); + wbits.addAll(bits); + bits = wbits; + } + return bits; + } + + public void run() { + try { + process = pb.start(); + out = new StreamGobbler(process.getInputStream()); + if (echo) out.setEcho(true); + out.start(); + if (!pb.redirectErrorStream()) { + err = new StreamGobbler(process.getErrorStream()); + if (echo) err.setEcho(true); + err.start(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void setDirectory(File dir) { + pb.directory(dir); + } + + public void setEcho(boolean echo) { + if (process != null) { + throw new AssertionError("do earlier!"); + } else { + this.echo = echo; + return; + } + } + + public void setRedirectErrorStream(boolean redirect) { + pb.redirectErrorStream(redirect); + } + + public String toString() { + String data = ""; + try { + data = out.getString(); + } catch (IOException e) {} + return (new StringBuilder(String.valueOf(pb.command().toString()))).append("\n").append(data).toString(); + } + + public int waitFor() { + sleep(5L); + int v = 0; + try { + v = process.waitFor(); + } catch (InterruptedException e) {} + closeStreams(); + return v; + } + + public int waitFor(long timeout) { + if (timeout < 1L) return waitFor(); + + sleep(5L); + + TimeOut interrupter = new TimeOut(timeout); + int v = 0; + try { + v = process.waitFor(); + } catch (InterruptedException e) {} + interrupter.cancel(); + closeStreams(); + return v; + + } + + private void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) {} + } +} diff --git a/plugin/src/winterwell/markdown/util/StackKey.java b/plugin/src/winterwell/markdown/util/StackKey.java new file mode 100644 index 0000000..8cbc436 --- /dev/null +++ b/plugin/src/winterwell/markdown/util/StackKey.java @@ -0,0 +1,8 @@ +package winterwell.markdown.util; + +final class StackKey extends Key { + + public StackKey(Key key) { + super((new StringBuilder(String.valueOf(key.getName()))).append(".envstack").toString()); + } +} diff --git a/plugin/src/winterwell/markdown/util/StreamGobbler.java b/plugin/src/winterwell/markdown/util/StreamGobbler.java new file mode 100644 index 0000000..b728044 --- /dev/null +++ b/plugin/src/winterwell/markdown/util/StreamGobbler.java @@ -0,0 +1,74 @@ +package winterwell.markdown.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +public final class StreamGobbler extends Thread { + + private boolean echo; + private IOException ex; + private final InputStream is; + private boolean stopFlag; + private StringBuffer stringBuffer; + + public StreamGobbler(InputStream is) { + super((new StringBuilder("gobbler:")).append(is.toString()).toString()); + setDaemon(true); + this.is = is; + stringBuffer = new StringBuffer(); + } + + public void clearString() { + stringBuffer = new StringBuffer(); + } + + public String getString() throws IOException { + if (ex != null) throw new IOException(ex); + return stringBuffer.toString(); + } + + public boolean hasText() { + return stringBuffer.length() != 0; + } + + public void pleaseStop() { + try { + is.close(); + } catch (IOException e) {} + stopFlag = true; + } + + public void run() { + try { + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr); + while (!stopFlag) { + int ich = br.read(); + if (ich == -1) { + break; + } + char ch = (char) ich; + stringBuffer.append(ch); + if (echo) { + System.out.print(ch); + } + } + } catch (IOException ioe) { + if (stopFlag) { + return; + } + ioe.printStackTrace(); + ex = ioe; + } + } + + public void setEcho(boolean echo) { + this.echo = echo; + } + + public String toString() { + return (new StringBuilder("StreamGobbler:")).append(stringBuffer.toString()).toString(); + } +} diff --git a/plugin/src/winterwell/markdown/util/Strings.java b/plugin/src/winterwell/markdown/util/Strings.java new file mode 100644 index 0000000..8b56558 --- /dev/null +++ b/plugin/src/winterwell/markdown/util/Strings.java @@ -0,0 +1,761 @@ +package winterwell.markdown.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class Strings { + + public static final String ISO_LATIN = "ISO-8859-1"; + public static final String UTF_8 = "UTF-8"; + public static final String EOL = System.getProperty("line.separator"); + public static final String EOL2 = EOL + EOL; + + public static final String APOSTROPHES = "'`\u2019\u2018\u2019\u02BC"; + public static final Pattern ASCII_PUNCTUATION = Pattern + .compile("[.<>,@~\\{\\}\\[\\]-_+=()*%?^$!\\\\/|\254:;#`'\"]"); + public static final Pattern BLANK_LINE = Pattern.compile("^\\s+$", 8); + public static final String COMMON_BULLETS = "-*\uE00Co"; + public static final String DASHES = "\u2010\u2011\u2012\u2013\u2014\u2015-"; + public static final Pattern LINEENDINGS = Pattern.compile("(\r\n|\r|\n)"); + public static final String LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + public static final String QUOTES = "\"\u201C\u201D\u201E\u201F\u275B\u275C\u275D\u275E\253\273"; + + public static final Pattern TAG_REGEX = Pattern.compile("<(/?[a-zA-Z][a-zA-Z0-9]*)[^>]*>", 32); + public static final Pattern pComment = Pattern.compile("", 32); + public static final Pattern pDocType = Pattern.compile("", 34); + public static final Pattern pScriptOrStyle = Pattern.compile("<(script|style)[^<>]*>.+?", 34); + + public static final String ARRAY[] = new String[0]; + private static final double TENS[]; + static { + TENS = new double[20]; + TENS[0] = Math.pow(10D, -6D); + for (int i = 1; i < TENS.length; i++) { + TENS[i] = 10D * TENS[i - 1]; + } + } + + public static char charAt(CharSequence chars, int i) { + return i >= chars.length() ? '\0' : chars.charAt(i); + } + + public static String compactWhitespace(String txt) { + if (txt == null) { + return null; + } else { + txt = txt.trim(); + txt = txt.replaceAll("\\s+", " "); + txt = txt.replaceAll("> <", "><"); + return txt; + } + } + + public static boolean containsIgnoreCase(CharSequence pageTitle, String snippet) { + String pt = pageTitle.toString().toLowerCase(); + return pt.contains(snippet.toLowerCase()); + } + + private static String convertToJavaString(String txt) { + String lines[] = splitLines(txt); + String jtxt = ""; + String as[]; + int j = (as = lines).length; + for (int i = 0; i < j; i++) { + String line = as[i]; + line = line.replace("\\", "\\\\"); + line = line.replace("\"", "\\\""); + jtxt = (new StringBuilder(String.valueOf(jtxt))).append("+\"").append(line).append("\\n\"\n").toString(); + } + + jtxt = jtxt.substring(1); + return jtxt; + } + + public static String ellipsize(String input, int maxLength) { + if (input == null) { + return null; + } + if (input.length() <= maxLength) { + return input; + } + if (maxLength < 3) { + return ""; + } + if (maxLength == 3) { + return "..."; + } + int i = input.lastIndexOf(' ', maxLength - 3); + if (i < 1 || i < maxLength - 10) { + i = maxLength - 3; + } + return (new StringBuilder(String.valueOf(substring(input, 0, i)))).append("...").toString(); + } + + public static void endLine(StringBuilder text) { + newLine(text); + } + + public static Map extractHeader(StringBuilder txt) { + if (txt == null) { + throw new AssertionError(); + } + String lines[] = splitLines(txt.toString()); + String key = null; + StringBuilder value = new StringBuilder(); + Map headers = new LinkedHashMap<>(); + for (String line : lines) { + if (line.trim().isEmpty()) break; + int i = line.indexOf(":"); + if (i == -1) { + value.append(EOL); + value.append(line); + } else { + if (key != null) { + headers.put(key, value.toString()); + } + value = new StringBuilder(); + key = line.substring(0, i).toLowerCase(); + if (++i != line.length()) { + if (line.charAt(i) == ' ') i++; + if (i != line.length()) { + value.append(line.substring(i)); + } + } + } + } + + if (key != null) { + headers.put(key, value.toString()); + } + if (headers.size() == 0) { + return headers; + } + Pattern blankLine = Pattern.compile("^\\s*$", 8); + Matcher m = blankLine.matcher(txt); + boolean ok = m.find(); + if (ok) { + txt.delete(0, m.end()); + if (txt.length() != 0) { + if (txt.charAt(0) == '\r' && txt.charAt(1) == '\n') { + txt.delete(0, 2); + } else { + txt.delete(0, 1); + } + } + } + return headers; + } + + public static String[] find(Pattern pattern, String input) { + Matcher m = pattern.matcher(input); + boolean fnd = m.find(); + if (!fnd) { + return null; + } + int n = m.groupCount() + 1; + String grps[] = new String[n]; + grps[0] = m.group(); + for (int i = 1; i < n; i++) { + grps[i] = m.group(i); + } + + return grps; + } + + public static String[] find(String regex, String string) { + return find(Pattern.compile(regex), string); + } + + public static Pair findLenient(String content, String text, int start) { + content = normalise(content); + text = normalise(text); + content = content.toLowerCase(); + text = text.toLowerCase(); + String regex = content.replace("\\", "\\\\"); + String SPECIAL = "()[]{}$^.*+?"; + for (int i = 0; i < SPECIAL.length(); i++) { + char c = SPECIAL.charAt(i); + regex = regex.replace((new StringBuilder()).append(c).toString(), + (new StringBuilder("\\")).append(c).toString()); + } + + regex = regex.replaceAll("\\s+", "\\\\s+"); + Pattern p = Pattern.compile(regex); + Matcher m = p.matcher(text); + if (m.find(start)) { + return new Pair<>(Integer.valueOf(m.start()), Integer.valueOf(m.end())); + } else { + return null; + } + } + + public static String getFirstName(String name) { + name = name.trim(); + if (name.contains("\n")) { + throw new AssertionError(name); + } + String nameBits[] = name.split("[ \t\\.,]+"); + String firstName = nameBits[0]; + firstName = toTitleCase(firstName); + List titles = Arrays + .asList(new String[] { "Mr", "Mrs", "Ms", "Dr", "Doctor", "Prof", "Professor", "Sir", "Director" }); + if (titles.contains(firstName)) { + firstName = nameBits[1]; + firstName = toTitleCase(firstName); + } + return firstName; + } + + public static String getHeaderString(Map header) { + StringBuilder sb = new StringBuilder(); + String ks; + String vs; + for (Iterator iterator = header.keySet().iterator(); iterator.hasNext(); sb + .append((new StringBuilder(String.valueOf(ks))).append(": ").append(vs).append(EOL).toString())) { + Object k = iterator.next(); + ks = k.toString().trim().toLowerCase(); + vs = header.get(k).toString(); + } + + return sb.toString(); + } + + public static int[] getLineStarts(String text) { + List starts = new ArrayList<>(); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '\n') { + starts.add(Integer.valueOf(i)); + } + if (c == '\r') { + int ni = i + 1; + if (ni == text.length() || text.charAt(ni) != '\n') { + starts.add(Integer.valueOf(i)); + } + } + } + int[] results = new int[starts.size()]; + for (int i = 0; i < starts.size(); i++) { + results[i] = starts.get(i); + } + return results; + } + + private static String hash(String hashAlgorithm, String txt) { + StringBuffer result; + MessageDigest md = null; + try { + md = MessageDigest.getInstance(hashAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + result = new StringBuffer(); + try { + byte abyte0[]; + int j = (abyte0 = md.digest(txt.getBytes("UTF8"))).length; + for (int i = 0; i < j; i++) { + byte b = abyte0[i]; + result.append(Integer.toHexString((b & 0xf0) >>> 4)); + result.append(Integer.toHexString(b & 0xf)); + } + + } catch (UnsupportedEncodingException e) { + byte abyte1[]; + int l = (abyte1 = md.digest(txt.getBytes())).length; + for (int k = 0; k < l; k++) { + byte b = abyte1[k]; + result.append(Integer.toHexString((b & 0xf0) >>> 4)); + result.append(Integer.toHexString(b & 0xf)); + } + + } + return result.toString(); + } + + public static boolean isJustDigits(String possNumber) { + for (int i = 0; i < possNumber.length(); i++) { + if (!Character.isDigit(possNumber.charAt(i))) { + return false; + } + } + + return true; + } + + public static boolean isNumber(String x) { + if (x == null) { + return false; + } + try { + Double.valueOf(x); + } catch (Exception e) { + return false; + } + return true; + } + + public static boolean isOnly(String txt, char c) { + if (txt == null || txt.length() == 0) { + return false; + } + for (int i = 0; i < txt.length(); i++) { + if (txt.charAt(i) != c) { + return false; + } + } + + return true; + } + + public static boolean isWord(String txt) { + return txt.matches("\\w+"); + } + + public static String join(Collection list, String separator) { + return Printer.toString(list, separator); + } + + public static StringBuilder join(String start, Collection list, String separator, String end) { + StringBuilder sb = new StringBuilder(start); + if (!list.isEmpty()) { + for (Iterator iterator = list.iterator(); iterator.hasNext();) { + Object t = (Object) iterator.next(); + if (t != null) { + sb.append(Printer.toString(t)); + sb.append(separator); + } + } + + if (sb.length() != 0) { + pop(sb, separator.length()); + } + } + sb.append(end); + return sb; + } + + public static String join(String array[], String separator) { + if (array.length == 0) { + return ""; + } + StringBuilder sb = new StringBuilder(); + String as[]; + int j = (as = array).length; + for (int i = 0; i < j; i++) { + String string = as[i]; + if (string != null) { + sb.append(string); + sb.append(separator); + } + } + + if (sb.length() != 0) { + pop(sb, separator.length()); + } + return sb.toString(); + } + + public static void join(StringBuilder sb, Collection list, String separator) { + Printer.append(sb, list, separator); + } + + public static void main(String args[]) throws IOException { + String txt = ""; + BufferedReader in = FileUtils.getReader(System.in); + do { + String line = in.readLine(); + if (!line.equals("EXIT") && !line.equals("QUIT")) { + txt = (new StringBuilder(String.valueOf(txt))).append(line).append("\n").toString(); + } else { + String jtxt = convertToJavaString(txt); + System.out.println(jtxt); + return; + } + } while (true); + } + + public static String md5(String txt) { + return hash("MD5", txt); + } + + public static void newLine(StringBuilder text) { + if (text.length() == 0) { + return; + } + char last = text.charAt(text.length() - 1); + if (last == '\r' || last == '\n') { + return; + } else { + text.append(EOL); + return; + } + } + + public static String normalise(String unicode) throws IllegalArgumentException { + boolean ascii = true; + int i = 0; + for (int n = unicode.length(); i < n; i++) { + char c = unicode.charAt(i); + if (c <= '\177' && c != 0) continue; + ascii = false; + break; + } + + if (ascii) return unicode; + + String normed = Normalizer.normalize(unicode, java.text.Normalizer.Form.NFD); + StringBuilder clean = new StringBuilder(normed.length()); + i = 0; + for (int n = normed.length(); i < n; i++) { + char c = normed.charAt(i); + if ("'`\u2019\u2018\u2019\u02BC".indexOf(c) != -1) { + clean.append('\''); + } else if ("\"\u201C\u201D\u201E\u201F\u275B\u275C\u275D\u275E\253\273".indexOf(c) != -1) { + clean.append('"'); + } else if ("\u2010\u2011\u2012\u2013\u2014\u2015-".indexOf(c) != -1) { + clean.append('-'); + } else if (c < '\200' && c != 0) { + clean.append(c); + } else if (Character.isLetter(c)) { + // Log.report((new StringBuilder("Could not normalise to ascii: + // ")).append(unicode).toString()); + } + } + + return clean.toString(); + } + + public static void pop(StringBuilder sb, int chars) { + sb.delete(sb.length() - chars, sb.length()); + } + + public static String remove(String string, String regex, final Collection removed) { + String s2 = replace(string, Pattern.compile(regex), new IReplace() { + + public void appendReplacementTo(StringBuilder sb, Matcher match) { + removed.add(match.group()); + } + }); + return s2; + } + + public static String repeat(char c, int n) { + char chars[] = new char[n]; + Arrays.fill(chars, c); + return new String(chars); + } + + public static String repeat(String string, int n) { + StringBuilder sb = new StringBuilder(string.length() * n); + for (int i = 0; i < n; i++) { + sb.append(string); + } + + return sb.toString(); + } + + public static String replace(String string, Pattern regex, IReplace replace) { + Matcher m = regex.matcher(string); + StringBuilder sb = new StringBuilder(string.length() + 16); + int pos; + for (pos = 0; m.find(); pos = m.end()) { + sb.append(string.substring(pos, m.start())); + replace.appendReplacementTo(sb, m); + } + + sb.append(string.substring(pos, string.length())); + return sb.toString(); + } + + public static StringBuilder sb(CharSequence charSeq) { + return (charSeq instanceof StringBuilder) ? (StringBuilder) charSeq : new StringBuilder(charSeq); + } + + public static List split(String line) { + if (line == null || line.length() == 0) return Collections.emptyList(); + + ArrayList row = new ArrayList<>(); + StringBuilder field = new StringBuilder(); + char quote = '"'; + boolean inQuotes = false; + int i = 0; + for (int n = line.length(); i < n; i++) { + char c = line.charAt(i); + if (c == quote) { + inQuotes = !inQuotes; + } else if (inQuotes) { + field.append(c); + } else if (Character.isWhitespace(c) || c == ',') { + if (field.length() != 0) { + row.add(field.toString()); + field = new StringBuilder(); + } + } else { + field.append(c); + } + } + + if (field.length() == 0) return row; + + String f = field.toString(); + row.add(f); + return row; + } + + public static String[] splitBlocks(String message) { + return message.split("\\s*\r?\n\\s*\r?\n"); + } + + public static Pair splitFirst(String line, char c) { + int i = line.indexOf(c); + if (i == -1) return null; + String end = i != line.length() ? line.substring(i + 1) : ""; + return new Pair<>(line.substring(0, i), end); + } + + public static String[] splitLines(String txt) { + return LINEENDINGS.split(txt); + } + + public static String substring(String string, int start, int end) { + if (string == null) { + return null; + } + int len = string.length(); + if (start < 0) { + start = len + start; + if (start < 0) { + start = 0; + } + } + if (end <= 0) { + end = len + end; + if (end < start) { + return ""; + } + } + if (end > len) { + end = len; + } + if (start == 0 && end == len) { + return string; + } else { + return string.substring(start, end); + } + } + + public static String toCanonical(String string) { + if (string == null) { + return ""; + } + StringBuilder sb = new StringBuilder(); + boolean spaced = false; + int i = 0; + for (int n = string.length(); i < n; i++) { + char c = string.charAt(i); + if (Character.isLetterOrDigit(c)) { + spaced = false; + c = Character.toLowerCase(c); + sb.append(c); + } else if (!spaced && sb.length() != 0) { + sb.append(' '); + spaced = true; + } + } + + if (spaced) { + pop(sb, 1); + } + string = sb.toString(); + return normalise(string); + } + + public static String toCleanLinux(String text) { + text = text.replace("\r\n", "\n"); + text = text.replace('\r', '\n'); + text = BLANK_LINE.matcher(text).replaceAll(""); + return text; + } + + public static String toInitials(String name) { + StringBuilder sb = new StringBuilder(); + boolean yes = true; + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (Character.isWhitespace(c)) { + yes = true; + } else { + if (yes) { + c = Character.toUpperCase(c); + sb.append(c); + } + yes = false; + } + } + + return sb.toString(); + } + + public static String toNSigFigs(double x, int n) { + if (n <= 0) { + throw new AssertionError(); + } + String sign = x >= 0.0D ? "" : "-"; + double v = Math.abs(x); + double lv = Math.floor(Math.log10(v)); + double keeper = Math.pow(10D, n - 1); + double tens = Math.pow(10D, lv); + int keepMe = (int) Math.round((v * keeper) / tens); + if (lv < 0.0D) { + String s = toNSigFigs2_small(n, sign, lv, keepMe); + if (s != null) { + return s; + } + } + double vt = ((double) keepMe * tens) / keeper; + String num = Printer.toStringNumber(Double.valueOf(vt)); + return (new StringBuilder(String.valueOf(sign))).append(num).toString(); + } + + private static String toNSigFigs2_small(int n, String sign, double lv, int keepMe) { + if (lv < -8D) { + return null; + } + StringBuilder sb = new StringBuilder(sign); + int zs = (int) (-lv); + String sKeepMe = Integer.toString(keepMe); + if (sKeepMe.length() > n) { + if (sKeepMe.charAt(sKeepMe.length() - 1) != '0') { + throw new AssertionError(); + } + zs--; + sKeepMe = sKeepMe.substring(0, sKeepMe.length() - 1); + if (zs == 0) { + return null; + } + } + sb.append("0."); + for (int i = 1; i < zs; i++) { + sb.append('0'); + } + + sb.append(sKeepMe); + return sb.toString(); + } + + public static String toTitleCase(String title) { + if (title.length() < 2) { + return title.toUpperCase(); + } + StringBuilder sb = new StringBuilder(title.length()); + boolean goUp = true; + int i = 0; + for (int n = title.length(); i < n; i++) { + char c = title.charAt(i); + if (Character.isLetterOrDigit(c) || c == '\'') { + if (goUp) { + sb.append(Character.toUpperCase(c)); + goUp = false; + } else { + sb.append(Character.toLowerCase(c)); + } + } else { + sb.append(c); + goUp = true; + } + } + + return sb.toString(); + } + + public static String toTitleCasePlus(String wouldBeTitle) { + String words[] = wouldBeTitle.split("(_|\\s+)"); + StringBuilder sb = new StringBuilder(); + String as[]; + int j = (as = words).length; + for (int i = 0; i < j; i++) { + String word = as[i]; + if (word.length() != 0) { + if (Character.isUpperCase(word.charAt(0))) { + sb.append(word); + sb.append(' '); + } else { + word = replace(word, Pattern.compile("[A-Z]?[^A-Z]+"), new IReplace() { + + public void appendReplacementTo(StringBuilder sb2, Matcher match) { + String w = match.group(); + w = Strings.toTitleCase(w); + sb2.append(w); + sb2.append(' '); + } + + }); + sb.append(word); + } + } + } + + if (sb.length() != 0) { + pop(sb, 1); + } + return sb.toString(); + } + + public static String trimPunctuation(String string) { + return string; + } + + public static String trimQuotes(String string) { + if (string.charAt(0) != '\'' && string.charAt(0) != '"') { + return string; + } + char c = string.charAt(string.length() - 1); + if (c != '\'' && c != '"') { + return string; + } else { + return string.substring(1, string.length() - 1); + } + } + + public static int wordCount(String text) { + return text.split("\\s+").length; + } + + public static boolean isBlank(String line) { + return line.trim().isEmpty(); + } + + public static String stripTags(String xml) { + if (xml == null) { + return null; + } + if (xml.indexOf('<') == -1) { + return xml; + } else { + Matcher m4 = pScriptOrStyle.matcher(xml); + xml = m4.replaceAll(""); + Matcher m2 = pComment.matcher(xml); + String txt = m2.replaceAll(""); + Matcher m = TAG_REGEX.matcher(txt); + String txt2 = m.replaceAll(""); + Matcher m3 = pDocType.matcher(txt2); + String txt3 = m3.replaceAll(""); + return txt3; + } + } + +} diff --git a/plugin/src/winterwell/markdown/util/TimeOut.java b/plugin/src/winterwell/markdown/util/TimeOut.java new file mode 100644 index 0000000..60d1720 --- /dev/null +++ b/plugin/src/winterwell/markdown/util/TimeOut.java @@ -0,0 +1,36 @@ +package winterwell.markdown.util; + +import java.util.Timer; +import java.util.TimerTask; + +public final class TimeOut { + + private static final Timer timer = new Timer("TimeOuter", true); + private TimerTask task; + private final long timeout; + + public TimeOut(long timeout) { + this.timeout = timeout; + start(); + } + + public void cancel() { + task.cancel(); + } + + private void start() { + if (task != null) { + return; + } else { + final Thread target = Thread.currentThread(); + task = new TimerTask() { + + public void run() { + target.interrupt(); + } + }; + timer.schedule(task, timeout); + return; + } + } +} diff --git a/plugin/src/winterwell/markdown/views/Limiter.java b/plugin/src/winterwell/markdown/views/Limiter.java new file mode 100644 index 0000000..09560d5 --- /dev/null +++ b/plugin/src/winterwell/markdown/views/Limiter.java @@ -0,0 +1,94 @@ +package winterwell.markdown.views; + +import org.eclipse.swt.SWTException; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.PlatformUI; + +import winterwell.markdown.MarkdownUI; +import winterwell.markdown.preferences.Prefs; + +/** + * Rate limits view updates by ignoring all but the last update trigger received within a limit + * window. + *

+ * A trigger received outside of an active limit window causes an immediate update and the opening + * of a an active limit window. + *

+ * All triggers received within an active limit window causes, on termination of that active limit + * window, a single update and the start of a new active limit window. + *

+ * If no trigger is received within the an active limit window, no terminal update is performed and + * no new active limit window is opened. + */ +public class Limiter extends Thread { + + private MarkdownPreview view; + private final Display display; + private final int delay; + + private boolean running; + private long start; + private long mark; + + public Limiter(MarkdownPreview view) { + super("Limiter"); + this.view = view; + this.display = PlatformUI.getWorkbench().getDisplay(); + this.delay = MarkdownUI.getDefault().getPreferenceStore().getInt(Prefs.PREF_UPDATE_DELAY) * 1000; + } + + public void dispose() { + this.view = null; + } + + public void trigger() { + if (view != null) { + if (!running) { + startTimer(); + } else { + mark(System.currentTimeMillis()); + } + } + } + + private synchronized void startTimer() { + running = true; + doUpdate(); + + try { + display.asyncExec(new Runnable() { + + public void run() { + while (running) { + start = System.currentTimeMillis(); + mark(start); + while (System.currentTimeMillis() < start + delay) { + try { + Thread.sleep(delay); + } catch (InterruptedException e) {} + } + if (mark > start) { // triggered during window + doUpdate(); + } else { // no trigger, so close window + running = false; + } + } + } + }); + } catch (SWTException e) {} + } + + private synchronized void mark(long mark) { + this.mark = System.currentTimeMillis(); + } + + // asynchronous callback to perform the actual update + private void doUpdate() { + display.asyncExec(new Runnable() { + + public void run() { + if (view != null) view.update(); + } + }); + } +} diff --git a/plugin/src/winterwell/markdown/views/MarkdownPreview.java b/plugin/src/winterwell/markdown/views/MarkdownPreview.java index c103953..ef08dd7 100755 --- a/plugin/src/winterwell/markdown/views/MarkdownPreview.java +++ b/plugin/src/winterwell/markdown/views/MarkdownPreview.java @@ -1,100 +1,319 @@ package winterwell.markdown.views; - import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.MultiStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.text.ITextListener; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.TextEvent; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.swt.SWT; import org.eclipse.swt.browser.Browser; +import org.eclipse.swt.browser.ProgressAdapter; +import org.eclipse.swt.browser.ProgressEvent; import org.eclipse.swt.widgets.Composite; import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IPartListener; import org.eclipse.ui.IPathEditorInput; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.PlatformUI; import org.eclipse.ui.part.ViewPart; +import org.osgi.framework.Bundle; +import winterwell.markdown.Log; +import winterwell.markdown.MarkdownUI; import winterwell.markdown.editors.ActionBarContributor; import winterwell.markdown.editors.MarkdownEditor; import winterwell.markdown.pagemodel.MarkdownPage; +import winterwell.markdown.preferences.Prefs; +import winterwell.markdown.util.PartListener; +import winterwell.markdown.util.Strings; + +public class MarkdownPreview extends ViewPart implements Prefs { + + public static final String ID = "winterwell.markdown.views.MarkdownPreview"; + + // script to return the current top scroll position of the browser widget + private static final String GETSCROLLTOP = "function getScrollTop() { " //$NON-NLS-1$ + + " if(typeof pageYOffset!='undefined') return pageYOffset;" //$NON-NLS-1$ + + " else{" //$NON-NLS-1$ + + "var B=document.body;" //$NON-NLS-1$ + + "var D=document.documentElement;" //$NON-NLS-1$ + + "D=(D.clientHeight)?D:B;return D.scrollTop;}" //$NON-NLS-1$ + + "}; return getScrollTop();"; //$NON-NLS-1$ + + private static MarkdownPreview view; + private Browser browser; + + private Limiter limiter; + + private IPartListener partListener = new PartListener() { + + @Override + public void partActivated(IWorkbenchPart part) { + if (part instanceof MarkdownEditor) { + ((MarkdownEditor) part).getViewer().addTextListener(textListener); + limiter.trigger(); + } else if (part instanceof MarkdownPreview) { + limiter.trigger(); + } + } + @Override + public void partClosed(IWorkbenchPart part) { + if (part instanceof MarkdownEditor) { + getActivePage().hideView(view); + } + } + }; + private final IPropertyChangeListener styleListener = new IPropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + switch (event.getProperty()) { + case PREF_CSS_CUSTOM: + case PREF_CSS_DEFAULT: + limiter.trigger(); + } + } + }; -public class MarkdownPreview extends ViewPart { - - public static MarkdownPreview preview = null; - - private Browser viewer = null; + private ITextListener textListener = new ITextListener() { + + @Override + public void textChanged(TextEvent event) { + limiter.trigger(); + } + }; /** * The constructor. */ public MarkdownPreview() { - preview = this; + view = this; } /** - * This is a callback that will allow us - * to create the viewer and initialize it. + * Callback to create and initialize the browser. */ @Override public void createPartControl(Composite parent) { - viewer = new Browser(parent, SWT.MULTI); // | SWT.H_SCROLL | SWT.V_SCROLL - } - - + browser = new Browser(parent, SWT.MULTI); + limiter = new Limiter(view); + getPreferenceStore().addPropertyChangeListener(styleListener); + getActivePage().addPartListener(partListener); + } - /** - * Passing the focus request to the viewer's control. - */ @Override - public void setFocus() { - if (viewer==null) return; - viewer.setFocus(); - update(); + public void dispose() { + getPreferenceStore().removePropertyChangeListener(styleListener); + getActivePage().removePartListener(partListener); + ITextViewer srcViewer = getSourceViewer(); + srcViewer.removeTextListener(textListener); + + if (limiter != null) { + limiter.dispose(); + limiter = null; + } + browser = null; + super.dispose(); } - public void update() { - if (viewer==null) return; + public synchronized void update() { + if (browser == null) return; + try { IEditorPart editor = ActionBarContributor.getActiveEditor(); if (!(editor instanceof MarkdownEditor)) { - viewer.setText(""); + browser.setText(""); return; } + + // // object used for wait/notify communications + // Boolean load = true; + + Object result = browser.evaluate(GETSCROLLTOP); + final int scrollTop = result != null ? ((Number) result).intValue() : 0; + + String html = ""; MarkdownEditor ed = (MarkdownEditor) editor; MarkdownPage page = ed.getMarkdownPage(); - String html = page.html(); - html = addBaseURL(editor, html); - if (page != null) viewer.setText(html); - else viewer.setText(""); - } catch (Exception ex) { - // Smother - System.out.println(ex); - - if (viewer != null && !viewer.isDisposed()) - viewer.setText(ex.getMessage()); + if (page != null) { + html = page.html(); + IPath path = getInputPath(editor); + html = addHeader(html, getBaseUrl(path), getMdStyles(path)); + + browser.addProgressListener(new ProgressAdapter() { + + @Override + public void completed(ProgressEvent event) { + browser.removeProgressListener(this); + browser.execute(String.format("window.scrollTo(0,%d);", scrollTop)); //$NON-NLS-1$ + browser.setRedraw(true); + // load.notify(); + } + }); + } + + browser.setRedraw(false); + browser.setText(html); + + // wait for the browser load operation to complete + // load.wait(getPreferenceStore().getInt(PREF_UPDATE_DELAY) * 1000); + + } catch (Exception e) { + StringWriter errors = new StringWriter(); + e.printStackTrace(new PrintWriter(errors)); + Log.error(e.getLocalizedMessage() + Strings.EOL + errors.toString()); + + List lines = new ArrayList<>(); + for (StackTraceElement line : e.getStackTrace()) { + lines.add(new Status(IStatus.ERROR, MarkdownUI.PLUGIN_ID, line.toString())); + } + MultiStatus status = new MultiStatus(MarkdownUI.PLUGIN_ID, IStatus.ERROR, + lines.toArray(new Status[lines.size()]), e.getLocalizedMessage(), e); + ErrorDialog.openError(null, "Viewer error", e.getMessage(), status); } } /** - * Adjust the URL base to be the file's directory. - * @param editor - * @param html - * @return + * Passing the focus request to the browser's control. */ - private String addBaseURL(IEditorPart editor, String html) { - try { + @Override + public void setFocus() { + if (browser != null) browser.setFocus(); + } + + protected IWorkbenchPage getActivePage() { + return PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + } + + protected ITextViewer getSourceViewer() { + MarkdownEditor editor = (MarkdownEditor) ActionBarContributor.getActiveEditor(); + return editor.getViewer(); + } + + protected IPreferenceStore getPreferenceStore() { + return MarkdownUI.getDefault().getPreferenceStore(); + } + + private IPath getInputPath(IEditorPart editor) { IPathEditorInput input = (IPathEditorInput) editor.getEditorInput(); - IPath path = input.getPath(); - path = path.removeLastSegments(1); - File f = path.toFile(); - URI fileURI = f.toURI(); - String html2 = "\r\n"+html - +"\r\n"; - return html2; - } catch (Exception ex) { - return html; + return input.getPath(); } + + private String getBaseUrl(IPath path) { + return path.removeLastSegments(1).toFile().toURI().toString(); } -} \ No newline at end of file + + private String getMdStyles(IPath path) { + // 1) look for a file having the same name as the input file, beginning in the + // current directory, parent directories, and the current project directory. + IPath styles = path.removeFileExtension().addFileExtension(CSS); + String pathname = find(styles); + if (pathname != null) return pathname; + + // 2) look for a file with the name 'markdown.css' in the same set of directories + styles = path.removeLastSegments(1).append(DEF_MDCSS); + pathname = find(styles); + if (pathname != null) return pathname; + + // 3) read the file identified by the pref key 'PREF_CSS_CUSTOM' from the filesystem + IPreferenceStore store = MarkdownUI.getDefault().getPreferenceStore(); + String customCss = store.getString(PREF_CSS_CUSTOM); + if (!customCss.isEmpty()) { + File file = new File(customCss); + if (file.isFile() && file.getName().endsWith("." + CSS)) { + return customCss; + } + } + + // 4) read the file identified by the pref key 'PREF_CSS_DEFAULT' from the bundle + String defaultCss = store.getString(PREF_CSS_DEFAULT); + if (!defaultCss.isEmpty()) { + try { + URI uri = new URI(defaultCss); + File file = new File(uri); + if (file.isFile()) return file.getPath(); + } catch (URISyntaxException e) { + MessageDialog.openInformation(null, "Default CSS from bundle", defaultCss); + } + } + + // 5) read 'markdown.css' from the bundle + Bundle bundle = Platform.getBundle(MarkdownUI.PLUGIN_ID); + URL url = FileLocator.find(bundle, new Path("resources/" + DEF_MDCSS), null); + try { + url = FileLocator.toFileURL(url); + return url.toURI().toString(); + } catch (IOException | URISyntaxException e) { + Log.error(e); + return null; + } + } + + private String find(IPath styles) { + String name = styles.lastSegment(); + IPath base = styles.removeLastSegments(1); + + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IContainer dir = root.getContainerForLocation(base); + + while (dir.getType() != IResource.ROOT) { + IResource member = dir.findMember(name); + if (member != null) { + return root.getLocation().append(member.getFullPath()).toFile().toURI().toString(); + } + dir = dir.getParent(); + } + return null; + } + + private String addHeader(String html, String base, String style) { + StringBuilder sb = new StringBuilder("" + Strings.EOL); + if (base != null) sb.append("" + Strings.EOL); + if (style != null) { + sb.append("" + Strings.EOL); + } + sb.append("" + Strings.EOL + html + Strings.EOL + ""); + return sb.toString(); + } + + // Notes for future enhancement: + // + // StyledText text = ed.getViewer().getTextWidget(); + // float offset = text.getCaretOffset(); + // float size = text.getCharCount(); + // float center = offset/size; + // + // var scrollTop = window.scrollTop(); + // var docHeight = document.height(); + // var winHeight = window.height(); + // var scrollPercent = (scrollTop) / (docHeight - winHeight); + // var scrollPercentRounded = Math.round(scrollPercent * 100) / 100; +} diff --git a/pom.xml b/pom.xml index 48d862b..a272cf3 100644 --- a/pom.xml +++ b/pom.xml @@ -1,14 +1,17 @@ - + 4.0.0 com.winterwell.markdown markdown.editor.parent - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT pom - Markdown Editor (parent) + + Markdown Editor parent + com.winterwell.markdown parent Winterwell Associates Ltd @@ -34,48 +37,38 @@ http://www.nodeclipse.org/ +8 + + Gerald Rosenberg + Certiv Analytics + http://www.certiv.net/ + -8 + - 3.0 + 3.3.3 - 0.18.1 - - UTF-8 UTF-8 - - + 0.24.0 + 0.24.0 + 1.8 - - plugin feature + plugin site - - - - kepler - p2 - http://download.eclipse.org/releases/kepler - - @@ -83,52 +76,19 @@ org.eclipse.tycho tycho-maven-plugin - ${tycho-version} + ${tycho.version} true - - - org.eclipse.tycho - tycho-compiler-plugin - ${tycho-version} - - 1.6 - 1.6 - - - - - org.eclipse.tycho target-platform-configuration - ${tycho-version} + ${tycho.version} @@ -159,9 +119,22 @@ - + - + + + + org.eclipse.tycho + tycho-packaging-plugin + ${tycho.version} + + yyyyMMdd-HHmm + + + + + + diff --git a/site/.project b/site/.project new file mode 100644 index 0000000..3bc7981 --- /dev/null +++ b/site/.project @@ -0,0 +1,17 @@ + + + winterwell.markdown.site + + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + + diff --git a/site/pom.xml b/site/pom.xml index de1ab2a..605be36 100644 --- a/site/pom.xml +++ b/site/pom.xml @@ -1,19 +1,20 @@ + xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 4.0.0 com.winterwell.markdown markdown.editor.parent - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT markdown.editor.site eclipse-repository - - Markdown Editor (site) - Markdown Editor (site) - + + Markdown Editor site + Markdown Editor site +