diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 54b5faa..0000000 --- a/.gitignore +++ /dev/null @@ -1,123 +0,0 @@ -# Uncomment these types if you want even more clean repository. But be careful. -# It can make harm to an existing project source. Read explanations below. -# -# Resource files are binaries containing manifest, project icon and version info. -# They can not be viewed as text or compared by diff-tools. Consider replacing them with .rc files. -#*.res -# -# Type library file (binary). In old Delphi versions it should be stored. -# Since Delphi 2009 it is produced from .ridl file and can safely be ignored. -#*.tlb -# -# Diagram Portfolio file. Used by the diagram editor up to Delphi 7. -# Uncomment this if you are not using diagrams or use newer Delphi version. -#*.ddp -# -# Visual LiveBindings file. Added in Delphi XE2. -# Uncomment this if you are not using LiveBindings Designer. -#*.vlb -# -# Deployment Manager configuration file for your project. Added in Delphi XE2. -# Uncomment this if it is not mobile development and you do not use remote debug feature. -#*.deployproj -# - -# Delphi compiler-generated binaries (safe to delete) -*.exe -*.dll -*.bpl -*.bpi -*.dcp -*.so -*.apk -*.drc -*.map -*.dres -*.rsm -*.tds -*.dcu -*.lib - -# Delphi autogenerated files (duplicated info) -*.cfg -*Resource.rc - -# Delphi local files (user-specific info) -*.local -*.identcache -*.projdata -*.tvsconfig -*.dsk - -# Delphi history and backups -__history/ -*.~* - -# Castalia statistics file -*.stat -source/Win32/Debug/MarkDownTests.jdbg -java/eclipse/workspace/.metadata/.lock -java/eclipse/workspace/.metadata/.mylyn/.taskListIndex/segments.gen -java/eclipse/workspace/.metadata/.mylyn/.taskListIndex/segments_1 -java/eclipse/workspace/.metadata/.mylyn/repositories.xml.zip -java/eclipse/workspace/.metadata/.mylyn/tasks.xml.zip -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/history.version -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.version -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.resources/.root/2.tree -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.debug.ui.prefs -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.epp.logging.aeri.ui.prefs -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.jdt.ui.prefs -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.m2e.discovery.prefs -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.mylyn.context.core.prefs -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.mylyn.monitor.ui.prefs -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.mylyn.tasks.ui.prefs -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.pde.api.tools.prefs -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.ide.prefs -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.prefs -java/eclipse/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.ui.workbench.prefs -java/eclipse/workspace/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/history/_0.fdt -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/history/_0.fdx -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/history/_0.fnm -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/history/_0.frq -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/history/_0.nrm -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/history/_0.tii -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/history/_0.tis -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/history/segments.gen -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/history/segments_1 -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/_0.fdt -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/_0.fdx -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/_0.tis -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/_1.fdt -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/_1.fdx -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/_1.fnm -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/_1.frq -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/_1.nrm -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/_1.prx -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/_1.tii -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/_1.tis -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/segments.gen -java/eclipse/workspace/.metadata/.plugins/org.eclipse.epp.logging.aeri.ui/remote-index/segments_2 -java/eclipse/workspace/.metadata/.plugins/org.eclipse.jdt.core/assumedExternalFilesCache -java/eclipse/workspace/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache -java/eclipse/workspace/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache -java/eclipse/workspace/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat -java/eclipse/workspace/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml -java/eclipse/workspace/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml -java/eclipse/workspace/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml -java/eclipse/workspace/.metadata/.plugins/org.eclipse.m2e.logback.configuration/logback.1.6.0.20150526-2032.xml -java/eclipse/workspace/.metadata/.plugins/org.eclipse.oomph.setup.ui/dialog_settings.xml -java/eclipse/workspace/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup -java/eclipse/workspace/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml -java/eclipse/workspace/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml -java/eclipse/workspace/.metadata/version.ini -source/MarkDownTests-ps.dpr -source/MarkDownTests-ps.dproj -source/MarkdownDaringFireball-ps.pas - -source/Win32/Debug/TestInsightSettings\.ini - -*.out diff --git a/README.md b/README.md index 874f0b6..47b96a9 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,34 @@ -# FPC-markdown +FPC-markdown +============ Markdown Processor for FPC. -## Basic Information +Basic Information +----------------- This is a Pascal (FPC) library that processes markdown to HTML. At present the following dialects of markdown are supported: -* The Daring Fireball dialect (see https://daringfireball.net/projects/markdown/) (translated from https://github.com/rjeschke/txtmark) +* The Daring Fireball dialect + (see ) -Wishlist: PEGDown (Github dialect), CommonMark +* Enhanced TxtMark dialect + (translated from ) -All you need to use the library is any unicode version of FPC. +Wishlist: PEGDown (Github dialect), CommonMark, etc. + +All you need to use the library is FPC version 3.0.4 or newer. ## Using the Library -Create a TMarkdownProcessor (MarkdownProcessor.pas) of the dialect you want: + +Declare a variable of the class TMarkdownProcessor: var md : TMarkdownProcessor; - + +Create a TMarkdownProcessor (MarkdownProcessor.pas) of the dialect you want: + md := TMarkdownProcessor.createDialect(mdDaringFireball) Decide whether you want to allow active content @@ -32,20 +41,24 @@ Generate HTML fragments from Markdown content: html := md.process(markdown); -Note that the HTML returned is an HTML fragment, not a full HTML page. +Note that the HTML returned is an HTML fragment, not a full HTML page. + +Do not forget to dispose the object after the use: + + md.free ## License Copyright (C) Miguel A. Risco-Castillo -FPC-markdown Implementation is a fork of Grahame Grieve -Delphi-markdown (pascal port) +FPC-markdown implementation is a fork of Grahame Grieve pascal port +[Delphi-markdown](https://github.com/grahamegrieve/delphi-markdown) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/demo/MarkDownTests.ico b/demo/MarkDownTests.ico deleted file mode 100644 index 0341321..0000000 Binary files a/demo/MarkDownTests.ico and /dev/null differ diff --git a/demo/MarkDownTests.lps b/demo/MarkDownTests.lps deleted file mode 100644 index 00a2775..0000000 --- a/demo/MarkDownTests.lps +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demo/MarkDownTests.res b/demo/MarkDownTests.res deleted file mode 100644 index a6fa3db..0000000 Binary files a/demo/MarkDownTests.res and /dev/null differ diff --git a/demo/backup/MarkDownTests.lps b/demo/backup/MarkDownTests.lps deleted file mode 100644 index 00a2775..0000000 --- a/demo/backup/MarkDownTests.lps +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demo/backup/markdowntestu.lfm b/demo/backup/markdowntestu.lfm deleted file mode 100644 index f8cdd5f..0000000 --- a/demo/backup/markdowntestu.lfm +++ /dev/null @@ -1,32 +0,0 @@ -object MainForm: TMainForm - Left = 859 - Height = 298 - Top = 610 - Width = 473 - Caption = 'MainForm' - ClientHeight = 298 - ClientWidth = 473 - LCLVersion = '1.8.0.6' - object Memo1: TMemo - Left = 0 - Height = 248 - Top = 0 - Width = 473 - Align = alTop - Anchors = [akTop, akLeft, akRight, akBottom] - Lines.Strings = ( - 'Paste here the Markdown text' - ) - TabOrder = 0 - end - object B_Convert: TButton - Left = 56 - Height = 25 - Top = 260 - Width = 369 - Anchors = [akLeft, akRight, akBottom] - Caption = 'Convert to HTML' - OnClick = B_ConvertClick - TabOrder = 1 - end -end diff --git a/demo/markdowntestu.lfm b/demo/markdowntestu.lfm deleted file mode 100644 index b17ff20..0000000 --- a/demo/markdowntestu.lfm +++ /dev/null @@ -1,33 +0,0 @@ -object MainForm: TMainForm - Left = 859 - Height = 298 - Top = 610 - Width = 473 - Caption = 'MainForm' - ClientHeight = 298 - ClientWidth = 473 - LCLVersion = '1.8.0.6' - object Memo1: TMemo - Left = 0 - Height = 248 - Top = 0 - Width = 473 - Align = alTop - Anchors = [akTop, akLeft, akRight, akBottom] - Lines.Strings = ( - 'Paste here the Markdown text' - ) - ScrollBars = ssAutoBoth - TabOrder = 0 - end - object B_Convert: TButton - Left = 56 - Height = 25 - Top = 260 - Width = 369 - Anchors = [akLeft, akRight, akBottom] - Caption = 'Convert to HTML' - OnClick = B_ConvertClick - TabOrder = 1 - end -end diff --git a/demo/markdowntestu.pas b/demo/markdowntestu.pas deleted file mode 100644 index 77783af..0000000 --- a/demo/markdowntestu.pas +++ /dev/null @@ -1,44 +0,0 @@ -unit MarkDownTestU; - -{$mode objfpc}{$H+} - -interface - -uses - Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls, - MarkdownProcessor; - -type - - { TMainForm } - - TMainForm = class(TForm) - B_Convert: TButton; - Memo1: TMemo; - procedure B_ConvertClick(Sender: TObject); - private - - public - - end; - -var - MainForm: TMainForm; - -implementation - -{$R *.lfm} - -{ TMainForm } - -procedure TMainForm.B_ConvertClick(Sender: TObject); -var - md:TMarkdownProcessor; -begin - md := TMarkdownProcessor.createDialect(mdDaringFireball); - md.UnSafe := true; - Memo1.Text:=md.process(Memo1.Text);; -end; - -end. - diff --git a/demo/backup/MarkDownTests.lpi b/demos/MarkDownBasicTest/MarkDownTests.lpi similarity index 93% rename from demo/backup/MarkDownTests.lpi rename to demos/MarkDownBasicTest/MarkDownTests.lpi index 292e4f1..b1cac2e 100644 --- a/demo/backup/MarkDownTests.lpi +++ b/demos/MarkDownBasicTest/MarkDownTests.lpi @@ -87,7 +87,7 @@ - + @@ -136,17 +136,4 @@ - - - - - - - - - - - - - diff --git a/demo/MarkDownTests.lpr b/demos/MarkDownBasicTest/MarkDownTests.lpr similarity index 100% rename from demo/MarkDownTests.lpr rename to demos/MarkDownBasicTest/MarkDownTests.lpr diff --git a/demos/MarkDownBasicTest/markdowntestu.lfm b/demos/MarkDownBasicTest/markdowntestu.lfm new file mode 100644 index 0000000..07b31a5 --- /dev/null +++ b/demos/MarkDownBasicTest/markdowntestu.lfm @@ -0,0 +1,100 @@ +object MainForm: TMainForm + Left = 1021 + Height = 317 + Top = 394 + Width = 473 + Caption = 'MainForm' + ClientHeight = 317 + ClientWidth = 473 + Position = poScreenCenter + LCLVersion = '1.8.0.6' + object Memo1: TMemo + Left = 0 + Height = 267 + Top = 0 + Width = 473 + Align = alTop + Anchors = [akTop, akLeft, akRight, akBottom] + Lines.Strings = ( + 'FPC-markdown' + '============' + '' + 'Markdown Processor for FPC. ' + '' + 'Basic Information' + '-----------------' + '' + 'This is a Pascal (`FPC`) library that processes markdown to HTML.' + 'At present the following dialects of markdown are supported:' + '' + '* The Daring Fireball dialect' + ' (see )' + '' + '* Enhanced TxtMark dialect' + ' (translated from )' + '' + 'Wishlist: PEGDown (Github dialect), CommonMark, etc.' + '' + 'All you need to use the library is FPC version 3.0.4 or newer.' + '' + '## Using the Library' + '' + '' + 'Declare a variable of the class TMarkdownProcessor:' + '' + ' var' + ' md : TMarkdownProcessor;' + '' + 'Create a TMarkdownProcessor (MarkdownProcessor.pas) of the dialect you want:' + '' + ' md := TMarkdownProcessor.createDialect(mdDaringFireball)' + ' ' + 'Decide whether you want to allow active content' + '' + ' md.UnSafe := true;' + ' ' + 'Note: you should only set this to true if you *need* to - active content can be a signficant safety/security issue. ' + ' ' + 'Generate HTML fragments from Markdown content:' + '' + ' html := md.process(markdown); ' + ' ' + 'Note that the HTML returned is an HTML fragment, not a full HTML page. ' + ' ' + 'Do not forget to dispose the object after the use:' + '' + ' md.free' + '' + '## License' + '' + 'Copyright (C) Miguel A. Risco-Castillo' + '' + 'FPC-markdown implementation is a fork of Grahame Grieve pascal port' + '[Delphi-markdown](https://github.com/grahamegrieve/delphi-markdown)' + '' + 'Licensed under the Apache License, Version 2.0 (the "License");' + 'you may not use this file except in compliance with the License.' + 'You may obtain a copy of the License at' + '' + '' + '' + 'Unless required by applicable law or agreed to in writing, software' + 'distributed under the License is distributed on an "AS IS" BASIS,' + 'WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.' + 'See the License for the specific language governing permissions and' + 'limitations under the License.' + ) + ScrollBars = ssAutoBoth + TabOrder = 0 + end + object B_Convert: TButton + Left = 56 + Height = 25 + Top = 279 + Width = 369 + Anchors = [akLeft, akRight, akBottom] + Caption = 'Convert to HTML' + OnClick = B_ConvertClick + TabOrder = 1 + end +end diff --git a/demo/backup/markdowntestu.pas b/demos/MarkDownBasicTest/markdowntestu.pas similarity index 92% rename from demo/backup/markdowntestu.pas rename to demos/MarkDownBasicTest/markdowntestu.pas index 77783af..ab06c65 100644 --- a/demo/backup/markdowntestu.pas +++ b/demos/MarkDownBasicTest/markdowntestu.pas @@ -37,7 +37,8 @@ procedure TMainForm.B_ConvertClick(Sender: TObject); begin md := TMarkdownProcessor.createDialect(mdDaringFireball); md.UnSafe := true; - Memo1.Text:=md.process(Memo1.Text);; + Memo1.Text:=md.process(Memo1.Text); + md.free; end; end. diff --git a/demo/MarkDownTests.lpi b/demos/MarkDownHtml/MarkDownHtml.lpi similarity index 86% rename from demo/MarkDownTests.lpi rename to demos/MarkDownHtml/MarkDownHtml.lpi index 292e4f1..ba3c488 100644 --- a/demo/MarkDownTests.lpi +++ b/demos/MarkDownHtml/MarkDownHtml.lpi @@ -6,7 +6,7 @@ - + <Title Value="MarkDownHtml"/> <ResourceType Value="res"/> <UseXPManifest Value="True"/> <XPManifest> @@ -37,6 +37,9 @@ <StackChecks Value="True"/> </Checks> <VerifyObjMethodCallValidity Value="True"/> + <Optimizations> + <OptimizationLevel Value="0"/> + </Optimizations> </CodeGeneration> <Linking> <Debugging> @@ -51,9 +54,6 @@ </Win32> </Options> </Linking> - <Other> - <CustomOptions Value="-dBorland -dVer150 -dDelphi7 -dCompiler6_Up -dPUREPASCAL"/> - </Other> </CompilerOptions> </Item2> <Item3 Name="Release"> @@ -87,7 +87,7 @@ </BuildModes> <PublishOptions> <Version Value="2"/> - <DestinationDirectory Value="D:\Users\Miguel\Documents\Trabajos\for_GIT\fpc-markdown\source"/> + <DestinationDirectory Value="D:\Users\Miguel\Documents\Trabajos\for_GIT\fpcmarkdown\demos\MarkDownHtml"/> <SaveEditorInfoOfNonProjectFiles Value="True"/> </PublishOptions> <RunParams> @@ -95,26 +95,32 @@ <FormatVersion Value="1"/> </local> </RunParams> - <RequiredPackages Count="2"> + <RequiredPackages Count="4"> <Item1> - <PackageName Value="fpc_markdown"/> + <PackageName Value="FrameViewer09"/> </Item1> <Item2> - <PackageName Value="LCL"/> + <PackageName Value="SynEdit"/> </Item2> + <Item3> + <PackageName Value="fpc_markdown"/> + </Item3> + <Item4> + <PackageName Value="LCL"/> + </Item4> </RequiredPackages> <Units Count="2"> <Unit0> - <Filename Value="MarkDownTests.lpr"/> + <Filename Value="MarkDownHtml.lpr"/> <IsPartOfProject Value="True"/> </Unit0> <Unit1> - <Filename Value="markdowntestu.pas"/> + <Filename Value="MarkDownHtmlu.pas"/> <IsPartOfProject Value="True"/> <ComponentName Value="MainForm"/> <HasResources Value="True"/> <ResourceBaseClass Value="Form"/> - <UnitName Value="MarkDownTestU"/> + <UnitName Value="MarkDownHtmlU"/> </Unit1> </Units> </ProjectOptions> @@ -136,17 +142,4 @@ <CustomOptions Value="-dBorland -dVer150 -dDelphi7 -dCompiler6_Up -dPUREPASCAL"/> </Other> </CompilerOptions> - <Debugging> - <Exceptions Count="3"> - <Item1> - <Name Value="EAbort"/> - </Item1> - <Item2> - <Name Value="ECodetoolError"/> - </Item2> - <Item3> - <Name Value="EFOpenError"/> - </Item3> - </Exceptions> - </Debugging> </CONFIG> diff --git a/demo/backup/MarkDownTests.lpr b/demos/MarkDownHtml/MarkDownHtml.lpr similarity index 76% rename from demo/backup/MarkDownTests.lpr rename to demos/MarkDownHtml/MarkDownHtml.lpr index 730d493..6765e18 100644 --- a/demo/backup/MarkDownTests.lpr +++ b/demos/MarkDownHtml/MarkDownHtml.lpr @@ -1,4 +1,4 @@ -program MarkDownTests; +program MarkDownHtml; {$mode objfpc}{$H+} @@ -7,8 +7,7 @@ cthreads, {$ENDIF}{$ENDIF} Interfaces, // this includes the LCL widgetset - Forms, MarkDownTestU - { you can add units after this }; + Forms, MarkDownHtmlU; {$R *.res} diff --git a/demos/MarkDownHtml/MarkDownHtmlu.lfm b/demos/MarkDownHtml/MarkDownHtmlu.lfm new file mode 100644 index 0000000..ff0ea01 --- /dev/null +++ b/demos/MarkDownHtml/MarkDownHtmlu.lfm @@ -0,0 +1,1219 @@ +object MainForm: TMainForm + Left = 945 + Height = 499 + Top = 273 + Width = 790 + Caption = 'Test MarkDown Convertion' + ClientHeight = 499 + ClientWidth = 790 + Constraints.MinHeight = 230 + Constraints.MinWidth = 530 + OnCreate = FormCreate + OnDestroy = FormDestroy + Position = poScreenCenter + LCLVersion = '1.8.0.6' + object Panel1: TPanel + Left = 0 + Height = 36 + Top = 463 + Width = 790 + Align = alBottom + AutoSize = True + ChildSizing.LeftRightSpacing = 5 + ChildSizing.TopBottomSpacing = 5 + ChildSizing.HorizontalSpacing = 2 + ChildSizing.VerticalSpacing = 5 + ChildSizing.Layout = cclLeftToRightThenTopToBottom + ChildSizing.ControlsPerLine = 7 + ClientHeight = 36 + ClientWidth = 790 + TabOrder = 0 + object B_OpenFile: TButton + Left = 5 + Height = 25 + Top = 5 + Width = 76 + Caption = 'Open File' + OnClick = B_OpenFileClick + TabOrder = 4 + end + object B_Save: TBitBtn + Left = 83 + Height = 25 + Top = 5 + Width = 85 + Caption = 'Save to File' + OnClick = B_SaveClick + TabOrder = 5 + end + object B_Copy: TButton + Left = 170 + Height = 25 + Top = 5 + Width = 71 + Caption = 'Copy All' + OnClick = B_CopyClick + TabOrder = 1 + end + object B_Paste: TButton + Left = 243 + Height = 25 + Top = 5 + Width = 97 + Caption = 'Erase && Paste' + OnClick = B_PasteClick + TabOrder = 2 + end + object B_Convert: TButton + Left = 342 + Height = 25 + Top = 5 + Width = 68 + Caption = 'Convert' + OnClick = B_ConvertClick + TabOrder = 0 + end + object B_ViewBrowser: TButton + Left = 412 + Height = 25 + Top = 5 + Width = 109 + Caption = 'View in Browser' + OnClick = B_ViewBrowserClick + TabOrder = 3 + end + end + object PageControl1: TPageControl + Left = 0 + Height = 463 + Top = 0 + Width = 387 + ActivePage = TS_MarkDown + Align = alLeft + TabIndex = 0 + TabOrder = 1 + object TS_MarkDown: TTabSheet + Caption = 'MarkDown' + ClientHeight = 435 + ClientWidth = 379 + inline SE_MarkDown: TSynEdit + Left = 0 + Height = 435 + Top = 0 + Width = 379 + Align = alClient + Font.Height = -13 + Font.Name = 'Courier New' + Font.Pitch = fpFixed + Font.Quality = fqNonAntialiased + ParentColor = False + ParentFont = False + TabOrder = 0 + Gutter.Width = 57 + Gutter.MouseActions = <> + RightGutter.Width = 0 + RightGutter.MouseActions = <> + Highlighter = SynHTMLSyn1 + Keystrokes = < + item + Command = ecUp + ShortCut = 38 + end + item + Command = ecSelUp + ShortCut = 8230 + end + item + Command = ecScrollUp + ShortCut = 16422 + end + item + Command = ecDown + ShortCut = 40 + end + item + Command = ecSelDown + ShortCut = 8232 + end + item + Command = ecScrollDown + ShortCut = 16424 + end + item + Command = ecLeft + ShortCut = 37 + end + item + Command = ecSelLeft + ShortCut = 8229 + end + item + Command = ecWordLeft + ShortCut = 16421 + end + item + Command = ecSelWordLeft + ShortCut = 24613 + end + item + Command = ecRight + ShortCut = 39 + end + item + Command = ecSelRight + ShortCut = 8231 + end + item + Command = ecWordRight + ShortCut = 16423 + end + item + Command = ecSelWordRight + ShortCut = 24615 + end + item + Command = ecPageDown + ShortCut = 34 + end + item + Command = ecSelPageDown + ShortCut = 8226 + end + item + Command = ecPageBottom + ShortCut = 16418 + end + item + Command = ecSelPageBottom + ShortCut = 24610 + end + item + Command = ecPageUp + ShortCut = 33 + end + item + Command = ecSelPageUp + ShortCut = 8225 + end + item + Command = ecPageTop + ShortCut = 16417 + end + item + Command = ecSelPageTop + ShortCut = 24609 + end + item + Command = ecLineStart + ShortCut = 36 + end + item + Command = ecSelLineStart + ShortCut = 8228 + end + item + Command = ecEditorTop + ShortCut = 16420 + end + item + Command = ecSelEditorTop + ShortCut = 24612 + end + item + Command = ecLineEnd + ShortCut = 35 + end + item + Command = ecSelLineEnd + ShortCut = 8227 + end + item + Command = ecEditorBottom + ShortCut = 16419 + end + item + Command = ecSelEditorBottom + ShortCut = 24611 + end + item + Command = ecToggleMode + ShortCut = 45 + end + item + Command = ecCopy + ShortCut = 16429 + end + item + Command = ecPaste + ShortCut = 8237 + end + item + Command = ecDeleteChar + ShortCut = 46 + end + item + Command = ecCut + ShortCut = 8238 + end + item + Command = ecDeleteLastChar + ShortCut = 8 + end + item + Command = ecDeleteLastChar + ShortCut = 8200 + end + item + Command = ecDeleteLastWord + ShortCut = 16392 + end + item + Command = ecUndo + ShortCut = 32776 + end + item + Command = ecRedo + ShortCut = 40968 + end + item + Command = ecLineBreak + ShortCut = 13 + end + item + Command = ecSelectAll + ShortCut = 16449 + end + item + Command = ecCopy + ShortCut = 16451 + end + item + Command = ecBlockIndent + ShortCut = 24649 + end + item + Command = ecLineBreak + ShortCut = 16461 + end + item + Command = ecInsertLine + ShortCut = 16462 + end + item + Command = ecDeleteWord + ShortCut = 16468 + end + item + Command = ecBlockUnindent + ShortCut = 24661 + end + item + Command = ecPaste + ShortCut = 16470 + end + item + Command = ecCut + ShortCut = 16472 + end + item + Command = ecDeleteLine + ShortCut = 16473 + end + item + Command = ecDeleteEOL + ShortCut = 24665 + end + item + Command = ecUndo + ShortCut = 16474 + end + item + Command = ecRedo + ShortCut = 24666 + end + item + Command = ecGotoMarker0 + ShortCut = 16432 + end + item + Command = ecGotoMarker1 + ShortCut = 16433 + end + item + Command = ecGotoMarker2 + ShortCut = 16434 + end + item + Command = ecGotoMarker3 + ShortCut = 16435 + end + item + Command = ecGotoMarker4 + ShortCut = 16436 + end + item + Command = ecGotoMarker5 + ShortCut = 16437 + end + item + Command = ecGotoMarker6 + ShortCut = 16438 + end + item + Command = ecGotoMarker7 + ShortCut = 16439 + end + item + Command = ecGotoMarker8 + ShortCut = 16440 + end + item + Command = ecGotoMarker9 + ShortCut = 16441 + end + item + Command = ecSetMarker0 + ShortCut = 24624 + end + item + Command = ecSetMarker1 + ShortCut = 24625 + end + item + Command = ecSetMarker2 + ShortCut = 24626 + end + item + Command = ecSetMarker3 + ShortCut = 24627 + end + item + Command = ecSetMarker4 + ShortCut = 24628 + end + item + Command = ecSetMarker5 + ShortCut = 24629 + end + item + Command = ecSetMarker6 + ShortCut = 24630 + end + item + Command = ecSetMarker7 + ShortCut = 24631 + end + item + Command = ecSetMarker8 + ShortCut = 24632 + end + item + Command = ecSetMarker9 + ShortCut = 24633 + end + item + Command = EcFoldLevel1 + ShortCut = 41009 + end + item + Command = EcFoldLevel2 + ShortCut = 41010 + end + item + Command = EcFoldLevel3 + ShortCut = 41011 + end + item + Command = EcFoldLevel4 + ShortCut = 41012 + end + item + Command = EcFoldLevel5 + ShortCut = 41013 + end + item + Command = EcFoldLevel6 + ShortCut = 41014 + end + item + Command = EcFoldLevel7 + ShortCut = 41015 + end + item + Command = EcFoldLevel8 + ShortCut = 41016 + end + item + Command = EcFoldLevel9 + ShortCut = 41017 + end + item + Command = EcFoldLevel0 + ShortCut = 41008 + end + item + Command = EcFoldCurrent + ShortCut = 41005 + end + item + Command = EcUnFoldCurrent + ShortCut = 41003 + end + item + Command = EcToggleMarkupWord + ShortCut = 32845 + end + item + Command = ecNormalSelect + ShortCut = 24654 + end + item + Command = ecColumnSelect + ShortCut = 24643 + end + item + Command = ecLineSelect + ShortCut = 24652 + end + item + Command = ecTab + ShortCut = 9 + end + item + Command = ecShiftTab + ShortCut = 8201 + end + item + Command = ecMatchBracket + ShortCut = 24642 + end + item + Command = ecColSelUp + ShortCut = 40998 + end + item + Command = ecColSelDown + ShortCut = 41000 + end + item + Command = ecColSelLeft + ShortCut = 40997 + end + item + Command = ecColSelRight + ShortCut = 40999 + end + item + Command = ecColSelPageDown + ShortCut = 40994 + end + item + Command = ecColSelPageBottom + ShortCut = 57378 + end + item + Command = ecColSelPageUp + ShortCut = 40993 + end + item + Command = ecColSelPageTop + ShortCut = 57377 + end + item + Command = ecColSelLineStart + ShortCut = 40996 + end + item + Command = ecColSelLineEnd + ShortCut = 40995 + end + item + Command = ecColSelEditorTop + ShortCut = 57380 + end + item + Command = ecColSelEditorBottom + ShortCut = 57379 + end> + MouseActions = <> + MouseTextActions = <> + MouseSelActions = <> + Lines.Strings = ( + 'FPC-markdown' + '============' + '' + 'Markdown Processor for FPC. ' + '' + 'Basic Information' + '-----------------' + '' + 'This is a Pascal (`FPC`) library that processes markdown to HTML.' + 'At present the following dialects of markdown are supported:' + '' + '* The Daring Fireball dialect' + ' (see <https://daringfireball.net/projects/markdown/>)' + '' + '* Enhanced TxtMark dialect' + ' (translated from <https://github.com/rjeschke/txtmark>)' + '' + 'Wishlist: PEGDown (Github dialect), CommonMark, etc.' + '' + 'All you need to use the library is FPC version 3.0.4 or newer.' + '' + '## Using the Library' + '' + '' + 'Declare a variable of the class TMarkdownProcessor:' + '' + ' var' + ' md : TMarkdownProcessor;' + '' + 'Create a TMarkdownProcessor (MarkdownProcessor.pas) of the dialect you want:' + '' + ' md := TMarkdownProcessor.createDialect(mdDaringFireball)' + ' ' + 'Decide whether you want to allow active content' + '' + ' md.UnSafe := true;' + ' ' + 'Note: you should only set this to true if you *need* to - active content can be a signficant safety/security issue. ' + ' ' + 'Generate HTML fragments from Markdown content:' + '' + ' html := md.process(markdown); ' + ' ' + 'Note that the HTML returned is an HTML fragment, not a full HTML page. ' + ' ' + 'Do not forget to dispose the object after the use:' + '' + ' md.free' + '' + '## License' + '' + 'Copyright (C) Miguel A. Risco-Castillo' + '' + 'FPC-markdown implementation is a fork of Grahame Grieve pascal port' + '[Delphi-markdown](https://github.com/grahamegrieve/delphi-markdown)' + '' + 'Licensed under the Apache License, Version 2.0 (the "License");' + 'you may not use this file except in compliance with the License.' + 'You may obtain a copy of the License at' + '' + '<http://www.apache.org/licenses/LICENSE-2.0>' + '' + 'Unless required by applicable law or agreed to in writing, software' + 'distributed under the License is distributed on an "AS IS" BASIS,' + 'WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.' + 'See the License for the specific language governing permissions and' + 'limitations under the License.' + ) + Options = [eoAutoIndent, eoBracketHighlight, eoGroupUndo, eoShowSpecialChars, eoSmartTabs] + MouseOptions = [emAltSetsColumnMode] + VisibleSpecialChars = [vscSpace, vscTabAtLast] + ScrollBars = ssAutoBoth + SelectedColor.BackPriority = 50 + SelectedColor.ForePriority = 50 + SelectedColor.FramePriority = 50 + SelectedColor.BoldPriority = 50 + SelectedColor.ItalicPriority = 50 + SelectedColor.UnderlinePriority = 50 + SelectedColor.StrikeOutPriority = 50 + BracketHighlightStyle = sbhsBoth + BracketMatchColor.Background = clNone + BracketMatchColor.Foreground = clNone + BracketMatchColor.Style = [fsBold] + FoldedCodeColor.Background = clNone + FoldedCodeColor.Foreground = clGray + FoldedCodeColor.FrameColor = clGray + MouseLinkColor.Background = clNone + MouseLinkColor.Foreground = clBlue + LineHighlightColor.Background = clCream + LineHighlightColor.Foreground = clNone + inline SynLeftGutterPartList1: TSynGutterPartList + object SynGutterMarks1: TSynGutterMarks + Width = 24 + MouseActions = <> + end + object SynGutterLineNumber1: TSynGutterLineNumber + Width = 17 + MouseActions = <> + MarkupInfo.Background = clBtnFace + MarkupInfo.Foreground = clNone + DigitCount = 2 + ShowOnlyLineNumbersMultiplesOf = 1 + ZeroStart = False + LeadingZeros = False + end + object SynGutterChanges1: TSynGutterChanges + Width = 4 + MouseActions = <> + ModifiedColor = 59900 + SavedColor = clGreen + end + object SynGutterSeparator1: TSynGutterSeparator + Width = 2 + MouseActions = <> + MarkupInfo.Background = clWhite + MarkupInfo.Foreground = clGray + end + object SynGutterCodeFolding1: TSynGutterCodeFolding + MouseActions = <> + MarkupInfo.Background = clNone + MarkupInfo.Foreground = clGray + MouseActionsExpanded = <> + MouseActionsCollapsed = <> + end + end + end + end + object TS_HTML: TTabSheet + Caption = 'HTML' + ClientHeight = 384 + ClientWidth = 419 + inline SE_HTML: TSynEdit + Left = 0 + Height = 384 + Top = 0 + Width = 419 + Align = alClient + Font.Height = -13 + Font.Name = 'Courier New' + Font.Pitch = fpFixed + Font.Quality = fqNonAntialiased + ParentColor = False + ParentFont = False + TabOrder = 0 + Gutter.Width = 57 + Gutter.MouseActions = <> + RightGutter.Width = 0 + RightGutter.MouseActions = <> + Highlighter = SynHTMLSyn1 + Keystrokes = < + item + Command = ecUp + ShortCut = 38 + end + item + Command = ecSelUp + ShortCut = 8230 + end + item + Command = ecScrollUp + ShortCut = 16422 + end + item + Command = ecDown + ShortCut = 40 + end + item + Command = ecSelDown + ShortCut = 8232 + end + item + Command = ecScrollDown + ShortCut = 16424 + end + item + Command = ecLeft + ShortCut = 37 + end + item + Command = ecSelLeft + ShortCut = 8229 + end + item + Command = ecWordLeft + ShortCut = 16421 + end + item + Command = ecSelWordLeft + ShortCut = 24613 + end + item + Command = ecRight + ShortCut = 39 + end + item + Command = ecSelRight + ShortCut = 8231 + end + item + Command = ecWordRight + ShortCut = 16423 + end + item + Command = ecSelWordRight + ShortCut = 24615 + end + item + Command = ecPageDown + ShortCut = 34 + end + item + Command = ecSelPageDown + ShortCut = 8226 + end + item + Command = ecPageBottom + ShortCut = 16418 + end + item + Command = ecSelPageBottom + ShortCut = 24610 + end + item + Command = ecPageUp + ShortCut = 33 + end + item + Command = ecSelPageUp + ShortCut = 8225 + end + item + Command = ecPageTop + ShortCut = 16417 + end + item + Command = ecSelPageTop + ShortCut = 24609 + end + item + Command = ecLineStart + ShortCut = 36 + end + item + Command = ecSelLineStart + ShortCut = 8228 + end + item + Command = ecEditorTop + ShortCut = 16420 + end + item + Command = ecSelEditorTop + ShortCut = 24612 + end + item + Command = ecLineEnd + ShortCut = 35 + end + item + Command = ecSelLineEnd + ShortCut = 8227 + end + item + Command = ecEditorBottom + ShortCut = 16419 + end + item + Command = ecSelEditorBottom + ShortCut = 24611 + end + item + Command = ecToggleMode + ShortCut = 45 + end + item + Command = ecCopy + ShortCut = 16429 + end + item + Command = ecPaste + ShortCut = 8237 + end + item + Command = ecDeleteChar + ShortCut = 46 + end + item + Command = ecCut + ShortCut = 8238 + end + item + Command = ecDeleteLastChar + ShortCut = 8 + end + item + Command = ecDeleteLastChar + ShortCut = 8200 + end + item + Command = ecDeleteLastWord + ShortCut = 16392 + end + item + Command = ecUndo + ShortCut = 32776 + end + item + Command = ecRedo + ShortCut = 40968 + end + item + Command = ecLineBreak + ShortCut = 13 + end + item + Command = ecSelectAll + ShortCut = 16449 + end + item + Command = ecCopy + ShortCut = 16451 + end + item + Command = ecBlockIndent + ShortCut = 24649 + end + item + Command = ecLineBreak + ShortCut = 16461 + end + item + Command = ecInsertLine + ShortCut = 16462 + end + item + Command = ecDeleteWord + ShortCut = 16468 + end + item + Command = ecBlockUnindent + ShortCut = 24661 + end + item + Command = ecPaste + ShortCut = 16470 + end + item + Command = ecCut + ShortCut = 16472 + end + item + Command = ecDeleteLine + ShortCut = 16473 + end + item + Command = ecDeleteEOL + ShortCut = 24665 + end + item + Command = ecUndo + ShortCut = 16474 + end + item + Command = ecRedo + ShortCut = 24666 + end + item + Command = ecGotoMarker0 + ShortCut = 16432 + end + item + Command = ecGotoMarker1 + ShortCut = 16433 + end + item + Command = ecGotoMarker2 + ShortCut = 16434 + end + item + Command = ecGotoMarker3 + ShortCut = 16435 + end + item + Command = ecGotoMarker4 + ShortCut = 16436 + end + item + Command = ecGotoMarker5 + ShortCut = 16437 + end + item + Command = ecGotoMarker6 + ShortCut = 16438 + end + item + Command = ecGotoMarker7 + ShortCut = 16439 + end + item + Command = ecGotoMarker8 + ShortCut = 16440 + end + item + Command = ecGotoMarker9 + ShortCut = 16441 + end + item + Command = ecSetMarker0 + ShortCut = 24624 + end + item + Command = ecSetMarker1 + ShortCut = 24625 + end + item + Command = ecSetMarker2 + ShortCut = 24626 + end + item + Command = ecSetMarker3 + ShortCut = 24627 + end + item + Command = ecSetMarker4 + ShortCut = 24628 + end + item + Command = ecSetMarker5 + ShortCut = 24629 + end + item + Command = ecSetMarker6 + ShortCut = 24630 + end + item + Command = ecSetMarker7 + ShortCut = 24631 + end + item + Command = ecSetMarker8 + ShortCut = 24632 + end + item + Command = ecSetMarker9 + ShortCut = 24633 + end + item + Command = EcFoldLevel1 + ShortCut = 41009 + end + item + Command = EcFoldLevel2 + ShortCut = 41010 + end + item + Command = EcFoldLevel3 + ShortCut = 41011 + end + item + Command = EcFoldLevel4 + ShortCut = 41012 + end + item + Command = EcFoldLevel5 + ShortCut = 41013 + end + item + Command = EcFoldLevel6 + ShortCut = 41014 + end + item + Command = EcFoldLevel7 + ShortCut = 41015 + end + item + Command = EcFoldLevel8 + ShortCut = 41016 + end + item + Command = EcFoldLevel9 + ShortCut = 41017 + end + item + Command = EcFoldLevel0 + ShortCut = 41008 + end + item + Command = EcFoldCurrent + ShortCut = 41005 + end + item + Command = EcUnFoldCurrent + ShortCut = 41003 + end + item + Command = EcToggleMarkupWord + ShortCut = 32845 + end + item + Command = ecNormalSelect + ShortCut = 24654 + end + item + Command = ecColumnSelect + ShortCut = 24643 + end + item + Command = ecLineSelect + ShortCut = 24652 + end + item + Command = ecTab + ShortCut = 9 + end + item + Command = ecShiftTab + ShortCut = 8201 + end + item + Command = ecMatchBracket + ShortCut = 24642 + end + item + Command = ecColSelUp + ShortCut = 40998 + end + item + Command = ecColSelDown + ShortCut = 41000 + end + item + Command = ecColSelLeft + ShortCut = 40997 + end + item + Command = ecColSelRight + ShortCut = 40999 + end + item + Command = ecColSelPageDown + ShortCut = 40994 + end + item + Command = ecColSelPageBottom + ShortCut = 57378 + end + item + Command = ecColSelPageUp + ShortCut = 40993 + end + item + Command = ecColSelPageTop + ShortCut = 57377 + end + item + Command = ecColSelLineStart + ShortCut = 40996 + end + item + Command = ecColSelLineEnd + ShortCut = 40995 + end + item + Command = ecColSelEditorTop + ShortCut = 57380 + end + item + Command = ecColSelEditorBottom + ShortCut = 57379 + end> + MouseActions = <> + MouseTextActions = <> + MouseSelActions = <> + Options = [eoAutoIndent, eoBracketHighlight, eoGroupUndo, eoSmartTabs, eoTabsToSpaces] + VisibleSpecialChars = [vscSpace, vscTabAtLast] + ScrollBars = ssAutoBoth + SelectedColor.BackPriority = 50 + SelectedColor.ForePriority = 50 + SelectedColor.FramePriority = 50 + SelectedColor.BoldPriority = 50 + SelectedColor.ItalicPriority = 50 + SelectedColor.UnderlinePriority = 50 + SelectedColor.StrikeOutPriority = 50 + BracketHighlightStyle = sbhsBoth + BracketMatchColor.Background = clNone + BracketMatchColor.Foreground = clNone + BracketMatchColor.Style = [fsBold] + FoldedCodeColor.Background = clNone + FoldedCodeColor.Foreground = clGray + FoldedCodeColor.FrameColor = clGray + MouseLinkColor.Background = clNone + MouseLinkColor.Foreground = clBlue + LineHighlightColor.Background = clNone + LineHighlightColor.Foreground = clNone + OnChange = SE_HTMLChange + inline SynLeftGutterPartList1: TSynGutterPartList + object SynGutterMarks1: TSynGutterMarks + Width = 24 + MouseActions = <> + end + object SynGutterLineNumber1: TSynGutterLineNumber + Width = 17 + MouseActions = <> + MarkupInfo.Background = clBtnFace + MarkupInfo.Foreground = clNone + DigitCount = 2 + ShowOnlyLineNumbersMultiplesOf = 1 + ZeroStart = False + LeadingZeros = False + end + object SynGutterChanges1: TSynGutterChanges + Width = 4 + MouseActions = <> + ModifiedColor = 59900 + SavedColor = clGreen + end + object SynGutterSeparator1: TSynGutterSeparator + Width = 2 + MouseActions = <> + MarkupInfo.Background = clWhite + MarkupInfo.Foreground = clGray + end + object SynGutterCodeFolding1: TSynGutterCodeFolding + MouseActions = <> + MarkupInfo.Background = clNone + MarkupInfo.Foreground = clGray + MouseActionsExpanded = <> + MouseActionsCollapsed = <> + end + end + end + end + end + object Splitter1: TSplitter + Left = 387 + Height = 463 + Top = 0 + Width = 5 + end + object HtmlViewer: THtmlViewer + Left = 392 + Height = 463 + Top = 0 + Width = 398 + BorderStyle = htSingle + HistoryMaxCount = 0 + NoSelect = False + PrintMarginBottom = 2 + PrintMarginLeft = 2 + PrintMarginRight = 2 + PrintMarginTop = 2 + PrintScale = 1 + Align = alClient + TabOrder = 3 + end + object SynHTMLSyn1: TSynHTMLSyn + DefaultFilter = 'Documento HTML (*.htm,*.html)|*.htm;*.html' + Enabled = False + KeyAttri.Foreground = clMaroon + KeyAttri.Style = [] + left = 72 + top = 48 + end + object OpenDialog1: TOpenDialog + DefaultExt = '.*.md' + Filter = 'MarkDown|*.md; *.text|TextFile|*.txt|All Files|*.*' + Options = [ofFileMustExist, ofEnableSizing, ofViewDetail] + left = 72 + top = 112 + end + object SaveDialog1: TSaveDialog + DefaultExt = '.*.md' + Filter = 'MarkDown|*.md;*.text|Text file|*.txt|All files|*.*' + Options = [ofPathMustExist, ofEnableSizing, ofViewDetail] + left = 72 + top = 184 + end +end diff --git a/demos/MarkDownHtml/MarkDownHtmlu.pas b/demos/MarkDownHtml/MarkDownHtmlu.pas new file mode 100644 index 0000000..b4b2035 --- /dev/null +++ b/demos/MarkDownHtml/MarkDownHtmlu.pas @@ -0,0 +1,255 @@ +unit MarkDownHtmlU; + +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, LazFileUtils, SynEdit, SynHighlighterHTML, Forms, Controls, + Graphics, Dialogs, StdCtrls, ExtCtrls, Clipbrd, MarkdownProcessor, LCLIntf, + ComCtrls, Buttons, StrUtils, HtmlView, HtmlGlobals, HTMLUn2; + +type + + { TMainForm } + + TMainForm = class(TForm) + B_Save: TBitBtn; + B_Copy: TButton; + B_Paste: TButton; + B_ViewBrowser: TButton; + B_OpenFile: TButton; + B_Convert: TButton; + HtmlViewer: THtmlViewer; + OpenDialog1: TOpenDialog; + PageControl1: TPageControl; + Panel1: TPanel; + SaveDialog1: TSaveDialog; + SE_MarkDown: TSynEdit; + SE_HTML: TSynEdit; + Splitter1: TSplitter; + SynHTMLSyn1: TSynHTMLSyn; + TS_MarkDown: TTabSheet; + TS_HTML: TTabSheet; + procedure B_CopyClick(Sender: TObject); + procedure B_PasteClick(Sender: TObject); + procedure B_ViewBrowserClick(Sender: TObject); + procedure B_OpenFileClick(Sender: TObject); + procedure B_ConvertClick(Sender: TObject); + procedure B_SaveClick(Sender: TObject); + procedure FormCreate(Sender: TObject); + procedure FormDestroy(Sender: TObject); + procedure HtmlViewerHotSpotClick(Sender: TObject; const SRC: ThtString; + var Handled: Boolean); + procedure HtmlViewerHotSpotTargetClick(Sender: TObject; const Target, + URL: ThtString; var Handled: boolean); + procedure HtmlViewerImageRequest(Sender: TObject; const SRC: ThtString; + var Stream: TStream); + procedure SE_HTMLChange(Sender: TObject); + private + procedure OpenInBrowser; + procedure SetPreview; + + public + + end; + +var + MainForm: TMainForm; + +implementation + +{$R *.lfm} + +var + RootPath,f:string; + md:TMarkdownProcessor=nil; + MStream: TMemoryStream = nil; + +const + CSSDecoration = '<style type="text/css">'+ + 'code{'+ + ' color: #A00;'+ + '}'+ + 'pre{'+ + ' background: #f4f4f4;'+ + ' border: 1px solid #ddd;'+ + ' border-left: 3px solid #f36d33;'+ + ' color: #555;'+ + ' overflow: auto;'+ + ' padding: 1em 1.5em;'+ + ' display: block;'+ + '}'+ + 'pre code{'+ + ' color: inherit;'+ + '}'+ + '</style>'; + + +(* +More decoration: + +<style type="text/css"> +pre { + background-color: #eee; + border: 1px solid #999; + display: block; + padding: 10px; +} + +Blockquote{ + border-left: 3px solid #d0d0d0; + padding-left: 0.5em; + margin-left:1em; +} +Blockquote p{ + margin: 0; +} +</style> +*) + + +{ TMainForm } + +procedure TMainForm.SetPreview; +begin + if SE_HTML.Modified then + begin + HtmlViewer.LoadFromString(CSSDecoration+SE_HTML.Text); + SE_HTML.Modified:=false; + end; +end; + +procedure TMainForm.B_ConvertClick(Sender: TObject); +begin + SE_HTML.Text:=md.process(SE_MarkDown.Text); + SE_HTML.Modified:=true; + SetPreview; +end; + +procedure TMainForm.B_SaveClick(Sender: TObject); +begin + PageControl1.ActivePageIndex:=0; + if savedialog1.Execute then + begin + SE_MarkDown.Lines.SaveToFile(savedialog1.FileName); + end; +end; + +procedure TMainForm.FormCreate(Sender: TObject); +var + i:integer; +begin + md := TMarkdownProcessor.createDialect(mdTxtMark); + md.UnSafe := false; + RootPath:=GetTempDir; + I:=0; + Repeat + f:=Format('%s%.3d.html',['markdown',I]); + Inc(I); + Until not FileExists(RootPath+f); + PageControl1.ActivePageIndex:=0; + HtmlViewer.DefBackground:=clWhite; + HtmlViewer.DefFontColor:=clBlack; + HtmlViewer.DefFontName:='Helvetica'; + HtmlViewer.DefFontSize:=10; +// HtmlViewer.DefPreFontName:='Lucida Console'; + HtmlViewer.DefPreFontName:='Courier'; + HtmlViewer.ServerRoot:=RootPath; +// HtmlViewer.OnHotSpotTargetClick:=@HtmlViewerHotSpotTargetClick; + HtmlViewer.OnHotSpotClick:=@HtmlViewerHotSpotClick; + HtmlViewer.OnImageRequest:=@HtmlViewerImageRequest; + MStream := TMemoryStream.Create; + B_ConvertClick(Self); +end; + +procedure TMainForm.FormDestroy(Sender: TObject); +begin + if FileExists(RootPath+f) then DeleteFile(RootPath+f); + if Assigned(MStream) then freeandnil(MStream); + if assigned(md) then md.Free; +end; + +procedure TMainForm.HtmlViewerHotSpotClick(Sender: TObject; + const SRC: ThtString; var Handled: Boolean); +begin + Handled:=OpenUrl(SRC); +end; + +procedure TMainForm.HtmlViewerHotSpotTargetClick(Sender: TObject; + const Target, URL: ThtString; var Handled: boolean); +begin + Handled:=OpenUrl(URL); +end; + +procedure TMainForm.HtmlViewerImageRequest(Sender: TObject; + const SRC: ThtString; var Stream: TStream); + +var + Filename: string; +begin + Stream:=nil; + FileName:=IfThen(FileExists(SRC),SRC,RootPath+SRC); + if FileExists(FileName) then + begin + MStream.LoadFromFile(FileName); + Stream := MStream; + end; +end; + +procedure TMainForm.SE_HTMLChange(Sender: TObject); +begin + SetPreview; +end; + +procedure TMainForm.B_CopyClick(Sender: TObject); +begin + Clipboard.AsText:=SE_MarkDown.text; +end; + +procedure TMainForm.B_PasteClick(Sender: TObject); +begin + SE_MarkDown.Clear; + SE_MarkDown.PasteFromClipboard; + PageControl1.ActivePageIndex:=0; +end; + +procedure TMainForm.B_ViewBrowserClick(Sender: TObject); +begin + OpenInBrowser; +end; + +procedure TMainForm.B_OpenFileClick(Sender: TObject); +var NewPath:string; +begin + if OpenDialog1.Execute then + begin + SE_MarkDown.Lines.LoadFromFile(OpenDialog1.FileName); + NewPath:=ExtractFilePath(OpenDialog1.FileName); + if NewPath<>RootPath then + begin + SE_HTML.Clear; + HtmlViewer.Clear; + if FileExists(RootPath+f) then DeleteFile(RootPath+f); + HtmlViewer.ServerRoot:=NewPath; + RootPath:=NewPath; + end; + SaveDialog1.FileName:=OpenDialog1.FileName; + PageControl1.ActivePageIndex:=0; + end; +end; + +procedure TMainForm.OpenInBrowser; +var p:string; +begin + p:=IfThen(DirectoryIsWritable(RootPath),RootPath+f,GetTempDir+f); + try + SE_HTML.Lines.SaveToFile(p); + OpenURL('file://'+p); + except + ShowMessage('Can not create and open the temp file'); + end; +end; + +end. + diff --git a/fpc_markdown.lpk b/fpc_markdown.lpk index d54f6f2..0384a65 100644 --- a/fpc_markdown.lpk +++ b/fpc_markdown.lpk @@ -15,18 +15,27 @@ </SearchPaths> <Parsing> <SyntaxOptions> + <IncludeAssertionCode Value="True"/> <UseAnsiStrings Value="False"/> </SyntaxOptions> </Parsing> <CodeGeneration> + <Checks> + <IOChecks Value="True"/> + <RangeChecks Value="True"/> + <OverflowChecks Value="True"/> + <StackChecks Value="True"/> + </Checks> + <VerifyObjMethodCallValidity Value="True"/> <Optimizations> - <OptimizationLevel Value="3"/> <VariablesInRegisters Value="True"/> </Optimizations> </CodeGeneration> <Linking> <Debugging> - <GenerateDebugInfo Value="False"/> + <UseHeaptrc Value="True"/> + <TrashVariables Value="True"/> + <UseExternalDbgSyms Value="True"/> </Debugging> </Linking> </CompilerOptions> @@ -46,7 +55,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License."/> <Version Major="1" Release="1"/> - <Files Count="2"> + <Files Count="5"> <Item1> <Filename Value="source\MarkdownProcessor.pas"/> <UnitName Value="MarkdownProcessor"/> @@ -55,6 +64,19 @@ limitations under the License."/> <Filename Value="source\MarkdownDaringFireball.pas"/> <UnitName Value="MarkdownDaringFireball"/> </Item2> + <Item3> + <Filename Value="source\markdowntxtmark.pas"/> + <UnitName Value="markdowntxtmark"/> + </Item3> + <Item4> + <Filename Value="source\MarkdownUtils.pas"/> + <AddToUsesPkgSection Value="False"/> + <UnitName Value="MarkdownUtils"/> + </Item4> + <Item5> + <Filename Value="source\MarkdownCommonMark.pas"/> + <UnitName Value="MarkdownCommonMark"/> + </Item5> </Files> <RequiredPkgs Count="2"> <Item1> diff --git a/fpc_markdown.pas b/fpc_markdown.pas index da92617..d00ab61 100644 --- a/fpc_markdown.pas +++ b/fpc_markdown.pas @@ -8,7 +8,8 @@ interface uses - MarkdownProcessor, MarkdownDaringFireball; + MarkdownProcessor, MarkdownDaringFireball, MarkdownTxtMark, + MarkdownCommonMark; implementation diff --git a/source/MarkdownCommonMark.pas b/source/MarkdownCommonMark.pas new file mode 100644 index 0000000..1eef610 --- /dev/null +++ b/source/MarkdownCommonMark.pas @@ -0,0 +1,63 @@ +{ +Copyright (C) Miguel A. Risco-Castillo + +FPC-markdown is a fork of Grahame Grieve <grahameg@gmail.com> +Delphi-markdown https://github.com/grahamegrieve/delphi-markdown + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +} + +Unit MarkdownCommonMark; + +{$mode objfpc}{$H+} + +interface + +uses + SysUtils, Classes, TypInfo, + MarkdownProcessor, MarkdownDaringFireball, MarkdownUtils; + +Type + + TMarkdownCommonMark = class(TMarkdownDaringFireball) + private + protected + public + Constructor Create; + Destructor Destroy; override; + function process(source: String): String; override; + end; + +implementation + + +{ TMarkdownCommonMark } + +constructor TMarkdownCommonMark.Create; +begin + inherited; +end; + +destructor TMarkdownCommonMark.Destroy; +begin + + inherited; +end; + +function TMarkdownCommonMark.process(source: String): String; +begin + result:=inherited process(source); +end; + + +end. diff --git a/source/MarkdownDaringFireball.pas b/source/MarkdownDaringFireball.pas index 56439ef..b9fe313 100644 --- a/source/MarkdownDaringFireball.pas +++ b/source/MarkdownDaringFireball.pas @@ -24,466 +24,11 @@ interface uses - SysUtils, StrUtils, Classes, Character, TypInfo, Math, - MarkdownProcessor; + SysUtils, Classes, TypInfo, + MarkdownProcessor, MarkdownUtils; -type - THTMLElement = (heNONE, hea, heabbr, heacronym, headdress, heapplet, hearea, heb, hebase, hebasefont, hebdo, hebig, heblockquote, hebody, hebr, hebutton, hecaption, hecite, - hecode, hecol, hecolgroup, hedd, hedel, hedfn, hediv, hedl, hedt, heem, hefieldset, hefont, heform, heframe, heframeset, heh1, heh2, heh3, heh4, heh5, heh6, hehead, hehr, - hehtml, hei, heiframe, heimg, heinput, heins, hekbd, helabel, helegend, heli, helink, hemap, hemeta, henoscript, heobject, heol, heoptgroup, heoption, hep, heparam, hepre, heq, - hes, hesamp, hescript, heselect, hesmall, hespan, hestrike, hestrong, hestyle, hesub, hesup, hetable, hetbody, hetd, hetextarea, hetfoot, heth, hethead, hetitle, hetr, hett, - heu, heul, hevar); - -const - // pstfix - ENTITY_NAMES: array[0..249] of String = ('Â', 'â', '´', 'Æ', 'æ', 'À', 'à', 'ℵ', 'Α', 'α', '&', '∧', '∠', - ''', 'Å', 'å', '≈', 'Ã', 'ã', 'Ä', 'ä', '„', 'Β', 'β', '¦', '•', '∩', 'Ç', 'ç', - '¸', '¢', 'Χ', 'χ', 'ˆ', '♣', '≅', '©', '↵', '∪', '¤', '‡', '†', '⇓', '↓', '°', 'Δ', - 'δ', '♦', '÷', 'É', 'é', 'Ê', 'ê', 'È', 'è', '∅', ' ', ' ', 'Ε', 'ε', '≡', - 'Η', 'η', 'Ð', 'ð', 'Ë', 'ë', '€', '∃', 'ƒ', '∀', '½', '¼', '¾', '⁄', 'Γ', 'γ', '≥', - '>', '⇔', '↔', '♥', '…', 'Í', 'í', 'Î', 'î', '¡', 'Ì', 'ì', 'ℑ', '∞', '∫', 'Ι', - 'ι', '¿', '∈', 'Ï', 'ï', 'Κ', 'κ', 'Λ', 'λ', '⟨', '«', '⇐', '←', '⌈', '“', '≤', - '⌊', '∗', '◊', '‎', '‹', '‘', '<', '¯', '—', 'µ', '·', '−', 'Μ', 'μ', '∇', ' ', '–', - '≠', '∋', '¬', '∉', '⊄', 'Ñ', 'ñ', 'Ν', 'ν', 'Ó', 'ó', 'Ô', 'ô', 'Œ', 'œ', 'Ò', - 'ò', '‾', 'Ω', 'ω', 'Ο', 'ο', '⊕', '∨', 'ª', 'º', 'Ø', 'ø', 'Õ', 'õ', '⊗', - 'Ö', 'ö', '¶', '∂', '‰', '⊥', 'Φ', 'φ', 'Π', 'π', 'ϖ', '±', '£', '″', '′', '∏', '∝', - 'Ψ', 'ψ', '"', '√', '⟩', '»', '⇒', '→', '⌉', '”', 'ℜ', '®', '⌋', 'Ρ', 'ρ', '‏', '›', - '’', '‚', 'Š', 'š', '⋅', '§', '­', 'Σ', 'σ', 'ς', '∼', '♠', '⊂', '⊆', '∑', '⊃', '¹', - '²', '³', '⊇', 'ß', 'Τ', 'τ', '∴', 'Θ', 'θ', 'ϑ', ' ', 'þ', '˜', '×', '™', 'Ú', - 'ú', '⇑', '↑', 'Û', 'û', 'Ù', 'ù', '¨', 'ϒ', 'Υ', 'υ', 'Ü', 'ü', '℘', 'Ξ', 'ξ', - 'Ý', 'ý', '¥', 'Ÿ', 'ÿ', 'Ζ', 'ζ', '‍', '‌'); - - // Characters corresponding to ENTITY_NAMES. */ - // pstfix - ENTITY_CHARS: array[0..249] of integer = ($00C2, $00E2, $00B4, $00C6, $00E6, $00C0, $00E0, $2135, $0391, $03B1, $0026, $2227, $2220, ord(''''), $00C5, $00E5, $2248, $00C3, $00E3, $00C4, - $00E4, $201E, $0392, $03B2, $00A6, $2022, $2229, $00C7, $00E7, $00B8, $00A2, $03A7, $03C7, $02C6, $2663, $2245, $00A9, $21B5, $222A, $00A4, $2021, $2020, $21D3, $2193, $00B0, - $0394, $03B4, $2666, $00F7, $00C9, $00E9, $00CA, $00EA, $00C8, $00E8, $2205, $2003, $2002, $0395, $03B5, $2261, $0397, $03B7, $00D0, $00F0, $00CB, $00EB, $20AC, $2203, $0192, - $2200, $00BD, $00BC, $00BE, $2044, $0393, $03B3, $2265, $003E, $21D4, $2194, $2665, $2026, $00CD, $00ED, $00CE, $00EE, $00A1, $00CC, $00EC, $2111, $221E, $222B, $0399, $03B9, - $00BF, $2208, $00CF, $00EF, $039A, $03BA, $039B, $03BB, $2329, $00AB, $21D0, $2190, $2308, $201C, $2264, $230A, $2217, $25CA, $200E, $2039, $2018, $003C, $00AF, $2014, $00B5, - $00B7, $2212, $039C, $03BC, $2207, $00A0, $2013, $2260, $220B, $00AC, $2209, $2284, $00D1, $00F1, $039D, $03BD, $00D3, $00F3, $00D4, $00F4, $0152, $0153, $00D2, $00F2, $203E, - $03A9, $03C9, $039F, $03BF, $2295, $2228, $00AA, $00BA, $00D8, $00F8, $00D5, $00F5, $2297, $00D6, $00F6, $00B6, $2202, $2030, $22A5, $03A6, $03C6, $03A0, $03C0, $03D6, $00B1, - $00A3, $2033, $2032, $220F, $221D, $03A8, $03C8, $0022, $221A, $232A, $00BB, $21D2, $2192, $2309, $201D, $211C, $00AE, $230B, $03A1, $03C1, $200F, $203A, $2019, $201A, $0160, - $0161, $22C5, $00A7, $00AD, $03A3, $03C3, $03C2, $223C, $2660, $2282, $2286, $2211, $2283, $00B9, $00B2, $00B3, $2287, $00DF, $03A4, $03C4, $2234, $0398, $03B8, $03D1, $00DE, - $00FE, $02DC, $00D7, $2122, $00DA, $00FA, $21D1, $2191, $00DB, $00FB, $00D9, $00F9, $00A8, $03D2, $03A5, $03C5, $00DC, $00FC, $2118, $039E, $03BE, $00DD, $00FD, $00A5, $0178, - $00FF, $0396, $03B6, $200D, $200C); - - LINK_PREFIXES: array[0..3] of String = ('http', 'https', 'ftp', 'ftps'); - - BLOCK_ELEMENTS: set of THTMLElement = [headdress, heblockquote, hedel, hediv, hedl, hefieldset, heform, heh1, heh2, heh3, heh4, heh5, heh6, hehr, heins, henoscript, heol, hep, - hepre, hetable, heul]; - - UNSAFE_ELEMENTS: set of THTMLElement = [heapplet, hehead, hehtml, hebody, heframe, heframeset, heiframe, hescript, heobject]; - - BUFFER_INCREMENT_SIZE = 1024; - -Type - TReader = class - private - FValue: String; - FCursor: integer; - public - Constructor Create(source: String); - function read: char; - end; - -{$IFDEF FPC} - TStringBuilder = class - private - FContent : String; - FLength : Integer; - FBufferSize : integer; - function GetChar(index: integer): char; - public - Constructor Create; - - procedure Clear; - procedure Append(value : String); overload; - procedure Append(value : integer); overload; - procedure Append(value : TStringBuilder); overload; - property ch[index : integer] : char read GetChar; default; - function toString : String; override; - Property Length : Integer Read FLength; - end; -{$ENDIF} - - TUtils = class - public - // Skips spaces in the given String. return The new position or -1 if EOL has been reached. - class function skipSpaces(s: String; start: integer): integer; - - // Process the given escape sequence. return The new position. - class function escape(out_: TStringBuilder; ch: char; position: integer): integer; - - // Reads characters until any 'end' character is encountered. return The new position or -1 if no 'end' char was found. - class function readUntil(out_: TStringBuilder; s: String; start: integer; cend: TSysCharSet): integer; overload; - - // Reads characters until the 'end' character is encountered. return The new position or -1 if no 'end' char was found. - class function readUntil(out_: TStringBuilder; s: String; start: integer; cend: char): integer; overload; - - // Reads a markdown link. return The new position or -1 if this is no valid markdown link. - class function readMdLink(out_: TStringBuilder; s: String; start: integer): integer; - class function readMdLinkId(out_: TStringBuilder; s: String; start: integer): integer; - - // Reads characters until any 'end' character is encountered, ignoring escape sequences. - class function readRawUntil(out_: TStringBuilder; s: String; start: integer; cend: TSysCharSet): integer; overload; - - // Reads characters until the end character is encountered, taking care of HTML/XML strings. - class function readRawUntil(out_: TStringBuilder; s: String; start: integer; cend: char): integer; overload; - - // Reads characters until any 'end' character is encountered, ignoring escape sequences. - class function readXMLUntil(out_: TStringBuilder; s: String; start: integer; cend: TSysCharSet): integer; - - // Appends the given string encoding special HTML characters. - class procedure appendCode(out_: TStringBuilder; s: String; start: integer; e: integer); - - // Appends the given string encoding special HTML characters (used in HTML - class procedure appendValue(out_: TStringBuilder; s: String; start: integer; e: integer); - - // Append the given char as a decimal HTML entity. - class procedure appendDecEntity(out_: TStringBuilder; value: char); - - // Append the given char as a hexadecimal HTML entity. - class procedure appendHexEntity(out_: TStringBuilder; value: char); - - // Appends the given mailto link using obfuscation. - class procedure appendMailto(out_: TStringBuilder; s: String; start: integer; e: integer); - - // Extracts the tag from an XML element. - class procedure getXMLTag(out_: TStringBuilder; bin: TStringBuilder); overload; - - // Extracts the tag from an XML element. - class procedure getXMLTag(out_: TStringBuilder; s: String); overload; - - // Reads an XML element. - // return The new position or -1 if this is no valid XML element. - class function readXML(out_: TStringBuilder; s: String; start: integer; safeMode: boolean): integer; - - // Appends the given string to the given StringBuilder, replacing '&', '<' and '>' by their respective HTML entities. - class procedure codeEncode(out_: TStringBuilder; value: String; offset: integer); - - // Removes trailing <code>`</code> or <code>~</code> and trims spaces. - class function getMetaFromFence(fenceLine: String): String; - end; - - THTML = class - public - class function isLinkPrefix(s: String): boolean; - class function isEntity(s: String): boolean; - class function isUnsafeHtmlElement(s: String): boolean; - class function isHtmlBlockElement(s: String): boolean; - end; - - TDecorator = class - private - public - procedure openParagraph(out_: TStringBuilder); virtual; - procedure closeParagraph(out_: TStringBuilder); virtual; - - procedure openBlockQuote(out_: TStringBuilder); virtual; - procedure closeBlockQuote(out_: TStringBuilder); virtual; - - procedure openCodeBlock(out_: TStringBuilder); virtual; - procedure closeCodeBlock(out_: TStringBuilder); virtual; - - procedure openCodeSpan(out_: TStringBuilder); virtual; - procedure closeCodeSpan(out_: TStringBuilder); virtual; - - procedure openHeadline(out_: TStringBuilder; level: integer); virtual; - procedure closeHeadline(out_: TStringBuilder; level: integer); virtual; - - procedure openStrong(out_: TStringBuilder); virtual; - procedure closeStrong(out_: TStringBuilder); virtual; - - procedure openEmphasis(out_: TStringBuilder); virtual; - procedure closeEmphasis(out_: TStringBuilder); virtual; - - procedure openSuper(out_: TStringBuilder); virtual; - procedure closeSuper(out_: TStringBuilder); virtual; - - procedure openOrderedList(out_: TStringBuilder); virtual; - procedure closeOrderedList(out_: TStringBuilder); virtual; - - procedure openUnOrderedList(out_: TStringBuilder); virtual; - procedure closeUnOrderedList(out_: TStringBuilder); virtual; - - procedure openListItem(out_: TStringBuilder); virtual; - procedure closeListItem(out_: TStringBuilder); virtual; - - procedure horizontalRuler(out_: TStringBuilder); virtual; - - procedure openLink(out_: TStringBuilder); virtual; - procedure closeLink(out_: TStringBuilder); virtual; - - procedure openImage(out_: TStringBuilder); virtual; - procedure closeImage(out_: TStringBuilder); virtual; - end; - - TSpanEmitter = class - public - procedure emitSpan(out_: TStringBuilder; content: String); virtual; abstract; - end; - - TBlockEmitter = class - public - procedure emitBlock(out_: TStringBuilder; lines: TStringList; meta: String); virtual; abstract; - end; - - TConfiguration = class - private - Fdecorator: TDecorator; - FsafeMode: boolean; - FallowSpacesInFencedDelimiters: boolean; - FforceExtendedProfile: boolean; - FcodeBlockEmitter: TBlockEmitter; - FpanicMode: boolean; - FspecialLinkEmitter: TSpanEmitter; - public - Constructor Create(safe : boolean); - Destructor Destroy; override; - - property safeMode: boolean read FsafeMode write FsafeMode; - property panicMode: boolean read FpanicMode write FpanicMode; - property decorator: TDecorator read Fdecorator write Fdecorator; - property codeBlockEmitter: TBlockEmitter read FcodeBlockEmitter write FcodeBlockEmitter; - property forceExtendedProfile: boolean read FforceExtendedProfile write FforceExtendedProfile; - property allowSpacesInFencedDelimiters: boolean read FallowSpacesInFencedDelimiters write FallowSpacesInFencedDelimiters; - property specialLinkEmitter: TSpanEmitter read FspecialLinkEmitter write FspecialLinkEmitter; - end; - - TLineType = ( - // Empty line. */ - ltEMPTY, - // Undefined content. */ - ltOTHER, - // A markdown headline. */ - ltHEADLINE, ltHEADLINE1, ltHEADLINE2, - // A code block line. */ - ltCODE, - // A list. */ - ltULIST, ltOLIST, - // A block quote. */ - ltBQUOTE, - // A horizontal ruler. */ - ltHR, - // Start of a XML block. */ - ltXML, - // Fenced code block start/end */ - ltFENCED_CODE); - - TLine = class - private - FXmlEndLine: TLine; - FPrevEmpty: boolean; - FPrevious: TLine; - FPosition: integer; - FValue: string; - FIsEmpty: boolean; - FTrailing: integer; - FNextEmpty: boolean; - FLeading: integer; - FNext: TLine; - function countChars(ch: char): integer; - function countCharsStart(ch: char; allowSpaces: boolean): integer; - function readXMLComment(firstLine: TLine; start: integer): integer; - function checkHTML(): boolean; - - public - Constructor Create; - Destructor Destroy; Override; - - // Current cursor position. - property position: integer read FPosition write FPosition; - // Leading and trailing spaces. - property leading: integer read FLeading write FLeading; - property trailing: integer read FTrailing write FTrailing; - // Is this line empty? - property isEmpty: boolean read FIsEmpty write FIsEmpty; - // This line's value. - property value: string read FValue write FValue; - // Previous and next line. - property previous: TLine read FPrevious write FPrevious; - property next: TLine read FNext write FNext; - - // Is previous/next line empty? - property prevEmpty: boolean read FPrevEmpty write FPrevEmpty; - property nextEmpty: boolean read FNextEmpty write FNextEmpty; - - // Final line of a XML block. - property xmlEndLine: TLine read FXmlEndLine write FXmlEndLine; - - procedure Init; - procedure InitLeading; - function skipSpaces: boolean; - function readUntil(chend: TSysCharSet): String; - procedure setEmpty; - function getLineType(configuration: TConfiguration): TLineType; - function stripID: String; - - end; - - TLinkRef = class - private - FLink: String; - FTitle: String; - FIsAbbrev: boolean; - public - Constructor Create(link, title: String; isAbbrev: boolean); - - property link: String read FLink write FLink; - property title: String read FTitle write FTitle; - property isAbbrev: boolean read FIsAbbrev write FIsAbbrev; - end; - - TBlockType = ( - // Unspecified. Used for root block and list items without paragraphs. - btNONE, - // A block quote. - btBLOCKQUOTE, - // A code block. - btCODE, - // A fenced code block. - btFENCED_CODE, - // A headline. - btHEADLINE, - // A list item. - btLIST_ITEM, - // An ordered list. - btORDERED_LIST, - // A paragraph. - btPARAGRAPH, - // A horizontal ruler. - btRULER, - // An unordered list. - btUNORDERED_LIST, - // A XML block. - btXML); - - TBlock = class - private - FType: TBlockType; - FId: String; - FBlocks: TBlock; - FBlockTail: TBlock; - FLines: TLine; - FLineTail: TLine; - FHlDepth: integer; - FNext: TBlock; - FMeta: String; - - procedure AppendLine(line: TLine); - function split(line: TLine): TBlock; - procedure removeListIndent(config: TConfiguration); - function removeLeadingEmptyLines: boolean; - procedure removeTrailingEmptyLines; - procedure transfromHeadline; - procedure expandListParagraphs; - function hasLines: boolean; - procedure removeSurroundingEmptyLines; - procedure removeBlockQuotePrefix; - procedure removeLine(line: TLine); - public - Constructor Create; - Destructor Destroy; Override; - - // This block's type. - property type_: TBlockType read FType write FType; - - property lines: TLine read FLines; - property lineTail: TLine read FLineTail; - - // child blocks. - property blocks: TBlock read FBlocks; - property blockTail: TBlock read FBlockTail; - - // Next block. - property next: TBlock read FNext write FNext; - // Depth of headline BlockType. - property hlDepth: integer read FHlDepth write FHlDepth; - // ID for headlines and list items - property id: String read FId write FId; - // Block meta information - property meta: String read FMeta write FMeta; - end; - - TMarkToken = ( - // No token. - mtNONE, - // * - mtEM_STAR, // x*x - // _ - mtEM_UNDERSCORE, // x_x - // ** - mtSTRONG_STAR, // x**x - // __ - mtSTRONG_UNDERSCORE, // x__x - // ` - mtCODE_SINGLE, // ` - // `` - mtCODE_DOUBLE, // `` - // [ - mtLINK, // [ - // < - mtHTML, // < - // ![ - mtIMAGE, // ![ - // & - mtENTITY, // & - // \ - mtESCAPE, // \x - // Extended: ^ - mtSUPER, // ^ - // Extended: (C) - mtX_COPY, // (C) - // Extended: (R) - mtX_REG, // (R) - // Extended: (TM) - mtX_TRADE, // (TM) - // Extended: << - mtX_LAQUO, // << - // Extended: >> - mtX_RAQUO, // >> - // Extended: -- - mtX_NDASH, // -- - // Extended: --- - mtX_MDASH, // --- - // Extended: ... - mtX_HELLIP, // ... - // Extended: "x - mtX_RDQUO, // " - // Extended: x" - mtX_LDQUO, // " - // [[ - mtX_LINK_OPEN, // [[ - // ]] - mtX_LINK_CLOSE // ]] - ); - - // Emitter class responsible for generating HTML output. - TEmitter = class - private - linkRefs: TStringList; - FConfig: TConfiguration; - FuseExtensions: boolean; - procedure emitCodeLines(out_: TStringBuilder; lines: TLine; meta: String; removeIndent: boolean); - procedure emitRawLines(out_: TStringBuilder; lines: TLine); - procedure emitMarkedLines(out_: TStringBuilder; lines: TLine); - function findToken(s: String; start: integer; token: TMarkToken): integer; - function getToken(s: String; position: integer): TMarkToken; - function checkLink(out_: TStringBuilder; s: String; start: integer; token: TMarkToken): integer; - function recursiveEmitLine(out_: TStringBuilder; s: String; start: integer; token: TMarkToken): integer; - function checkHTML(out_: TStringBuilder; s: String; start: integer): integer; - class function checkEntity(out_: TStringBuilder; s: String; start: integer): integer; - class function whitespaceToSpace(c: char): char; - public - Constructor Create(config: TConfiguration); - Destructor Destroy; override; - - procedure addLinkRef(key: String; linkRef: TLinkRef); - procedure emit(out_: TStringBuilder; root: TBlock); - procedure emitLines(out_: TStringBuilder; block: TBlock); - - end; +type TMarkdownDaringFireball = class(TMarkdownProcessor) private @@ -500,51 +45,13 @@ TMarkdownDaringFireball = class(TMarkdownProcessor) public Constructor Create; Destructor Destroy; override; - function process(source: String): String; override; - property config: TConfiguration read FConfig; + end; implementation -Function StringsContains(Const aNames: Array Of String; Const sName: String): boolean; -var - i: integer; -Begin - for i := 0 to length(aNames) - 1 do - if sName <> aNames[i] then - exit(true); - result := false; -End; - -function StringToEnum(ATypeInfo: PTypeInfo; const AStr: String; defValue: integer): integer; -var - LTypeData: PTypeData; - LPChar: PAnsiChar; - LValue: ShortString; -begin - LValue := ShortString(AStr); - - if ATypeInfo^.Kind = tkEnumeration then - begin - LTypeData := GetTypeData(ATypeInfo); - if LTypeData^.MinValue <> 0 then - exit(defValue); - LPChar := @LTypeData^.NameList[0]; - result := 0; - while (result <= LTypeData^.MaxValue) and (ShortString(pointer(LPChar)^) <> LValue) do - begin - inc(LPChar, ord(LPChar^) + 1); // move to next string - inc(result); - end; - if result > LTypeData^.MaxValue then - exit(defValue); - end - else - exit(defValue); -end; - { TMarkdownDaringFireball } constructor TMarkdownDaringFireball.Create; @@ -590,6 +97,7 @@ function TMarkdownDaringFireball.process(source: String): String; rdr : TReader; begin FuseExtensions := config.forceExtendedProfile; + Femitter.FuseExtensions:=config.forceExtendedProfile; rdr := TReader.Create(source); try out_ := TStringBuilder.Create; @@ -698,45 +206,47 @@ function TMarkdownDaringFireball.readLines(reader : TReader): TBlock; // Read ID up to ']' id := line.readUntil([']']); // Is ID valid and are there any more characters? - if (id <> '') and (line.position + 2 < Length(line.value)) then + if (id <> '') and ((line.position + 2) < Length(line.value)) then begin // Check for ':' ([...]:...) if (line.value[1 + line.position + 1] = ':') then begin line.position := line.position + 2; - line.skipSpaces(); - // Check for link syntax - if (line.value[1 + line.position] = '<') then - begin - line.position := line.position + 1; - link := line.readUntil(['>']); - line.position := line.position + 1; - end - else - link := line.readUntil([' ', #10]); - - // Is link valid? - if (link <> '') then + if line.skipSpaces() then begin - // Any non-whitespace characters following? - if (line.skipSpaces()) then + // Check for link syntax + if (line.value[1 + line.position] = '<') then begin - ch := line.value[1 + line.position]; - // Read comment - if (ch = '"') or (ch = '''') or (ch = '(') then - begin - line.position := line.position + 1; - if ch = '(' then - comment := line.readUntil([')']) - else - comment := line.readUntil([ch]); - // Valid linkRef only if comment is valid - if (comment <> '') then - isLinkRef := true; - end; + line.position := line.position + 1; + link := line.readUntil(['>']); + line.position := line.position + 1; end else - isLinkRef := true; + link := line.readUntil([' ', #10]); + + // Is link valid? + if (link <> '') then + begin + // Any non-whitespace characters following? + if (line.skipSpaces()) then + begin + ch := line.value[1 + line.position]; + // Read comment + if (ch = '"') or (ch = '''') or (ch = '(') then + begin + line.position := line.position + 1; + if ch = '(' then + comment := line.readUntil([')']) + else + comment := line.readUntil([ch]); + // Valid linkRef only if comment is valid + if (comment <> '') then + isLinkRef := true; + end; + end + else + isLinkRef := true; + end; end; end; end; @@ -746,14 +256,18 @@ function TMarkdownDaringFireball.readLines(reader : TReader): TBlock; begin if (LowerCase(id) = '$profile$') then begin - FuseExtensions := LowerCase(link) = 'extended'; - Femitter.FuseExtensions := FuseExtensions; + if LowerCase(link) = 'extended' then + begin + FuseExtensions:=true; + Config.forceExtendedProfile:=true; + Femitter.FuseExtensions := true; + end; lastLinkRef := nil; end else begin // Store linkRef and skip line - lr := TLinkRef.Create(link, comment, (comment <> '') and (Length(link) = 1) and (link[1 + 1] = '*')); + lr := TLinkRef.Create(link, comment, (comment <> '') and (Length(link) = 1) and (link[1] = '*')); Femitter.addLinkRef(id, lr); if (comment = '') then lastLinkRef := lr; @@ -928,6 +442,7 @@ procedure TMarkdownDaringFireball.recurse(root: TBlock; listMode: boolean); block := root.split(line.previous) else block := root.split(root.lineTail); + block.removeSurroundingEmptyLines(); block.type_ := btFENCED_CODE; block.meta := TUtils.getMetaFromFence(block.lines.value); block.lines.setEmpty(); @@ -996,2360 +511,4 @@ procedure TMarkdownDaringFireball.SetUnSafe(const value: boolean); FConfig.safeMode := not value; end; -{ TLine } - -constructor TLine.Create; -begin - inherited; - FIsEmpty := true; -end; - -destructor TLine.Destroy; -begin - FNext.Free; - inherited; -end; - -{ TConfiguration } - -constructor TConfiguration.Create(safe : boolean); -begin - inherited Create; - FallowSpacesInFencedDelimiters := true; - Fdecorator := TDecorator.Create; - FsafeMode := safe; -end; - -destructor TConfiguration.Destroy; -begin - FcodeBlockEmitter.Free; - Fdecorator.Free; - FspecialLinkEmitter.Free; - inherited; -end; - -{ TDecorator } - -procedure TDecorator.openParagraph(out_: TStringBuilder); -begin - out_.append('<p>'); -end; - -procedure TDecorator.closeParagraph(out_: TStringBuilder); -begin - out_.append('</p>'#10); -end; - -procedure TDecorator.openBlockQuote(out_: TStringBuilder); -begin - out_.append('<blockquote>'); -end; - -procedure TDecorator.closeBlockQuote(out_: TStringBuilder); -begin - out_.append('</blockquote>'#10); -end; - -procedure TDecorator.openCodeBlock(out_: TStringBuilder); -begin - out_.append('<pre><code>'); -end; - -procedure TDecorator.closeCodeBlock(out_: TStringBuilder); -begin - out_.append('</code></pre>'#10); -end; - -procedure TDecorator.openCodeSpan(out_: TStringBuilder); -begin - out_.append('<code>'); -end; - -procedure TDecorator.closeCodeSpan(out_: TStringBuilder); -begin - out_.append('</code>'); -end; - -procedure TDecorator.openHeadline(out_: TStringBuilder; level: integer); -begin - out_.append('<h'); - out_.append(level); -end; - -procedure TDecorator.closeHeadline(out_: TStringBuilder; level: integer); -begin - out_.append('</h'); - out_.append(level); - out_.append('>'#10); -end; - -procedure TDecorator.openStrong(out_: TStringBuilder); -begin - out_.append('<strong>'); -end; - -procedure TDecorator.closeStrong(out_: TStringBuilder); -begin - out_.append('</strong>'); -end; - -procedure TDecorator.openEmphasis(out_: TStringBuilder); -begin - out_.append('<em>'); -end; - -procedure TDecorator.closeEmphasis(out_: TStringBuilder); -begin - out_.append('</em>'); -end; - -procedure TDecorator.openSuper(out_: TStringBuilder); -begin - out_.append('<sup>'); -end; - -procedure TDecorator.closeSuper(out_: TStringBuilder); -begin - out_.append('</sup>'); -end; - -procedure TDecorator.openOrderedList(out_: TStringBuilder); -begin - out_.append('<ol>'#10); -end; - -procedure TDecorator.closeOrderedList(out_: TStringBuilder); -begin - out_.append('</ol>'#10); -end; - -procedure TDecorator.openUnOrderedList(out_: TStringBuilder); -begin - out_.append('<ul>'#10); -end; - -procedure TDecorator.closeUnOrderedList(out_: TStringBuilder); -begin - out_.append('</ul>'#10); -end; - -procedure TDecorator.openListItem(out_: TStringBuilder); -begin - out_.append('<li'); -end; - -procedure TDecorator.closeListItem(out_: TStringBuilder); -begin - out_.append('</li>'#10); -end; - -procedure TDecorator.horizontalRuler(out_: TStringBuilder); -begin - out_.append('<hr/>'#10); -end; - -procedure TDecorator.openLink(out_: TStringBuilder); -begin - out_.append('<a'); -end; - -procedure TDecorator.closeLink(out_: TStringBuilder); -begin - out_.append('</a>'); -end; - -procedure TDecorator.openImage(out_: TStringBuilder); -begin - out_.append('<img'); -end; - -procedure TDecorator.closeImage(out_: TStringBuilder); -begin - out_.append(' />'); -end; - -{ TEmitter } - -constructor TEmitter.Create(config: TConfiguration); -begin - inherited Create; - FConfig := config; - linkRefs := TStringList.Create; - linkRefs.Sorted := true; - linkRefs.Duplicates := dupError; -end; - -destructor TEmitter.Destroy; -var - i : integer; -begin - for i := 0 to linkRefs.Count - 1 do - linkRefs.Objects[i].Free; - linkRefs.Free; - inherited; -end; - -procedure TEmitter.addLinkRef(key: String; linkRef: TLinkRef); -var - k : String; - i : integer; -begin - k := LowerCase(key); - if linkRefs.find(k, i) then - begin - linkRefs.Objects[i].Free; - linkRefs.Objects[i] := linkRef; - end - else - linkRefs.AddObject(k, linkRef); -end; - -procedure TEmitter.emit(out_: TStringBuilder; root: TBlock); -var - block: TBlock; -begin - root.removeSurroundingEmptyLines(); - - case root.type_ of - btRULER: - begin - FConfig.decorator.horizontalRuler(out_); - exit; - end; - btNONE, btXML: - ; // nothing - btHEADLINE: - begin - FConfig.decorator.openHeadline(out_, root.hlDepth); - if (FuseExtensions and (root.id <> '')) then - begin - out_.append(' id="'); - TUtils.appendCode(out_, root.id, 0, Length(root.id)); - out_.append('"'); - end; - out_.append('>'); - end; - btPARAGRAPH: - FConfig.decorator.openParagraph(out_); - btCODE, btFENCED_CODE: - if (FConfig.codeBlockEmitter = nil) then - FConfig.decorator.openCodeBlock(out_); - btBLOCKQUOTE: - FConfig.decorator.openBlockQuote(out_); - btUNORDERED_LIST: - FConfig.decorator.openUnOrderedList(out_); - btORDERED_LIST: - FConfig.decorator.openOrderedList(out_); - btLIST_ITEM: - begin - FConfig.decorator.openListItem(out_); - if (FuseExtensions and (root.id <> '')) then - begin - out_.append(' id="'); - TUtils.appendCode(out_, root.id, 0, Length(root.id)); - out_.append('"'); - end; - out_.append('>'); - end; - end; - - if (root.hasLines()) then - emitLines(out_, root) - else - begin - block := root.blocks; - while (block <> nil) do - begin - emit(out_, block); - block := block.next; - end; - end; - - case (root.type_) of - btRULER, btNONE, btXML: - ; // nothing - btHEADLINE: - FConfig.decorator.closeHeadline(out_, root.hlDepth); - btPARAGRAPH: - FConfig.decorator.closeParagraph(out_); - btCODE, btFENCED_CODE: - if (FConfig.codeBlockEmitter = nil) then - FConfig.decorator.closeCodeBlock(out_); - btBLOCKQUOTE: - FConfig.decorator.closeBlockQuote(out_); - btUNORDERED_LIST: - FConfig.decorator.closeUnOrderedList(out_); - btORDERED_LIST: - FConfig.decorator.closeOrderedList(out_); - btLIST_ITEM: - FConfig.decorator.closeListItem(out_); - end; -end; - -procedure TEmitter.emitLines(out_: TStringBuilder; block: TBlock); -begin - case (block.type_) of - btCODE: - emitCodeLines(out_, block.lines, block.meta, true); - btFENCED_CODE: - emitCodeLines(out_, block.lines, block.meta, false); - btXML: - emitRawLines(out_, block.lines); - else - emitMarkedLines(out_, block.lines); - end; -end; - -function TEmitter.findToken(s: String; start: integer; token: TMarkToken): integer; -var - position: integer; -begin - position := start; - while (position < Length(s)) do - begin - if getToken(s, position) = token then - exit(position); - inc(position); - end; - result := -1; -end; - -function TEmitter.checkLink(out_: TStringBuilder; s: String; start: integer; token: TMarkToken): integer; -var - isAbbrev, useLt, hasLink: boolean; - position, oldPos, i: integer; - temp: TStringBuilder; - name, link, comment, id: String; - lr: TLinkRef; -begin - isAbbrev := false; - if (token = mtLINK) then - position := start + 1 - else - position := start + 2; - temp := TStringBuilder.Create; - try - position := TUtils.readMdLinkId(temp, s, position); - if (position < start) then - exit(-1); - name := temp.ToString(); - link := ''; - hasLink := false; - comment := ''; - oldPos := position; - inc(position); - position := TUtils.skipSpaces(s, position); - if (position < start) then - begin - if linkRefs.find(LowerCase(name), i) then - begin - lr := TLinkRef(linkRefs.Objects[i]); - isAbbrev := lr.isAbbrev; - link := lr.link; - hasLink := true; - comment := lr.title; - position := oldPos; - end - else - exit(-1); - end - else if (s[1 + position] = '(') then - begin - inc(position); - position := TUtils.skipSpaces(s, position); - if (position < start) then - exit(-1); - temp.Clear; - useLt := s[1 + position] = '<'; - if useLt then - position := TUtils.readUntil(temp, s, position + 1, '>') - else - position := TUtils.readMdLink(temp, s, position); - if (position < start) then - exit(-1); - if (useLt) then - inc(position); - link := temp.ToString(); - hasLink := true; - if (s[1 + position] = ' ') then - begin - position := TUtils.skipSpaces(s, position); - if (position > start) and (s[1 + position] = '"') then - begin - inc(position); - temp.Clear; - position := TUtils.readUntil(temp, s, position, '"'); - if (position < start) then - exit(-1); - comment := temp.ToString(); - inc(position); - position := TUtils.skipSpaces(s, position); - if (position = -1) then - exit(-1); - end; - end; - if (s[1 + position] <> ')') then - exit(-1); - end - else if (s[1 + position] = '[') then - begin - inc(position); - temp.Clear; - position := TUtils.readRawUntil(temp, s, position, ']'); - if (position < start) then - exit(-1); - if temp.length > 0 then - id := temp.ToString() - else - id := name; - if linkRefs.find(LowerCase(id), i) then - begin - lr := TLinkRef(linkRefs.Objects[i]); - link := lr.link; - hasLink := true; - comment := lr.title; - end - end - else - begin - if linkRefs.find(LowerCase(name), i) then - begin - lr := TLinkRef(linkRefs.Objects[i]); - isAbbrev := lr.isAbbrev; - link := lr.link; - hasLink := true; - comment := lr.title; - position := oldPos; - end - else - exit(-1); - end; - if (not hasLink) then - exit(-1); - - if (token = mtLINK) then - begin - if (isAbbrev) and (comment <> '') then - begin - if (not FuseExtensions) then - exit(-1); - out_.append('<abbr title:="'); - TUtils.appendValue(out_, comment, 0, Length(comment)); - out_.append('">'); - recursiveEmitLine(out_, name, 0, mtNONE); - out_.append('</abbr>'); - end - else - begin - FConfig.decorator.openLink(out_); - out_.append(' href="'); - TUtils.appendValue(out_, link, 0, Length(link)); - out_.append('"'); - if (comment <> '') then - begin - out_.append(' title="'); - TUtils.appendValue(out_, comment, 0, Length(comment)); - out_.append('"'); - end; - out_.append('>'); - recursiveEmitLine(out_, name, 0, mtNONE); - FConfig.decorator.closeLink(out_); - end - end - else - begin - FConfig.decorator.openImage(out_); - out_.append(' src="'); - TUtils.appendValue(out_, link, 0, Length(link)); - out_.append('" alt="'); - TUtils.appendValue(out_, name, 0, Length(name)); - out_.append('"'); - if (comment <> '') then - begin - out_.append(' title="'); - TUtils.appendValue(out_, comment, 0, Length(comment)); - out_.append('"'); - end; - FConfig.decorator.closeImage(out_); - end; - result := position; - finally - temp.Free; - end; -end; - -function TEmitter.checkHTML(out_: TStringBuilder; s: String; start: integer): integer; -var - temp: TStringBuilder; - position: integer; - link: String; -begin - temp := TStringBuilder.Create(); - try - // Check for auto links - temp.Clear; - position := TUtils.readUntil(temp, s, start + 1, [':', ' ', '>', #10]); - if (position <> -1) and (s[1 + position] = ':') and (THTML.isLinkPrefix(temp.ToString())) then - begin - position := TUtils.readUntil(temp, s, position, ['>']); - if (position <> -1) then - begin - link := temp.ToString(); - FConfig.decorator.openLink(out_); - out_.append(' href="'); - TUtils.appendValue(out_, link, 0, Length(link)); - out_.append('">'); - TUtils.appendValue(out_, link, 0, Length(link)); - FConfig.decorator.closeLink(out_); - exit(position); - end; - end; - - // Check for mailto auto link - temp.Clear; - position := TUtils.readUntil(temp, s, start + 1, ['@', ' ', '>', #10]); - if (position <> -1) and (s[1 + position] = '@') then - begin - position := TUtils.readUntil(temp, s, position, '>'); - if (position <> -1) then - begin - link := temp.ToString(); - FConfig.decorator.openLink(out_); - out_.append(' href="'); - TUtils.appendMailto(out_, 'mailto:', 0, 7); - TUtils.appendMailto(out_, link, 0, Length(link)); - out_.append('">'); - TUtils.appendMailto(out_, link, 0, Length(link)); - FConfig.decorator.closeLink(out_); - exit(position); - end; - end; - - // Check for inline html - if (start + 2 < Length(s)) then - begin - temp.Clear; - exit(TUtils.readXML(out_, s, start, FConfig.safeMode)); - end; - - result := -1; - finally - temp.Free; - end; -end; - -class function TEmitter.checkEntity(out_: TStringBuilder; s: String; start: integer): integer; -var - position, i: integer; - c: char; -begin - position := TUtils.readUntil(out_, s, start, ';'); - if (position < 0) or (out_.length < 3) then - exit(-1); - if (out_[1] = '#') then - begin - if (out_[2] = 'x') or (out_[2] = 'X') then - begin - if (out_.length < 4) then - exit(-1); - for i := 3 to out_.length do - begin - c := out_[i]; - if ((c < '0') or (c > '9')) and (((c < 'a') or (c > 'f')) and ((c < 'A') or (c > 'F'))) then - exit(-1); - end; - end - else - begin - for i := 2 to out_.length do - begin - c := out_[i]; - if (c < '0') or (c > '9') then - exit(-1); - end; - end; - out_.append(';'); - end - else - begin - for i := 1 to out_.length - 1 do - begin - c := out_[i]; // zero based - if (not isLetterOrDigit(c)) then - exit(-1); - end; - out_.append(';'); - if THTML.isEntity(out_.ToString()) then - exit(position) - else - exit(-1); - end; - - result := position; -end; - -function TEmitter.recursiveEmitLine(out_: TStringBuilder; s: String; start: integer; token: TMarkToken): integer; -var - position, a, b: integer; - temp: TStringBuilder; - mt: TMarkToken; -begin - position := start; - temp := TStringBuilder.Create(); - try - while (position < Length(s)) do - begin - mt := getToken(s, position); - if (token <> mtNONE) and ((mt = token) or ((token = mtEM_STAR) and (mt = mtSTRONG_STAR)) or ((token = mtEM_UNDERSCORE) and (mt = mtSTRONG_UNDERSCORE))) then - exit(position); - - case mt of - mtIMAGE, mtLINK: - begin - temp.Clear; - b := checkLink(temp, s, position, mt); - if (b > 0) then - begin - out_.append(temp); - position := b; - end - else - out_.append(s[1 + position]); - end; - mtEM_STAR, mtEM_UNDERSCORE: - begin - temp.Clear; - b := recursiveEmitLine(temp, s, position + 1, mt); - if (b > 0) then - begin - FConfig.decorator.openEmphasis(out_); - out_.append(temp); - FConfig.decorator.closeEmphasis(out_); - position := b; - end - else - out_.append(s[1 + position]); - end; - mtSTRONG_STAR, mtSTRONG_UNDERSCORE: - begin - temp.Clear; - b := recursiveEmitLine(temp, s, position + 2, mt); - if (b > 0) then - begin - FConfig.decorator.openStrong(out_); - out_.append(temp); - FConfig.decorator.closeStrong(out_); - position := b + 1; - end - else - out_.append(s[1 + position]); - end; - mtSUPER: - begin - temp.Clear; - b := recursiveEmitLine(temp, s, position + 1, mt); - if (b > 0) then - begin - FConfig.decorator.openSuper(out_); - out_.append(temp); - FConfig.decorator.closeSuper(out_); - position := b; - end - else - out_.append(s[1 + position]); - end; - mtCODE_SINGLE, mtCODE_DOUBLE: - begin - if mt = mtCODE_DOUBLE then - a := position + 2 - else - a := position + 1; - b := findToken(s, a, mt); - if (b > 0) then - begin - if mt = mtCODE_DOUBLE then - position := b + 1 - else - position := b + 0; - while (a < b) and (s[1 + a] = ' ') do - inc(a); - if (a < b) then - begin - while (s[1 + b - 1] = ' ') do - dec(b); - end; - FConfig.decorator.openCodeSpan(out_); - TUtils.appendCode(out_, s, a, b); - FConfig.decorator.closeCodeSpan(out_); - end - else - out_.append(s[1 + position]); - end; - mtHTML: - begin - temp.Clear; - b := checkHTML(temp, s, position); - if (b > 0) then - begin - out_.append(temp); - position := b; - end - else - out_.append('<'); - end; - mtENTITY: - begin - temp.Clear; - b := checkEntity(temp, s, position); - if (b > 0) then - begin - out_.append(temp); - position := b; - end - else - out_.append('&'); - end; - mtX_LINK_OPEN: - begin - temp.Clear; - b := recursiveEmitLine(temp, s, position + 2, mtX_LINK_CLOSE); - if (b > 0) and (FConfig.specialLinkEmitter <> nil) then - begin - FConfig.specialLinkEmitter.emitSpan(out_, temp.ToString()); - position := b + 1; - end - else - out_.append(s[1 + position]); - end; - mtX_COPY: - begin - out_.append('©'); - inc(position, 2); - end; - mtX_REG: - begin - out_.append('®'); - inc(position, 2); - end; - mtX_TRADE: - begin - out_.append('™'); - inc(position, 3); - end; - mtX_NDASH: - begin - out_.append('–'); - inc(position); - end; - mtX_MDASH: - begin - out_.append('—'); - inc(position, 2); - end; - mtX_HELLIP: - begin - out_.append('…'); - inc(position, 2); - end; - mtX_LAQUO: - begin - out_.append('«'); - inc(position); - end; - mtX_RAQUO: - begin - out_.append('»'); - inc(position); - end; - mtX_RDQUO: - out_.append('”'); - mtX_LDQUO: - out_.append('“'); - mtESCAPE: - begin - inc(position); - out_.append(s[1 + position]); - end; - // $FALL-THROUGH$ - else - out_.append(s[1 + position]); - end; - inc(position); - end; - result := -1; - finally - temp.Free; - end; -end; - -class function TEmitter.whitespaceToSpace(c: char): char; -begin - if isWhitespace(c) then - result := ' ' - else - result := c; -end; - -function TEmitter.getToken(s: String; position: integer): TMarkToken; -var - c0, c, c1, c2, c3: char; -begin - - result := mtNONE; - if (position > 0) then - c0 := whitespaceToSpace(s[1 + position - 1]) - else - c0 := ' '; - c := whitespaceToSpace(s[1 + position]); - if (position + 1 < Length(s)) then - c1 := whitespaceToSpace(s[1 + position + 1]) - else - c1 := ' '; - if (position + 2 < Length(s)) then - c2 := whitespaceToSpace(s[1 + position + 2]) - else - c2 := ' '; - if (position + 3 < Length(s)) then - c3 := whitespaceToSpace(s[1 + position + 3]) - else - c3 := ' '; - - case (c) of - '*': - if (c1 = '*') then - begin - if (c0 <> ' ') or (c2 <> ' ') then - exit(mtSTRONG_STAR) - else - exit(mtEM_STAR); - end - else if (c0 <> ' ') or (c1 <> ' ') then - exit(mtEM_STAR) - else - exit(mtNONE); - '_': - if (c1 = '_') then - begin - if (c0 <> ' ') or (c2 <> ' ') then - exit(mtSTRONG_UNDERSCORE) - else - exit(mtEM_UNDERSCORE); - end - else if (FuseExtensions) then - begin - if (isLetterOrDigit(c0)) and (c0 <> '_') and (isLetterOrDigit(c1)) then - exit(mtNONE) - else - exit(mtEM_UNDERSCORE); - end - else if (c0 <> ' ') or (c1 <> ' ') then - exit(mtEM_UNDERSCORE) - else - exit(mtNONE); - '!': - if (c1 = '[') then - exit(mtIMAGE) - else - exit(mtNONE); - '[': - if (FuseExtensions) and (c1 = '[') then - exit(mtX_LINK_OPEN) - else - exit(mtLINK); - ']': - if (FuseExtensions) and (c1 = ']') then - exit(mtX_LINK_CLOSE) - else - exit(mtNONE); - '`': - if (c1 = '`') then - exit(mtCODE_DOUBLE) - else - exit(mtCODE_SINGLE); - '\': - if CharInSet(c1, ['\', '[', ']', '(', ')', '{', '}', '#', '"', '''', '.', '>', '<', '*', '+', '-', '_', '!', '`', '~', '^']) then - exit(mtESCAPE) - else - exit(mtNONE); - '<': - if (FuseExtensions) and (c1 = '<') then - exit(mtX_LAQUO) - else - exit(mtHTML); - '&': - exit(mtENTITY); - else - if (FuseExtensions) then - case (c) of - '-': - if (c1 = '-') and (c2 = '-') then - exit(mtX_MDASH) - else - exit(mtX_NDASH); - '^': - if (c0 = '^') or (c1 = '^') then - exit(mtNONE) - else - exit(mtSUPER); - '>': - if (c1 = '>') then - exit(mtX_RAQUO); - '.': - if (c1 = '.') and (c2 = '.') then - exit(mtX_HELLIP); - '(': - begin - if (c1 = 'C') and (c2 = ')') then - exit(mtX_COPY); - if (c1 = 'R') and (c2 = ')') then - exit(mtX_REG); - if (c1 = 'T') and (c2 = 'M') and (c3 = ')') then - exit(mtX_TRADE); - end; - '"': - begin - if (not isLetterOrDigit(c0)) and (c1 <> ' ') then - exit(mtX_LDQUO); - if (c0 <> ' ') and (not isLetterOrDigit(c1)) then - exit(mtX_RDQUO); - exit(mtNONE); - end; - end; - end; -end; - -procedure TEmitter.emitMarkedLines(out_: TStringBuilder; lines: TLine); -var - s: TStringBuilder; - line: TLine; -begin - s := TStringBuilder.Create(); - try - line := lines; - while (line <> nil) do - begin - if (not line.isEmpty) then - begin -// s.append(line.value.substring(line.leading, line.value.length - line.trailing)); PSTfix - s.Append( Copy(line.value, line.leading + 1, Length(line.value) - line.trailing)); - if (line.trailing >= 2) then - s.append('<br />'); - end; - if (line.next <> nil) then - s.append(#10); - line := line.next; - end; - recursiveEmitLine(out_, s.ToString(), 0, mtNONE); - finally - s.Free; - end; -end; - -procedure TEmitter.emitRawLines(out_: TStringBuilder; lines: TLine); -var - s: String; - line: TLine; - temp: TStringBuilder; - position, t: integer; -begin - line := lines; - if (FConfig.safeMode) then - begin - temp := TStringBuilder.Create(); - try - while (line <> nil) do - begin - if (not line.isEmpty) then - temp.append(line.value); - temp.append(#10); - line := line.next; - end; - s := temp.ToString(); - position := 0; - while position < length(s) do - begin - if (s[1 + position] = '<') then - begin - temp.Clear; - t := TUtils.readXML(temp, s, position, FConfig.safeMode); - if (t <> -1) then - begin - out_.append(temp); - position := t; - end - else - out_.append(s[1 + position]); - end - else - out_.append(s[1 + position]); - inc(position); - end - finally - temp.Free; - end; - end - else - begin - while (line <> nil) do - begin - if (not line.isEmpty) then - out_.append(line.value); - out_.append(#10); - line := line.next; - end; - end; -end; - -procedure TEmitter.emitCodeLines(out_: TStringBuilder; lines: TLine; meta: String; removeIndent: boolean); -var - line: TLine; - list: TStringList; - i, sp: integer; - c: char; -begin - line := lines; - if (FConfig.codeBlockEmitter <> nil) then - begin - list := TStringList.Create; - try - while (line <> nil) do - begin - if (line.isEmpty) then - list.add('') - else if removeIndent then -// list.add(line.value.substring(4)) P{STfix - list.Add( Copy(line.value, 5)) - else - list.add(line.value); - line := line.next; - end; - FConfig.codeBlockEmitter.emitBlock(out_, list, meta); - finally - list.Free - end - end - else - begin - while (line <> nil) do - begin - if (not line.isEmpty) then - begin - if removeIndent then - sp := 4 - else - sp := 0; - for i := sp to Length(line.value) - 1 do - begin - c := line.value[1 + i]; - case c of - '&': - out_.append('&'); - '<': - out_.append('<'); - '>': - out_.append('>'); - else - out_.append(c); - end; - end; - end; - out_.append(#10); - line := line.next; - end; - end; -end; - -{ TReader } - -constructor TReader.Create(source: String); -begin - inherited Create; - FValue := source; - FCursor := 0; -end; - -function TReader.read: char; -begin - inc(FCursor); - if FCursor > Length(FValue) then - result := #0 - else - result := FValue[FCursor]; -end; - -{ TUtils } - -class function TUtils.skipSpaces(s: String; start: integer): integer; -var - position: integer; -begin - position := start; - while (position < Length(s)) and ((s[1 + position] = ' ') or (s[1 + position] = #10)) do - inc(position); - if position < Length(s) then - result := position - else - result := -1; -end; - -class function TUtils.escape(out_: TStringBuilder; ch: char; position: integer): integer; -begin - if CharInSet(ch, ['\', '[', ']', '(', ')', '{', '}', '#', '"', '''', '.', '>', '<', '*', '+', '-', '_', '!', '`', '^']) then - begin - out_.append(ch); - result := position + 1; - end - else - begin - out_.append('\'); - result := position; - end; -end; - -class function TUtils.readUntil(out_: TStringBuilder; s: String; start: integer; cend: TSysCharSet): integer; -var - position: integer; - ch: char; -begin - position := start; - while (position < Length(s)) do - begin - ch := s[1 + position]; - if (ch = '\') and (position + 1 < Length(s)) then - position := escape(out_, s[1 + position + 1], position) - else - begin - if CharInSet(ch, cend) then - break - else - out_.append(ch); - end; - inc(position); - end; - if position = Length(s) then - result := -1 - else - result := position; -end; - -class function TUtils.readUntil(out_: TStringBuilder; s: String; start: integer; cend: char): integer; -var - position: integer; - ch: char; -begin - position := start; - while (position < Length(s)) do - begin - ch := s[1 + position]; - if (ch = '\') and (position + 1 < Length(s)) then - position := escape(out_, s[1 + position + 1], position) - else - begin - if (ch = cend) then - break; - out_.append(ch); - end; - inc(position); - end; - if position = Length(s) then - result := -1 - else - result := position; -end; - -class function TUtils.readMdLink(out_: TStringBuilder; s: String; start: integer): integer; -var - position, counter: integer; - ch: char; - endReached: boolean; -begin - position := start; - counter := 1; - while (position < Length(s)) do - begin - ch := s[1 + position]; - if (ch = '\') and (position + 1 < Length(s)) then - position := escape(out_, s[1 + position + 1], position) - else - begin - endReached := false; - case ch of - '(': - inc(counter); - ' ': - if (counter = 1) then - endReached := true; - ')': - begin - dec(counter); - if (counter = 0) then - endReached := true; - end; - end; - if (endReached) then - break; - out_.append(ch); - end; - inc(position); - end; - if position = Length(s) then - result := -1 - else - result := position; -end; - -class function TUtils.readMdLinkId(out_: TStringBuilder; s: String; start: integer): integer; -var - position, counter: integer; - ch: char; - endReached: boolean; -begin - position := start; - counter := 1; - while (position < Length(s)) do - begin - ch := s[1 + position]; - endReached := false; - case ch of - #10: - out_.append(' '); - '[': - begin - inc(counter); - out_.append(ch); - end; - ']': - begin - dec(counter); - if (counter = 0) then - endReached := true - else - out_.append(ch); - end; - else - out_.append(ch); - end; - if (endReached) then - break; - inc(position); - end; - if position = Length(s) then - result := -1 - else - result := position; -end; - -class function TUtils.readRawUntil(out_: TStringBuilder; s: String; start: integer; cend: TSysCharSet): integer; -var - position: integer; - ch: char; -begin - position := start; - while (position < Length(s)) do - begin - ch := s[1 + position]; - if CharInSet(ch, cend) then - break; - out_.append(ch); - inc(position); - end; - if position = Length(s) then - result := -1 - else - result := position; -end; - -class function TUtils.readRawUntil(out_: TStringBuilder; s: String; start: integer; cend: char): integer; -var - position: integer; - ch: char; -begin - position := start; - while (position < Length(s)) do - begin - ch := s[1 + position]; - if (ch = cend) then - break; - out_.append(ch); - inc(position); - end; - if position = Length(s) then - result := -1 - else - result := position; -end; - -class function TUtils.readXMLUntil(out_: TStringBuilder; s: String; start: integer; cend: TSysCharSet): integer; -var - position : integer; - ch, stringChar: char; - inString: boolean; -begin - position := start; - inString := false; - stringChar := #0; - while (position < Length(s)) do - begin - ch := s[1 + position]; - if (inString) then - begin - if (ch = '\') then - begin - out_.append(ch); - inc(position); - if (position < Length(s)) then - begin - out_.append(ch); - inc(position); - end; - continue; - end; - if (ch = stringChar) then - begin - inString := false; - out_.append(ch); - inc(position); - continue; - end; - end; - if CharInSet(ch, ['"', '''']) then - begin - inString := true; - stringChar := ch; - end; - if (not inString) then - begin - if CharInSet(ch, cend) then - break; - end; - out_.append(ch); - inc(position); - end; - if position = Length(s) then - result := -1 - else - result := position; -end; - -class procedure TUtils.appendCode(out_: TStringBuilder; s: String; start: integer; e: integer); -var - i: integer; - c: char; -begin - for i := start to e - 1 do - begin - c := s[1 + i]; - case c of - '&': - out_.append('&'); - '<': - out_.append('<'); - '>': - out_.append('>'); - else - out_.append(c); - end; - end; -end; - -class procedure TUtils.appendValue(out_: TStringBuilder; s: String; start: integer; e: integer); -var - i: integer; - c: char; -begin - for i := start to e - 1 do - begin - c := s[1 + i]; - case c of - '&': - out_.append('&'); - '<': - out_.append('<'); - '>': - out_.append('>'); - '"': - out_.append('"'); - '''': - out_.append('''); - else - out_.append(c); - end; - end; -end; - -class procedure TUtils.appendDecEntity(out_: TStringBuilder; value: char); -begin - out_.append('&#'); - out_.append(IntToStr(ord(value))); - out_.append(';'); -end; - -class procedure TUtils.appendHexEntity(out_: TStringBuilder; value: char); -begin - out_.append('&#'); - out_.append(IntToHex(ord(value), 2)); - out_.append(';'); -end; - -class procedure TUtils.appendMailto(out_: TStringBuilder; s: String; start: integer; e: integer); -var - i: integer; - c: char; -begin - for i := start to e - 1 do - begin - c := s[1 + i]; - if CharInSet(c, ['&', '<', '>', '"', '''', '@']) then - appendHexEntity(out_, c) - else - out_.append(c); - end; -end; - -class procedure TUtils.getXMLTag(out_: TStringBuilder; bin: TStringBuilder); -var - position: integer; -begin - position := 1; - if (bin[1] = '/') then - inc(position); - while (isLetterOrDigit(bin[position])) do - begin - out_.append(bin[position]); - inc(position) - end; -end; - -class procedure TUtils.getXMLTag(out_: TStringBuilder; s: String); -var - position: integer; -begin - position := 1; - if (s[1 + 1] = '/') then - inc(position); - while (isLetterOrDigit(s[1 + position])) do - begin - out_.append(s[1 + position]); - inc(position) - end; -end; - -class function TUtils.readXML(out_: TStringBuilder; s: String; start: integer; safeMode: boolean): integer; -var - position: integer; - isCloseTag: boolean; - temp: TStringBuilder; - tag: String; -begin - if (s[1 + start + 1] = '/') then - begin - isCloseTag := true; - position := start + 2; - end - else if (s[1 + start + 1] = '!') then - begin - out_.append('<!'); - exit(start + 1); - end - else - begin - isCloseTag := false; - position := start + 1; - end; - - if (safeMode) then - begin - temp := TStringBuilder.Create(); - try - position := readXMLUntil(temp, s, position, [' ', '/', '>']); - if (position = -1) then - exit(-1); -// tag := temp.ToString().trim().ToLower; PSTFix - tag := LowerCase( Trim( temp.ToString)); - if (THTML.isUnsafeHtmlElement(tag)) then - out_.append('<') - else - out_.append('<'); - if (isCloseTag) then - out_.append('/'); - out_.append(temp); - finally - temp.Free; - end; - end - else - begin - out_.append('<'); - if (isCloseTag) then - out_.append('/'); - position := readXMLUntil(out_, s, position, [' ', '/', '>']); - end; - if (position = -1) then - exit(-1); - position := readXMLUntil(out_, s, position, ['/', '>']); - if (position = -1) then - exit(-1); - - if (s[1 + position] = '/') then - begin - out_.append(' /'); - position := readXMLUntil(out_, s, position + 1, ['>']); - if (position = -1) then - exit(-1); - end; - - if (s[1 + position] = '>') then - begin - out_.append('>'); - exit(position); - end; - result := -1; -end; - -class procedure TUtils.codeEncode(out_: TStringBuilder; value: String; offset: integer); -var - i: integer; - c: char; -begin - for i := offset to Length(value) - 1 do - begin - c := value[1 + i]; - case c of - '&': - out_.append('&'); - '<': - out_.append('<'); - '>': - out_.append('>'); - else - out_.append(c); - end; - end; -end; - -class function TUtils.getMetaFromFence(fenceLine: String): String; -var - i: integer; - c: char; -begin - for i := 0 to Length(fenceLine) - 1 do - begin - c := fenceLine[1 + i]; - if (not isWhitespace(c)) and (c <> '`') and (c <> '~') then -// exit(fenceLine.substring(i).trim()); PSTfix - Exit( Trim( Copy(fenceLine, i+1))); - end; - result := ''; -end; - -{ THTML } - -class function THTML.isHtmlBlockElement(s: String): boolean; -var - ht: THTMLElement; -begin - ht := THTMLElement(StringToEnum(TypeInfo(THTMLElement), 'he' + s, ord(heNONE))); - result := ht in BLOCK_ELEMENTS; -end; - -class function THTML.isLinkPrefix(s: String): boolean; -begin - result := StringsContains(LINK_PREFIXES, s); -end; - -class function THTML.isEntity(s: String): boolean; -begin - result := StringsContains(ENTITY_NAMES, s); -end; - -class function THTML.isUnsafeHtmlElement(s: String): boolean; -var - ht: THTMLElement; -begin - ht := THTMLElement(StringToEnum(TypeInfo(THTMLElement), s, ord(heNONE))); - result := ht in UNSAFE_ELEMENTS; -end; - -{ TLine } - -procedure TLine.Init(); -begin - FLeading := 0; - while (leading < Length(value)) and (value[1 + leading] = ' ') do - inc(FLeading); - - if (leading = Length(value)) then - setEmpty() - else - begin - isEmpty := false; - trailing := 0; - while (value[1 + Length(value) - trailing - 1] = ' ') do - inc(FTrailing); - end; -end; - -procedure TLine.InitLeading(); -begin - FLeading := 0; - while (leading < Length(value)) and (value[1 + leading] = ' ') do - inc(FLeading); - if (leading = Length(value)) then - setEmpty(); -end; - -// TODO use Util#skipSpaces -function TLine.skipSpaces(): boolean; -begin - while (position < Length(value)) and (value[1 + position] = ' ') do - inc(FPosition); - result := position < Length(value); -end; - -// TODO use Util#readUntil -function TLine.readUntil(chend: TSysCharSet): String; -var - sb: TStringBuilder; - p: integer; - ch, c: char; -begin - sb := TStringBuilder.Create(); - try - p := self.position; - while (p < Length(value)) do - begin - ch := value[1 + p]; - if (ch = '\') and (p + 1 < Length(value)) then - begin - c := value[1 + p + 1]; - if CharInSet(c, ['\', '[', ']', '(', ')', '{', '}', '#', '"', '''', '.', '>', '*', '+', '-', '_', '!', '`', '~']) then - begin - sb.append(c); - inc(FPosition); - end - else - begin - sb.append(ch); - break; - end; - end - else if CharInSet(ch, chend) then - break - else - sb.append(ch); - inc(p); - end; - - if (p < Length(value)) then - ch := value[1 + p] - else - ch := #10; - if CharInSet(ch, chend) then - begin - self.position := p; - result := sb.ToString(); - end - else - result := ''; - finally - sb.Free; - end; -end; - -procedure TLine.setEmpty(); -begin - value := ''; - leading := 0; - trailing := 0; - isEmpty := true; - if (previous <> nil) then - previous.nextEmpty := true; - if (next <> nil) then - next.prevEmpty := true; -end; - -function TLine.countChars(ch: char): integer; -var - count, i: integer; - c: char; -begin - count := 0; - for i := 0 to Length(value) - 1 do - begin - c := value[1 + i]; - if (c = ' ') then - continue; - if (c = ch) then - begin - inc(count); - continue; - end; - count := 0; - break; - end; - result := count; -end; - -function TLine.countCharsStart(ch: char; allowSpaces: boolean): integer; -var - count, i: integer; - c: char; -begin - count := 0; - for i := 0 to Length(value) - 1 do - begin - c := value[1 + i]; - if (c = ' ') and (allowSpaces) then - begin - continue; - end; - if (c = ch) then - inc(count) - else - break; - end; - result := count; -end; - -function TLine.getLineType(configuration: TConfiguration): TLineType; -var - i: integer; -begin - if (isEmpty) then - exit(ltEMPTY); - - if (leading > 3) then - exit(ltCODE); - - if (value[1 + leading] = '#') then - exit(ltHEADLINE); - - if (value[1 + leading] = '>') then - exit(ltBQUOTE); - - if (configuration.forceExtendedProfile) then - begin - if (Length(value) - leading - trailing > 2) then - begin - if (value[1 + leading] = '`') and (countCharsStart('`', configuration.allowSpacesInFencedDelimiters) >= 3) then - exit(ltFENCED_CODE); - if (value[1 + leading] = '~') and (countCharsStart('~', configuration.allowSpacesInFencedDelimiters) >= 3) then - exit(ltFENCED_CODE); - end; - end; - - if (Length(value) - leading - trailing > 2) and ((value[1 + leading] = '*') or (value[1 + leading] = '-') or (value[1 + leading] = '_')) then - begin - if (countChars(value[1 + leading]) >= 3) then - exit(ltHR); - end; - - if (Length(value) - leading >= 2) and (value[1 + leading + 1] = ' ') then - begin - if CharInSet(value[1 + leading], ['*', '-', '+']) then - exit(ltULIST); - end; - - if (Length(value) - leading >= 3) and (isDigit(value[1 + leading])) then - begin - i := leading + 1; - while (i < Length(value)) and (isDigit(value[1 + i])) do - inc(i); - if (i + 1 < Length(value)) and (value[1 + i] = '.') and (value[1 + i + 1] = ' ') then - exit(ltOLIST); - end; - - if (value[1 + leading] = '<') then - begin - if (checkHTML()) then - exit(ltXML); - end; - - if (next <> nil) and (not next.isEmpty) then - begin - if ((next.value[1 + 0] = '-')) and ((next.countChars('-') > 0)) then - exit(ltHEADLINE2); - if ((next.value[1 + 0] = '=')) and ((next.countChars('=') > 0)) then - exit(ltHEADLINE1); - end; - - exit(ltOTHER); -end; - -function TLine.readXMLComment(firstLine: TLine; start: integer): integer; -var - line: TLine; - p: integer; -begin - line := firstLine; - if (start + 3 < Length(line.value)) then - begin - if (line.value[1 + 2] = '-') and (line.value[1 + 3] = '-') then - begin - p := start + 4; - while (line <> nil) do - begin - while (p < Length(line.value)) and (line.value[1 + p] <> '-') do - inc(p); - if (p = Length(line.value)) then - begin - line := line.next; - p := 0; - end - else - begin - if (p + 2 < Length(line.value)) then - begin - if (line.value[1 + p + 1] = '-') and (line.value[1 + p + 2] = '>') then - begin - xmlEndLine := line; - exit(p + 3); - end; - end; - inc(p); - end; - end; - end; - end; - exit(-1); -end; - -// FIXME ... hack -function TLine.stripID(): String; -var - p, start: integer; - found: boolean; - id: String; -begin - if (isEmpty or (value[1 + Length(value) - trailing - 1] <> '}')) then - exit(''); - - p := leading; - found := false; - while (p < Length(value)) and (not found) do - begin - case value[1 + p] of - '\': - begin - if (p + 1 < Length(value)) then - begin - if (value[1 + p + 1]) = '{' then - begin - inc(p); - break; - end; - end; - inc(p); - break; - end; - '{': - begin - found := true; - break; - end - else - begin - inc(p); - break; - end; - end; - end; - - if (found) then - begin - if (p + 1 < Length(value)) and (value[1 + p + 1] = '#') then - begin - start := p + 2; - p := start; - found := false; - while (p < Length(value)) and (not found) do - begin - case (value[1 + p]) of - '\': - begin - if (p + 1 < Length(value)) then - begin - if (value[1 + p + 1]) = '}' then - begin - inc(p); - break; - end; - end; - inc(p); - break; - end; - '}': - begin - found := true; - break; - end; - else - begin - inc(p); - break; - end; - end; - - if (found) then - begin -// id := value.substring(start, p).trim(); PSTfix - id := Trim( Copy(value, start + 1, p)); - if (leading <> 0) then - begin -// value := value.substring(0, leading) + value.substring(leading, start - 2).trim(); PSTfix - value := Copy(value, 1, leading) + Trim( Copy( value, leading + 1, start -2)); - end - else - begin -// value := value.substring(leading, start - 2).trim(); PSTFix - value := Trim( Copy(value, leading +1, start -2)); - end; - trailing := 0; - if (Length(id) > 0) then - exit(id) - else - exit(''); - end; - end; - end; - end; - exit(''); -end; - -function TLine.checkHTML: boolean; -var - tags: TStringList; - temp: TStringBuilder; - element, tag: String; - line: TLine; - newPos: integer; -begin - result := false; - tags := TStringList.Create(); - temp := TStringBuilder.Create(); - try - position := leading; - if (value.length >= 1 + leading + 1) and (value[1 + leading + 1] = '!') then - begin - if (readXMLComment(self, leading) > 0) then - begin - exit(true); - end; - end; - position := TUtils.readXML(temp, value, leading, false); - if (position > -1) then - begin - element := temp.ToString(); - temp.Clear; - TUtils.getXMLTag(temp, element); - tag := LowerCase(temp.ToString()); - if (not THTML.isHtmlBlockElement(tag)) then - exit(false); -// if (tag.equals('hr') or element.endsWith('/>')) then PSTFix - if (tag = 'hr') or AnsiEndsText('/>', element) then - - begin - xmlEndLine := self; - exit(true); - end; - tags.add(tag); - - line := self; - while (line <> nil) do - begin - while (position < Length(line.value)) and (line.value[1 + position] <> '<') do - inc(FPosition); - if (position >= Length(line.value)) then - begin - line := line.next; - position := 0; - end - else - begin - temp.Clear; - newPos := TUtils.readXML(temp, line.value, position, false); - if (newPos > 0) then - begin - element := temp.ToString(); - temp.Clear; - TUtils.getXMLTag(temp, element); - tag := LowerCase(temp.ToString()); - if (THTML.isHtmlBlockElement(tag)) and (tag <> 'hr') and (not AnsiEndsText('/>', element)) then - begin - if (element[1 + 1] = '/') then - begin - if (tags[tags.Count - 1] <> tag) then - exit(false); - tags.Delete(tags.count - 1); - end - else - tags.add(tag); - end; - if (tags.count = 0) then - begin - xmlEndLine := line; - break; - end; - position := newPos; - end - else - begin - inc(FPosition); - end; - end; - end; - result := tags.count = 0; - end; - finally - temp.Free; - tags.Free; - end; -end; - -{ TLinkRef } - -constructor TLinkRef.Create(link, title: String; isAbbrev: boolean); -begin - inherited Create; - FLink := link; - FTitle := title; - FIsAbbrev := isAbbrev; -end; - -{ TBlock } - -constructor TBlock.Create; -begin - inherited; -end; - -destructor TBlock.Destroy; -begin - FLines.Free; - FBlocks.Free; - FNext.free; - inherited; -end; - -procedure TBlock.AppendLine(line: TLine); -begin - if (self.lineTail = nil) then - begin - self.FLines := line; - self.FLineTail := line; - end - else - begin - self.lineTail.nextEmpty := line.isEmpty; - line.prevEmpty := self.lineTail.isEmpty; - line.previous := self.lineTail; - self.lineTail.next := line; - self.FLineTail := line; - end; - -end; - -procedure TBlock.expandListParagraphs; -var - outer: TBlock; - inner: TBlock; - hasParagraph: boolean; -begin - if (self.type_ <> btORDERED_LIST) and (self.type_ <> btUNORDERED_LIST) then - exit; - - outer := self.blocks; - hasParagraph := false; - while (outer <> nil) and (not hasParagraph) do - begin - if (outer.type_ = btLIST_ITEM) then - begin - inner := outer.blocks; - while (inner <> nil) and (not hasParagraph) do - begin - if (inner.type_ = btPARAGRAPH) then - begin - hasParagraph := true; - end; - inner := inner.next; - end; - end; - outer := outer.next; - end; - - if (hasParagraph) then - begin - outer := self.blocks; - while (outer <> nil) do - begin - if (outer.type_ = btLIST_ITEM) then - begin - inner := outer.blocks; - while (inner <> nil) do - begin - if (inner.type_ = btNONE) then - begin - inner.type_ := btPARAGRAPH; - end; - inner := inner.next; - end; - end; - outer := outer.next; - end; - end; -end; - -function TBlock.hasLines: boolean; -begin - result := lines <> nil; -end; - -procedure TBlock.removeLine(line: TLine); -begin - if (line.previous = nil) then - begin - self.FLines := line.next; - end - else - begin - line.previous.next := line.next; - end; - - if (line.next = nil) then - begin - self.FLineTail := line.previous; - end - else - begin - line.next.previous := line.previous; - end; - line.previous := nil; - - line.next := nil; - line.free; -end; - -procedure TBlock.removeBlockQuotePrefix; -var - line: TLine; - rem: integer; -begin - line := self.lines; - while (line <> nil) do - begin - if (not line.isEmpty) then - begin - if (line.value[1 + line.leading] = '>') then - begin - rem := line.leading + 1; - if (line.leading + 1 < Length(line.value)) and (line.value[1 + line.leading + 1] = ' ') then - begin - inc(rem); - end; - line.value := Copy(line.value, rem+1); - line.InitLeading(); - end; - end; - line := line.next; - end; -end; - -function TBlock.removeLeadingEmptyLines: boolean; -var - wasEmpty: boolean; - line: TLine; -begin - wasEmpty := false; - line := self.lines; - while (line <> nil) and (line.isEmpty) do - begin - self.removeLine(line); - line := self.lines; - wasEmpty := true; - end; - result := wasEmpty; - -end; - -procedure TBlock.removeTrailingEmptyLines; -var - line: TLine; -begin - line := self.lineTail; - while (line <> nil) and (line.isEmpty) do - begin - self.removeLine(line); - line := self.lineTail; - end; -end; - -procedure TBlock.removeListIndent(config: TConfiguration); -var - line: TLine; -begin - line := self.lines; - while (line <> nil) do - begin - if (not line.isEmpty) then - begin - case (line.getLineType(config)) of - ltULIST: -// line.value := line.value.substring(line.leading + 2); PSTfix - line.value := Copy(line.value, line.leading +3); - ltOLIST: -// line.value := line.value.substring(line.value.indexOf('.') + 2); pstfix - line.value := Copy(line.value, pos('.', line.value) + 2); - else -// line.value := line.value.substring(Math.min(line.leading, 4)); pstfix - line.value := Copy(line.value, Math.Min(line.leading + 1, 5)); - end; - line.InitLeading(); - end; - line := line.next; - end; - -end; - -procedure TBlock.removeSurroundingEmptyLines; -begin - if (self.lines <> nil) then - begin - self.removeTrailingEmptyLines(); - self.removeLeadingEmptyLines(); - end; - -end; - -function TBlock.split(line: TLine): TBlock; -var - block: TBlock; -begin - block := TBlock.Create(); - block.FLines := self.lines; - block.FLineTail := line; - self.FLines := line.next; - line.next := nil; - if (self.lines = nil) then - begin - self.FLineTail := nil; - end - else - begin - self.lines.previous := nil; - end; - - if (self.blocks = nil) then - begin - self.FBlocks := block; - self.FBlockTail := block; - end - else - begin - self.blockTail.next := block; - self.FBlockTail := block; - end; - result := block; -end; - -procedure TBlock.transfromHeadline; -var - level, start, end_: integer; - line: TLine; -begin - if (self.hlDepth > 0) then - begin - exit; - end; - level := 0; - line := self.lines; - if (line.isEmpty) then - begin - exit; - end; - start := line.leading; - while (start < Length(line.value)) and (line.value[1 + start] = '#') do - begin - inc(level); - inc(start); - end; - while (start < Length(line.value)) and (line.value[1 + start] = ' ') do - begin - inc(start); - end; - if (start >= Length(line.value)) then - begin - line.setEmpty(); - end - else - begin - end_ := Length(line.value) - line.trailing - 1; - while (line.value[1 + end_] = '#') do - begin - dec(end_); - end; - while (line.value[1 + end_] = ' ') do - begin - dec(end_); - end; - line.value := Copy(line.value, start+1, end_-start+1); - line.leading := 0; - line.trailing := 0; - end; - self.hlDepth := Math.min(level, 6); - -end; - -{$IFDEF FPC} -{ TStringBuilder } - -constructor TStringBuilder.Create; -begin - Inherited; - FBufferSize := BUFFER_INCREMENT_SIZE; -end; - -procedure TStringBuilder.Append(value: TStringBuilder); -begin - append(value.ToString); -end; - -procedure TStringBuilder.Append(value: integer); -begin - append(inttostr(value)); -end; - -procedure TStringBuilder.Append(value: String); -begin - If (value <> '') Then - Begin - If FLength + System.Length(value) > System.Length(FContent) Then - SetLength(FContent, System.Length(FContent) + Math.Max(FBufferSize, System.Length(value))); - - Move(value[1], FContent[FLength + 1], System.Length(value) * SizeOf(Char)); - - Inc(FLength, System.Length(value)); - End; -end; - -procedure TStringBuilder.Clear; -begin - FContent := ''; - FLength := 0; -end; - -function TStringBuilder.GetChar(index: integer): char; -begin - if (index < 0) or (index >= Length) then - raise Exception.Create('Out of bounds'); - result := FContent[index+1]; -end; - - -function TStringBuilder.toString: String; -begin - Result := Copy(FContent, 1, FLength); -end; - -{$ENDIF} - end. diff --git a/source/MarkdownProcessor.pas b/source/MarkdownProcessor.pas index c7a7772..46329cc 100644 --- a/source/MarkdownProcessor.pas +++ b/source/MarkdownProcessor.pas @@ -27,7 +27,7 @@ interface SysUtils; Type - TMarkdownProcessorDialect = (mdDaringFireball, mdCommonMark, mdAsciiDoc); + TMarkdownProcessorDialect = (mdDaringFireball, mdTxtMark, mdCommonMark, mdAsciiDoc); TMarkdownProcessor = {abstract} class protected @@ -44,8 +44,9 @@ interface implementation uses - MarkdownDaringFireball; -// MarkdownCommonMark; + MarkdownDaringFireball, + MarkdownCommonMark, + MarkdownTxtMark; { TMarkdownProcessor } @@ -53,7 +54,8 @@ class function TMarkdownProcessor.CreateDialect(dialect: TMarkdownProcessorDiale begin case dialect of mdDaringFireball : result := TMarkdownDaringFireball.Create; -// mdCommonMark : result := TMarkdownCommonMark.Create; + mdCommonMark : result := TMarkdownCommonMark.Create; + mdTxtMark : result := TMarkdownTxtMark.Create; else raise Exception.Create('Unknown Markdown dialect'); end; diff --git a/source/MarkdownUtils.pas b/source/MarkdownUtils.pas new file mode 100644 index 0000000..145151a --- /dev/null +++ b/source/MarkdownUtils.pas @@ -0,0 +1,2901 @@ +{ +Copyright (C) Miguel A. Risco-Castillo + +FPC-markdown is a fork of Grahame Grieve <grahameg@gmail.com> +Delphi-markdown https://github.com/grahamegrieve/delphi-markdown + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +} + +Unit MarkdownUtils; + +{$mode objfpc}{$H+} + +interface + +uses + SysUtils, StrUtils, Classes, Character, TypInfo, Math; + +type + THTMLElement = (heNONE, hea, heabbr, heacronym, headdress, heapplet, hearea, heb, hebase, hebasefont, hebdo, hebig, heblockquote, hebody, hebr, hebutton, hecaption, hecite, + hecode, hecol, hecolgroup, hedd, hedel, hedfn, hediv, hedl, hedt, heem, hefieldset, hefont, heform, heframe, heframeset, heh1, heh2, heh3, heh4, heh5, heh6, hehead, hehr, + hehtml, hei, heiframe, heimg, heinput, heins, hekbd, helabel, helegend, heli, helink, hemap, hemeta, henoscript, heobject, heol, heoptgroup, heoption, hep, heparam, hepre, heq, + hes, hesamp, hescript, heselect, hesmall, hespan, hestrike, hestrong, hestyle, hesub, hesup, hetable, hetbody, hetd, hetextarea, hetfoot, heth, hethead, hetitle, hetr, hett, + heu, heul, hevar); + +const + // pstfix + ENTITY_NAMES: array[0..249] of String = ('Â', 'â', '´', 'Æ', 'æ', 'À', 'à', 'ℵ', 'Α', 'α', '&', '∧', '∠', + ''', 'Å', 'å', '≈', 'Ã', 'ã', 'Ä', 'ä', '„', 'Β', 'β', '¦', '•', '∩', 'Ç', 'ç', + '¸', '¢', 'Χ', 'χ', 'ˆ', '♣', '≅', '©', '↵', '∪', '¤', '‡', '†', '⇓', '↓', '°', 'Δ', + 'δ', '♦', '÷', 'É', 'é', 'Ê', 'ê', 'È', 'è', '∅', ' ', ' ', 'Ε', 'ε', '≡', + 'Η', 'η', 'Ð', 'ð', 'Ë', 'ë', '€', '∃', 'ƒ', '∀', '½', '¼', '¾', '⁄', 'Γ', 'γ', '≥', + '>', '⇔', '↔', '♥', '…', 'Í', 'í', 'Î', 'î', '¡', 'Ì', 'ì', 'ℑ', '∞', '∫', 'Ι', + 'ι', '¿', '∈', 'Ï', 'ï', 'Κ', 'κ', 'Λ', 'λ', '⟨', '«', '⇐', '←', '⌈', '“', '≤', + '⌊', '∗', '◊', '‎', '‹', '‘', '<', '¯', '—', 'µ', '·', '−', 'Μ', 'μ', '∇', ' ', '–', + '≠', '∋', '¬', '∉', '⊄', 'Ñ', 'ñ', 'Ν', 'ν', 'Ó', 'ó', 'Ô', 'ô', 'Œ', 'œ', 'Ò', + 'ò', '‾', 'Ω', 'ω', 'Ο', 'ο', '⊕', '∨', 'ª', 'º', 'Ø', 'ø', 'Õ', 'õ', '⊗', + 'Ö', 'ö', '¶', '∂', '‰', '⊥', 'Φ', 'φ', 'Π', 'π', 'ϖ', '±', '£', '″', '′', '∏', '∝', + 'Ψ', 'ψ', '"', '√', '⟩', '»', '⇒', '→', '⌉', '”', 'ℜ', '®', '⌋', 'Ρ', 'ρ', '‏', '›', + '’', '‚', 'Š', 'š', '⋅', '§', '­', 'Σ', 'σ', 'ς', '∼', '♠', '⊂', '⊆', '∑', '⊃', '¹', + '²', '³', '⊇', 'ß', 'Τ', 'τ', '∴', 'Θ', 'θ', 'ϑ', ' ', 'þ', '˜', '×', '™', 'Ú', + 'ú', '⇑', '↑', 'Û', 'û', 'Ù', 'ù', '¨', 'ϒ', 'Υ', 'υ', 'Ü', 'ü', '℘', 'Ξ', 'ξ', + 'Ý', 'ý', '¥', 'Ÿ', 'ÿ', 'Ζ', 'ζ', '‍', '‌'); + + // Characters corresponding to ENTITY_NAMES. */ + // pstfix + ENTITY_CHARS: array[0..249] of integer = ($00C2, $00E2, $00B4, $00C6, $00E6, $00C0, $00E0, $2135, $0391, $03B1, $0026, $2227, $2220, ord(''''), $00C5, $00E5, $2248, $00C3, $00E3, $00C4, + $00E4, $201E, $0392, $03B2, $00A6, $2022, $2229, $00C7, $00E7, $00B8, $00A2, $03A7, $03C7, $02C6, $2663, $2245, $00A9, $21B5, $222A, $00A4, $2021, $2020, $21D3, $2193, $00B0, + $0394, $03B4, $2666, $00F7, $00C9, $00E9, $00CA, $00EA, $00C8, $00E8, $2205, $2003, $2002, $0395, $03B5, $2261, $0397, $03B7, $00D0, $00F0, $00CB, $00EB, $20AC, $2203, $0192, + $2200, $00BD, $00BC, $00BE, $2044, $0393, $03B3, $2265, $003E, $21D4, $2194, $2665, $2026, $00CD, $00ED, $00CE, $00EE, $00A1, $00CC, $00EC, $2111, $221E, $222B, $0399, $03B9, + $00BF, $2208, $00CF, $00EF, $039A, $03BA, $039B, $03BB, $2329, $00AB, $21D0, $2190, $2308, $201C, $2264, $230A, $2217, $25CA, $200E, $2039, $2018, $003C, $00AF, $2014, $00B5, + $00B7, $2212, $039C, $03BC, $2207, $00A0, $2013, $2260, $220B, $00AC, $2209, $2284, $00D1, $00F1, $039D, $03BD, $00D3, $00F3, $00D4, $00F4, $0152, $0153, $00D2, $00F2, $203E, + $03A9, $03C9, $039F, $03BF, $2295, $2228, $00AA, $00BA, $00D8, $00F8, $00D5, $00F5, $2297, $00D6, $00F6, $00B6, $2202, $2030, $22A5, $03A6, $03C6, $03A0, $03C0, $03D6, $00B1, + $00A3, $2033, $2032, $220F, $221D, $03A8, $03C8, $0022, $221A, $232A, $00BB, $21D2, $2192, $2309, $201D, $211C, $00AE, $230B, $03A1, $03C1, $200F, $203A, $2019, $201A, $0160, + $0161, $22C5, $00A7, $00AD, $03A3, $03C3, $03C2, $223C, $2660, $2282, $2286, $2211, $2283, $00B9, $00B2, $00B3, $2287, $00DF, $03A4, $03C4, $2234, $0398, $03B8, $03D1, $00DE, + $00FE, $02DC, $00D7, $2122, $00DA, $00FA, $21D1, $2191, $00DB, $00FB, $00D9, $00F9, $00A8, $03D2, $03A5, $03C5, $00DC, $00FC, $2118, $039E, $03BE, $00DD, $00FD, $00A5, $0178, + $00FF, $0396, $03B6, $200D, $200C); + + LINK_PREFIXES: array[0..3] of String = ('http', 'https', 'ftp', 'ftps'); + + BLOCK_ELEMENTS: set of THTMLElement = [headdress, heblockquote, hedel, hediv, hedl, hefieldset, heform, heh1, heh2, heh3, heh4, heh5, heh6, hehr, heins, henoscript, heol, hep, + hepre, hetable, heul]; + + UNSAFE_ELEMENTS: set of THTMLElement = [heapplet, hehead, hehtml, hebody, heframe, heframeset, heiframe, hescript, heobject]; + + BUFFER_INCREMENT_SIZE = 1024; + +Type + TReader = class + private + FValue: String; + FCursor: integer; + public + Constructor Create(source: String); + function read: char; + end; + +{$IFDEF FPC} + TStringBuilder = class + private + FContent : String; + FLength : Integer; + FBufferSize : integer; + function GetChar(index: integer): char; + public + Constructor Create; + + procedure Clear; + procedure Append(value : String); overload; + procedure Append(value : integer); overload; + procedure Append(value : TStringBuilder); overload; + property ch[index : integer] : char read GetChar; default; + function toString : String; override; + Property Length : Integer Read FLength; + end; +{$ENDIF} + + TUtils = class + public + // Skips spaces in the given String. return The new position or -1 if EOL has been reached. + class function skipSpaces(s: String; start: integer): integer; + + // Process the given escape sequence. return The new position. + class function escape(out_: TStringBuilder; ch: char; position: integer): integer; + + // Reads characters until any 'end' character is encountered. return The new position or -1 if no 'end' char was found. + class function readUntil(out_: TStringBuilder; s: String; start: integer; cend: TSysCharSet): integer; overload; + + // Reads characters until the 'end' character is encountered. return The new position or -1 if no 'end' char was found. + class function readUntil(out_: TStringBuilder; s: String; start: integer; cend: char): integer; overload; + + // Reads a markdown link. return The new position or -1 if this is no valid markdown link. + class function readMdLink(out_: TStringBuilder; s: String; start: integer): integer; + class function readMdLinkId(out_: TStringBuilder; s: String; start: integer): integer; + + // Reads characters until any 'end' character is encountered, ignoring escape sequences. + class function readRawUntil(out_: TStringBuilder; s: String; start: integer; cend: TSysCharSet): integer; overload; + + // Reads characters until the end character is encountered, taking care of HTML/XML strings. + class function readRawUntil(out_: TStringBuilder; s: String; start: integer; cend: char): integer; overload; + + // Reads characters until any 'end' character is encountered, ignoring escape sequences. + class function readXMLUntil(out_: TStringBuilder; s: String; start: integer; cend: TSysCharSet): integer; + + // Appends the given string encoding special HTML characters. + class procedure appendCode(out_: TStringBuilder; s: String; start: integer; e: integer); + + // Appends the given string encoding special HTML characters (used in HTML + class procedure appendValue(out_: TStringBuilder; s: String; start: integer; e: integer); + + // Append the given char as a decimal HTML entity. + class procedure appendDecEntity(out_: TStringBuilder; value: char); + + // Append the given char as a hexadecimal HTML entity. + class procedure appendHexEntity(out_: TStringBuilder; value: char); + + // Appends the given mailto link using obfuscation. + class procedure appendMailto(out_: TStringBuilder; s: String; start: integer; e: integer); + + // Extracts the tag from an XML element. + class procedure getXMLTag(out_: TStringBuilder; bin: TStringBuilder); overload; + + // Extracts the tag from an XML element. + class procedure getXMLTag(out_: TStringBuilder; s: String); overload; + + // Reads an XML element. + // return The new position or -1 if this is no valid XML element. + class function readXML(out_: TStringBuilder; s: String; start: integer; safeMode: boolean): integer; + + // Appends the given string to the given StringBuilder, replacing '&', '<' and '>' by their respective HTML entities. + class procedure codeEncode(out_: TStringBuilder; value: String; offset: integer); + + // Removes trailing <code>`</code> or <code>~</code> and trims spaces. + class function getMetaFromFence(fenceLine: String): String; + end; + + THTML = class + public + class function isLinkPrefix(s: String): boolean; + class function isEntity(s: String): boolean; + class function isUnsafeHtmlElement(s: String): boolean; + class function isHtmlBlockElement(s: String): boolean; + end; + + { TDecorator } + + TDecorator = class + private + public + procedure openParagraph(out_: TStringBuilder); virtual; + procedure closeParagraph(out_: TStringBuilder); virtual; + + procedure openBlockQuote(out_: TStringBuilder); virtual; + procedure closeBlockQuote(out_: TStringBuilder); virtual; + + procedure openCodeBlock(out_: TStringBuilder); virtual; + procedure openFencedCodeBlock(out_: TStringBuilder; classlabel:string); + procedure closeCodeBlock(out_: TStringBuilder); virtual; + + procedure openCodeSpan(out_: TStringBuilder); virtual; + procedure closeCodeSpan(out_: TStringBuilder); virtual; + + procedure openHeadline(out_: TStringBuilder; level: integer); virtual; + procedure closeHeadline(out_: TStringBuilder; level: integer); virtual; + + procedure openStrong(out_: TStringBuilder); virtual; + procedure closeStrong(out_: TStringBuilder); virtual; + + procedure openEmphasis(out_: TStringBuilder); virtual; + procedure closeEmphasis(out_: TStringBuilder); virtual; + + procedure openSuper(out_: TStringBuilder); virtual; + procedure closeSuper(out_: TStringBuilder); virtual; + + procedure openOrderedList(out_: TStringBuilder); virtual; + procedure closeOrderedList(out_: TStringBuilder); virtual; + + procedure openUnOrderedList(out_: TStringBuilder); virtual; + procedure closeUnOrderedList(out_: TStringBuilder); virtual; + + procedure openListItem(out_: TStringBuilder); virtual; + procedure closeListItem(out_: TStringBuilder); virtual; + + procedure horizontalRuler(out_: TStringBuilder); virtual; + + procedure openLink(out_: TStringBuilder); virtual; + procedure closeLink(out_: TStringBuilder); virtual; + + procedure openImage(out_: TStringBuilder); virtual; + procedure closeImage(out_: TStringBuilder); virtual; + end; + + TSpanEmitter = class + public + procedure emitSpan(out_: TStringBuilder; content: String); virtual; abstract; + end; + + TBlockEmitter = class + public + procedure emitBlock(out_: TStringBuilder; lines: TStringList; meta: String); virtual; abstract; + end; + + TConfiguration = class + private + Fdecorator: TDecorator; + FsafeMode: boolean; + FallowSpacesInFencedDelimiters: boolean; + FforceExtendedProfile: boolean; + FcodeBlockEmitter: TBlockEmitter; + FpanicMode: boolean; + FspecialLinkEmitter: TSpanEmitter; + public + Constructor Create(safe : boolean); + Destructor Destroy; override; + + property safeMode: boolean read FsafeMode write FsafeMode; + property panicMode: boolean read FpanicMode write FpanicMode; + property decorator: TDecorator read Fdecorator write Fdecorator; + property codeBlockEmitter: TBlockEmitter read FcodeBlockEmitter write FcodeBlockEmitter; + property forceExtendedProfile: boolean read FforceExtendedProfile write FforceExtendedProfile; + property allowSpacesInFencedDelimiters: boolean read FallowSpacesInFencedDelimiters write FallowSpacesInFencedDelimiters; + property specialLinkEmitter: TSpanEmitter read FspecialLinkEmitter write FspecialLinkEmitter; + end; + + TLineType = ( + // Empty line. */ + ltEMPTY, + // Undefined content. */ + ltOTHER, + // A markdown headline. */ + ltHEADLINE, ltHEADLINE1, ltHEADLINE2, + // A code block line. */ + ltCODE, + // A list. */ + ltULIST, ltOLIST, + // A block quote. */ + ltBQUOTE, + // A horizontal ruler. */ + ltHR, + // Start of a XML block. */ + ltXML, + // Fenced code block start/end */ + ltFENCED_CODE); + + TLine = class + private + FXmlEndLine: TLine; + FPrevEmpty: boolean; + FPrevious: TLine; + FPosition: integer; + FValue: string; + FIsEmpty: boolean; + FTrailing: integer; + FNextEmpty: boolean; + FLeading: integer; + FNext: TLine; + function countChars(ch: char): integer; + function countCharsStart(ch: char; allowSpaces: boolean): integer; + function readXMLComment(firstLine: TLine; start: integer): integer; + function checkHTML(): boolean; + + public + Constructor Create; + Destructor Destroy; Override; + + // Current cursor position. + property position: integer read FPosition write FPosition; + // Leading and trailing spaces. + property leading: integer read FLeading write FLeading; + property trailing: integer read FTrailing write FTrailing; + // Is this line empty? + property isEmpty: boolean read FIsEmpty write FIsEmpty; + // This line's value. + property value: string read FValue write FValue; + // Previous and next line. + property previous: TLine read FPrevious write FPrevious; + property next: TLine read FNext write FNext; + + // Is previous/next line empty? + property prevEmpty: boolean read FPrevEmpty write FPrevEmpty; + property nextEmpty: boolean read FNextEmpty write FNextEmpty; + + // Final line of a XML block. + property xmlEndLine: TLine read FXmlEndLine write FXmlEndLine; + + procedure Init; + procedure InitLeading; + function skipSpaces: boolean; + function readUntil(chend: TSysCharSet): String; + procedure setEmpty; + function getLineType(configuration: TConfiguration): TLineType; + function stripID: String; + + end; + + TLinkRef = class + private + FLink: String; + FTitle: String; + FIsAbbrev: boolean; + public + Constructor Create(link, title: String; isAbbrev: boolean); + + property link: String read FLink write FLink; + property title: String read FTitle write FTitle; + property isAbbrev: boolean read FIsAbbrev write FIsAbbrev; + end; + + TBlockType = ( + // Unspecified. Used for root block and list items without paragraphs. + btNONE, + // A block quote. + btBLOCKQUOTE, + // A code block. + btCODE, + // A fenced code block. + btFENCED_CODE, + // A headline. + btHEADLINE, + // A list item. + btLIST_ITEM, + // An ordered list. + btORDERED_LIST, + // A paragraph. + btPARAGRAPH, + // A horizontal ruler. + btRULER, + // An unordered list. + btUNORDERED_LIST, + // A XML block. + btXML); + + TBlock = class + private + FType: TBlockType; + FId: String; + FBlocks: TBlock; + FBlockTail: TBlock; + FLines: TLine; + FLineTail: TLine; + FHlDepth: integer; + FNext: TBlock; + FMeta: String; + + public + procedure AppendLine(line: TLine); + function split(line: TLine): TBlock; + procedure removeListIndent(config: TConfiguration); + function removeLeadingEmptyLines: boolean; + procedure removeTrailingEmptyLines; + procedure transfromHeadline; + procedure expandListParagraphs; + function hasLines: boolean; + procedure removeSurroundingEmptyLines; + procedure removeBlockQuotePrefix; + procedure removeLine(line: TLine); + Constructor Create; + Destructor Destroy; Override; + + // This block's type. + property type_: TBlockType read FType write FType; + + property lines: TLine read FLines; + property lineTail: TLine read FLineTail; + + // child blocks. + property blocks: TBlock read FBlocks; + property blockTail: TBlock read FBlockTail; + + // Next block. + property next: TBlock read FNext write FNext; + // Depth of headline BlockType. + property hlDepth: integer read FHlDepth write FHlDepth; + // ID for headlines and list items + property id: String read FId write FId; + // Block meta information + property meta: String read FMeta write FMeta; + + end; + + TMarkToken = ( + // No token. + mtNONE, + // * + mtEM_STAR, // x*x + // _ + mtEM_UNDERSCORE, // x_x + // ** + mtSTRONG_STAR, // x**x + // __ + mtSTRONG_UNDERSCORE, // x__x + // ` + mtCODE_SINGLE, // ` + // `` + mtCODE_DOUBLE, // `` + // [ + mtLINK, // [ + // < + mtHTML, // < + // ![ + mtIMAGE, // ![ + // & + mtENTITY, // & + // \ + mtESCAPE, // \x + // Extended: ^ + mtSUPER, // ^ + // Extended: (C) + mtX_COPY, // (C) + // Extended: (R) + mtX_REG, // (R) + // Extended: (TM) + mtX_TRADE, // (TM) + // Extended: << + mtX_LAQUO, // << + // Extended: >> + mtX_RAQUO, // >> + // Extended: -- + mtX_NDASH, // -- + // Extended: --- + mtX_MDASH, // --- + // Extended: ... + mtX_HELLIP, // ... + // Extended: "x + mtX_RDQUO, // " + // Extended: x" + mtX_LDQUO, // " + // [[ + mtX_LINK_OPEN, // [[ + // ]] + mtX_LINK_CLOSE // ]] + ); + + // Emitter class responsible for generating HTML output. + TEmitter = class + private + linkRefs: TStringList; + FConfig: TConfiguration; + + public + FuseExtensions: boolean; + + Constructor Create(config: TConfiguration); + Destructor Destroy; override; + + procedure emitCodeLines(out_: TStringBuilder; lines: TLine; meta: String; removeIndent: boolean); + procedure emitRawLines(out_: TStringBuilder; lines: TLine); + procedure emitMarkedLines(out_: TStringBuilder; lines: TLine); + function findToken(s: String; start: integer; token: TMarkToken): integer; + function getToken(s: String; position: integer): TMarkToken; + function checkLink(out_: TStringBuilder; s: String; start: integer; token: TMarkToken): integer; + function recursiveEmitLine(out_: TStringBuilder; s: String; start: integer; token: TMarkToken): integer; + function checkHTML(out_: TStringBuilder; s: String; start: integer): integer; + class function checkEntity(out_: TStringBuilder; s: String; start: integer): integer; + class function whitespaceToSpace(c: char): char; + procedure addLinkRef(key: String; linkRef: TLinkRef); + procedure emit(out_: TStringBuilder; root: TBlock); + procedure emitLines(out_: TStringBuilder; block: TBlock); + end; + + + +Function StringsContains(Const aNames: Array Of String; Const sName: String): boolean; +function StringToEnum(ATypeInfo: PTypeInfo; const AStr: String; defValue: integer): integer; + +implementation + +Function StringsContains(Const aNames: Array Of String; Const sName: String): boolean; +var + i: integer; +Begin + for i := 0 to length(aNames) - 1 do + if sName <> aNames[i] then + exit(true); + result := false; +End; + +function StringToEnum(ATypeInfo: PTypeInfo; const AStr: String; defValue: integer): integer; +var + LTypeData: PTypeData; + LPChar: PAnsiChar; + LValue: ShortString; +begin + LValue := ShortString(AStr); + + if ATypeInfo^.Kind = tkEnumeration then + begin + LTypeData := GetTypeData(ATypeInfo); + if LTypeData^.MinValue <> 0 then + exit(defValue); + LPChar := @LTypeData^.NameList[0]; + result := 0; + while (result <= LTypeData^.MaxValue) and (ShortString(pointer(LPChar)^) <> LValue) do + begin + inc(LPChar, ord(LPChar^) + 1); // move to next string + inc(result); + end; + if result > LTypeData^.MaxValue then + exit(defValue); + end + else + exit(defValue); +end; + +{ TLine } + +constructor TLine.Create; +begin + inherited; + FIsEmpty := true; +end; + +destructor TLine.Destroy; +begin + FNext.Free; + inherited; +end; + +{ TConfiguration } + +constructor TConfiguration.Create(safe : boolean); +begin + inherited Create; + FallowSpacesInFencedDelimiters := true; + Fdecorator := TDecorator.Create; + FsafeMode := safe; +end; + +destructor TConfiguration.Destroy; +begin + FcodeBlockEmitter.Free; + Fdecorator.Free; + FspecialLinkEmitter.Free; + inherited; +end; + +{ TDecorator } + +procedure TDecorator.openParagraph(out_: TStringBuilder); +begin + out_.append('<p>'); +end; + +procedure TDecorator.closeParagraph(out_: TStringBuilder); +begin + out_.append('</p>'#10); +end; + +procedure TDecorator.openBlockQuote(out_: TStringBuilder); +begin + out_.append('<blockquote>'); +end; + +procedure TDecorator.closeBlockQuote(out_: TStringBuilder); +begin + out_.append('</blockquote>'#10); +end; + +procedure TDecorator.openCodeBlock(out_: TStringBuilder); +begin + out_.append('<pre><code>'); +end; + +procedure TDecorator.openFencedCodeBlock(out_: TStringBuilder; + classlabel: string); +begin + out_.append('<pre><code class="'+classlabel+'">'); +end; + +procedure TDecorator.closeCodeBlock(out_: TStringBuilder); +begin + out_.append('</code></pre>'#10); +end; + +procedure TDecorator.openCodeSpan(out_: TStringBuilder); +begin + out_.append('<code>'); +end; + +procedure TDecorator.closeCodeSpan(out_: TStringBuilder); +begin + out_.append('</code>'); +end; + +procedure TDecorator.openHeadline(out_: TStringBuilder; level: integer); +begin + out_.append('<h'); + out_.append(level); +end; + +procedure TDecorator.closeHeadline(out_: TStringBuilder; level: integer); +begin + out_.append('</h'); + out_.append(level); + out_.append('>'#10); +end; + +procedure TDecorator.openStrong(out_: TStringBuilder); +begin + out_.append('<strong>'); +end; + +procedure TDecorator.closeStrong(out_: TStringBuilder); +begin + out_.append('</strong>'); +end; + +procedure TDecorator.openEmphasis(out_: TStringBuilder); +begin + out_.append('<em>'); +end; + +procedure TDecorator.closeEmphasis(out_: TStringBuilder); +begin + out_.append('</em>'); +end; + +procedure TDecorator.openSuper(out_: TStringBuilder); +begin + out_.append('<sup>'); +end; + +procedure TDecorator.closeSuper(out_: TStringBuilder); +begin + out_.append('</sup>'); +end; + +procedure TDecorator.openOrderedList(out_: TStringBuilder); +begin + out_.append('<ol>'#10); +end; + +procedure TDecorator.closeOrderedList(out_: TStringBuilder); +begin + out_.append('</ol>'#10); +end; + +procedure TDecorator.openUnOrderedList(out_: TStringBuilder); +begin + out_.append('<ul>'#10); +end; + +procedure TDecorator.closeUnOrderedList(out_: TStringBuilder); +begin + out_.append('</ul>'#10); +end; + +procedure TDecorator.openListItem(out_: TStringBuilder); +begin + out_.append('<li'); +end; + +procedure TDecorator.closeListItem(out_: TStringBuilder); +begin + out_.append('</li>'#10); +end; + +procedure TDecorator.horizontalRuler(out_: TStringBuilder); +begin + out_.append('<hr/>'#10); +end; + +procedure TDecorator.openLink(out_: TStringBuilder); +begin + out_.append('<a'); +end; + +procedure TDecorator.closeLink(out_: TStringBuilder); +begin + out_.append('</a>'); +end; + +procedure TDecorator.openImage(out_: TStringBuilder); +begin + out_.append('<img'); +end; + +procedure TDecorator.closeImage(out_: TStringBuilder); +begin + out_.append('/>'); +end; + +{ TEmitter } + +constructor TEmitter.Create(config: TConfiguration); +begin + inherited Create; + FConfig := config; + linkRefs := TStringList.Create; + linkRefs.Sorted := true; + linkRefs.Duplicates := dupError; +end; + +destructor TEmitter.Destroy; +var + i : integer; +begin + for i := 0 to linkRefs.Count - 1 do + linkRefs.Objects[i].Free; + linkRefs.Free; + inherited; +end; + +procedure TEmitter.addLinkRef(key: String; linkRef: TLinkRef); +var + k : String; + i : integer; +begin + k := LowerCase(key); + if linkRefs.find(k, i) then + begin + linkRefs.Objects[i].Free; + linkRefs.Objects[i] := linkRef; + end + else + linkRefs.AddObject(k, linkRef); +end; + +procedure TEmitter.emit(out_: TStringBuilder; root: TBlock); +var + block: TBlock; +begin + root.removeSurroundingEmptyLines(); + + case root.type_ of + btRULER: + begin + FConfig.decorator.horizontalRuler(out_); + exit; + end; + btNONE, btXML: + ; // nothing + btHEADLINE: + begin + FConfig.decorator.openHeadline(out_, root.hlDepth); + if (FuseExtensions and (root.id <> '')) then + begin + out_.append(' id="'); + TUtils.appendCode(out_, root.id, 0, Length(root.id)); + out_.append('"'); + end; + out_.append('>'); + end; + btPARAGRAPH: + FConfig.decorator.openParagraph(out_); + btCODE: + if (FConfig.codeBlockEmitter = nil) then + FConfig.decorator.openCodeBlock(out_); + btFENCED_CODE: + if (FConfig.codeBlockEmitter = nil) then + FConfig.decorator.openFencedCodeBlock(out_,root.meta); + btBLOCKQUOTE: + FConfig.decorator.openBlockQuote(out_); + btUNORDERED_LIST: + FConfig.decorator.openUnOrderedList(out_); + btORDERED_LIST: + FConfig.decorator.openOrderedList(out_); + btLIST_ITEM: + begin + FConfig.decorator.openListItem(out_); + if (FuseExtensions and (root.id <> '')) then + begin + out_.append(' id="'); + TUtils.appendCode(out_, root.id, 0, Length(root.id)); + out_.append('"'); + end; + out_.append('>'); + end; + end; + + if (root.hasLines()) then + emitLines(out_, root) + else + begin + block := root.blocks; + while (block <> nil) do + begin + emit(out_, block); + block := block.next; + end; + end; + + case (root.type_) of + btRULER, btNONE, btXML: + ; // nothing + btHEADLINE: + FConfig.decorator.closeHeadline(out_, root.hlDepth); + btPARAGRAPH: + FConfig.decorator.closeParagraph(out_); + btCODE, btFENCED_CODE: + if (FConfig.codeBlockEmitter = nil) then + FConfig.decorator.closeCodeBlock(out_); + btBLOCKQUOTE: + FConfig.decorator.closeBlockQuote(out_); + btUNORDERED_LIST: + FConfig.decorator.closeUnOrderedList(out_); + btORDERED_LIST: + FConfig.decorator.closeOrderedList(out_); + btLIST_ITEM: + FConfig.decorator.closeListItem(out_); + end; +end; + +procedure TEmitter.emitLines(out_: TStringBuilder; block: TBlock); +begin + case (block.type_) of + btCODE: + emitCodeLines(out_, block.lines, block.meta, true); + btFENCED_CODE: + emitCodeLines(out_, block.lines, block.meta, false); + btXML: + emitRawLines(out_, block.lines); + else + emitMarkedLines(out_, block.lines); + end; +end; + +function TEmitter.findToken(s: String; start: integer; token: TMarkToken): integer; +var + position: integer; +begin + position := start; + while (position < Length(s)) do + begin + if getToken(s, position) = token then + exit(position); + inc(position); + end; + result := -1; +end; + +function TEmitter.checkLink(out_: TStringBuilder; s: String; start: integer; token: TMarkToken): integer; +var + isAbbrev, useLt, hasLink: boolean; + position, oldPos, i: integer; + temp: TStringBuilder; + name, link, comment, id: String; + lr: TLinkRef; +begin + isAbbrev := false; + if (token = mtLINK) then + position := start + 1 + else + position := start + 2; + temp := TStringBuilder.Create; + try + position := TUtils.readMdLinkId(temp, s, position); + if (position < start) then + exit(-1); + name := temp.ToString(); + link := ''; + hasLink := false; + comment := ''; + oldPos := position; + inc(position); + position := TUtils.skipSpaces(s, position); + if (position < start) then + begin + if linkRefs.find(LowerCase(name), i) then + begin + lr := TLinkRef(linkRefs.Objects[i]); + isAbbrev := lr.isAbbrev; + link := lr.link; + hasLink := true; + comment := lr.title; + position := oldPos; + end + else + exit(-1); + end + else if (s[1 + position] = '(') then + begin + inc(position); + position := TUtils.skipSpaces(s, position); + if (position < start) then + exit(-1); + temp.Clear; + useLt := s[1 + position] = '<'; + if useLt then + position := TUtils.readUntil(temp, s, position + 1, '>') + else + position := TUtils.readMdLink(temp, s, position); + if (position < start) then + exit(-1); + if (useLt) then + inc(position); + link := temp.ToString(); + hasLink := true; + if (s[1 + position] = ' ') then + begin + position := TUtils.skipSpaces(s, position); + if (position > start) and (s[1 + position] = '"') then + begin + inc(position); + temp.Clear; + position := TUtils.readUntil(temp, s, position, '"'); + if (position < start) then + exit(-1); + comment := temp.ToString(); + inc(position); + position := TUtils.skipSpaces(s, position); + if (position = -1) then + exit(-1); + end; + end; + if (s[1 + position] <> ')') then + exit(-1); + end + else if (s[1 + position] = '[') then + begin + inc(position); + temp.Clear; + position := TUtils.readRawUntil(temp, s, position, ']'); + if (position < start) then + exit(-1); + if temp.length > 0 then + id := temp.ToString() + else + id := name; + if linkRefs.find(LowerCase(id), i) then + begin + lr := TLinkRef(linkRefs.Objects[i]); + link := lr.link; + hasLink := true; + comment := lr.title; + end + end + else + begin + if linkRefs.find(LowerCase(name), i) then + begin + lr := TLinkRef(linkRefs.Objects[i]); + isAbbrev := lr.isAbbrev; + link := lr.link; + hasLink := true; + comment := lr.title; + position := oldPos; + end + else + exit(-1); + end; + if (not hasLink) then + exit(-1); + + if (token = mtLINK) then + begin + if (isAbbrev) and (comment <> '') then + begin + if (not FuseExtensions) then + exit(-1); + out_.append('<abbr title:="'); + TUtils.appendValue(out_, comment, 0, Length(comment)); + out_.append('">'); + recursiveEmitLine(out_, name, 0, mtNONE); + out_.append('</abbr>'); + end + else + begin + FConfig.decorator.openLink(out_); + out_.append(' href="'); + TUtils.appendValue(out_, link, 0, Length(link)); + out_.append('"'); + if (comment <> '') then + begin + out_.append(' title="'); + TUtils.appendValue(out_, comment, 0, Length(comment)); + out_.append('"'); + end; + out_.append('>'); + recursiveEmitLine(out_, name, 0, mtNONE); + FConfig.decorator.closeLink(out_); + end + end + else + begin + FConfig.decorator.openImage(out_); + out_.append(' src="'); + TUtils.appendValue(out_, link, 0, Length(link)); + out_.append('" alt="'); + TUtils.appendValue(out_, name, 0, Length(name)); + out_.append('"'); + if (comment <> '') then + begin + out_.append(' title="'); + TUtils.appendValue(out_, comment, 0, Length(comment)); + out_.append('"'); + end; + FConfig.decorator.closeImage(out_); + end; + result := position; + finally + temp.Free; + end; +end; + +function TEmitter.checkHTML(out_: TStringBuilder; s: String; start: integer): integer; +var + temp: TStringBuilder; + position: integer; + link: String; +begin + temp := TStringBuilder.Create(); + try + // Check for auto links + temp.Clear; + position := TUtils.readUntil(temp, s, start + 1, [':', ' ', '>', #10]); + if (position <> -1) and (s[1 + position] = ':') and (THTML.isLinkPrefix(temp.ToString())) then + begin + position := TUtils.readUntil(temp, s, position, ['>']); + if (position <> -1) then + begin + link := temp.ToString(); + FConfig.decorator.openLink(out_); + out_.append(' href="'); + TUtils.appendValue(out_, link, 0, Length(link)); + out_.append('">'); + TUtils.appendValue(out_, link, 0, Length(link)); + FConfig.decorator.closeLink(out_); + exit(position); + end; + end; + + // Check for mailto auto link + temp.Clear; + position := TUtils.readUntil(temp, s, start + 1, ['@', ' ', '>', #10]); + if (position <> -1) and (s[1 + position] = '@') then + begin + position := TUtils.readUntil(temp, s, position, '>'); + if (position <> -1) then + begin + link := temp.ToString(); + FConfig.decorator.openLink(out_); + out_.append(' href="'); + TUtils.appendMailto(out_, 'mailto:', 0, 7); + TUtils.appendMailto(out_, link, 0, Length(link)); + out_.append('">'); + TUtils.appendMailto(out_, link, 0, Length(link)); + FConfig.decorator.closeLink(out_); + exit(position); + end; + end; + + // Check for inline html + if (start + 2 < Length(s)) then + begin + temp.Clear; + exit(TUtils.readXML(out_, s, start, FConfig.safeMode)); + end; + + result := -1; + finally + temp.Free; + end; +end; + +class function TEmitter.checkEntity(out_: TStringBuilder; s: String; start: integer): integer; +var + position, i: integer; + c: char; +begin + position := TUtils.readUntil(out_, s, start, ';'); + if (position < 0) or (out_.length < 3) then + exit(-1); + if (out_[1] = '#') then + begin + if (out_[2] = 'x') or (out_[2] = 'X') then + begin + if (out_.length < 4) then + exit(-1); + for i := 3 to out_.length-1 do + begin + c := out_[i]; + if ((c < '0') or (c > '9')) and (((c < 'a') or (c > 'f')) and ((c < 'A') or (c > 'F'))) then + exit(-1); + end; + end + else + begin + for i := 2 to out_.length-1 do + begin + c := out_[i]; + if (c < '0') or (c > '9') then + exit(-1); + end; + end; + out_.append(';'); + end + else + begin + for i := 1 to out_.length - 1 do + begin + c := out_[i]; // zero based + if (not isLetterOrDigit(c)) then + exit(-1); + end; + out_.append(';'); + if THTML.isEntity(out_.ToString()) then + exit(position) + else + exit(-1); + end; + + result := position; +end; + +function TEmitter.recursiveEmitLine(out_: TStringBuilder; s: String; start: integer; token: TMarkToken): integer; +var + position, a, b: integer; + temp: TStringBuilder; + mt: TMarkToken; +begin + position := start; + temp := TStringBuilder.Create(); + try + while (position < Length(s)) do + begin + mt := getToken(s, position); + if (token <> mtNONE) and ((mt = token) or ((token = mtEM_STAR) and (mt = mtSTRONG_STAR)) or ((token = mtEM_UNDERSCORE) and (mt = mtSTRONG_UNDERSCORE))) then + exit(position); + + case mt of + mtIMAGE, mtLINK: + begin + temp.Clear; + b := checkLink(temp, s, position, mt); + if (b > 0) then + begin + out_.append(temp); + position := b; + end + else + out_.append(s[1 + position]); + end; + mtEM_STAR, mtEM_UNDERSCORE: + begin + temp.Clear; + b := recursiveEmitLine(temp, s, position + 1, mt); + if (b > 0) then + begin + FConfig.decorator.openEmphasis(out_); + out_.append(temp); + FConfig.decorator.closeEmphasis(out_); + position := b; + end + else + out_.append(s[1 + position]); + end; + mtSTRONG_STAR, mtSTRONG_UNDERSCORE: + begin + temp.Clear; + b := recursiveEmitLine(temp, s, position + 2, mt); + if (b > 0) then + begin + FConfig.decorator.openStrong(out_); + out_.append(temp); + FConfig.decorator.closeStrong(out_); + position := b + 1; + end + else + out_.append(s[1 + position]); + end; + mtSUPER: + begin + temp.Clear; + b := recursiveEmitLine(temp, s, position + 1, mt); + if (b > 0) then + begin + FConfig.decorator.openSuper(out_); + out_.append(temp); + FConfig.decorator.closeSuper(out_); + position := b; + end + else + out_.append(s[1 + position]); + end; + mtCODE_SINGLE, mtCODE_DOUBLE: + begin + if mt = mtCODE_DOUBLE then + a := position + 2 + else + a := position + 1; + b := findToken(s, a, mt); + if (b > 0) then + begin + if mt = mtCODE_DOUBLE then + position := b + 1 + else + position := b + 0; + while (a < b) and (s[1 + a] = ' ') do + inc(a); + if (a < b) then + begin + while (s[1 + b - 1] = ' ') do + dec(b); + end; + FConfig.decorator.openCodeSpan(out_); + TUtils.appendCode(out_, s, a, b); + FConfig.decorator.closeCodeSpan(out_); + end + else + out_.append(s[1 + position]); + end; + mtHTML: + begin + temp.Clear; + b := checkHTML(temp, s, position); + if (b > 0) then + begin + out_.append(temp); + position := b; + end + else + out_.append('<'); + end; + mtENTITY: + begin + temp.Clear; + b := checkEntity(temp, s, position); + if (b > 0) then + begin + out_.append(temp); + position := b; + end + else + out_.append('&'); + end; + mtX_LINK_OPEN: + begin + temp.Clear; + b := recursiveEmitLine(temp, s, position + 2, mtX_LINK_CLOSE); + if (b > 0) and (FConfig.specialLinkEmitter <> nil) then + begin + FConfig.specialLinkEmitter.emitSpan(out_, temp.ToString()); + position := b + 1; + end + else + out_.append(s[1 + position]); + end; + mtX_COPY: + begin + out_.append('©'); + inc(position, 2); + end; + mtX_REG: + begin + out_.append('®'); + inc(position, 2); + end; + mtX_TRADE: + begin + out_.append('™'); + inc(position, 3); + end; + mtX_NDASH: + begin + out_.append('–'); + inc(position); + end; + mtX_MDASH: + begin + out_.append('—'); + inc(position, 2); + end; + mtX_HELLIP: + begin + out_.append('…'); + inc(position, 2); + end; + mtX_LAQUO: + begin + out_.append('«'); + inc(position); + end; + mtX_RAQUO: + begin + out_.append('»'); + inc(position); + end; + mtX_RDQUO: + out_.append('”'); + mtX_LDQUO: + out_.append('“'); + mtESCAPE: + begin + inc(position); + out_.append(s[1 + position]); + end; + // $FALL-THROUGH$ + else + out_.append(s[1 + position]); + end; + inc(position); + end; + result := -1; + finally + temp.Free; + end; +end; + +class function TEmitter.whitespaceToSpace(c: char): char; +begin + if isWhitespace(c) then + result := ' ' + else + result := c; +end; + +function TEmitter.getToken(s: String; position: integer): TMarkToken; +var + c0, c, c1, c2, c3: char; +begin + + result := mtNONE; + if (position > 0) then + c0 := whitespaceToSpace(s[1 + position - 1]) + else + c0 := ' '; + c := whitespaceToSpace(s[1 + position]); + if (position + 1 < Length(s)) then + c1 := whitespaceToSpace(s[1 + position + 1]) + else + c1 := ' '; + if (position + 2 < Length(s)) then + c2 := whitespaceToSpace(s[1 + position + 2]) + else + c2 := ' '; + if (position + 3 < Length(s)) then + c3 := whitespaceToSpace(s[1 + position + 3]) + else + c3 := ' '; + + case (c) of + '*': + if (c1 = '*') then + begin + if (c0 <> ' ') or (c2 <> ' ') then + exit(mtSTRONG_STAR) + else + exit(mtEM_STAR); + end + else if (c0 <> ' ') or (c1 <> ' ') then + exit(mtEM_STAR) + else + exit(mtNONE); + '_': + if (c1 = '_') then + begin + if (c0 <> ' ') or (c2 <> ' ') then + exit(mtSTRONG_UNDERSCORE) + else + exit(mtEM_UNDERSCORE); + end + else if (FuseExtensions) then + begin + if (isLetterOrDigit(c0)) and (c0 <> '_') and (isLetterOrDigit(c1)) then + exit(mtNONE) + else + exit(mtEM_UNDERSCORE); + end + else if (c0 <> ' ') or (c1 <> ' ') then + exit(mtEM_UNDERSCORE) + else + exit(mtNONE); + '!': + if (c1 = '[') then + exit(mtIMAGE) + else + exit(mtNONE); + '[': + if (FuseExtensions) and (c1 = '[') then + exit(mtX_LINK_OPEN) + else + exit(mtLINK); + ']': + if (FuseExtensions) and (c1 = ']') then + exit(mtX_LINK_CLOSE) + else + exit(mtNONE); + '`': + if (c1 = '`') then + exit(mtCODE_DOUBLE) + else + exit(mtCODE_SINGLE); + '\': + if CharInSet(c1, ['\', '[', ']', '(', ')', '{', '}', '#', '"', '''', '.', '>', '<', '*', '+', '-', '_', '!', '`', '~', '^']) then + exit(mtESCAPE) + else + exit(mtNONE); + '<': + if (FuseExtensions) and (c1 = '<') then + exit(mtX_LAQUO) + else + exit(mtHTML); + '&': + exit(mtENTITY); + else + if (FuseExtensions) then + case (c) of + '-': + if (c1 = '-') and (c2 = '-') then + exit(mtX_MDASH) + else if (c1 = '-') then + exit(mtX_NDASH); + '^': + if (c0 = '^') or (c1 = '^') then + exit(mtNONE) + else + exit(mtSUPER); + '>': + if (c1 = '>') then + exit(mtX_RAQUO); + '.': + if (c1 = '.') and (c2 = '.') then + exit(mtX_HELLIP); + '(': + begin + if (c1 = 'C') and (c2 = ')') then + exit(mtX_COPY); + if (c1 = 'R') and (c2 = ')') then + exit(mtX_REG); + if (c1 = 'T') and (c2 = 'M') and (c3 = ')') then + exit(mtX_TRADE); + end; + '"': + begin + if (not isLetterOrDigit(c0)) and (c1 <> ' ') then + exit(mtX_LDQUO); + if (c0 <> ' ') and (not isLetterOrDigit(c1)) then + exit(mtX_RDQUO); + exit(mtNONE); + end; + end; + end; +end; + +procedure TEmitter.emitMarkedLines(out_: TStringBuilder; lines: TLine); +var + s: TStringBuilder; + line: TLine; +begin + s := TStringBuilder.Create(); + try + line := lines; + while (line <> nil) do + begin + if (not line.isEmpty) then + begin +// s.append(line.value.substring(line.leading, line.value.length - line.trailing)); PSTfix + s.Append( Copy(line.value, line.leading + 1, Length(line.value) - line.trailing)); + if (line.trailing >= 2) then + s.append('<br />'); + end; + if (line.next <> nil) then + s.append(#10); + line := line.next; + end; + recursiveEmitLine(out_, s.ToString(), 0, mtNONE); + finally + s.Free; + end; +end; + +procedure TEmitter.emitRawLines(out_: TStringBuilder; lines: TLine); +var + s: String; + line: TLine; + temp: TStringBuilder; + position, t: integer; +begin + line := lines; + if (FConfig.safeMode) then + begin + temp := TStringBuilder.Create(); + try + while (line <> nil) do + begin + if (not line.isEmpty) then + temp.append(line.value); + temp.append(#10); + line := line.next; + end; + s := temp.ToString(); + position := 0; + while position < length(s) do + begin + if (s[1 + position] = '<') then + begin + temp.Clear; + t := TUtils.readXML(temp, s, position, FConfig.safeMode); + if (t <> -1) then + begin + out_.append(temp); + position := t; + end + else + out_.append(s[1 + position]); + end + else + out_.append(s[1 + position]); + inc(position); + end + finally + temp.Free; + end; + end + else + begin + while (line <> nil) do + begin + if (not line.isEmpty) then + out_.append(line.value); + out_.append(#10); + line := line.next; + end; + end; +end; + +procedure TEmitter.emitCodeLines(out_: TStringBuilder; lines: TLine; meta: String; removeIndent: boolean); +var + line: TLine; + list: TStringList; + i, sp: integer; + c: char; +begin + line := lines; + if (FConfig.codeBlockEmitter <> nil) then + begin + list := TStringList.Create; + try + while (line <> nil) do + begin + if (line.isEmpty) then + list.add('') + else if removeIndent then +// list.add(line.value.substring(4)) P{STfix + list.Add( Copy(line.value, 5)) + else + list.add(line.value); + line := line.next; + end; + FConfig.codeBlockEmitter.emitBlock(out_, list, meta); + finally + list.Free + end + end + else + begin + while (line <> nil) do + begin + if (not line.isEmpty) then + begin + if removeIndent then + sp := 4 + else + sp := 0; + for i := sp to Length(line.value) - 1 do + begin + c := line.value[1 + i]; + case c of + '&': + out_.append('&'); + '<': + out_.append('<'); + '>': + out_.append('>'); + else + out_.append(c); + end; + end; + end; + out_.append(#10); + line := line.next; + end; + end; +end; + +{ TReader } + +constructor TReader.Create(source: String); +begin + inherited Create; + FValue := source; + FCursor := 0; +end; + +function TReader.read: char; +begin + inc(FCursor); + if FCursor > Length(FValue) then + result := #0 + else + result := FValue[FCursor]; +end; + +{ TUtils } + +class function TUtils.skipSpaces(s: String; start: integer): integer; +var + position: integer; +begin + position := start; + while (position < Length(s)) and ((s[1 + position] = ' ') or (s[1 + position] = #10)) do + inc(position); + if position < Length(s) then + result := position + else + result := -1; +end; + +class function TUtils.escape(out_: TStringBuilder; ch: char; position: integer): integer; +begin + if CharInSet(ch, ['\', '[', ']', '(', ')', '{', '}', '#', '"', '''', '.', '>', '<', '*', '+', '-', '_', '!', '`', '^']) then + begin + out_.append(ch); + result := position + 1; + end + else + begin + out_.append('\'); + result := position; + end; +end; + +class function TUtils.readUntil(out_: TStringBuilder; s: String; start: integer; cend: TSysCharSet): integer; +var + position: integer; + ch: char; +begin + position := start; + while (position < Length(s)) do + begin + ch := s[1 + position]; + if (ch = '\') and (position + 1 < Length(s)) then + position := escape(out_, s[1 + position + 1], position) + else + begin + if CharInSet(ch, cend) then + break + else + out_.append(ch); + end; + inc(position); + end; + if position = Length(s) then + result := -1 + else + result := position; +end; + +class function TUtils.readUntil(out_: TStringBuilder; s: String; start: integer; cend: char): integer; +var + position: integer; + ch: char; +begin + position := start; + while (position < Length(s)) do + begin + ch := s[1 + position]; + if (ch = '\') and (position + 1 < Length(s)) then + position := escape(out_, s[1 + position + 1], position) + else + begin + if (ch = cend) then + break; + out_.append(ch); + end; + inc(position); + end; + if position = Length(s) then + result := -1 + else + result := position; +end; + +class function TUtils.readMdLink(out_: TStringBuilder; s: String; start: integer): integer; +var + position, counter: integer; + ch: char; + endReached: boolean; +begin + position := start; + counter := 1; + while (position < Length(s)) do + begin + ch := s[1 + position]; + if (ch = '\') and (position + 1 < Length(s)) then + position := escape(out_, s[1 + position + 1], position) + else + begin + endReached := false; + case ch of + '(': + inc(counter); + ' ': + if (counter = 1) then + endReached := true; + ')': + begin + dec(counter); + if (counter = 0) then + endReached := true; + end; + end; + if (endReached) then + break; + out_.append(ch); + end; + inc(position); + end; + if position = Length(s) then + result := -1 + else + result := position; +end; + +class function TUtils.readMdLinkId(out_: TStringBuilder; s: String; start: integer): integer; +var + position, counter: integer; + ch: char; + endReached: boolean; +begin + position := start; + counter := 1; + while (position < Length(s)) do + begin + ch := s[1 + position]; + endReached := false; + case ch of + #10: + out_.append(' '); + '[': + begin + inc(counter); + out_.append(ch); + end; + ']': + begin + dec(counter); + if (counter = 0) then + endReached := true + else + out_.append(ch); + end; + else + out_.append(ch); + end; + if (endReached) then + break; + inc(position); + end; + if position = Length(s) then + result := -1 + else + result := position; +end; + +class function TUtils.readRawUntil(out_: TStringBuilder; s: String; start: integer; cend: TSysCharSet): integer; +var + position: integer; + ch: char; +begin + position := start; + while (position < Length(s)) do + begin + ch := s[1 + position]; + if CharInSet(ch, cend) then + break; + out_.append(ch); + inc(position); + end; + if position = Length(s) then + result := -1 + else + result := position; +end; + +class function TUtils.readRawUntil(out_: TStringBuilder; s: String; start: integer; cend: char): integer; +var + position: integer; + ch: char; +begin + position := start; + while (position < Length(s)) do + begin + ch := s[1 + position]; + if (ch = cend) then + break; + out_.append(ch); + inc(position); + end; + if position = Length(s) then + result := -1 + else + result := position; +end; + +class function TUtils.readXMLUntil(out_: TStringBuilder; s: String; start: integer; cend: TSysCharSet): integer; +var + position : integer; + ch, stringChar: char; + inString: boolean; +begin + position := start; + inString := false; + stringChar := #0; + while (position < Length(s)) do + begin + ch := s[1 + position]; + if (inString) then + begin + if (ch = '\') then + begin + out_.append(ch); + inc(position); + if (position < Length(s)) then + begin + out_.append(ch); + inc(position); + end; + continue; + end; + if (ch = stringChar) then + begin + inString := false; + out_.append(ch); + inc(position); + continue; + end; + end; + if CharInSet(ch, ['"', '''']) then + begin + inString := true; + stringChar := ch; + end; + if (not inString) then + begin + if CharInSet(ch, cend) then + break; + end; + out_.append(ch); + inc(position); + end; + if position = Length(s) then + result := -1 + else + result := position; +end; + +class procedure TUtils.appendCode(out_: TStringBuilder; s: String; start: integer; e: integer); +var + i: integer; + c: char; +begin + for i := start to e - 1 do + begin + c := s[1 + i]; + case c of + '&': + out_.append('&'); + '<': + out_.append('<'); + '>': + out_.append('>'); + else + out_.append(c); + end; + end; +end; + +class procedure TUtils.appendValue(out_: TStringBuilder; s: String; start: integer; e: integer); +var + i: integer; + c: char; +begin + for i := start to e - 1 do + begin + c := s[1 + i]; + case c of + '&': + out_.append('&'); + '<': + out_.append('<'); + '>': + out_.append('>'); + '"': + out_.append('"'); + '''': + out_.append('''); + else + out_.append(c); + end; + end; +end; + +class procedure TUtils.appendDecEntity(out_: TStringBuilder; value: char); +begin + out_.append('&#'); + out_.append(IntToStr(ord(value))); + out_.append(';'); +end; + +class procedure TUtils.appendHexEntity(out_: TStringBuilder; value: char); +begin + out_.append('&#x'); + out_.append(IntToHex(ord(value), 2)); + out_.append(';'); +end; + +class procedure TUtils.appendMailto(out_: TStringBuilder; s: String; start: integer; e: integer); +var + i: integer; + c: char; +begin + for i := start to e - 1 do + begin + c := s[1 + i]; + if CharInSet(c, ['a'..'z','A'..'Z','0'..'9','&', '<', '>', '"', '''', '@']) then + if random(2)=0 then appendHexEntity(out_, c) else appendDecEntity(out_, c) + else + out_.append(c); + end; +end; + +class procedure TUtils.getXMLTag(out_: TStringBuilder; bin: TStringBuilder); +var + position: integer; +begin + position := 1; + if (bin[1] = '/') then + inc(position); + while (isLetterOrDigit(bin[position])) do + begin + out_.append(bin[position]); + inc(position) + end; +end; + +class procedure TUtils.getXMLTag(out_: TStringBuilder; s: String); +var + position: integer; +begin + position := 1; + if (s[1 + 1] = '/') then + inc(position); + while (isLetterOrDigit(s[1 + position])) do + begin + out_.append(s[1 + position]); + inc(position) + end; +end; + +class function TUtils.readXML(out_: TStringBuilder; s: String; start: integer; safeMode: boolean): integer; +var + position: integer; + isCloseTag: boolean; + temp: TStringBuilder; + tag: String; +begin + if (length(s)<1 + start +1 ) then exit(-1); + if (s[1 + start + 1] = '/') then + begin + isCloseTag := true; + position := start + 2; + end + else if (s[1 + start + 1] = '!') then + begin + out_.append('<!'); + exit(start + 1); + end + else + begin + isCloseTag := false; + position := start + 1; + end; + + if (safeMode) then + begin + temp := TStringBuilder.Create(); + try + position := readXMLUntil(temp, s, position, [' ', '/', '>']); + if (position = -1) then + exit(-1); +// tag := temp.ToString().trim().ToLower; PSTFix + tag := LowerCase( Trim( temp.ToString)); + if (THTML.isUnsafeHtmlElement(tag)) then + out_.append('<') + else + out_.append('<'); + if (isCloseTag) then + out_.append('/'); + out_.append(temp); + finally + temp.Free; + end; + end + else + begin + out_.append('<'); + if (isCloseTag) then + out_.append('/'); + position := readXMLUntil(out_, s, position, [' ', '/', '>']); + end; + if (position = -1) then + exit(-1); + position := readXMLUntil(out_, s, position, ['/', '>']); + if (position = -1) then + exit(-1); + + if (s[1 + position] = '/') then + begin + out_.append(' /'); + position := readXMLUntil(out_, s, position + 1, ['>']); + if (position = -1) then + exit(-1); + end; + + if (s[1 + position] = '>') then + begin + out_.append('>'); + exit(position); + end; + result := -1; +end; + +class procedure TUtils.codeEncode(out_: TStringBuilder; value: String; offset: integer); +var + i: integer; + c: char; +begin + for i := offset to Length(value) - 1 do + begin + c := value[1 + i]; + case c of + '&': + out_.append('&'); + '<': + out_.append('<'); + '>': + out_.append('>'); + else + out_.append(c); + end; + end; +end; + +class function TUtils.getMetaFromFence(fenceLine: String): String; +var + i: integer; + c: char; +begin + for i := 0 to Length(fenceLine) - 1 do + begin + c := fenceLine[1 + i]; + if (not isWhitespace(c)) and (c <> '`') and (c <> '~') then +// exit(fenceLine.substring(i).trim()); PSTfix + Exit( Trim( Copy(fenceLine, i+1))); + end; + result := ''; +end; + +{ THTML } + +class function THTML.isHtmlBlockElement(s: String): boolean; +var + ht: THTMLElement; +begin + ht := THTMLElement(StringToEnum(TypeInfo(THTMLElement), 'he' + s, ord(heNONE))); + result := ht in BLOCK_ELEMENTS; +end; + +class function THTML.isLinkPrefix(s: String): boolean; +begin + result := StringsContains(LINK_PREFIXES, s); +end; + +class function THTML.isEntity(s: String): boolean; +begin + result := StringsContains(ENTITY_NAMES, s); +end; + +class function THTML.isUnsafeHtmlElement(s: String): boolean; +var + ht: THTMLElement; +begin + ht := THTMLElement(StringToEnum(TypeInfo(THTMLElement), s, ord(heNONE))); + result := ht in UNSAFE_ELEMENTS; +end; + +{ TLine } + +procedure TLine.Init(); +begin + FLeading := 0; + while (leading < Length(value)) and (value[1 + leading] = ' ') do + inc(FLeading); + + if (leading = Length(value)) then + setEmpty() + else + begin + isEmpty := false; + trailing := 0; + while (value[1 + Length(value) - trailing - 1] = ' ') do + inc(FTrailing); + end; +end; + +procedure TLine.InitLeading(); +begin + FLeading := 0; + while (leading < Length(value)) and (value[1 + leading] = ' ') do + inc(FLeading); + if (leading = Length(value)) then + setEmpty(); +end; + +// TODO use Util#skipSpaces +function TLine.skipSpaces(): boolean; +begin + while (position < Length(value)) and (value[1 + position] = ' ') do + inc(FPosition); + result := position < Length(value); +end; + +// TODO use Util#readUntil +function TLine.readUntil(chend: TSysCharSet): String; +var + sb: TStringBuilder; + p: integer; + ch, c: char; +begin + sb := TStringBuilder.Create(); + try + p := self.position; + while (p < Length(value)) do + begin + ch := value[1 + p]; + if (ch = '\') and (p + 1 < Length(value)) then + begin + c := value[1 + p + 1]; + if CharInSet(c, ['\', '[', ']', '(', ')', '{', '}', '#', '"', '''', '.', '>', '*', '+', '-', '_', '!', '`', '~']) then + begin + sb.append(c); + inc(FPosition); + end + else + begin + sb.append(ch); + break; + end; + end + else if CharInSet(ch, chend) then + break + else + sb.append(ch); + inc(p); + end; + + if (p < Length(value)) then + ch := value[1 + p] + else + ch := #10; + if CharInSet(ch, chend) then + begin + self.position := p; + result := sb.ToString(); + end + else + result := ''; + finally + sb.Free; + end; +end; + +procedure TLine.setEmpty(); +begin + value := ''; + leading := 0; + trailing := 0; + isEmpty := true; + if (previous <> nil) then + previous.nextEmpty := true; + if (next <> nil) then + next.prevEmpty := true; +end; + +function TLine.countChars(ch: char): integer; +var + count, i: integer; + c: char; +begin + count := 0; + for i := 0 to Length(value) - 1 do + begin + c := value[1 + i]; + if (c = ' ') then + continue; + if (c = ch) then + begin + inc(count); + continue; + end; + count := 0; + break; + end; + result := count; +end; + +function TLine.countCharsStart(ch: char; allowSpaces: boolean): integer; +var + count, i: integer; + c: char; +begin + count := 0; + for i := 0 to Length(value) - 1 do + begin + c := value[1 + i]; + if (c = ' ') and (allowSpaces) then + begin + continue; + end; + if (c = ch) then + inc(count) + else + break; + end; + result := count; +end; + +function TLine.getLineType(configuration: TConfiguration): TLineType; +var + i: integer; +begin + if (isEmpty) then + exit(ltEMPTY); + + if (leading > 3) then + exit(ltCODE); + + if (value[1 + leading] = '#') then + exit(ltHEADLINE); + + if (value[1 + leading] = '>') then + exit(ltBQUOTE); + + if (configuration.forceExtendedProfile) then + begin + if (Length(value) - leading - trailing > 2) then + begin + if (value[1 + leading] = '`') and (countCharsStart('`', configuration.allowSpacesInFencedDelimiters) >= 3) then + exit(ltFENCED_CODE); + if (value[1 + leading] = '~') and (countCharsStart('~', configuration.allowSpacesInFencedDelimiters) >= 3) then + exit(ltFENCED_CODE); + end; + end; + + if (Length(value) - leading - trailing > 2) and ((value[1 + leading] = '*') or (value[1 + leading] = '-') or (value[1 + leading] = '_')) then + begin + if (countChars(value[1 + leading]) >= 3) then + exit(ltHR); + end; + + if (Length(value) - leading >= 2) and (value[1 + leading + 1] = ' ') then + begin + if CharInSet(value[1 + leading], ['*', '-', '+']) then + exit(ltULIST); + end; + + if (Length(value) - leading >= 3) and (isDigit(value[1 + leading])) then + begin + i := leading + 1; + while (i < Length(value)) and (isDigit(value[1 + i])) do + inc(i); + if (i + 1 < Length(value)) and (value[1 + i] = '.') and (value[1 + i + 1] = ' ') then + exit(ltOLIST); + end; + + if (value[1 + leading] = '<') then + begin + if (checkHTML()) then + exit(ltXML); + end; + + if (next <> nil) and (not next.isEmpty) then + begin + if ((next.value[1 + 0] = '-')) and ((next.countChars('-') > 0)) then + exit(ltHEADLINE2); + if ((next.value[1 + 0] = '=')) and ((next.countChars('=') > 0)) then + exit(ltHEADLINE1); + end; + + exit(ltOTHER); +end; + +function TLine.readXMLComment(firstLine: TLine; start: integer): integer; +var + line: TLine; + p: integer; +begin + line := firstLine; + if (start + 3 < Length(line.value)) then + begin + if (line.value[1 + 2] = '-') and (line.value[1 + 3] = '-') then + begin + p := start + 4; + while (line <> nil) do + begin + while (p < Length(line.value)) and (line.value[1 + p] <> '-') do + inc(p); + if (p = Length(line.value)) then + begin + line := line.next; + p := 0; + end + else + begin + if (p + 2 < Length(line.value)) then + begin + if (line.value[1 + p + 1] = '-') and (line.value[1 + p + 2] = '>') then + begin + xmlEndLine := line; + exit(p + 3); + end; + end; + inc(p); + end; + end; + end; + end; + exit(-1); +end; + +// FIXME ... hack +function TLine.stripID(): String; +var + p, start: integer; + found: boolean; + id: String; +begin + if (isEmpty or (value[1 + Length(value) - trailing - 1] <> '}')) then + exit(''); + + p := leading; + found := false; + while (p < Length(value)) and (not found) do + begin + case value[1 + p] of + '\': + begin + if (p + 1 < Length(value)) then + begin + if (value[1 + p + 1]) = '{' then + begin + inc(p); + break; + end; + end; + inc(p); + break; + end; + '{': + begin + found := true; + break; + end + else + begin + inc(p); + break; + end; + end; + end; + + if (found) then + begin + if (p + 1 < Length(value)) and (value[1 + p + 1] = '#') then + begin + start := p + 2; + p := start; + found := false; + while (p < Length(value)) and (not found) do + begin + case (value[1 + p]) of + '\': + begin + if (p + 1 < Length(value)) then + begin + if (value[1 + p + 1]) = '}' then + begin + inc(p); + break; + end; + end; + inc(p); + break; + end; + '}': + begin + found := true; + break; + end; + else + begin + inc(p); + break; + end; + end; + + if (found) then + begin +// id := value.substring(start, p).trim(); PSTfix + id := Trim( Copy(value, start + 1, p)); + if (leading <> 0) then + begin +// value := value.substring(0, leading) + value.substring(leading, start - 2).trim(); PSTfix + value := Copy(value, 1, leading) + Trim( Copy( value, leading + 1, start -2)); + end + else + begin +// value := value.substring(leading, start - 2).trim(); PSTFix + value := Trim( Copy(value, leading +1, start -2)); + end; + trailing := 0; + if (Length(id) > 0) then + exit(id) + else + exit(''); + end; + end; + end; + end; + exit(''); +end; + +function TLine.checkHTML: boolean; +var + tags: TStringList; + temp: TStringBuilder; + element, tag: String; + line: TLine; + newPos: integer; +begin + result := false; + tags := TStringList.Create(); + temp := TStringBuilder.Create(); + try + position := leading; + if (value.length >= 1 + leading + 1) and (value[1 + leading + 1] = '!') then + begin + if (readXMLComment(self, leading) > 0) then + begin + exit(true); + end; + end; + position := TUtils.readXML(temp, value, leading, false); + if (position > -1) then + begin + element := temp.ToString(); + temp.Clear; + TUtils.getXMLTag(temp, element); + tag := LowerCase(temp.ToString()); + if (not THTML.isHtmlBlockElement(tag)) then + exit(false); +// if (tag.equals('hr') or element.endsWith('/>')) then PSTFix + if (tag = 'hr') or AnsiEndsText('/>', element) then + + begin + xmlEndLine := self; + exit(true); + end; + tags.add(tag); + + line := self; + while (line <> nil) do + begin + while (position < Length(line.value)) and (line.value[1 + position] <> '<') do + inc(FPosition); + if (position >= Length(line.value)) then + begin + line := line.next; + position := 0; + end + else + begin + temp.Clear; + newPos := TUtils.readXML(temp, line.value, position, false); + if (newPos > 0) then + begin + element := temp.ToString(); + temp.Clear; + TUtils.getXMLTag(temp, element); + tag := LowerCase(temp.ToString()); + if (THTML.isHtmlBlockElement(tag)) and (tag <> 'hr') and (not AnsiEndsText('/>', element)) then + begin + if (element[1 + 1] = '/') then + begin + if (tags[tags.Count - 1] <> tag) then + exit(false); + tags.Delete(tags.count - 1); + end + else + tags.add(tag); + end; + if (tags.count = 0) then + begin + xmlEndLine := line; + break; + end; + position := newPos; + end + else + begin + inc(FPosition); + end; + end; + end; + result := tags.count = 0; + end; + finally + temp.Free; + tags.Free; + end; +end; + +{ TLinkRef } + +constructor TLinkRef.Create(link, title: String; isAbbrev: boolean); +begin + inherited Create; + FLink := link; + FTitle := title; + FIsAbbrev := isAbbrev; +end; + +{ TBlock } + +constructor TBlock.Create; +begin + inherited; +end; + +destructor TBlock.Destroy; +begin + FLines.Free; + FBlocks.Free; + FNext.free; + inherited; +end; + +procedure TBlock.AppendLine(line: TLine); +begin + if (self.lineTail = nil) then + begin + self.FLines := line; + self.FLineTail := line; + end + else + begin + self.lineTail.nextEmpty := line.isEmpty; + line.prevEmpty := self.lineTail.isEmpty; + line.previous := self.lineTail; + self.lineTail.next := line; + self.FLineTail := line; + end; + +end; + +procedure TBlock.expandListParagraphs; +var + outer: TBlock; + inner: TBlock; + hasParagraph: boolean; +begin + if (self.type_ <> btORDERED_LIST) and (self.type_ <> btUNORDERED_LIST) then + exit; + + outer := self.blocks; + hasParagraph := false; + while (outer <> nil) and (not hasParagraph) do + begin + if (outer.type_ = btLIST_ITEM) then + begin + inner := outer.blocks; + while (inner <> nil) and (not hasParagraph) do + begin + if (inner.type_ = btPARAGRAPH) then + begin + hasParagraph := true; + end; + inner := inner.next; + end; + end; + outer := outer.next; + end; + + if (hasParagraph) then + begin + outer := self.blocks; + while (outer <> nil) do + begin + if (outer.type_ = btLIST_ITEM) then + begin + inner := outer.blocks; + while (inner <> nil) do + begin + if (inner.type_ = btNONE) then + begin + inner.type_ := btPARAGRAPH; + end; + inner := inner.next; + end; + end; + outer := outer.next; + end; + end; +end; + +function TBlock.hasLines: boolean; +begin + result := lines <> nil; +end; + +procedure TBlock.removeLine(line: TLine); +begin + if (line.previous = nil) then + begin + self.FLines := line.next; + end + else + begin + line.previous.next := line.next; + end; + + if (line.next = nil) then + begin + self.FLineTail := line.previous; + end + else + begin + line.next.previous := line.previous; + end; + line.previous := nil; + + line.next := nil; + line.free; +end; + +procedure TBlock.removeBlockQuotePrefix; +var + line: TLine; + rem: integer; +begin + line := self.lines; + while (line <> nil) do + begin + if (not line.isEmpty) then + begin + if (line.value[1 + line.leading] = '>') then + begin + rem := line.leading + 1; + if (line.leading + 1 < Length(line.value)) and (line.value[1 + line.leading + 1] = ' ') then + begin + inc(rem); + end; + line.value := Copy(line.value, rem+1); + line.InitLeading(); + end; + end; + line := line.next; + end; +end; + +function TBlock.removeLeadingEmptyLines: boolean; +var + wasEmpty: boolean; + line: TLine; +begin + wasEmpty := false; + line := self.lines; + while (line <> nil) and (line.isEmpty) do + begin + self.removeLine(line); + line := self.lines; + wasEmpty := true; + end; + result := wasEmpty; + +end; + +procedure TBlock.removeTrailingEmptyLines; +var + line: TLine; +begin + line := self.lineTail; + while (line <> nil) and (line.isEmpty) do + begin + self.removeLine(line); + line := self.lineTail; + end; +end; + +procedure TBlock.removeListIndent(config: TConfiguration); +var + line: TLine; +begin + line := self.lines; + while (line <> nil) do + begin + if (not line.isEmpty) then + begin + case (line.getLineType(config)) of + ltULIST: +// line.value := line.value.substring(line.leading + 2); PSTfix + line.value := Copy(line.value, line.leading +3); + ltOLIST: +// line.value := line.value.substring(line.value.indexOf('.') + 2); pstfix + line.value := Copy(line.value, pos('.', line.value) + 2); + else +// line.value := line.value.substring(Math.min(line.leading, 4)); pstfix + line.value := Copy(line.value, Math.Min(line.leading + 1, 5)); + end; + line.InitLeading(); + end; + line := line.next; + end; + +end; + +procedure TBlock.removeSurroundingEmptyLines; +begin + if (self.lines <> nil) then + begin + self.removeTrailingEmptyLines(); + self.removeLeadingEmptyLines(); + end; + +end; + +function TBlock.split(line: TLine): TBlock; +var + block: TBlock; +begin + block := TBlock.Create(); + block.FLines := self.lines; + block.FLineTail := line; + self.FLines := line.next; + line.next := nil; + if (self.lines = nil) then + begin + self.FLineTail := nil; + end + else + begin + self.lines.previous := nil; + end; + + if (self.blocks = nil) then + begin + self.FBlocks := block; + self.FBlockTail := block; + end + else + begin + self.blockTail.next := block; + self.FBlockTail := block; + end; + result := block; +end; + +procedure TBlock.transfromHeadline; +var + level, start, end_: integer; + line: TLine; +begin + if (self.hlDepth > 0) then + begin + exit; + end; + level := 0; + line := self.lines; + if (line.isEmpty) then + begin + exit; + end; + start := line.leading; + while (start < Length(line.value)) and (line.value[1 + start] = '#') do + begin + inc(level); + inc(start); + end; + while (start < Length(line.value)) and (line.value[1 + start] = ' ') do + begin + inc(start); + end; + if (start >= Length(line.value)) then + begin + line.setEmpty(); + end + else + begin + end_ := Length(line.value) - line.trailing - 1; + while (line.value[1 + end_] = '#') do + begin + dec(end_); + end; + while (line.value[1 + end_] = ' ') do + begin + dec(end_); + end; + line.value := Copy(line.value, start+1, end_-start+1); + line.leading := 0; + line.trailing := 0; + end; + self.hlDepth := Math.min(level, 6); + +end; + +{$IFDEF FPC} +{ TStringBuilder } + +constructor TStringBuilder.Create; +begin + Inherited; + FBufferSize := BUFFER_INCREMENT_SIZE; +end; + +procedure TStringBuilder.Append(value: TStringBuilder); +begin + append(value.ToString); +end; + +procedure TStringBuilder.Append(value: integer); +begin + append(inttostr(value)); +end; + +procedure TStringBuilder.Append(value: String); +begin + If (value <> '') Then + Begin + If FLength + System.Length(value) > System.Length(FContent) Then + SetLength(FContent, System.Length(FContent) + Math.Max(FBufferSize, System.Length(value))); + + Move(value[1], FContent[FLength + 1], System.Length(value) * SizeOf(Char)); + + Inc(FLength, System.Length(value)); + End; +end; + +procedure TStringBuilder.Clear; +begin + FContent := ''; + FLength := 0; +end; + +function TStringBuilder.GetChar(index: integer): char; +begin + if (index < 0) or (index >= Length) then + raise Exception.Create('Out of bounds'); + result := FContent[index+1]; +end; + + +function TStringBuilder.toString: String; +begin + Result := Copy(FContent, 1, FLength); +end; + +{$ENDIF} + +end. diff --git a/source/markdowntxtmark.pas b/source/markdowntxtmark.pas new file mode 100644 index 0000000..7e8da26 --- /dev/null +++ b/source/markdowntxtmark.pas @@ -0,0 +1,65 @@ +{ +Copyright (C) Miguel A. Risco-Castillo + +FPC-markdown is a fork of Grahame Grieve <grahameg@gmail.com> +Delphi-markdown https://github.com/grahamegrieve/delphi-markdown + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +} + +Unit MarkdownTxtMark; + +{$mode objfpc}{$H+} + +interface + +uses + SysUtils, Classes, TypInfo, + MarkdownDaringFireball, MarkdownUtils; + +Type + + TMarkdownTxtMark = class(TMarkdownDaringFireball) + private + protected + public + Constructor Create; + Destructor Destroy; override; + function process(source: String): String; override; + end; + +implementation + + +{ TMarkdownTxtMark } + +constructor TMarkdownTxtMark.Create; +begin + //Enable Txtmark extensions: + inherited; + Config.forceExtendedProfile:=true; +end; + +destructor TMarkdownTxtMark.Destroy; +begin + + inherited; +end; + +function TMarkdownTxtMark.process(source: String): String; +begin + result:=inherited process(source); +end; + + +end.